View Source Ash.Type behaviour (ash v2.21.13)

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

Lists/Arrays

To specify a list of values, use {:array, Type}. Arrays are special, and have special constraints:

  • :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 is false.

  • :empty_values (list of term/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

Composite Types

Composite types are composite in the data layer. Many data layers do not support this, but some (like AshPostgres), do. To define a composite type, the following things should be true:

  1. A casted value should be a map or struct, for example for a point: %{x: 1, y: 2}
  2. The data layer must support composite types, and the data layer representation will be a tuple, i.e {1, 2}
  3. Define def composite?(_), do: true in your composite type
  4. Define the type & constraints of each item in the tuple, and its name in the map representation: def composite_types(_), do: [{:x, :integer, []}, {:y, :integer, []}]. You can also define a storage key for each item in the tuple, if the underlying type implementation has a different reference for an item, i.e def composite_types(_), do: [{:x, :x_coord, :integer, []}, {:y, :y_coord, :integer, []}]

With the above implemented, your composite type can be used in expressions, for example:

Ash.Query.filter(expr(coordinates[:x] == 1))k

And you can also construct composite types in expressions, for example:

calculate :coordinates, :composite_point, expr(composite_type(%{x: some_value, y: some_other_value}, Point))

Summary

Callbacks

The implementation for any overloaded implementations.

Useful for typed data layers (like ash_postgres) to instruct them not to attempt to cast input values.

A map of operators with overloaded implementations.

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

Determine types for a given function or operator.

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 constraint_error() :: String.t() | {String.t(), Keyword.t()}
@type constraints() :: Keyword.t()
@type error() :: :error | {:error, String.t() | Keyword.t()}
@type load_context() :: %{
  api: Ash.Api.t(),
  actor: term() | nil,
  tenant: term(),
  tracer: [Ash.Tracer.t()] | Ash.Tracer.t() | nil,
  authorize?: boolean() | nil
}
@type merge_load_context() :: %{
  api: Ash.Api.t(),
  calc_name: term(),
  calc_load: term(),
  calc_path: [atom()],
  relationship_path: [atom()],
  initial_data: {:ok, [Ash.Resource.record()]} | :error
}
@type rewrite() :: {{[atom()], rewrite_data(), atom(), atom()}, source :: term()}
@type rewrite_data() ::
  {type :: :calc | :agg, rewriting_name :: atom(), rewriting_load :: atom()}
  | {:rel, rewriting_name :: atom()}
@type t() :: atom() | {:array, atom()}

Callbacks

Link to this callback

apply_constraints(term, constraints)

View Source
@callback apply_constraints(term(), constraints()) ::
  {:ok, new_value :: term()}
  | :ok
  | {:error, constraint_error() | [constraint_error()]}
Link to this callback

apply_constraints_array(list, constraints)

View Source (optional)
@callback apply_constraints_array([term()], constraints()) ::
  {:ok, new_values :: [term()]}
  | :ok
  | {:error, constraint_error() | [constraint_error()]}
Link to this callback

array_constraints()

View Source (optional)
@callback array_constraints() :: constraints()
@callback can_load?(constraints()) :: boolean()
Link to this callback

cast_atomic_update(new_value, constraints)

View Source
@callback cast_atomic_update(new_value :: Ash.Expr.t(), constraints()) ::
  {:atomic, Ash.Expr.t()} | {:error, Ash.Error.t()} | {:not_atomic, String.t()}
Link to this callback

cast_atomic_update_array(new_value, constraints)

View Source
@callback cast_atomic_update_array(new_value :: Ash.Expr.t(), constraints()) ::
  {:atomic, Ash.Expr.t()} | {:error, Ash.Error.t()} | {:not_atomic, String.t()}
Link to this callback

cast_in_query?(constraints)

View Source
@callback cast_in_query?(constraints()) :: boolean()
Link to this callback

cast_input(term, constraints)

View Source
@callback cast_input(term(), constraints()) :: {:ok, term()} | error()
Link to this callback

cast_input_array(list, constraints)

View Source (optional)
@callback cast_input_array([term()], constraints()) :: {:ok, [term()]} | error()
Link to this callback

cast_stored(term, constraints)

View Source
@callback cast_stored(term(), constraints()) :: {:ok, term()} | error()
Link to this callback

cast_stored_array(list, constraints)

View Source (optional)
@callback cast_stored_array([term()], constraints()) :: {:ok, [term()]} | error()
@callback composite?(constraints()) :: boolean()
Link to this callback

composite_types(constraints)

View Source
@callback composite_types(constraints()) :: [
  {name, type, constraints()} | {name, storage_key, type, constraints()}
]
when name: atom(), type: t(), storage_key: atom()
@callback constraints() :: constraints()
Link to this callback

custom_apply_constraints_array?()

View Source
@callback custom_apply_constraints_array?() :: boolean()
@callback describe(constraints()) :: String.t() | nil
Link to this callback

dump_to_embedded(term, constraints)

View Source (optional)
@callback dump_to_embedded(term(), constraints()) :: {:ok, term()} | :error
Link to this callback

dump_to_embedded_array(list, constraints)

View Source (optional)
@callback dump_to_embedded_array([term()], constraints()) :: {:ok, term()} | error()
Link to this callback

dump_to_native(term, constraints)

View Source
@callback dump_to_native(term(), constraints()) :: {:ok, term()} | error()
Link to this callback

dump_to_native_array(list, constraints)

View Source (optional)
@callback dump_to_native_array([term()], constraints()) :: {:ok, term()} | error()
@callback ecto_type() :: Ecto.Type.t()
@callback embedded?() :: boolean()
@callback equal?(term(), term()) :: boolean()
Link to this callback

evaluate_operator(term)

View Source (optional)
@callback evaluate_operator(term()) :: {:known, term()} | :unknown | {:error, term()}

The implementation for any overloaded implementations.

Link to this callback

generator(constraints)

View Source (optional)
@callback generator(constraints()) :: Enumerable.t()
Link to this callback

get_rewrites(merged_load, calculation, path, constraints)

View Source (optional)
@callback get_rewrites(
  merged_load :: term(),
  calculation :: Ash.Query.Calculation.t(),
  path :: [atom()],
  constraints :: Keyword.t()
) :: [rewrite()]
Link to this callback

handle_change(old_term, new_term, constraints)

View Source
@callback handle_change(old_term :: term(), new_term :: term(), constraints()) ::
  {:ok, term()} | error()
Link to this callback

handle_change_array(old_term, new_term, constraints)

View Source (optional)
@callback handle_change_array(old_term :: [term()], new_term :: [term()], constraints()) ::
  {:ok, term()} | error()
@callback handle_change_array?() :: boolean()
Link to this callback

include_source(constraints, t)

View Source (optional)
@callback include_source(constraints(), Ash.Changeset.t()) :: constraints()
Link to this callback

init(constraints)

View Source (optional)
@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.

Link to this callback

load(values, load, constraints, context)

View Source (optional)
@callback load(
  values :: [term()],
  load :: Keyword.t(),
  constraints :: Keyword.t(),
  context :: load_context()
) :: {:ok, [term()]} | {:error, Ash.Error.t()}
Link to this callback

loaded?(value, path_to_load, constraints, opts)

View Source
@callback loaded?(
  value :: term(),
  path_to_load :: [atom()],
  constraints :: Keyword.t(),
  opts :: Keyword.t()
) :: boolean()
Link to this callback

merge_load(left, right, constraints, context)

View Source (optional)
@callback merge_load(
  left :: term(),
  right :: term(),
  constraints :: Keyword.t(),
  context :: merge_load_context() | nil
) :: {:ok, term()} | {:error, error()} | :error
Link to this callback

operator_overloads()

View Source (optional)
@callback operator_overloads() :: %{optional(atom()) => %{optional(term()) => module()}}

A map of operators with overloaded implementations.

These will only be honored if the type is placed in config :ash, :known_types, [...Type]

A corresponding evaluate_operator/1 clause should match.

Link to this callback

prepare_change(old_term, new_uncasted_term, constraints)

View Source
@callback prepare_change(old_term :: term(), new_uncasted_term :: term(), constraints()) ::
  {:ok, term()} | error()
Link to this callback

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 prepare_change_array?() :: boolean()
Link to this callback

rewrite(value, list, constraints)

View Source (optional)
@callback rewrite(value :: term(), [rewrite()], constraints :: Keyword.t()) ::
  value :: term()
@callback simple_equality?() :: boolean()
Link to this callback

storage_type()

View Source (optional)
@callback storage_type() :: Ecto.Type.t()
Link to this callback

storage_type(constraints)

View Source
@callback storage_type(constraints()) :: Ecto.Type.t()

Functions

Link to this function

apply_constraints(type, term, constraints)

View Source
@spec apply_constraints(t(), term(), constraints()) ::
  {:ok, term()} | {:error, String.t()}

Confirms if a casted value matches the provided constraints.

@spec ash_type?(term()) :: boolean()

Returns true if the value is a builtin type or adopts the Ash.Type behaviour

Link to this function

can_load?(type, constraints \\ [])

View Source
@spec can_load?(t(), Keyword.t()) :: boolean()
Link to this function

cast_atomic_update(type, term, constraints)

View Source
@spec cast_atomic_update(t(), term(), constraints()) ::
  {:atomic, Ash.Expr.t()} | {:error, Ash.Error.t()} | {:not_atomic, String.t()}
Link to this function

cast_in_query?(type, constraints \\ [])

View Source
Link to this function

cast_input(type, term, constraints \\ nil)

View Source
@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

Link to this function

cast_stored(type, term, constraints \\ [])

View Source
@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

Link to this function

composite?(type, constraints)

View Source
@spec composite?(
  t(),
  constraints()
) :: Enumerable.t()
Link to this function

composite_types(type, constraints)

View Source
@spec composite_types(
  t(),
  constraints()
) :: Enumerable.t()
@spec constraints(t()) :: constraints()
Link to this function

describe(type, constraints)

View Source
Link to this function

determine_types(types, values)

View Source

Determine types for a given function or operator.

Link to this function

dump_to_embedded(type, term, constraints \\ [])

View Source
@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.

Link to this function

dump_to_native(type, term, constraints \\ [])

View Source
@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)

