Search documentation of ash

# Ash.Type behaviour (ash v3.4.71)

View Source

The `Ash.Type` behaviour is used to define a value type in Ash.

## 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`
- `:file` - `Ash.Type.File`
- `: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`
- `:uuid_v7` - `Ash.Type.UUIDv7`
- `: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`

## 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`.

- `:remove_nil_items?` (`boolean/0`) - Whether or not to remove the nil items from the list instead of adding errors. 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).

Another option is to use `Ash.Type.NewType`, which supports defining a new type that is the combination of an existing type and custom constraints. This can be helpful when defining a custom attribute (e.g. struct) for a resource.

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

### 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.

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))

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)
    )

## Constraints

Constraints are a way of validating an input type. This validation can be used in both attributes and arguments. The kinds of constraints you can apply depends on the type of data. You can find all types in `Ash.Type` . Each type has its own page on which the available constraints are listed. For example in `Ash.Type.String` you can find 5 constraints:

- `:max_length`
- `:min_length`
- `:match`
- `:trim?`
- `:allow_empty?`

You can also discover these constraints from iex:

    $ iex -S mix
    iex(1)> Ash.Type.String.constraints
    [
      max_length: [
        type: :non_neg_integer,
        doc: "Enforces a maximum length on the value"
      ],
      min_length: [
        type: :non_neg_integer,
        doc: "Enforces a minimum length on the value"
      ],
      match: [
        type: {:custom, Ash.Type.String, :match, []},
        doc: "Enforces that the string matches a passed in regex"
      ],
      trim?: [type: :boolean, doc: "Trims the value.", default: true],
      allow_empty?: [
        type: :boolean,
        doc: "If false, the value is set to `nil` if it's empty.",
        default: false
      ]
    ]

### Attribute example

To show how constraints can be used in a attribute, here is an example attribute describing a username:

    defmodule MyProject.MyDomain.Account do
      # ...

      code_interface do
        define :create, action: :create
      end

      actions do
        default [:create, :read, :update, :destroy]
      end

      attributes do
        uuid_primary_key :id

        attribute :username, :string do
          constraints [
            max_length: 20,
            min_length: 3,
            match: ~r/^[a-z_-]*$/,
            trim?: true,
            allow_empty?: false
          ]
        end
      end

      # ...
    end

If, when creating or updating this attribute, one of the constraints are not met, an error will be given telling you which constraint was broken. See below:

    iex(1)> MyProject.MyDomain.Account.create!(%{username: "hi"})

    ** (Ash.Error.Invalid) Invalid Error

    * Invalid value provided for username: length must be greater than or equal to 3.

    "hi"

    iex(2)> MyProject.MyDomain.Account.create!(%{username: "Hello there this is a long string"})

    ** (Ash.Error.Invalid) Invalid Error

    * Invalid value provided for username: length must be less than or equal to 20.

    "Hello there this is a long string"

    iex(3)> MyProject.MyDomain.Account.create!(%{username: "hello there"})
    ** (Ash.Error.Invalid) Invalid Error

    * Invalid value provided for username: must match the pattern ~r/^[a-z_-]*$/.

    "hello there"

    iex(4)> MyProject.MyDomain.Account.create!(%{username: ""})
    ** (Ash.Error.Invalid) Invalid Error

    * attribute title is required

It will give you the resource as usual on successful requests:

    iex(5)> MyProject.MyDomain.Account.create!(%{username: "hello"})
    #MyProject.MyDomain.Account<
      __meta__: #Ecto.Schema.Metadata<:loaded, "account">,
      id: "7ba467dd-277c-4916-88ae-f62c93fee7a3",
      username: "hello",
      ...
    >

# Summary

## Types

constraints()

A keyword list of constraints for a type

error()

An error value that can be returned from various callbacks

load_context()

The context that is provided to the `load/4` callback.

merge_load_context()

The context that is provided to the `merge_load/4` callback.

t()

A valid Ash.Type

## Callbacks

apply_atomic_constraints(new_value, constraints)

Applies type constraints within an expression.

apply_atomic_constraints_array(new_value, constraints)

Applies type constraints to a list of values within an expression. See `apply_atomic_constraints/2` for more.

