defmodule Tarams.Type do
@moduledoc """
Cast data to target type.
Much code of this module is borrowed from `Ecto.Type`
"""
def cast({:embed, mod, params}, value), do: mod.cast(value, params)
def cast(type, value) do
cast_fun(type).(value)
end
def cast!(type, value) do
with {:ok, data} <- cast(type, value) do
data
else
_ ->
raise "invalid #{inspect(type)}"
end
end
defp cast_fun(:boolean), do: &cast_boolean/1
defp cast_fun(:integer), do: &cast_integer/1
defp cast_fun(:float), do: &cast_float/1
defp cast_fun(:string), do: &cast_binary/1
defp cast_fun(:binary), do: &cast_binary/1
defp cast_fun(:any), do: &{:ok, &1}
defp cast_fun(:date), do: &cast_date/1
defp cast_fun(:time), do: &maybe_truncate_usec(cast_time(&1))
defp cast_fun(:datetime), do: &maybe_truncate_usec(cast_utc_datetime(&1))
defp cast_fun(:naive_datetime), do: &maybe_truncate_usec(cast_naive_datetime(&1))
defp cast_fun(:utc_datetime), do: &maybe_truncate_usec(cast_utc_datetime(&1))
defp cast_fun(:map), do: &cast_map/1
defp cast_fun(:decimal), do: &cast_decimal/1
defp cast_fun(mod) when is_atom(mod), do: &maybe_cast_custom_type(mod, &1)
defp cast_fun({:array, {:embed, _, _} = type}) do
fun = fn value -> cast(type, value) end
&array(&1, fun, true, [])
end
defp cast_fun({:array, type}) do
fun = cast_fun(type)
&array(&1, fun, true, [])
end
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 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
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_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_binary(term) when is_binary(term), do: {:ok, term}
defp cast_binary(_), do: :error
## 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(%Date{} = date), do: {:ok, date}
defp cast_date(%DateTime{} = date), do: {:ok, DateTime.to_date(date)}
defp cast_date(%NaiveDateTime{} = date), do: {:ok, NaiveDateTime.to_date(date)}
defp cast_date(%{year: year, month: month, day: day})
when is_integer(year) and is_integer(month) and is_integer(day),
do: Date.new(year, month, day)
defp cast_date(_), do: :error
## Time
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(%Time{} = time), do: {:ok, time}
defp cast_time(%DateTime{} = date), do: {:ok, DateTime.to_time(date)}
defp cast_time(%NaiveDateTime{} = date), do: {:ok, NaiveDateTime.to_time(date)}
defp cast_time(%{hour: hour, minute: minute, second: second})
when is_integer(hour) and is_integer(minute) and is_integer(second),
do: Time.new(hour, minute, second)
defp cast_time(_), do: :error
## Naive datetime
defp cast_naive_datetime("-" <> rest) do
with {:ok, datetime} <- cast_naive_datetime(rest) do
{:ok, %{datetime | year: datetime.year * -1}}
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,
second: 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(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
defp cast_map(term) when is_map(term), do: {:ok, term}
defp cast_map(_), do: :error
defp maybe_cast_custom_type(_mod, %_schema{} = value) do
{:ok, value}
end
defp maybe_cast_custom_type(mod, value) do
mod.cast(value)
end
defp array(term, _, _, _) when not is_list(term) do
:error
end
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
end
end
defp array([], _fun, _skip_nil?, acc) do
{:ok, Enum.reverse(acc)}
end
defp maybe_truncate_usec({:ok, struct}), do: {:ok, truncate_usec(struct)}
defp maybe_truncate_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}}
end