Link to this function

equal?(type, left, right)

View Source
@spec equal?(t(), term(), term()) :: boolean()

Determines if two values of a given type are equal.

Maps to Ecto.Type.equal?/3

Link to this function

generator(type, constraints)

View Source
@spec generator(
  module() | {:array, module()},
  constraints()
) :: Enumerable.t()
Link to this function

get_rewrites(type, merged_load, calculation, path, constraints)

View Source
@spec get_type(atom() | module() | {:array, atom() | module()}) ::
  atom() | module() | {:array, atom() | module()}
Link to this function

handle_change(type, old_value, new_value, constraints)

View Source

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.

Link to this function

handle_change_array?(type)

View Source
@spec handle_change_array?(t()) :: boolean()
Link to this function

include_source(type, changeset_or_query, constraints)

View Source
@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

Link to this function

load(type, value, loads, constraints, context)

View Source
@spec load(
  type :: t(),
  values :: [term()],
  load :: Keyword.t(),
  constraints :: Keyword.t(),
  context :: load_context()
) :: {:ok, [term()]} | {:error, Ash.Error.t()}
Link to this function

loaded?(type, values, load, constraints, opts \\ [])

View Source
@spec loaded?(
  type :: t(),
  value_or_values :: term(),
  path_to_load :: [atom()],
  constraints :: Keyword.t(),
  opts :: Keyword.t()
) :: boolean()
Link to this function

merge_load(type, left, right, constraints, context)

View Source
@spec merge_load(
  type :: t(),
  left :: term(),
  right :: term(),
  constraints :: Keyword.t(),
  context :: merge_load_context() | nil
) :: {:ok, [term()]} | :error | {:error, Ash.Error.t()}
Link to this function

prepare_change(type, old_value, new_value, constraints)

View Source

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.

Link to this function

prepare_change_array?(type)

View Source
@spec prepare_change_array?(t()) :: boolean()
Link to this function

rewrite(type, value, rewrites, constraints)

View Source
@spec simple_equality?(t()) :: boolean()

Determines if a type can be compared using ==

Link to this function

storage_type(type, constraints \\ [])

View Source

Returns the underlying storage type (the underlying type of the ecto type of the ash type)