apply_constraints(term, constraints)

Called after casting, to apply additional constraints to the value.

apply_constraints_array(list, constraints)

Called after casting a list of values, to apply additional constraints to the value.

array_constraints()

Returns a `Spark.Options` spec for the additional constraints supported when used in a list.

can_load?(constraints)

Whether or not `load/4` can be used. Defined automatically

cast_atomic(new_value, constraints)

Casts a value within an expression.

cast_atomic_array(new_value, constraints)

Casts a list of values within an expression. See `cast_atomic/2` for more.

cast_in_query?(constraints)

Whether or not data layers that build queries should attempt to type cast values of this type while doing so.

cast_input(term, constraints)

Attempt to cast unknown, potentially user-provided input, into a valid instance of the type.

cast_input_array(list, constraints)

Attempt to cast a list of unknown, potentially user-provided inputs, into a list of valid instances of type.

cast_stored(term, constraints)

Attempt to load a stored value from the data layer into a valid instance of the type.

cast_stored_array(list, constraints)

Attempt to load a list of stored values from the data layer into a list of valid instances of the type.

coerce(term, constraints)

Attempt to coerce unknown, potentially user-provided input, into a valid instance of the type.

composite?(constraints)

Return true if the type is a composite type, meaning it is made up of one or more values. How this works is up to the data layer.

composite_types(constraints)

Information about each member of the composite type, if it is a composite type

constraints()

Returns a `Spark.Options` spec for the constraints supported by the type.

custom_apply_constraints_array?()

Whether or not an `apply_constraints_array/2` callback has been defined. This is defined automatically.

describe(constraints)

Describes a type given its constraints. Can be used to generate docs, for example.

dump_to_embedded(term, constraints)

Transform a valid instance of the type into a format that can be JSON encoded.

dump_to_embedded_array(list, constraints)

Transform a list of valid instances of the type into a format that can be JSON encoded.

dump_to_native(term, constraints)

Transform a valid instance of the type into a format that the data layer can store.

dump_to_native_array(list, constraints)

Transform a list of valid instance of the type into a format that the data layer can store.

ecto_type()

The underlying Ecto.Type.

embedded?()

Whether or not the type is an embedded resource. This is defined by embedded resources, you should not define this.

equal?(term, term)

Determine if two valid instances of the type are equal.

evaluate_operator(term)

The implementation for any overloaded implementations.

generator(constraints)

An Enumerable that produces valid instances of the type.

get_rewrites(merged_load, calculation, path, constraints)

Gets any "rewrites" necessary to apply a given load statement.

handle_change(old_term, new_term, constraints)

React to a changing value. This could be used, for example, to have a type like `:strictly_increasing_integer`.

handle_change_array(old_term, new_term, constraints)

React to a changing list of values. This could be used, for example, to have a type like `:unique_integer`, which when used in a list all items must be unique.

handle_change_array?()

Whether or not a custom `handle_change_array/3` has been defined by the type. Defined automatically.

include_source(constraints, t)

Add the source changeset to the constraints, in cases where it is needed for type casting logic

init(constraints)

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

load(values, load, constraints, context)

Applies a load statement through a list of values.

loaded?(value, path_to_load, constraints, opts)

Checks if the given path has been loaded on the type.

matches_type?(term, constraints)

Whether or not the value a valid instance of the type.

may_support_atomic_update?(constraints)

Whether or not a value with given constraints may support being cast atomic

merge_load(left, right, constraints, context)

Merges a load statement with an existing load statement for the type.

operator_overloads()

A map of operators with overloaded implementations.

prepare_change(old_term, new_uncasted_term, constraints)

Prepare a change, given the old value and the new uncasted value.

prepare_change_array(old_term, new_uncasted_term, constraints)

Prepare a changing list of values, given the old value and the new uncasted value.

prepare_change_array?()

Whether or not a custom `prepare_change_array/3` has been defined by the type. Defined automatically.

rewrite(value, list, constraints)

Apply any "rewrites" necessary to provide the results of a load statement to calculations that depended on a given load.

simple_equality?()

Whether or not `==` can be used to compare instances of the type.

storage_type()

