defmodule Ash.Type.Float do @constraints [ max: [ type: {:custom, __MODULE__, :float, []}, doc: "Enforces a maximum on the value" ], min: [ type: {:custom, __MODULE__, :float, []}, doc: "Enforces a minimum on the value" ], greater_than: [ type: {:custom, __MODULE__, :float, []}, doc: "Enforces a minimum on the value (exclusive)" ], less_than: [ type: {:custom, __MODULE__, :float, []}, doc: "Enforces a maximum on the value (exclusive)" ] ] import Ash.Expr @moduledoc """ Represents a float (floating point number) A builtin type that be referenced via `:float` ### Constraints #{Spark.Options.docs(@constraints)} """ use Ash.Type @impl true def storage_type(_), do: :float @impl true def constraints, do: @constraints @doc false def float(value) do case cast_input(value, []) do {:ok, float} -> {:ok, float} :error -> {:error, "cannot be casted to float"} end end @impl true def matches_type?(v, _) do is_float(v) end @impl true def generator(constraints) do constraints |> Keyword.take([:min, :max]) |> StreamData.float() |> StreamData.filter(fn value -> (!constraints[:less_than] || value < constraints[:less_than]) && (!constraints[:greater_than] || value > constraints[:greater_than]) end) end @impl true def apply_constraints(nil, _), do: {:ok, nil} def apply_constraints(value, constraints) do errors = Enum.reduce(constraints, [], fn {:max, max}, errors -> if value > max do [[message: "must be less than or equal to %{max}", max: max] | errors] else errors end {:min, min}, errors -> if value < min do [[message: "must be more than or equal to %{min}", min: min] | errors] else errors end {:less_than, less_than}, errors -> if value < less_than do errors else [[message: "must be less than %{less_than}", less_than: less_than] | errors] end {:greater_than, greater_than}, errors -> if value > greater_than do errors else [[message: "must be more than %{greater_than}", greater_than: greater_than] | errors] end end) case errors do [] -> {:ok, value} errors -> {:error, errors} end end @impl true def cast_input(value, _) do Ecto.Type.cast(:float, value) end @impl true def cast_stored(nil, _), do: {:ok, nil} def cast_stored(value, _) do Ecto.Type.load(:float, value) end def cast_atomic(expr, _constraints) do {:atomic, expr} end def apply_atomic_constraints(expr, constraints) do expr = Enum.reduce(constraints, expr, fn {:max, max}, expr -> expr( if ^expr > ^max do error( Ash.Error.Changes.InvalidChanges, message: "must be less than or equal to %{max}", vars: %{max: ^max} ) else ^expr end ) {:min, min}, expr -> expr( if ^expr < ^min do error( Ash.Error.Changes.InvalidChanges, message: "must be greater than or equal to %{min}", vars: %{min: ^min} ) else ^expr end ) {:less_than, less_than}, expr -> expr( if ^expr < ^less_than do ^expr else error( Ash.Error.Changes.InvalidChanges, message: "must be greater than %{less_than}", vars: %{less_than: ^less_than} ) end ) {:greater_than, greater_than}, expr -> expr( if ^expr > ^greater_than do ^expr else error( Ash.Error.Changes.InvalidChanges, message: "must be greater than %{greater_than}", vars: %{greater_than: ^greater_than} ) end ) end) {:ok, expr} end @impl true def dump_to_native(nil, _), do: {:ok, nil} def dump_to_native(value, _) do Ecto.Type.dump(:float, value) end end