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. ## 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 Base.decode64(id) 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. """ @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 ## 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(<>), 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(<>) 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(<>) 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 ## 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