The storage type, which should be known by a data layer supporting this type.

storage_type(constraints)

The storage type, which should be known by a data layer supporting this type.

## Functions

apply_atomic_constraints(type, term, constraints)

Applies a types constraints to an expression.

apply_constraints(type, term, constraints)

Confirms if a casted value matches the provided constraints.

array_constraints(type)

Gets the array constraints for a type

ash_type?(module)

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

builtin?(type)

Returns true if the type is an ash builtin type

can_load?(type, constraints \\ [])

Returns true if the type supports nested loads

cast_atomic(type, term, constraints)

Modifies an expression to apply a type's casting logic to the value it produces.

cast_in_query?(type, constraints \\ [])

Returns `true` if the type should be cast in underlying queries

cast_input(type, term, constraints \\ nil)

Casts input (e.g. unknown) data to an instance of the type, or errors

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

Casts a value from the data store to an instance of the type, or errors

coerce(type, term, constraints \\ nil)

Coerces input (e.g. unknown) data to an instance of the type, or errors.

composite?(type, constraints)

Returns true if the type is a composite type

composite_types(type, constraints)

Returns the wrapped composite types

constraints(type)

Returns the constraint schema for a type

describe(type, constraints)

Calls the type's `describe` function with the given constraints

determine_types(types, values)

Determine types for a given function or operator.

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

Casts a value from the Elixir type to a value that can be embedded in another data structure.

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

Casts a value from the Elixir type to a value that the data store can persist

ecto_type(type)

Returns the ecto compatible type for an Ash.Type.

embedded_type?(type)

Returns true if the type is an embedded resource

equal?(type, left, right)

Determines if two values of a given type are equal.

generator(type, constraints)

Returns the StreamData generator for a given type

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

Gets the load rewrites for a given type, load, calculation and path.

get_type(value)

Gets the type module for a given short name or module

get_type!(value)

Gets the type module for a given short name or module, ensures that it is a valid `type`

handle_change(type, old_value, new_value, constraints)

Process the old casted values alongside the new casted values.

handle_change_array?(type)

Handles the change of a given array of values for an attribute change. Runs after casting.

include_source(type, changeset_or_query, constraints)

Provides the changeset, action_input or query to the type, to potentially store in its constraints.

init(type, constraints)

Initializes the constraints according to the underlying type

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

Apply a load statement to a value.

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

Checks if a given path has been loaded on a type.

matches_type?(type, value, constraints \\ [])

Detects as a best effort if an arbitrary value matches the given type

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

Merges two load statements for a given type.

prepare_change(type, old_value, new_value, constraints)

Process the old casted values alongside the new uncasted values.

prepare_change_array?(type)

Prepares a given array of values for an attribute change. Runs before casting.

rewrite(type, value, rewrites, constraints)

Applies rewrites to a given value.

short_names()

Returns the list of available type short names

simple_equality?(type)

Determines if a type can be compared using the `==` operator.

storage_type(type, constraints \\ [])

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

# Types

# constraints()

    @type constraints() :: Keyword.t()

A keyword list of constraints for a type

# error()

    @type error() ::
      :error
      | {:error,
         String.t()
         | [field: atom(), fields: [atom()], message: String.t(), value: any()]
         | Ash.Error.t()}

An error value that can be returned from various callbacks

# load_context()

    @type load_context() :: %{
      domain: Ash.Domain.t(),
      actor: term() | nil,
      tenant: term(),
      tracer: [Ash.Tracer.t()] | Ash.Tracer.t() | nil,
      authorize?: boolean() | nil
    }

The context that is provided to the `load/4` callback.

# merge_load_context()

    @type merge_load_context() :: %{
      domain: Ash.Domain.t(),
      calc_name: term(),
      calc_load: term(),
      calc_path: [atom()],
      reuse_values?: boolean(),
      strict_loads?: boolean(),
      initial_data: term(),
      relationship_path: [atom()],
      authorize?: boolean()
    }

The context that is provided to the `merge_load/4` callback.

# t()

    @type t() :: module() | {:array, atom()}

A valid Ash.Type

# Callbacks

