defmodule Flagsmith.Schemas.Traits.Trait.Value do
@behaviour Ecto.Type
@moduledoc """
Ecto type to aid casting values returned by the Flagsmith API that need to be
compared. Since those values are weakly typed in different places and they need
to be compared, it's necessary to be able to convert and assign a type to a value
in order to when comparing that value with another one, the other one can be cast
to the same type.
This ecto type works as a struct containing the converted value in its correctly
typed format and a field `:type` containing the corresponding type, so that it's
easier to then cast any other stringed type into the same format.
"""
@impl Ecto.Type
def type(), do: :map
@type t() :: %__MODULE__{
:value => String.t() | Decimal.t() | Version.t() | number() | boolean(),
:type => :string | :decimal | :boolean | :semver
}
@derive {Jason.Encoder, only: [:value, :type]}
@enforce_keys [:value, :type]
defstruct [:value, :type]
@impl Ecto.Type
def load(data), do: cast(data)
@impl Ecto.Type
def cast(%__MODULE__{value: _, type: _} = data), do: {:ok, data}
def cast(%{"value" => value, "type" => "decimal"}),
do: {:ok, %__MODULE__{value: Decimal.new(value), type: :decimal}}
def cast(%{"value" => value, "type" => type}),
do: {:ok, %__MODULE__{value: value, type: String.to_existing_atom(type)}}
def cast(data) when is_number(data),
do: {:ok, %__MODULE__{value: convert_number(data), type: :decimal}}
def cast(%Decimal{} = data),
do: {:ok, %__MODULE__{value: data, type: :decimal}}
def cast(data) when data in ["false", "true"],
do: {:ok, %__MODULE__{value: String.to_existing_atom(data), type: :boolean}}
def cast(data) when data in ["False", "True"],
do:
{:ok,
%__MODULE__{value: String.downcase(data) |> String.to_existing_atom(), type: :boolean}}
def cast(data) when data in [false, true],
do: {:ok, %__MODULE__{value: data, type: :boolean}}
def cast(data) when is_binary(data) do
case String.ends_with?(data, ":semver") do
true ->
create_semver(data)
false ->
{:ok, %__MODULE__{value: data, type: :string}}
end
end
def cast(_), do: :error
@impl Ecto.Type
def dump(%__MODULE__{value: _, type: _} = data), do: {:ok, data}
def dump(%{"value" => _, "type" => _type} = data), do: {:ok, data}
def dump(data) when is_number(data),
do: {:ok, %__MODULE__{value: data, type: :decimal}}
def dump(%Decimal{} = data),
do: {:ok, %__MODULE__{value: Decimal.to_string(data), type: :decimal}}
def dump(data) when data in ["false", "true"],
do: {:ok, %__MODULE__{value: String.to_existing_atom(data), type: :boolean}}
def dump(data) when data in ["False", "True"],
do:
{:ok,
%__MODULE__{value: String.downcase(data) |> String.to_existing_atom(), type: :boolean}}
def dump(data) when data in [false, true],
do: {:ok, %__MODULE__{value: data, type: :boolean}}
def dump(data) when is_binary(data),
do: {:ok, %__MODULE__{value: data, type: :string}}
def dump(_), do: :error
@impl Ecto.Type
def embed_as(_), do: :dump
@impl Ecto.Type
def equal?(term_1, term_1), do: true
def equal?(term_1, term_2), do: get_term(term_1) == get_term(term_2)
def convert_value_to(%__MODULE__{type: :boolean}, to_convert)
when to_convert in ["false", "true", "False", "True"],
do: cast(to_convert)
def convert_value_to(%__MODULE__{type: :semver}, to_convert),
do: Version.parse(to_convert)
def convert_value_to(%__MODULE__{type: type}, to_convert),
do: Ecto.Type.cast(type, to_convert)
defp get_term(data) do
case cast(data) do
{:ok, value} -> value
:error -> {:error, data}
end
end
defp convert_number(data) when is_float(data),
do: Decimal.from_float(data)
defp convert_number(data) when is_integer(data) or is_binary(data),
do: Decimal.new(data)
def is_semver(data) when is_binary(data),
do: String.ends_with?(data, ":semver")
def is_semver(_), do: false
def create_semver(%Version{} = version), do: {:ok, %__MODULE__{value: version, type: :semver}}
def create_semver(data) when is_binary(data) do
without_semver = String.replace_suffix(data, ":semver", "")
semver = Version.parse!(without_semver)
{:ok, %__MODULE__{value: semver, type: :semver}}
end
end