defmodule Ecto.Type do
@moduledoc """
Defines functions and the `Ecto.Type` behaviour for implementing
basic custom types.
Ecto provides two types of custom types: basic types and
parameterized types. Basic types are simple, requiring only four
callbacks to be implemented, and are enough for most occasions.
Parameterized types can be customized on the field definition and
provide a wide variety of callbacks.
The definition of basic custom types and all of their callbacks are
available in this module. You can learn more about parameterized
types in `Ecto.ParameterizedType`. If in doubt, prefer to use
basic custom types and rely on parameterized types if you need
the extra functionality.
## Example
Imagine you want to store a URI struct as part of a schema in a
url-shortening service. There isn't an Ecto field type to support
that value at runtime therefore a custom one is needed.
You also want to query not only by the full url, but for example
by specific ports used. This is possible by putting the URI data
into a map field instead of just storing the plain
string representation.
from s in ShortUrl,
where: fragment("?->>? ILIKE ?", s.original_url, "port", "443")
So the custom type does need to handle the conversion from
external data to runtime data (`c:cast/1`) as well as
transforming that runtime data into the `:map` Ecto native type and
back (`c:dump/1` and `c:load/1`).
defmodule EctoURI do
use Ecto.Type
def type, do: :map
# Provide custom casting rules.
# Cast strings into the URI struct to be used at runtime
def cast(uri) when is_binary(uri) do
{:ok, URI.parse(uri)}
end
# Accept casting of URI structs as well
def cast(%URI{} = uri), do: {:ok, uri}
# Everything else is a failure though
def cast(_), do: :error
# When loading data from the database, as long as it's a map,
# we just put the data back into a URI struct to be stored in
# the loaded schema struct.
def load(data) when is_map(data) do
data =
for {key, val} <- data do
{String.to_existing_atom(key), val}
end
{:ok, struct!(URI, data)}
end
# When dumping data to the database, we *expect* a URI struct
# but any value could be inserted into the schema struct at runtime,
# so we need to guard against them.
def dump(%URI{} = uri), do: {:ok, Map.from_struct(uri)}
def dump(_), do: :error
end
Now we can use our new field type above in our schemas:
defmodule ShortUrl do
use Ecto.Schema
schema "posts" do
field :original_url, EctoURI
end
end
Note: `nil` values are always bypassed and cannot be handled by
custom types.
> #### `use Ecto.Type` {: .info}
>
> When you `use Ecto.Type`, it will set `@behaviour Ecto.Type` and define
> default, overridable implementations for `c:embed_as/1` and `c:equal?/2`.
## Custom types and primary keys
Remember that, if you change the type of your primary keys,
you will also need to change the type of all associations that
point to said primary key.
Imagine you want to encode the ID so they cannot enumerate the
content in your application. An Ecto type could handle the conversion
between the encoded version of the id and its representation in the
database. For the sake of simplicity, we'll use base64 encoding in
this example:
defmodule EncodedId do
use Ecto.Type
def type, do: :id
def cast(id) when is_integer(id) do
{:ok, encode_id(id)}
end
def cast(_), do: :error
def dump(id) when is_binary(id) do
{:ok, id_decoded} = Base.decode64(id)
{:ok, String.to_integer(id_decoded)}
end
def load(id) when is_integer(id) do
{:ok, encode_id(id)}
end
defp encode_id(id) do
id
|> Integer.to_string()
|> Base.encode64()
end
end
To use it as the type for the id in our schema, we can use the
`@primary_key` module attribute:
defmodule BlogPost do
use Ecto.Schema
@primary_key {:id, EncodedId, autogenerate: true}
schema "posts" do
belongs_to :author, Author, type: EncodedId
field :content, :string
end
end
defmodule Author do
use Ecto.Schema
@primary_key {:id, EncodedId, autogenerate: true}
schema "authors" do
field :name, :string
has_many :posts, BlogPost
end
end
The `@primary_key` attribute will tell ecto which type to
use for the id.
Note the `type: EncodedId` option given to `belongs_to` in
the `BlogPost` schema. By default, Ecto will treat
associations as if their keys were `:integer`s. Our primary
keys are a custom type, so when Ecto tries to cast those
ids, it will fail.
Alternatively, you can set `@foreign_key_type EncodedId`
after `@primary_key` to automatically configure the type
of all `belongs_to` fields.
"""
import Kernel, except: [match?: 2]
@doc false
defmacro __using__(_opts) do
quote location: :keep do
@behaviour Ecto.Type
def embed_as(_), do: :self
def equal?(term1, term2), do: term1 == term2
defoverridable [embed_as: 1, equal?: 2]
end
end
@typedoc "An Ecto type, primitive or custom."
@type t :: primitive | custom
@typedoc "Primitive Ecto types (handled by Ecto)."
@type primitive :: base | composite
@typedoc "Custom types are represented by user-defined modules."
@type custom :: module | {:parameterized, module, term}
@type base :: :integer | :float | :boolean | :string | :map |
:binary | :decimal | :id | :binary_id |
:utc_datetime | :naive_datetime | :date | :time | :any |
:utc_datetime_usec | :naive_datetime_usec | :time_usec
@type composite :: {:array, t} | {:map, t} | private_composite
@typep private_composite :: {:maybe, t} | {:in, t} | {:param, :any_datetime}
@base ~w(
integer float decimal boolean string map binary id binary_id any
utc_datetime naive_datetime date time
utc_datetime_usec naive_datetime_usec time_usec
)a
@composite ~w(array map maybe in param)a
@doc """
Returns the underlying schema type for the custom type.
For example, if you want to provide your own date
structures, the type function should return `:date`.
Note this function is not required to return Ecto primitive
types, the type is only required to be known by the adapter.
"""
@callback type :: t
@doc """
Casts the given input to the custom type.
This callback is called on external input and can return any type,
as long as the `dump/1` function is able to convert the returned
value into an Ecto native type. There are two situations where
this callback is called:
1. When casting values by `Ecto.Changeset`
2. When passing arguments to `Ecto.Query`
You can return `:error` if the given term cannot be cast.
A default error message of "is invalid" will be added to the
changeset.
You may also return `{:error, keyword()}` to customize the
changeset error message and its metadata. Passing a `:message`
key, will override the default message. It is not possible to
override the `:type` key.
For `{:array, CustomType}` or `{:map, CustomType}` the returned
keyword list will be erased and the default error will be shown.
"""
@callback cast(term) :: {:ok, term} | :error | {:error, keyword()}
@doc """
Loads the given term into a custom type.
This callback is called when loading data from the database and
receives an Ecto native type. It can return any type, as long as
the `dump/1` function is able to convert the returned value back
into an Ecto native type.
"""
@callback load(term) :: {:ok, term} | :error
@doc """
Dumps the given term into an Ecto native type.
This callback is called with any term that was stored in the struct
and it needs to validate them and convert it to an Ecto native type.
"""
@callback dump(term) :: {:ok, term} | :error
@doc """
Checks if two terms are semantically equal.
This callback is used for determining equality of types in
`Ecto.Changeset`.
"""
@callback equal?(term, term) :: boolean
@doc """
Dictates how the type should be treated inside embeds.
By default, the type is sent as itself, without calling
dumping to keep the higher level representation. But
it can be set to `:dump` so that it is dumped before
being encoded.
"""
@callback embed_as(format :: atom) :: :self | :dump
@doc """
Generates a loaded version of the data.
This is callback is invoked when a custom type is given
to `field` with the `:autogenerate` flag.
"""
@callback autogenerate() :: term()
@optional_callbacks autogenerate: 0
## Functions
@doc """
Checks if we have a primitive type.
iex> primitive?(:string)
true
iex> primitive?(Another)
false
iex> primitive?({:array, :string})
true
iex> primitive?({:array, Another})
true
"""
@spec primitive?(t) :: boolean
def primitive?({:parameterized, _, _}), do: true
def primitive?({composite, _}) when composite in @composite, do: true
def primitive?(base) when base in @base, do: true
def primitive?(_), do: false
@doc """
Checks if the given atom can be used as composite type.
iex> composite?(:array)
true
iex> composite?(:string)
false
"""
@spec composite?(atom) :: boolean
def composite?(atom), do: atom in @composite
@doc """
Checks if the given atom can be used as base type.
iex> base?(:string)
true
iex> base?(:array)
false
iex> base?(Custom)
false
"""
@spec base?(atom) :: boolean
def base?(atom), do: atom in @base
@doc """
Gets how the type is treated inside embeds for the given format.
See `c:embed_as/1`.
"""
def embed_as({:parameterized, module, params}, format), do: module.embed_as(format, params)
def embed_as({composite, type}, format) when composite in @composite, do: embed_as(type, format)
def embed_as(base, _format) when base in @base, do: :self
def embed_as(mod, format), do: mod.embed_as(format)
@doc """
Dumps the `value` for `type` considering it will be embedded in `format`.
## Examples
iex> Ecto.Type.embedded_dump(:decimal, Decimal.new("1"), :json)
{:ok, Decimal.new("1")}
"""
def embedded_dump(type, value, format) do
case embed_as(type, format) do
:self -> {:ok, value}
:dump -> dump(type, value, &embedded_dump(&1, &2, format))
end
end
@doc """
Loads the `value` for `type` considering it was embedded in `format`.
## Examples
iex> Ecto.Type.embedded_load(:decimal, "1", :json)
{:ok, Decimal.new("1")}
"""
def embedded_load(type, value, format) do
case embed_as(type, format) do
:self ->
case cast(type, value) do
{:ok, _} = ok -> ok
_ -> :error
end
:dump ->
load(type, value, &embedded_load(&1, &2, format))
end
end
@doc """
Retrieves the underlying schema type for the given, possibly custom, type.
iex> type(:string)
:string
iex> type(Ecto.UUID)
:uuid
iex> type({:array, :string})
{:array, :string}
iex> type({:array, Ecto.UUID})
{:array, :uuid}
iex> type({:map, Ecto.UUID})
{:map, :uuid}
"""
@spec type(t) :: t
def type(type)
def type({:parameterized, type, params}), do: type.type(params)
def type({:array, type}), do: {:array, type(type)}
def type({:map, type}), do: {:map, type(type)}
def type(type) when type in @base, do: type
def type(type) when is_atom(type), do: type.type()
def type(type), do: type
@doc """
Checks if a given type matches with a primitive type
that can be found in queries.
iex> match?(:string, :any)
true
iex> match?(:any, :string)
true
iex> match?(:string, :string)
true
iex> match?({:array, :string}, {:array, :any})
true
iex> match?(Ecto.UUID, :uuid)
true
iex> match?(Ecto.UUID, :string)
false
"""
@spec match?(t, primitive) :: boolean
def match?(schema_type, query_type) do
if primitive?(schema_type) do
do_match?(schema_type, query_type)
else
do_match?(schema_type.type, query_type)
end
end
defp do_match?(_left, :any), do: true
defp do_match?(:any, _right), do: true
defp do_match?({outer, left}, {outer, right}), do: match?(left, right)
defp do_match?(:decimal, type) when type in [:float, :integer], do: true
defp do_match?(:binary_id, :binary), do: true
defp do_match?(:id, :integer), do: true
defp do_match?(type, type), do: true
defp do_match?(:naive_datetime, {:param, :any_datetime}), do: true
defp do_match?(:naive_datetime_usec, {:param, :any_datetime}), do: true
defp do_match?(:utc_datetime, {:param, :any_datetime}), do: true
defp do_match?(:utc_datetime_usec, {:param, :any_datetime}), do: true
defp do_match?(_, _), do: false
@doc """
Dumps a value to the given type.
Opposite to casting, dumping requires the returned value
to be a valid Ecto type, as it will be sent to the
underlying data store.
iex> dump(:string, nil)
{:ok, nil}
iex> dump(:string, "foo")
{:ok, "foo"}
iex> dump(:integer, 1)
{:ok, 1}
iex> dump(:integer, "10")
:error
iex> dump(:binary, "foo")
{:ok, "foo"}
iex> dump(:binary, 1)
:error
iex> dump({:array, :integer}, [1, 2, 3])
{:ok, [1, 2, 3]}
iex> dump({:array, :integer}, [1, "2", 3])
:error
iex> dump({:array, :binary}, ["1", "2", "3"])
{:ok, ["1", "2", "3"]}
"""
@spec dump(t, term) :: {:ok, term} | :error
@spec dump(t, term, (t, term -> {:ok, term} | :error)) :: {:ok, term} | :error
def dump(type, value, dumper \\ &dump/2)
def dump({:parameterized, module, params}, value, dumper) do
module.dump(value, dumper, params)
end
def dump(_type, nil, _dumper) do
{:ok, nil}
end
def dump({:maybe, type}, value, dumper) do
case dump(type, value, dumper) do
{:ok, _} = ok -> ok
:error -> {:ok, value}
end
end
def dump({:in, type}, value, dumper) do
case dump({:array, type}, value, dumper) do
{:ok, value} -> {:ok, {:in, value}}
:error -> :error
end
end
def dump({:array, {_, _, _} = type}, value, dumper), do: array(value, type, dumper, false, [])
def dump({:array, type}, value, dumper), do: array(value, type, dumper, true, [])
def dump({:map, type}, value, dumper), do: map(value, type, dumper, false, %{})
def dump(:any, value, _dumper), do: {:ok, value}
def dump(:integer, value, _dumper), do: same_integer(value)
def dump(:float, value, _dumper), do: dump_float(value)
def dump(:boolean, value, _dumper), do: same_boolean(value)
def dump(:map, value, _dumper), do: same_map(value)
def dump(:string, value, _dumper), do: same_binary(value)
def dump(:binary, value, _dumper), do: same_binary(value)
def dump(:id, value, _dumper), do: same_integer(value)
def dump(:binary_id, value, _dumper), do: same_binary(value)
def dump(:decimal, value, _dumper), do: same_decimal(value)
def dump(:date, value, _dumper), do: same_date(value)
def dump(:time, value, _dumper), do: dump_time(value)
def dump(:time_usec, value, _dumper), do: dump_time_usec(value)
def dump(:naive_datetime, value, _dumper), do: dump_naive_datetime(value)
def dump(:naive_datetime_usec, value, _dumper), do: dump_naive_datetime_usec(value)
def dump(:utc_datetime, value, _dumper), do: dump_utc_datetime(value)
def dump(:utc_datetime_usec, value, _dumper), do: dump_utc_datetime_usec(value)
def dump({:param, :any_datetime}, value, _dumper), do: dump_any_datetime(value)
def dump(mod, value, _dumper) when is_atom(mod), do: mod.dump(value)
defp dump_float(term) when is_float(term), do: {:ok, term}
defp dump_float(_), do: :error
defp dump_time(%Time{} = term), do: {:ok, check_no_usec!(term, :time)}
defp dump_time(_), do: :error
defp dump_time_usec(%Time{} = term), do: {:ok, check_usec!(term, :time_usec)}
defp dump_time_usec(_), do: :error
defp dump_any_datetime(%NaiveDateTime{} = term), do: {:ok, term}
defp dump_any_datetime(%DateTime{} = term), do: {:ok, term}
defp dump_any_datetime(_), do: :error
defp dump_naive_datetime(%NaiveDateTime{} = term), do:
{:ok, check_no_usec!(term, :naive_datetime)}
defp dump_naive_datetime(_), do: :error
defp dump_naive_datetime_usec(%NaiveDateTime{} = term),
do: {:ok, check_usec!(term, :naive_datetime_usec)}
defp dump_naive_datetime_usec(_), do: :error
defp dump_utc_datetime(%DateTime{} = datetime) do
kind = :utc_datetime
{:ok, datetime |> check_utc_timezone!(kind) |> check_no_usec!(kind)}
end
defp dump_utc_datetime(_), do: :error
defp dump_utc_datetime_usec(%DateTime{} = datetime) do
kind = :utc_datetime_usec
{:ok, datetime |> check_utc_timezone!(kind) |> check_usec!(kind)}
end
defp dump_utc_datetime_usec(_), do: :error
@doc """
Loads a value with the given type.
iex> load(:string, nil)
{:ok, nil}
iex> load(:string, "foo")
{:ok, "foo"}
iex> load(:integer, 1)
{:ok, 1}
iex> load(:integer, "10")
:error
"""
@spec load(t, term) :: {:ok, term} | :error
@spec load(t, term, (t, term -> {:ok, term} | :error)) :: {:ok, term} | :error
def load(type, value, loader \\ &load/2)
def load({:parameterized, module, params}, value, loader) do
module.load(value, loader, params)
end
def load(_type, nil, _loader) do
{:ok, nil}
end
def load({:maybe, type}, value, loader) do
case load(type, value, loader) do
{:ok, _} = ok -> ok
:error -> {:ok, value}
end
end
def load({:array, {_, _, _} = type}, value, loader), do: array(value, type, loader, false, [])
def load({:array, type}, value, loader), do: array(value, type, loader, true, [])
def load({:map, type}, value, loader), do: map(value, type, loader, false, %{})
def load(:any, value, _loader), do: {:ok, value}
def load(:integer, value, _loader), do: same_integer(value)
def load(:float, value, _loader), do: load_float(value)
def load(:boolean, value, _loader), do: same_boolean(value)
def load(:map, value, _loader), do: same_map(value)
def load(:string, value, _loader), do: same_binary(value)
def load(:binary, value, _loader), do: same_binary(value)
def load(:id, value, _loader), do: same_integer(value)
def load(:binary_id, value, _loader), do: same_binary(value)
def load(:decimal, value, _loader), do: same_decimal(value)
def load(:date, value, _loader), do: same_date(value)
def load(:time, value, _loader), do: load_time(value)
def load(:time_usec, value, _loader), do: load_time_usec(value)
def load(:naive_datetime, value, _loader), do: load_naive_datetime(value)
def load(:naive_datetime_usec, value, _loader), do: load_naive_datetime_usec(value)
def load(:utc_datetime, value, _loader), do: load_utc_datetime(value)
def load(:utc_datetime_usec, value, _loader), do: load_utc_datetime_usec(value)
def load(mod, value, _loader), do: mod.load(value)
defp load_float(term) when is_float(term), do: {:ok, term}
defp load_float(term) when is_integer(term), do: {:ok, :erlang.float(term)}
defp load_float(_), do: :error
defp load_time(%Time{} = time), do: {:ok, truncate_usec(time)}
defp load_time(_), do: :error
defp load_time_usec(%Time{} = time), do: {:ok, pad_usec(time)}
defp load_time_usec(_), do: :error
# This is a downcast, which is always fine, and in case
# we try to send a naive datetime where a datetime is expected,
# the adapter will either explicitly error (Postgres) or it will
# accept the data (MySQL), which is fine as we always assume UTC
defp load_naive_datetime(%DateTime{} = datetime),
do: {:ok, datetime |> check_utc_timezone!(:naive_datetime) |> DateTime.to_naive() |> truncate_usec()}
defp load_naive_datetime(%NaiveDateTime{} = naive_datetime),
do: {:ok, truncate_usec(naive_datetime)}
defp load_naive_datetime(_), do: :error
defp load_naive_datetime_usec(%DateTime{} = datetime),
do: {:ok, datetime |> check_utc_timezone!(:naive_datetime_usec) |> DateTime.to_naive() |> pad_usec()}
defp load_naive_datetime_usec(%NaiveDateTime{} = naive_datetime),
do: {:ok, pad_usec(naive_datetime)}
defp load_naive_datetime_usec(_), do: :error
# This is an upcast but because we assume the database
# is always in UTC, we can perform it.
defp load_utc_datetime(%NaiveDateTime{} = naive_datetime),
do: {:ok, naive_datetime |> truncate_usec() |> DateTime.from_naive!("Etc/UTC")}
defp load_utc_datetime(%DateTime{} = datetime),
do: {:ok, datetime |> check_utc_timezone!(:utc_datetime) |> truncate_usec()}
defp load_utc_datetime(_),
do: :error
defp load_utc_datetime_usec(%NaiveDateTime{} = naive_datetime),
do: {:ok, naive_datetime |> pad_usec() |> DateTime.from_naive!("Etc/UTC")}
defp load_utc_datetime_usec(%DateTime{} = datetime),
do: {:ok, datetime |> check_utc_timezone!(:utc_datetime_usec) |> pad_usec()}
defp load_utc_datetime_usec(_),
do: :error
@doc """
Casts a value to the given type.
`cast/2` is used by the finder queries and changesets to cast outside values to
specific types.
Note that nil can be cast to all primitive types as data stores allow nil to be
set on any column.
NaN and infinite decimals are not supported, use custom types instead.
iex> cast(:any, "whatever")
{:ok, "whatever"}
iex> cast(:any, nil)
{:ok, nil}
iex> cast(:string, nil)
{:ok, nil}
iex> cast(:integer, 1)
{:ok, 1}
iex> cast(:integer, "1")
{:ok, 1}
iex> cast(:integer, "1.0")
:error
iex> cast(:id, 1)
{:ok, 1}
iex> cast(:id, "1")
{:ok, 1}
iex> cast(:id, "1.0")
:error
iex> cast(:float, 1.0)
{:ok, 1.0}
iex> cast(:float, 1)
{:ok, 1.0}
iex> cast(:float, "1")
{:ok, 1.0}
iex> cast(:float, "1.0")
{:ok, 1.0}
iex> cast(:float, "1-foo")
:error
iex> cast(:boolean, true)
{:ok, true}
iex> cast(:boolean, false)
{:ok, false}
iex> cast(:boolean, "1")
{:ok, true}
iex> cast(:boolean, "0")
{:ok, false}
iex> cast(:boolean, "whatever")
:error
iex> cast(:string, "beef")
{:ok, "beef"}
iex> cast(:binary, "beef")
{:ok, "beef"}
iex> cast(:decimal, Decimal.new("1.0"))
{:ok, Decimal.new("1.0")}
iex> cast(:decimal, "1.0bad")
:error
iex> cast({:array, :integer}, [1, 2, 3])
{:ok, [1, 2, 3]}
iex> cast({:array, :integer}, ["1", "2", "3"])
{:ok, [1, 2, 3]}
iex> cast({:array, :string}, [1, 2, 3])
:error
iex> cast(:string, [1, 2, 3])
:error
"""
@spec cast(t, term) :: {:ok, term} | {:error, keyword()} | :error
def cast({:parameterized, type, params}, value), do: type.cast(value, params)
def cast({:in, _type}, nil), do: :error
def cast(_type, nil), do: {:ok, nil}
def cast({:maybe, type}, value) do
case cast(type, value) do
{:ok, _} = ok -> ok
_ -> {:ok, value}
end
end
def cast(type, value) do
cast_fun(type).(value)
end
defp cast_fun(:integer), do: &cast_integer/1
defp cast_fun(:float), do: &cast_float/1
defp cast_fun(:boolean), do: &cast_boolean/1
defp cast_fun(:map), do: &cast_map/1
defp cast_fun(:string), do: &cast_binary/1
defp cast_fun(:binary), do: &cast_binary/1
defp cast_fun(:id), do: &cast_integer/1
defp cast_fun(:binary_id), do: &cast_binary/1
defp cast_fun(:any), do: &{:ok, &1}
defp cast_fun(:decimal), do: &cast_decimal/1
defp cast_fun(:date), do: &cast_date/1
defp cast_fun(:time), do: &maybe_truncate_usec(cast_time(&1))
defp cast_fun(:time_usec), do: &maybe_pad_usec(cast_time(&1))
defp cast_fun(:naive_datetime), do: &maybe_truncate_usec(cast_naive_datetime(&1))
defp cast_fun(:naive_datetime_usec), do: &maybe_pad_usec(cast_naive_datetime(&1))
defp cast_fun(:utc_datetime), do: &maybe_truncate_usec(cast_utc_datetime(&1))
defp cast_fun(:utc_datetime_usec), do: &maybe_pad_usec(cast_utc_datetime(&1))
defp cast_fun({:param, :any_datetime}), do: &cast_any_datetime(&1)
defp cast_fun({:parameterized, mod, params}), do: &mod.cast(&1, params)
defp cast_fun({:in, type}), do: cast_fun({:array, type})
defp cast_fun({:array, {:parameterized, _, _} = type}) do
fun = cast_fun(type)
&array(&1, fun, false, [])
end
defp cast_fun({:array, type}) do
fun = cast_fun(type)
&array(&1, fun, true, [])
end
defp cast_fun({:map, {:parameterized, _, _} = type}) do
fun = cast_fun(type)
&map(&1, fun, false, %{})
end
defp cast_fun({:map, type}) do
fun = cast_fun(type)
&map(&1, fun, true, %{})
end
defp cast_fun(mod) when is_atom(mod) do
fn
nil -> {:ok, nil}
value -> mod.cast(value)
end
end
defp cast_integer(term) when is_binary(term) do
case Integer.parse(term) do
{integer, ""} -> {:ok, integer}
_ -> :error
end
end
defp cast_integer(term) when is_integer(term), do: {:ok, term}
defp cast_integer(_), do: :error
defp cast_float(term) when is_binary(term) do
case Float.parse(term) do
{float, ""} -> {:ok, float}
_ -> :error
end
end
defp cast_float(term) when is_float(term), do: {:ok, term}
defp cast_float(term) when is_integer(term), do: {:ok, :erlang.float(term)}
defp cast_float(_), do: :error
defp cast_decimal(term) when is_binary(term) do
case Decimal.parse(term) do
{:ok, decimal} -> check_decimal(decimal, false)
# The following two clauses exist to support earlier versions of Decimal.
{decimal, ""} -> check_decimal(decimal, false)
{_, remainder} when is_binary(remainder) and byte_size(remainder) > 0 -> :error
:error -> :error
end
end
defp cast_decimal(term), do: same_decimal(term)
defp cast_boolean(term) when term in ~w(true 1), do: {:ok, true}
defp cast_boolean(term) when term in ~w(false 0), do: {:ok, false}
defp cast_boolean(term) when is_boolean(term), do: {:ok, term}
defp cast_boolean(_), do: :error
defp cast_binary(term) when is_binary(term), do: {:ok, term}
defp cast_binary(_), do: :error
defp cast_map(term) when is_map(term), do: {:ok, term}
defp cast_map(_), do: :error
## Shared helpers
@compile {:inline, same_integer: 1, same_boolean: 1, same_map: 1, same_decimal: 1, same_date: 1}
defp same_integer(term) when is_integer(term), do: {:ok, term}
defp same_integer(_), do: :error
defp same_boolean(term) when is_boolean(term), do: {:ok, term}
defp same_boolean(_), do: :error
defp same_binary(term) when is_binary(term), do: {:ok, term}
defp same_binary(_), do: :error
defp same_map(term) when is_map(term), do: {:ok, term}
defp same_map(_), do: :error
defp same_decimal(term) when is_integer(term), do: {:ok, Decimal.new(term)}
defp same_decimal(term) when is_float(term), do: {:ok, Decimal.from_float(term)}
defp same_decimal(%Decimal{} = term), do: check_decimal(term, true)
defp same_decimal(_), do: :error
defp same_date(%Date{} = term), do: {:ok, term}
defp same_date(_), do: :error
@doc false
def empty_trimmed_string?(value) do
is_binary(value) and String.trim_leading(value) == ""
end
## Adapter related
@doc false
def adapter_autogenerate(adapter, type) do
type
|> type()
|> adapter.autogenerate()
end
@doc false
def adapter_load(adapter, {:parameterized, module, params} = type, value) do
process_loaders(adapter.loaders(module.type(params), type), {:ok, value}, adapter)
end
def adapter_load(_adapter, _type, nil) do
{:ok, nil}
end
def adapter_load(adapter, type, value) do
if of_base_type?(type, value) do
{:ok, value}
else
process_loaders(adapter.loaders(type(type), type), {:ok, value}, adapter)
end
end
defp process_loaders(_, :error, _adapter),
do: :error
defp process_loaders([fun|t], {:ok, value}, adapter) when is_function(fun),
do: process_loaders(t, fun.(value), adapter)
defp process_loaders([type|t], {:ok, value}, adapter),
do: process_loaders(t, load(type, value, &adapter_load(adapter, &1, &2)), adapter)
defp process_loaders([], {:ok, _} = acc, _adapter),
do: acc
@doc false
def adapter_dump(adapter, {:parameterized, module, params} = type, value) do
process_dumpers(adapter.dumpers(module.type(params), type), {:ok, value}, adapter)
end
def adapter_dump(_adapter, type, nil) do
dump(type, nil)
end
def adapter_dump(adapter, type, value) do
process_dumpers(adapter.dumpers(type(type), type), {:ok, value}, adapter)
end
defp process_dumpers(_, :error, _adapter),
do: :error
defp process_dumpers([fun|t], {:ok, value}, adapter) when is_function(fun),
do: process_dumpers(t, fun.(value), adapter)
defp process_dumpers([type|t], {:ok, value}, adapter),
do: process_dumpers(t, dump(type, value, &adapter_dump(adapter, &1, &2)), adapter)
defp process_dumpers([], {:ok, _} = acc, _adapter),
do: acc
## Date
defp cast_date(binary) when is_binary(binary) do
case Date.from_iso8601(binary) do
{:ok, _} = ok ->
ok
{:error, _} ->
case NaiveDateTime.from_iso8601(binary) do
{:ok, naive_datetime} -> {:ok, NaiveDateTime.to_date(naive_datetime)}
{:error, _} -> :error
end
end
end
defp cast_date(%{"year" => empty, "month" => empty, "day" => empty}) when empty in ["", nil],
do: {:ok, nil}
defp cast_date(%{year: empty, month: empty, day: empty}) when empty in ["", nil],
do: {:ok, nil}
defp cast_date(%{"year" => year, "month" => month, "day" => day}),
do: cast_date(to_i(year), to_i(month), to_i(day))
defp cast_date(%{year: year, month: month, day: day}),
do: cast_date(to_i(year), to_i(month), to_i(day))
defp cast_date(_),
do: :error
defp cast_date(year, month, day) when is_integer(year) and is_integer(month) and is_integer(day) do
case Date.new(year, month, day) do
{:ok, _} = ok -> ok
{:error, _} -> :error
end
end
defp cast_date(_, _, _),
do: :error
## Time
defp cast_time(<<hour::2-bytes, ?:, minute::2-bytes>>),
do: cast_time(to_i(hour), to_i(minute), 0, nil)
defp cast_time(binary) when is_binary(binary) do
case Time.from_iso8601(binary) do
{:ok, _} = ok -> ok
{:error, _} -> :error
end
end
defp cast_time(%{"hour" => empty, "minute" => empty}) when empty in ["", nil],
do: {:ok, nil}
defp cast_time(%{hour: empty, minute: empty}) when empty in ["", nil],
do: {:ok, nil}
defp cast_time(%{"hour" => hour, "minute" => minute} = map),
do: cast_time(to_i(hour), to_i(minute), to_i(Map.get(map, "second")), to_i(Map.get(map, "microsecond")))
defp cast_time(%{hour: hour, minute: minute, second: second, microsecond: {microsecond, precision}}),
do: cast_time(to_i(hour), to_i(minute), to_i(second), {to_i(microsecond), to_i(precision)})
defp cast_time(%{hour: hour, minute: minute} = map),
do: cast_time(to_i(hour), to_i(minute), to_i(Map.get(map, :second)), to_i(Map.get(map, :microsecond)))
defp cast_time(_),
do: :error
defp cast_time(hour, minute, sec, usec) when is_integer(usec) do
cast_time(hour, minute, sec, {usec, 6})
end
defp cast_time(hour, minute, sec, nil) do
cast_time(hour, minute, sec, {0, 0})
end
defp cast_time(hour, minute, sec, {usec, precision})
when is_integer(hour) and is_integer(minute) and
(is_integer(sec) or is_nil(sec)) and is_integer(usec) and is_integer(precision) do
case Time.new(hour, minute, sec || 0, {usec, precision}) do
{:ok, _} = ok -> ok
{:error, _} -> :error
end
end
defp cast_time(_, _, _, _) do
:error
end
defp cast_any_datetime(%DateTime{} = datetime), do: cast_utc_datetime(datetime)
defp cast_any_datetime(other), do: cast_naive_datetime(other)
## Naive datetime
defp cast_naive_datetime("-" <> rest) do
with {:ok, naive_datetime} <- cast_naive_datetime(rest) do
{:ok, %{naive_datetime | year: naive_datetime.year * -1}}
end
end
defp cast_naive_datetime(<<year::4-bytes, ?-, month::2-bytes, ?-, day::2-bytes, sep, hour::2-bytes, ?:, minute::2-bytes>>)
when sep in [?\s, ?T] do
case NaiveDateTime.new(to_i(year), to_i(month), to_i(day), to_i(hour), to_i(minute), 0) do
{:ok, _} = ok -> ok
_ -> :error
end
end
defp cast_naive_datetime(binary) when is_binary(binary) do
case NaiveDateTime.from_iso8601(binary) do
{:ok, _} = ok -> ok
{:error, _} -> :error
end
end
defp cast_naive_datetime(%{"year" => empty, "month" => empty, "day" => empty,
"hour" => empty, "minute" => empty}) when empty in ["", nil],
do: {:ok, nil}
defp cast_naive_datetime(%{year: empty, month: empty, day: empty,
hour: empty, minute: empty}) when empty in ["", nil],
do: {:ok, nil}
defp cast_naive_datetime(%{} = map) do
with {:ok, %Date{} = date} <- cast_date(map),
{:ok, %Time{} = time} <- cast_time(map) do
NaiveDateTime.new(date, time)
else
_ -> :error
end
end
defp cast_naive_datetime(_) do
:error
end
## UTC datetime
defp cast_utc_datetime("-" <> rest) do
with {:ok, utc_datetime} <- cast_utc_datetime(rest) do
{:ok, %{utc_datetime | year: utc_datetime.year * -1}}
end
end
defp cast_utc_datetime(<<year::4-bytes, ?-, month::2-bytes, ?-, day::2-bytes, sep, hour::2-bytes, ?:, minute::2-bytes>>)
when sep in [?\s, ?T] do
case NaiveDateTime.new(to_i(year), to_i(month), to_i(day), to_i(hour), to_i(minute), 0) do
{:ok, naive_datetime} -> {:ok, DateTime.from_naive!(naive_datetime, "Etc/UTC")}
_ -> :error
end
end
defp cast_utc_datetime(binary) when is_binary(binary) do
case DateTime.from_iso8601(binary) do
{:ok, datetime, _offset} -> {:ok, datetime}
{:error, :missing_offset} ->
case NaiveDateTime.from_iso8601(binary) do
{:ok, naive_datetime} -> {:ok, DateTime.from_naive!(naive_datetime, "Etc/UTC")}
{:error, _} -> :error
end
{:error, _} -> :error
end
end
defp cast_utc_datetime(%DateTime{time_zone: "Etc/UTC"} = datetime), do: {:ok, datetime}
defp cast_utc_datetime(%DateTime{} = datetime) do
case (datetime |> DateTime.to_unix(:microsecond) |> DateTime.from_unix(:microsecond)) do
{:ok, _} = ok -> ok
{:error, _} -> :error
end
end
defp cast_utc_datetime(value) do
case cast_naive_datetime(value) do
{:ok, %NaiveDateTime{} = naive_datetime} ->
{:ok, DateTime.from_naive!(naive_datetime, "Etc/UTC")}
{:ok, _} = ok ->
ok
:error ->
:error
end
end
@doc """
Checks if two terms are equal.
Depending on the given `type` performs a structural or semantical comparison.
## Examples
iex> equal?(:integer, 1, 1)
true
iex> equal?(:decimal, Decimal.new("1"), Decimal.new("1.00"))
true
"""
@spec equal?(t, term, term) :: boolean
def equal?(_, nil, nil), do: true
def equal?(type, term1, term2) do
if fun = equal_fun(type) do
fun.(term1, term2)
else
term1 == term2
end
end
@doc """
Checks if `collection` includes a `term`.
Depending on the given `type` performs a structural or semantical comparison.
## Examples
iex> include?(:integer, 1, 1..3)
true
iex> include?(:decimal, Decimal.new("1"), [Decimal.new("1.00"), Decimal.new("2.00")])
true
"""
@spec include?(t, term, Enum.t()) :: boolean
def include?(type, term, collection) do
if fun = equal_fun(type) do
Enum.any?(collection, &fun.(term, &1))
else
term in collection
end
end
defp equal_fun(:decimal), do: &equal_decimal?/2
defp equal_fun(t) when t in [:time, :time_usec], do: &equal_time?/2
defp equal_fun(t) when t in [:utc_datetime, :utc_datetime_usec], do: &equal_utc_datetime?/2
defp equal_fun(t) when t in [:naive_datetime, :naive_datetime_usec], do: &equal_naive_datetime?/2
defp equal_fun(t) when t in @base, do: nil
defp equal_fun({:array, type}) do
if fun = equal_fun(type) do
&equal_list?(fun, &1, &2)
end
end
defp equal_fun({:map, type}) do
if fun = equal_fun(type) do
&equal_map?(fun, &1, &2)
end
end
defp equal_fun({:parameterized, mod, params}) do
&mod.equal?(&1, &2, params)
end
defp equal_fun(mod) when is_atom(mod), do: &mod.equal?/2
defp equal_decimal?(%Decimal{} = a, %Decimal{} = b), do: Decimal.equal?(a, b)
defp equal_decimal?(_, _), do: false
defp equal_time?(%Time{} = a, %Time{} = b), do: Time.compare(a, b) == :eq
defp equal_time?(_, _), do: false
defp equal_utc_datetime?(%DateTime{} = a, %DateTime{} = b), do: DateTime.compare(a, b) == :eq
defp equal_utc_datetime?(_, _), do: false
defp equal_naive_datetime?(%NaiveDateTime{} = a, %NaiveDateTime{} = b),
do: NaiveDateTime.compare(a, b) == :eq
defp equal_naive_datetime?(_, _),
do: false
defp equal_list?(fun, [nil | xs], [nil | ys]), do: equal_list?(fun, xs, ys)
defp equal_list?(fun, [x | xs], [y | ys]), do: fun.(x, y) and equal_list?(fun, xs, ys)
defp equal_list?(_fun, [], []), do: true
defp equal_list?(_fun, _, _), do: false
defp equal_map?(_fun, map1, map2) when map_size(map1) != map_size(map2) do
false
end
defp equal_map?(fun, %{} = map1, %{} = map2) do
equal_map?(fun, Map.to_list(map1), map2)
end
defp equal_map?(fun, [{key, nil} | tail], other_map) do
case other_map do
%{^key => nil} -> equal_map?(fun, tail, other_map)
_ -> false
end
end
defp equal_map?(fun, [{key, val} | tail], other_map) do
case other_map do
%{^key => other_val} -> fun.(val, other_val) and equal_map?(fun, tail, other_map)
_ -> false
end
end
defp equal_map?(_fun, [], _) do
true
end
defp equal_map?(_fun, _, _) do
false
end
@doc """
Format type for error messaging and logs.
"""
def format({composite, type}) when composite in [:array, :map] do
"{#{inspect(composite)}, #{format(type)}}"
end
def format({:parameterized, type, params}) do
if function_exported?(type, :format, 1) do
apply(type, :format, [params])
else
"##{inspect(type)}<#{inspect(params)}>"
end
end
def format(type), do: inspect(type)
## Helpers
# Checks if a value is of the given primitive type.
defp of_base_type?(:any, _), do: true
defp of_base_type?(:id, term), do: is_integer(term)
defp of_base_type?(:float, term), do: is_float(term)
defp of_base_type?(:integer, term), do: is_integer(term)
defp of_base_type?(:boolean, term), do: is_boolean(term)
defp of_base_type?(:binary, term), do: is_binary(term)
defp of_base_type?(:string, term), do: is_binary(term)
defp of_base_type?(:map, term), do: is_map(term) and not Map.has_key?(term, :__struct__)
defp of_base_type?(:decimal, value), do: Kernel.match?(%Decimal{}, value)
defp of_base_type?(:date, value), do: Kernel.match?(%Date{}, value)
defp of_base_type?(_, _), do: false
defp array([nil | t], fun, true, acc) do
array(t, fun, true, [nil | acc])
end
defp array([h | t], fun, skip_nil?, acc) do
case fun.(h) do
{:ok, h} -> array(t, fun, skip_nil?, [h | acc])
:error -> :error
{:error, _custom_errors} -> :error
end
end
defp array([], _fun, _skip_nil?,acc) do
{:ok, Enum.reverse(acc)}
end
defp array(_, _, _, _) do
:error
end
defp map(map, fun, skip_nil?, acc) when is_map(map) do
map_each(Map.to_list(map), fun, skip_nil?, acc)
end
defp map(_, _, _, _) do
:error
end
defp map_each([{key, nil} | t], fun, true, acc) do
map_each(t, fun, true, Map.put(acc, key, nil))
end
defp map_each([{key, value} | t], fun, skip_nil?, acc) do
case fun.(value) do
{:ok, value} -> map_each(t, fun, skip_nil?, Map.put(acc, key, value))
:error -> :error
{:error, _custom_errors} -> :error
end
end
defp map_each([], _fun, _skip_nil?, acc) do
{:ok, acc}
end
defp array([nil | t], type, fun, true, acc) do
array(t, type, fun, true, [nil | acc])
end
defp array([h | t], type, fun, skip_nil?, acc) do
case fun.(type, h) do
{:ok, h} -> array(t, type, fun, skip_nil?, [h | acc])
:error -> :error
end
end
defp array([], _type, _fun, _skip_nil?, acc) do
{:ok, Enum.reverse(acc)}
end
defp array(_, _, _, _, _) do
:error
end
defp map(map, type, fun, skip_nil?, acc) when is_map(map) do
map_each(Map.to_list(map), type, fun, skip_nil?, acc)
end
defp map(_, _, _, _, _) do
:error
end
defp map_each([{key, value} | t], type, fun, skip_nil?, acc) do
case fun.(type, value) do
{:ok, value} -> map_each(t, type, fun, skip_nil?, Map.put(acc, key, value))
:error -> :error
end
end
defp map_each([], _type, _fun, _skip_nil?, acc) do
{:ok, acc}
end
defp to_i(nil), do: nil
defp to_i(int) when is_integer(int), do: int
defp to_i(bin) when is_binary(bin) do
case Integer.parse(bin) do
{int, ""} -> int
_ -> nil
end
end
defp maybe_truncate_usec({:ok, struct}), do: {:ok, truncate_usec(struct)}
defp maybe_truncate_usec(:error), do: :error
defp maybe_pad_usec({:ok, struct}), do: {:ok, pad_usec(struct)}
defp maybe_pad_usec(:error), do: :error
defp truncate_usec(nil), do: nil
defp truncate_usec(%{microsecond: {0, 0}} = struct), do: struct
defp truncate_usec(struct), do: %{struct | microsecond: {0, 0}}
defp pad_usec(nil), do: nil
defp pad_usec(%{microsecond: {_, 6}} = struct), do: struct
defp pad_usec(%{microsecond: {microsecond, _}} = struct),
do: %{struct | microsecond: {microsecond, 6}}
defp check_utc_timezone!(%{time_zone: "Etc/UTC"} = datetime, _kind), do: datetime
defp check_utc_timezone!(datetime, kind) do
raise ArgumentError,
"#{inspect kind} expects the time zone to be \"Etc/UTC\", got `#{inspect(datetime)}`"
end
defp check_usec!(%{microsecond: {_, 6}} = datetime, _kind), do: datetime
defp check_usec!(datetime, kind) do
raise ArgumentError,
"#{inspect(kind)} expects microsecond precision, got: #{inspect(datetime)}"
end
defp check_no_usec!(%{microsecond: {0, 0}} = datetime, _kind), do: datetime
defp check_no_usec!(%struct{} = datetime, kind) do
raise ArgumentError, """
#{inspect(kind)} expects microseconds to be empty, got: #{inspect(datetime)}
Use `#{inspect(struct)}.truncate(#{kind}, :second)` (available in Elixir v1.6+) to remove microseconds.
"""
end
defp check_decimal(%Decimal{coef: coef} = decimal, _) when is_integer(coef), do: {:ok, decimal}
defp check_decimal(_decimal, false), do: :error
defp check_decimal(decimal, true) do
raise ArgumentError, """
#{inspect(decimal)} is not allowed for type :decimal
`+Infinity`, `-Infinity`, and `NaN` values are not supported, even though the `Decimal` library handles them. \
To support them, you can create a custom type.
"""
end
end