# apply_atomic_constraints(new_value, constraints)

    @callback apply_atomic_constraints(new_value :: Ash.Expr.t(), constraints()) ::
      :ok | {:ok, Ash.Expr.t()} | {:error, Ash.Error.t()}

Applies type constraints within an expression.

# apply_atomic_constraints_array(new_value, constraints)

    @callback apply_atomic_constraints_array(new_value :: Ash.Expr.t(), constraints()) ::
      :ok | {:ok, Ash.Expr.t()} | {:error, Ash.Error.t()}

Applies type constraints to a list of values within an expression. See `apply_atomic_constraints/2` for more.

# apply_constraints(term, constraints)

    @callback apply_constraints(term(), constraints()) ::
      {:ok, new_value :: term()} | :ok | error()

Called after casting, to apply additional constraints to the value.

# apply_constraints_array(list, constraints)

(optional)

    @callback apply_constraints_array([term()], constraints()) ::
      {:ok, new_values :: [term()]} | :ok | error()

Called after casting a list of values, to apply additional constraints to the value.

If not defined, `apply_constraints/2` is called for each item.

# array_constraints()

(optional)

    @callback array_constraints() :: constraints()

Returns a `Spark.Options` spec for the additional constraints supported when used in a list.

# can_load?(constraints)

    @callback can_load?(constraints()) :: boolean()

Whether or not `load/4` can be used. Defined automatically

# cast_atomic(new_value, constraints)

    @callback cast_atomic(new_value :: Ash.Expr.t(), constraints()) ::
      {:atomic, Ash.Expr.t()} | {:error, Ash.Error.t()} | {:not_atomic, String.t()}

Casts a value within an expression.

For instance, if you had a type like `:non_neg_integer`, you might do:

    def cast_atomic(value, _constraints)  do
      expr(
        if ^value < 0 do
          error(Ash.Error.Changes.InvalidChanges, %{message: "must be positive", value: ^value})
        else
          value
        end
      )
    end

# cast_atomic_array(new_value, constraints)

    @callback cast_atomic_array(new_value :: Ash.Expr.t(), constraints()) ::
      {:atomic, Ash.Expr.t()} | {:error, Ash.Error.t()} | {:not_atomic, String.t()}

Casts a list of values within an expression. See `cast_atomic/2` for more.

# cast_in_query?(constraints)

    @callback cast_in_query?(constraints()) :: boolean()

Whether or not data layers that build queries should attempt to type cast values of this type while doing so.

# cast_input(term, constraints)

    @callback cast_input(term(), constraints()) :: {:ok, term()} | Ash.Error.t()

Attempt to cast unknown, potentially user-provided input, into a valid instance of the type.

# cast_input_array(list, constraints)

(optional)

    @callback cast_input_array([term()], constraints()) :: {:ok, [term()]} | error()

Attempt to cast a list of unknown, potentially user-provided inputs, into a list of valid instances of type.

This callback allows to define types that are "collection-aware", i.e an integer that is unique whenever it appears in a list.

If not defined, `cast_input/2` is called for each item.

# cast_stored(term, constraints)

    @callback cast_stored(term(), constraints()) :: {:ok, term()} | error()

Attempt to load a stored value from the data layer into a valid instance of the type.

# cast_stored_array(list, constraints)

(optional)

    @callback cast_stored_array([term()], constraints()) :: {:ok, [term()]} | error()

Attempt to load a list of stored values from the data layer into a list of valid instances of the type.

If not defined, `cast_stored/2` is called for each item.

# coerce(term, constraints)

    @callback coerce(term(), constraints()) :: {:ok, term()} | Ash.Error.t()

Attempt to coerce unknown, potentially user-provided input, into a valid instance of the type.

## Coercion vs Casting

Coercion can be summed up as a more "insistent" form of casting. It means "we really want to use this value as this type, so please try to convert it to that type". This is used in expressions as opposed to `cast_input`. For example, the value `10`, if passed into `Ash.Type.cast_input(:string, 10)` would fail to cast. However, if used in the following expression: `expr(type(10, :string) <> " minutes")` the `10` would be "coerced" (using `to_string/1`) into `"10"`.

By default, coercion uses `cast_input/2` unless

# composite?(constraints)

    @callback composite?(constraints()) :: boolean()

