View Source Ash.Type behaviour (ash v2.17.0)
Describes how to convert data to Ecto.Type
and eventually into the database.
This behaviour is a superset of the Ecto.Type
behaviour, that also contains
API level information, like what kinds of filters are allowed.
Built in types
:map
-Ash.Type.Map
:keyword
-Ash.Type.Keyword
:term
-Ash.Type.Term
:atom
-Ash.Type.Atom
:string
-Ash.Type.String
:integer
-Ash.Type.Integer
:float
-Ash.Type.Float
:duration_name
-Ash.Type.DurationName
:function
-Ash.Type.Function
:boolean
-Ash.Type.Boolean
:struct
-Ash.Type.Struct
:uuid
-Ash.Type.UUID
:binary
-Ash.Type.Binary
:date
-Ash.Type.Date
:time
-Ash.Type.Time
:decimal
-Ash.Type.Decimal
:ci_string
-Ash.Type.CiString
:naive_datetime
-Ash.Type.NaiveDatetime
:utc_datetime
-Ash.Type.UtcDatetime
:utc_datetime_usec
-Ash.Type.UtcDatetimeUsec
:datetime
-Ash.Type.DateTime
:url_encoded_binary
-Ash.Type.UrlEncodedBinary
:union
-Ash.Type.Union
:module
-Ash.Type.Module
:vector
-Ash.Type.Vector
Composite Types
Currently, the only composite type supported is a list type, specified via:
{:array, Type}
. The constraints available are:
:items
(term/0
) - Constraints for the elements of the list. See the contained type's docs for more.:min_length
(non_neg_integer/0
) - A minimum length for the items:max_length
(non_neg_integer/0
) - A maximum length for the items:nil_items?
(boolean/0
) - Whether or not the list can contain nil items The default value isfalse
.:empty_values
(list ofterm/0
) - A set of values that, if encountered, will be considered an empty list. The default value is[""]
.
Defining Custom Types
Generally you add use Ash.Type
to your module (it is possible to add @behaviour Ash.Type
and define everything yourself, but this is more work and error-prone).
Overriding the {:array, type}
behaviour. By defining the *_array
versions
of cast_input
, cast_stored
, dump_to_native
and apply_constraints
, you can
override how your type behaves as a collection. This is how the features of embedded
resources are implemented. No need to implement them unless you wish to override the
default behaviour. Your type is responsible for handling nil values in each callback as well.
Simple example of a float custom type
defmodule GenTracker.AshFloat do
use Ash.Type
@impl Ash.Type
def storage_type(_), do: :float
@impl Ash.Type
def cast_input(nil, _), do: {:ok, nil}
def cast_input(value, _) do
Ecto.Type.cast(:float, value)
end
@impl Ash.Type
def cast_stored(nil, _), do: {:ok, nil}
def cast_stored(value, _) do
Ecto.Type.load(:float, value)
end
@impl Ash.Type
def dump_to_native(nil, _), do: {:ok, nil}
def dump_to_native(value, _) do
Ecto.Type.dump(:float, value)
end
end
All the Ash built-in types are implemented with use Ash.Type
so they are good
examples to look at to create your own Ash.Type
.
Short names
You can define short :atom_names
for your custom types by adding them to your Ash configuration:
config :ash, :custom_types, [ash_float: GenTracker.AshFloat]
Doing this will require a recompilation of the :ash
dependency which can be triggered by calling:
$ mix deps.compile ash --force
Summary
Callbacks
Useful for typed data layers (like ash_postgres) to instruct them not to attempt to cast input values.
Functions
Confirms if a casted value matches the provided constraints.
Returns true if the value is a builtin type or adopts the Ash.Type
behaviour
Casts input (e.g. unknown) data to an instance of the type, or errors
Casts a value from the data store to an instance of the type, or errors
Casts a value from the Elixir type to a value that can be embedded in another data structure.
Casts a value from the Elixir type to a value that the data store can persist
Returns the ecto compatible type for an Ash.Type.
Determines if two values of a given type are equal.
Process the old casted values alongside the new casted values.
Initializes the constraints according to the underlying type
Process the old casted values alongside the new uncasted values.
Determines if a type can be compared using ==
Returns the underlying storage type (the underlying type of the ecto type of the ash type)
Types
@type constraints() :: Keyword.t()
@type load_context() :: %{ api: Ash.Api.t(), actor: term() | nil, tenant: String.t() | nil, tracer: [Ash.Tracer.t()] | Ash.Tracer.t() | nil, authorize?: boolean() | nil }
Callbacks
@callback apply_constraints(term(), constraints()) :: {:ok, new_value :: term()} | :ok | {:error, constraint_error() | [constraint_error()]}
@callback apply_constraints_array([term()], constraints()) :: {:ok, new_values :: [term()]} | :ok | {:error, constraint_error() | [constraint_error()]}
@callback array_constraints() :: constraints()
@callback can_load?(constraints()) :: boolean()
@callback cast_in_query?(constraints()) :: boolean()
@callback cast_input(term(), constraints()) :: {:ok, term()} | error()
@callback cast_input_array([term()], constraints()) :: {:ok, [term()]} | error()
@callback cast_stored(term(), constraints()) :: {:ok, term()} | error()
@callback cast_stored_array([term()], constraints()) :: {:ok, [term()]} | error()
@callback constraints() :: constraints()
@callback custom_apply_constraints_array?() :: boolean()
@callback describe(constraints()) :: String.t() | nil
@callback dump_to_embedded(term(), constraints()) :: {:ok, term()} | :error
@callback dump_to_embedded_array([term()], constraints()) :: {:ok, term()} | error()
@callback dump_to_native(term(), constraints()) :: {:ok, term()} | error()
@callback dump_to_native_array([term()], constraints()) :: {:ok, term()} | error()
@callback ecto_type() :: Ecto.Type.t()
@callback embedded?() :: boolean()
@callback generator(constraints()) :: Enumerable.t()
@callback handle_change(old_term :: term(), new_term :: term(), constraints()) :: {:ok, term()} | error()
@callback handle_change_array(old_term :: [term()], new_term :: [term()], constraints()) :: {:ok, term()} | error()
@callback include_source(constraints(), Ash.Changeset.t()) :: constraints()
@callback init(constraints()) :: {:ok, constraints()} | {:error, Ash.Error.t()}
Useful for typed data layers (like ash_postgres) to instruct them not to attempt to cast input values.
You generally won't need this, but it can be an escape hatch for certain cases.
@callback load( values :: [term()], load :: Keyword.t(), constraints :: Keyword.t(), context :: load_context() ) :: {:ok, [term()]} | {:error, Ash.Error.t()}
@callback prepare_change(old_term :: term(), new_uncasted_term :: term(), constraints()) :: {:ok, term()} | error()
prepare_change_array(old_term, new_uncasted_term, constraints)
View Source (optional)@callback prepare_change_array( old_term :: [term()], new_uncasted_term :: [term()], constraints() ) :: {:ok, term()} | error()
@callback simple_equality?() :: boolean()
@callback storage_type() :: Ecto.Type.t()
@callback storage_type(constraints()) :: Ecto.Type.t()
Functions
@spec apply_constraints(t(), term(), constraints()) :: {:ok, term()} | {:error, String.t()}
Confirms if a casted value matches the provided constraints.
Returns true if the value is a builtin type or adopts the Ash.Type
behaviour
@spec cast_input(t(), term(), constraints() | nil) :: {:ok, term()} | {:error, Keyword.t()} | :error
Casts input (e.g. unknown) data to an instance of the type, or errors
Maps to Ecto.Type.cast/2
@spec cast_stored(t(), term(), constraints() | nil) :: {:ok, term()} | {:error, keyword()} | :error
Casts a value from the data store to an instance of the type, or errors
Maps to Ecto.Type.load/2
@spec constraints(t()) :: constraints()
@spec dump_to_embedded(t(), term(), constraints() | nil) :: {:ok, term()} | {:error, keyword()} | :error
Casts a value from the Elixir type to a value that can be embedded in another data structure.
Embedded resources expect to be stored in JSON, so this allows things like UUIDs to be stored as strings in embedded resources instead of binary.
@spec dump_to_native(t(), term(), constraints() | nil) :: {:ok, term()} | {:error, keyword()} | :error
Casts a value from the Elixir type to a value that the data store can persist
Maps to Ecto.Type.dump/2
@spec ecto_type(t()) :: Ecto.Type.t()
Returns the ecto compatible type for an Ash.Type.
If you use Ash.Type
, this is created for you. For builtin types
this may return a corresponding ecto builtin type (atom)
Determines if two values of a given type are equal.
Maps to Ecto.Type.equal?/3
@spec generator( module() | {:array, module()}, constraints() ) :: Enumerable.t()
Process the old casted values alongside the new casted values.
This is leveraged by embedded types to know if something is being updated or destroyed. This is not called on creates.
@spec include_source(t(), Ash.Changeset.t() | Ash.Query.t(), constraints()) :: constraints()
@spec init(t(), constraints()) :: {:ok, constraints()} | {:error, Ash.Error.t()}
Initializes the constraints according to the underlying type
@spec load( type :: t(), values :: [term()], load :: Keyword.t(), constraints :: Keyword.t(), context :: load_context() ) :: {:ok, [term()]} | {:error, Ash.Error.t()}
Process the old casted values alongside the new uncasted values.
This is leveraged by embedded types to know if something is being updated or destroyed. This is not called on creates.
Determines if a type can be compared using ==
Returns the underlying storage type (the underlying type of the ecto type of the ash type)