Return true if the type is a composite type, meaning it is made up of one or more values. How this works is up to the data layer.

For example, `AshMoney` provides a type that is composite with a "currency" and an "amount".

# composite_types(constraints)

    @callback composite_types(constraints()) :: [
      {name, type, constraints()} | {name, storage_key, type, constraints()}
    ]
    when name: atom(), type: t(), storage_key: atom()

Information about each member of the composite type, if it is a composite type

An example given the `AshMoney` example listed above:

`[{:currency, :string, []}, {:amount, :decimal, []}]`

# constraints()

    @callback constraints() :: constraints()

Returns a `Spark.Options` spec for the constraints supported by the type.

# custom_apply_constraints_array?()

    @callback custom_apply_constraints_array?() :: boolean()

Whether or not an `apply_constraints_array/2` callback has been defined. This is defined automatically.

# describe(constraints)

    @callback describe(constraints()) :: String.t() | nil

Describes a type given its constraints. Can be used to generate docs, for example.

# dump_to_embedded(term, constraints)

(optional)

    @callback dump_to_embedded(term(), constraints()) :: {:ok, term()} | :error

Transform a valid instance of the type into a format that can be JSON encoded.

# dump_to_embedded_array(list, constraints)

(optional)

    @callback dump_to_embedded_array([term()], constraints()) :: {:ok, term()} | error()

Transform a list of valid instances of the type into a format that can be JSON encoded.

If not defined, `dump_to_embedded/2` is called for each item.

# dump_to_native(term, constraints)

    @callback dump_to_native(term(), constraints()) :: {:ok, term()} | error()

Transform a valid instance of the type into a format that the data layer can store.

# dump_to_native_array(list, constraints)

(optional)

    @callback dump_to_native_array([term()], constraints()) :: {:ok, term()} | error()

Transform a list of valid instance of the type into a format that the data layer can store.

If not defined, `dump_to_native/2` is called for each item.

# ecto_type()

    @callback ecto_type() :: Ecto.Type.t()

The underlying Ecto.Type.

# embedded?()

    @callback embedded?() :: boolean()

Whether or not the type is an embedded resource. This is defined by embedded resources, you should not define this.

# equal?(term, term)

    @callback equal?(term(), term()) :: boolean()

Determine if two valid instances of the type are equal.

Do not define this if `==` is sufficient for your type. See `simple_equality?/0` for more.

# evaluate_operator(term)

(optional)

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

The implementation for any overloaded implementations.

# generator(constraints)

(optional)

    @callback generator(constraints()) :: Enumerable.t()

An Enumerable that produces valid instances of the type.

This can be used for property testing, or generating valid inputs for seeding. Typically you would use `StreamData` for this.

# get_rewrites(merged_load, calculation, path, constraints)

(optional)

    @callback get_rewrites(
      merged_load :: term(),
      calculation :: Ash.Query.Calculation.t(),
      path :: [atom()],
      constraints :: Keyword.t()
    ) :: [rewrite()]

Gets any "rewrites" necessary to apply a given load statement.

This is a low level tool used when types can contain instances of resources. You generally should not need to know how this works. See `Ash.Type.Union` and `Ash.Type.Struct` for examples if you are trying to write a similar type.

# handle_change(old_term, new_term, constraints)

    @callback handle_change(old_term :: term(), new_term :: term(), constraints()) ::
      {:ok, term()} | error()

React to a changing value. This could be used, for example, to have a type like `:strictly_increasing_integer`.

# handle_change_array(old_term, new_term, constraints)

(optional)

    @callback handle_change_array(old_term :: [term()], new_term :: [term()], constraints()) ::
      {:ok, term()} | error()

React to a changing list of values. This could be used, for example, to have a type like `:unique_integer`, which when used in a list all items must be unique.

If not defined, `handle_change/3` is called for each item with a `nil` old value.

# handle_change_array?()

    @callback handle_change_array?() :: boolean()

Whether or not a custom `handle_change_array/3` has been defined by the type. Defined automatically.

# include_source(constraints, t)

(optional)

    @callback include_source(constraints(), Ash.Changeset.t()) :: constraints()

Add the source changeset to the constraints, in cases where it is needed for type casting logic

# init(constraints)

(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.

# load(values, load, constraints, context)

(optional)

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

Applies a load statement through a list of values.

This allows types to support load statements, like `Ash.Type.Union`, embedded resources, or the `Ash.Type.Struct` when it is an `instance_of` a resource.

# loaded?(value, path_to_load, constraints, opts)

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

Checks if the given path has been loaded on the type.

# matches_type?(term, constraints)

    @callback matches_type?(term(), constraints()) :: boolean()

Whether or not the value a valid instance of the type.

# may_support_atomic_update?(constraints)

    @callback may_support_atomic_update?(constraints()) :: boolean()

Whether or not a value with given constraints may support being cast atomic

Defaults to checking if `cast_atomic/2` is defined on the type.

# merge_load(left, right, constraints, context)

(optional)

    @callback merge_load(
      left :: term(),
      right :: term(),
      constraints :: Keyword.t(),
      context :: merge_load_context() | nil
    ) :: {:ok, term()} | {:error, error()} | :error

Merges a load statement with an existing load statement for the type.

# operator_overloads()

(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.

# prepare_change(old_term, new_uncasted_term, constraints)

    @callback prepare_change(old_term :: term(), new_uncasted_term :: term(), constraints()) ::
      {:ok, term()} | error()

Prepare a change, given the old value and the new uncasted value.

# prepare_change_array(old_term, new_uncasted_term, constraints)

(optional)

    @callback prepare_change_array(
      old_term :: [term()],
      new_uncasted_term :: [term()],
      constraints()
    ) :: {:ok, term()} | error()

Prepare a changing list of values, given the old value and the new uncasted value.

If not defined, `prepare_change/3` is called for each item with a `nil` old value.

# prepare_change_array?()

    @callback prepare_change_array?() :: boolean()

Whether or not a custom `prepare_change_array/3` has been defined by the type. Defined automatically.

# rewrite(value, list, constraints)

(optional)

    @callback rewrite(value :: term(), [rewrite()], constraints :: Keyword.t()) ::
      value :: term()

Apply any "rewrites" necessary to provide the results of a load statement to calculations that depended on a given load.

This is a low level tool used when types can contain instances of resources. You generally should not need to know how this works. See `Ash.Type.Union` and `Ash.Type.Struct` for examples if you are trying to write a similar type.

# simple_equality?()

    @callback simple_equality?() :: boolean()

Whether or not `==` can be used to compare instances of the type.

This is defined automatically to return `false` if `equal?/2` is defined.

Types that cannot be compared using `==` incur significant runtime costs when used in certain ways. For example, if a resource's primary key cannot be compared with `==`, we cannot do things like key a list of records by their primary key. Implementing `equal?/2` will cause various code paths to be considerably slower, so only do it when necessary.

# storage_type()

(optional)

    @callback storage_type() :: Ecto.Type.t()

The storage type, which should be known by a data layer supporting this type.

Use `storage_type/1`, as this will be deprecated in the future.

# storage_type(constraints)

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

The storage type, which should be known by a data layer supporting this type.

# Functions

# apply_atomic_constraints(type, term, constraints)

    @spec apply_atomic_constraints(t(), term(), constraints()) ::
      {:ok, Ash.Expr.t()} | {:error, Ash.Error.t()}

Applies a types constraints to an expression.

This delegates to the underlying types implementaiton of `apply_atomic_constraints/2`.

# apply_constraints(type, term, constraints)

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

Confirms if a casted value matches the provided constraints.

# array_constraints(type)

Gets the array constraints for a type

# ash_type?(module)

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

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

# builtin?(type)

Returns true if the type is an ash builtin type

# can_load?(type, constraints \\ [])

    @spec can_load?(t(), Keyword.t()) :: boolean()

Returns true if the type supports nested loads

# cast_atomic(type, term, constraints)

    @spec cast_atomic(t(), term(), constraints()) ::
      {:atomic, Ash.Expr.t()}
      | {:ok, term()}
      | {:error, Ash.Error.t()}
      | {:not_atomic, String.t()}

Modifies an expression to apply a type's casting logic to the value it produces.

This delegates to the underlying types implementaiton of `cast_atomic/2`.

# cast_in_query?(type, constraints \\ [])

Returns `true` if the type should be cast in underlying queries

# cast_input(type, term, constraints \\ nil)

    @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`

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

    @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`

# coerce(type, term, constraints \\ nil)

    @spec coerce(t(), term(), constraints() | nil) ::
      {:ok, term()} | {:error, Keyword.t()} | :error

Coerces input (e.g. unknown) data to an instance of the type, or errors.

See `Ash.Type.coerce/2`

# composite?(type, constraints)

    @spec composite?(
      t(),
      constraints()
    ) :: Enumerable.t()

Returns true if the type is a composite type

# composite_types(type, constraints)

    @spec composite_types(
      t(),
      constraints()
    ) :: Enumerable.t()

Returns the wrapped composite types

# constraints(type)

    @spec constraints(t()) :: constraints()

Returns the constraint schema for a type

# describe(type, constraints)

Calls the type's `describe` function with the given constraints

# determine_types(types, values)

Determine types for a given function or operator.

# dump_to_embedded(type, term, 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.

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

    @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`

# ecto_type(type)

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

# embedded_type?(type)

Returns true if the type is an embedded resource

# equal?(type, left, right)

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

Determines if two values of a given type are equal.

Maps to `Ecto.Type.equal?/3`

# generator(type, constraints)

    @spec generator(
      module() | {:array, module()},
      constraints()
    ) :: Enumerable.t()

Returns the StreamData generator for a given type

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

Gets the load rewrites for a given type, load, calculation and path.

This is used for defining types that support a nested load statement. See the embedded type and union type implementations for examples of how to use this.

# get_type(value)

    @spec get_type(atom() | module() | {:array, atom() | module()}) ::
      atom() | module() | {:array, atom() | module()}

Gets the type module for a given short name or module

# get_type!(value)

    @spec get_type!(atom() | module() | {:array, atom() | module()}) ::
      atom() | module() | {:array, atom() | module()}

Gets the type module for a given short name or module, ensures that it is a valid `type`

## Raises

- `RuntimeError`: If the provided type module is not found or invalid.

# handle_change(type, old_value, new_value, constraints)

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.

# handle_change_array?(type)

    @spec handle_change_array?(t()) :: boolean()

Handles the change of a given array of values for an attribute change. Runs after casting.

# include_source(type, changeset_or_query, constraints)

    @spec include_source(
      t(),
      Ash.Changeset.t() | Ash.Query.t() | Ash.ActionInput.t(),
      constraints()
    ) :: constraints()

Provides the changeset, action_input or query to the type, to potentially store in its constraints.

This is used for embedded types to allow accessing the parent changeset in certain cases.

# init(type, constraints)

    @spec init(t(), constraints()) :: {:ok, constraints()} | {:error, Ash.Error.t()}

Initializes the constraints according to the underlying type

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

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

Apply a load statement to a value.

This is used for types that can be "loaded through". For example, maps, unions and structs. If they have keys that are embedded types, for example, we want to be able to apply a load statements to their contents.

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

    @spec loaded?(
      type :: t(),
      value_or_values :: term(),
      path_to_load :: [atom()],
      constraints :: Keyword.t(),
      opts :: Keyword.t()
    ) :: boolean()

Checks if a given path has been loaded on a type.

This is used to "load through" types. For more see `load/5`.

# matches_type?(type, value, constraints \\ [])

Detects as a best effort if an arbitrary value matches the given type

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

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

Merges two load statements for a given type.

This is used to "load through" types. For more see `load/5`.

# prepare_change(type, old_value, new_value, constraints)

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.

# prepare_change_array?(type)

    @spec prepare_change_array?(t()) :: boolean()

Prepares a given array of values for an attribute change. Runs before casting.

# rewrite(type, value, rewrites, constraints)

Applies rewrites to a given value.

This is used for defining types that support a nested load statement. See the embedded type and union type implementations for examples of how to use this.

# short_names()

Returns the list of available type short names

# simple_equality?(type)

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

Determines if a type can be compared using the `==` operator.

# storage_type(type, constraints \\ [])

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