Packages
ash
3.0.0-rc.45
3.29.3
3.29.2
3.29.1
3.29.0
3.28.0
3.27.8
3.27.7
3.27.6
3.27.5
3.27.4
3.27.3
3.27.2
3.27.1
3.27.0
3.26.0
3.25.2
3.25.1
3.25.0
retired
3.24.7
3.24.6
3.24.5
3.24.4
3.24.3
3.24.2
3.24.1
3.24.0
3.23.1
3.23.0
3.22.2
3.22.1
3.22.0
3.21.3
3.21.2
3.21.1
3.21.0
3.20.0
3.19.3
3.19.2
3.19.1
3.19.0
3.18.0
3.17.1
3.17.0
3.16.0
3.15.0
3.14.1
3.14.0
retired
3.13.2
3.13.1
3.13.0
3.12.0
3.11.3
3.11.2
3.11.1
3.11.0
3.10.1
3.10.0
3.9.0
3.8.0
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
retired
3.6.3
retired
3.6.2
3.6.1
3.6.0
3.5.43
3.5.42
3.5.41
3.5.40
3.5.39
3.5.38
3.5.37
3.5.36
3.5.35
3.5.34
3.5.33
3.5.32
3.5.31
3.5.30
3.5.29
3.5.28
3.5.27
3.5.26
3.5.25
3.5.24
3.5.23
3.5.22
3.5.21
3.5.20
3.5.19
3.5.18
3.5.17
3.5.16
3.5.15
3.5.14
3.5.13
3.5.12
3.5.11
3.5.10
3.5.9
3.5.8
3.5.7
3.5.6
3.5.5
3.5.4
3.5.3
3.5.2
3.5.1
3.5.0
3.4.74
retired
3.4.73
3.4.72
3.4.71
3.4.70
3.4.69
3.4.68
3.4.67
3.4.66
3.4.65
3.4.64
3.4.63
3.4.62
3.4.61
3.4.60
3.4.59
3.4.58
3.4.57
3.4.56
3.4.55
3.4.54
3.4.53
3.4.52
3.4.51
3.4.50
3.4.49
3.4.48
3.4.47
3.4.46
3.4.45
3.4.44
3.4.43
3.4.42
3.4.41
3.4.40
3.4.39
3.4.38
3.4.37
3.4.36
3.4.35
3.4.34
3.4.33
3.4.32
3.4.31
3.4.30
3.4.29
3.4.28
3.4.27
3.4.26
3.4.25
3.4.24
3.4.23
3.4.22
3.4.21
3.4.20
3.4.19
3.4.18
3.4.17
3.4.16
3.4.15
3.4.14
3.4.13
3.4.12
3.4.11
3.4.10
3.4.9
3.4.8
3.4.7
3.4.6
3.4.5
3.4.4
3.4.3
3.4.2
3.4.1
3.4.0
3.3.3
3.3.2
3.3.1
3.3.0
3.2.6
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.1.8
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.16
3.0.15
3.0.14
3.0.13
3.0.12
3.0.11
3.0.10
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.46
3.0.0-rc.45
3.0.0-rc.44
3.0.0-rc.43
3.0.0-rc.42
3.0.0-rc.41
3.0.0-rc.40
3.0.0-rc.39
3.0.0-rc.38
3.0.0-rc.37
3.0.0-rc.36
3.0.0-rc.35
3.0.0-rc.34
3.0.0-rc.33
3.0.0-rc.32
3.0.0-rc.31
3.0.0-rc.29
3.0.0-rc.28
3.0.0-rc.27
3.0.0-rc.26
3.0.0-rc.25
3.0.0-rc.24
3.0.0-rc.23
3.0.0-rc.22
3.0.0-rc.21
3.0.0-rc.20
3.0.0-rc.19
3.0.0-rc.18
3.0.0-rc.17
3.0.0-rc.16
3.0.0-rc.15
3.0.0-rc.14
3.0.0-rc.13
3.0.0-rc.12
3.0.0-rc.11
3.0.0-rc.10
3.0.0-rc.9
3.0.0-rc.8
3.0.0-rc.7
3.0.0-rc.6
3.0.0-rc.5
3.0.0-rc.4
3.0.0-rc.3
3.0.0-rc.1
3.0.0-rc.0
2.21.15
2.21.14
2.21.13
2.21.12
2.21.11
2.21.10
2.21.9
2.21.8
2.21.7
2.21.6
2.21.5
2.21.4
2.21.3
2.21.2
2.21.1
2.21.0
2.20.3
2.20.2
2.20.1
2.20.0
2.19.14
2.19.13
2.19.12
2.19.11
2.19.10
2.19.9
2.19.8
2.19.7
2.19.6
2.19.5
2.19.4
2.19.3
retired
2.19.2
retired
2.19.1
retired
2.19.0
retired
2.18.2
2.18.1
2.18.0
2.17.24
2.17.23
2.17.22
2.17.21
2.17.20
2.17.19
2.17.18
2.17.17
2.17.16
2.17.15
2.17.14
2.17.13
2.17.12
2.17.11
2.17.10
2.17.9
2.17.8
2.17.7
2.17.6
2.17.5
2.17.4
2.17.3
2.17.2
2.17.1
2.17.0
2.16.1
2.16.0
2.15.20
2.15.19
2.15.18
2.15.17
2.15.16
2.15.15
2.15.14
2.15.13
2.15.12
2.15.11
2.15.10
2.15.9
2.15.8
2.15.7
2.15.6
2.15.5
2.15.4
2.15.2
2.15.1
2.15.0
2.14.21
2.14.20
2.14.19
2.14.18
2.14.17
2.14.16
2.14.15
2.14.14
2.14.13
2.14.12
2.14.11
2.14.10
2.14.9
2.14.8
2.14.7
2.14.6
2.14.5
2.14.4
2.14.3
2.14.2
2.14.1
2.14.0
2.13.4
retired
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.11
2.11.10
2.11.9
2.11.8
2.11.7
2.11.6
2.11.5
2.11.4
2.11.3
2.11.2
2.11.1
2.11.0
2.11.0-rc.3
2.11.0-rc.2
2.11.0-rc.1
2.11.0-rc.0
2.10.2
2.10.1
2.10.0
2.9.29
2.9.28
2.9.27
2.9.26
2.9.25
2.9.24
2.9.23
2.9.22
2.9.21
2.9.20
2.9.19
2.9.18
2.9.17
2.9.16
2.9.15
2.9.14
2.9.13
2.9.12
2.9.11
2.9.10
2.9.9
2.9.8
2.9.7
2.9.6
2.9.5
2.9.4
2.9.3
2.9.2
2.9.1
2.9.0
2.8.1
2.8.0
2.7.1
2.7.0
2.6.31
2.6.30
2.6.29
2.6.28
2.6.27
2.6.26
2.6.25
2.6.24
2.6.23
2.6.22
2.6.21
2.6.20
2.6.19
2.6.18
2.6.17
2.6.16
2.6.15
2.6.14
2.6.13
2.6.11
2.6.10
2.6.9
2.6.8
2.6.7
2.6.6
2.6.5
2.6.4
2.6.3
2.6.2
2.6.1
2.6.0
2.5.16
2.5.15
2.5.14
2.5.13
2.5.12
2.5.11
2.5.10
2.5.9
2.5.8
2.5.7
2.5.6
2.5.5
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.5.0-rc.6
2.5.0-rc.5
2.5.0-rc.4
2.5.0-rc.3
2.5.0-rc.2
2.5.0-rc.1
2.5.0-rc.0
2.4.30
2.4.29
2.4.28
2.4.27
2.4.26
2.4.25
2.4.24
2.4.23
2.4.22
2.4.21
2.4.20
2.4.19
2.4.18
2.4.17
2.4.16
2.4.15
2.4.14
2.4.13
2.4.12
2.4.11
2.4.10
2.4.9
2.4.8
2.4.7
2.4.6
2.4.5
2.4.4
2.4.3
2.4.2
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.0
2.0.0-rc.15
2.0.0-rc.14
2.0.0-rc.13
2.0.0-rc.12
2.0.0-rc.11
2.0.0-rc.10
2.0.0-rc.9
2.0.0-rc.8
2.0.0-rc.7
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
2.0.0-pre.8
2.0.0-pre.7
2.0.0-pre.6
2.0.0-pre.5
2.0.0-pre.4
2.0.0-pre.3
2.0.0-pre.2
2.0.0-pre.1
2.0.0-pre.0
1.53.3
1.53.2
1.53.0
1.52.0-rc.22
1.52.0-rc.21
1.52.0-rc.20
1.52.0-rc.19
1.52.0-rc.18
1.52.0-rc.17
1.52.0-rc.16
1.52.0-rc.15
1.52.0-rc.14
1.52.0-rc.13
1.52.0-rc.12
1.52.0-rc.11
1.52.0-rc.10
1.52.0-rc.9
1.52.0-rc.8
1.52.0-rc.7
1.52.0-rc.6
1.52.0-rc.5
1.52.0-rc.4
1.52.0-rc.3
1.52.0-rc.2
1.52.0-rc.1
1.52.0-rc.0
1.51.2
1.51.1
retired
1.51.0
1.50.21
1.50.20
1.50.19
1.50.18
1.50.17
1.50.16
1.50.15
1.50.14
1.50.13
1.50.12
1.50.11
1.50.10
1.50.9
1.50.8
1.50.7
1.50.6
1.50.5
1.50.4
1.50.3
1.50.2
1.50.1
1.50.0
1.49.0
1.48.0-rc.30
1.48.0-rc.29
1.48.0-rc.28
1.48.0-rc.27
1.48.0-rc.26
1.48.0-rc.25
1.48.0-rc.24
1.48.0-rc.23
1.48.0-rc.22
1.48.0-rc.21
1.48.0-rc.20
1.48.0-rc.19
1.48.0-rc.18
1.48.0-rc.17
1.48.0-rc.16
1.48.0-rc.15
1.48.0-rc.14
1.48.0-rc.13
1.48.0-rc.12
1.48.0-rc.11
1.48.0-rc.10
1.48.0-rc.9
1.48.0-rc.8
1.48.0-rc.7
1.48.0-rc.6
1.48.0-rc.5
1.48.0-rc.4
1.48.0-rc.3
1.48.0-rc.2
1.48.0-rc.1
1.48.0-rc.0
1.47.12
1.47.11
1.47.10
1.47.9
1.47.8
1.47.7
1.47.6
1.47.5
1.47.4
1.47.3
1.47.2
1.47.1
1.47.0
1.46.13
1.46.12
1.46.11
1.46.10
1.46.9
1.46.8
1.46.7
1.46.6
1.46.5
1.46.4
1.46.3
1.46.2
1.46.1
1.46.0
1.45.0-rc9
1.45.0-rc8
1.45.0-rc7
1.45.0-rc6
1.45.0-rc5
1.45.0-rc4
1.45.0-rc3
1.45.0-rc20
1.45.0-rc2
1.45.0-rc19
1.45.0-rc18
1.45.0-rc17
1.45.0-rc16
1.45.0-rc15
1.45.0-rc14
1.45.0-rc13
1.45.0-rc12
1.45.0-rc11
1.45.0-rc10
1.45.0-rc1
1.45.0-rc0
1.44.13
1.44.12
1.44.11
1.44.10
1.44.9
1.44.8
1.44.7
1.44.6
1.44.5
1.44.4
1.44.3
1.44.2
1.44.1
1.44.0
1.43.12
1.43.11
1.43.10
1.43.9
1.43.8
1.43.7
1.43.6
1.43.5
1.43.4
1.43.3
1.43.2
1.43.1
1.43.0
1.42.0
1.41.12
1.41.11
1.41.10
1.41.9
1.41.8
1.41.7
1.41.6
1.41.5
1.41.4
1.41.3
1.41.2
1.41.1
1.41.0
1.40.0
1.39.7
1.39.6
1.39.5
1.39.4
1.39.3
1.39.2
1.39.1
1.39.0
1.38.0
1.37.2
1.37.1
1.37.0
1.36.22
1.36.21
1.36.19
1.36.18
1.36.17
1.36.16
1.36.15
1.36.14
1.36.13
1.36.12
1.36.11
1.36.10
1.36.9
1.36.8
1.36.7
1.36.6
1.36.5
1.36.4
1.36.3
1.36.2
1.36.0
1.35.1
1.35.0
1.34.9
1.34.8
1.34.7
1.34.6
1.34.5
1.34.4
1.34.3
1.34.2
1.34.1
1.34.0
1.33.0
1.32.2
1.32.1
1.32.0
1.31.1
1.31.0
1.30.2
1.30.1
1.29.0-rc1
1.29.0-rc0
1.28.1
1.28.0
1.27.1
1.27.0
1.26.13
1.26.12
1.26.11
1.26.10
1.26.9
1.26.8
1.26.7
1.26.6
1.26.5
1.26.4
1.26.2
1.26.1
1.26.0
1.25.8
1.25.7
1.25.6
1.25.5
1.25.4
1.25.3
1.25.2
1.25.1
1.25.0
1.24.2
1.24.1
1.24.0
1.23.3
1.23.2
1.23.1
1.23.0
1.22.1
1.22.0
1.20.1
1.20.0
1.19.1
1.19.0
1.18.1
1.18.0
1.17.1
1.17.0
1.16.2
1.15.1
1.15.0
1.14.0
1.13.4
1.13.3
1.13.2
1.13.1
1.13.0
1.12.0
1.11.1
1.11.0
1.10.0
1.9.0
1.8.0
1.7.0
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.1
1.4.0
1.3.1
1.3.0
1.2.1
1.2.0
1.1.3
1.1.2
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
0.13.1
0.13.0
0.12.0
0.10.0
0.9.1
0.9.0
0.8.0
0.7.0
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.9
0.1.8
0.1.3
0.1.1
0.1.0
A declarative, extensible framework for building Elixir applications.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/ash/type/type.ex
defmodule Ash.Type do
@array_constraints [
min_length: [
type: :non_neg_integer,
doc: "A minimum length for the items"
],
items: [
type: :any,
doc: "A schema for individual items"
],
max_length: [
type: :non_neg_integer,
doc: "A maximum length for the items"
],
nil_items?: [
type: :boolean,
doc: "Whether or not the list can contain nil items",
default: false
],
empty_values: [
type: {:list, :any},
doc: "A set of values that, if encountered, will be considered an empty list.",
default: [""]
]
]
@builtin_short_names [
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
]
@custom_short_names Application.compile_env(:ash, :custom_types, [])
@short_names @custom_short_names ++ @builtin_short_names
@doc_array_constraints Keyword.put(@array_constraints, :items,
type: :any,
doc:
"Constraints for the elements of the list. See the contained type's docs for more."
)
@moduledoc """
The `Ash.Type` behaviour is used to define a value type in Ash.
## Built in types
#{Enum.map_join(@builtin_short_names, fn {key, module} -> "* `#{inspect(key)}` - `#{inspect(module)}`\n" end)}
## Lists/Arrays
To specify a list of values, use `{:array, Type}`. Arrays are special, and have special constraints:
#{Spark.Options.docs(@doc_array_constraints)}
## 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
```Elixir
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:
```Elixir
config :ash, :custom_types, [ash_float: GenTracker.AshFloat]
```
Doing this will require a recompilation of the `:ash` dependency which can be triggered by calling:
```bash
$ 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:
```elixir
Ash.Query.filter(expr(coordinates[:x] == 1))k
```
And you can also *construct* composite types in expressions, for example:
```elixir
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 the 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:
```bash
$ 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:
```elixir
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:
```elixir
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:
```elixir
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",
...
>
```
"""
@type constraints :: Keyword.t()
@type constraint_error :: String.t() | {String.t(), Keyword.t()}
@type t :: atom | {:array, atom}
@type error :: :error | {:error, String.t() | Keyword.t()}
@type load_context :: %{
domain: Ash.Domain.t(),
actor: term() | nil,
tenant: term(),
tracer: list(Ash.Tracer.t()) | Ash.Tracer.t() | nil,
authorize?: boolean | nil
}
@type merge_load_context :: %{
domain: Ash.Domain.t(),
calc_name: term(),
calc_load: term(),
calc_path: list(atom),
relationship_path: list(atom),
initial_data: {:ok, list(Ash.Resource.record())} | :error
}
@callback storage_type() :: Ecto.Type.t()
@callback storage_type(constraints) :: Ecto.Type.t()
@callback include_source(constraints, Ash.Changeset.t()) :: constraints
@doc """
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.
"""
@callback operator_overloads() :: %{optional(atom) => %{optional(term) => module()}}
@doc """
The implementation for any overloaded implementations.
"""
@callback evaluate_operator(term) :: {:known, term} | :unknown | {:error, term()}
@doc """
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 init(constraints) :: {:ok, constraints} | {:error, Ash.Error.t()}
@callback cast_in_query?(constraints) :: boolean
@callback can_load?(constraints) :: boolean
@callback ecto_type() :: Ecto.Type.t()
@callback cast_input(term, constraints) ::
{:ok, term} | error()
@callback cast_input_array(list(term), constraints) :: {:ok, list(term)} | error()
@callback cast_stored(term, constraints) :: {:ok, term} | error()
@callback cast_stored_array(list(term), constraints) ::
{:ok, list(term)} | error()
@callback dump_to_native(term, constraints) :: {:ok, term} | error()
@callback dump_to_native_array(list(term), constraints) :: {:ok, term} | error()
@callback dump_to_embedded(term, constraints) :: {:ok, term} | :error
@callback dump_to_embedded_array(list(term), constraints) :: {:ok, term} | error()
@callback handle_change(old_term :: term, new_term :: term, constraints) ::
{:ok, term} | error()
@callback composite?(constraints) :: boolean
@callback composite_types(constraints) ::
list({name, type, constraints} | {name, storage_key, type, constraints})
when name: atom, type: t, storage_key: atom
@callback handle_change_array(old_term :: list(term), new_term :: list(term), constraints) ::
{:ok, term} | error()
@callback prepare_change(old_term :: term, new_uncasted_term :: term, constraints) ::
{:ok, term} | error()
@callback prepare_change_array(
old_term :: list(term),
new_uncasted_term :: list(term),
constraints
) ::
{:ok, term} | error()
@callback constraints() :: constraints()
@callback array_constraints() :: constraints()
@callback apply_constraints(term, constraints) ::
{:ok, new_value :: term}
| :ok
| {:error, constraint_error() | list(constraint_error)}
@callback apply_constraints_array(list(term), constraints) ::
{:ok, new_values :: list(term)}
| :ok
| {:error, constraint_error() | list(constraint_error)}
@callback describe(constraints()) :: String.t() | nil
@callback equal?(term, term) :: boolean
@callback embedded?() :: boolean
@callback generator(constraints) :: Enumerable.t()
@callback simple_equality?() :: boolean
@callback cast_atomic(new_value :: Ash.Expr.t(), constraints) ::
{:atomic, Ash.Expr.t()} | {:error, Ash.Error.t()} | {:not_atomic, String.t()}
@callback cast_atomic_array(new_value :: Ash.Expr.t(), constraints) ::
{:atomic, Ash.Expr.t()} | {:error, Ash.Error.t()} | {:not_atomic, String.t()}
@callback custom_apply_constraints_array?() :: boolean
@callback load(
values :: list(term),
load :: Keyword.t(),
constraints :: Keyword.t(),
context :: load_context()
) ::
{:ok, list(term)} | {:error, Ash.Error.t()}
@callback loaded?(
value :: term,
path_to_load :: list(atom),
constraints :: Keyword.t(),
opts :: Keyword.t()
) :: boolean
@callback merge_load(
left :: term,
right :: term,
constraints :: Keyword.t(),
context :: merge_load_context() | nil
) ::
{:ok, term} | {:error, error} | :error
@type rewrite_data ::
{type :: :calc | :agg, rewriting_name :: atom, rewriting_load :: atom}
| {:rel, rewriting_name :: atom}
@type rewrite :: {{list(atom), rewrite_data, atom, atom}, source :: term}
@callback get_rewrites(
merged_load :: term,
calculation :: Ash.Query.Calculation.t(),
path :: list(atom),
constraints :: Keyword.t()
) :: [rewrite]
@callback rewrite(value :: term, [rewrite], constraints :: Keyword.t()) :: value :: term
@callback prepare_change_array?() :: boolean()
@callback handle_change_array?() :: boolean()
@optional_callbacks [
init: 1,
storage_type: 0,
cast_stored_array: 2,
generator: 1,
cast_input_array: 2,
dump_to_native_array: 2,
handle_change_array: 3,
prepare_change_array: 3,
apply_constraints_array: 2,
array_constraints: 0,
dump_to_embedded: 2,
dump_to_embedded_array: 2,
include_source: 2,
load: 4,
merge_load: 4,
get_rewrites: 4,
rewrite: 3,
operator_overloads: 0,
evaluate_operator: 1
]
@builtin_types Keyword.values(@builtin_short_names)
@doc false
def builtin_types do
@builtin_types
end
@doc "Returns the list of available type short names"
def short_names, do: @short_names
@doc "Returns true if the type is an ash builtin type"
def builtin?(type) when type in @builtin_types, do: true
def builtin?(_), do: false
@doc "Returns true if the type is an embedded resource"
def embedded_type?({:array, type}) do
embedded_type?(type)
end
def embedded_type?(type) do
type = get_type(type)
type.embedded?()
end
@doc "Calls the type's `describe` function with the given constraints"
def describe(type, constraints) do
case get_type(type) do
{:array, type} ->
"#{type.describe(constraints)}[]"
type ->
type.describe(constraints)
end
end
@doc "Gets the array constraints for a type"
def array_constraints({:array, type}) do
[items: array_constraints(type)]
end
def array_constraints(type) do
type.array_constraints()
end
@spec get_type(atom | module | {:array, atom | module}) ::
atom | module | {:array, atom | module}
@doc "Gets the type module for a given short name or module"
def get_type({:array, value}) do
{:array, get_type(value)}
end
for {short_name, value} <- @short_names do
def get_type(unquote(short_name)), do: unquote(value)
end
def get_type(value) when is_atom(value) do
value
end
def get_type(value) do
value
end
@doc "Returns true if the type is a composite type"
@spec composite?(
t(),
constraints
) :: Enumerable.t()
def composite?(type, constraints) do
type = get_type(type)
type.composite?(constraints)
end
@doc "Returns the wrapped composite types"
@spec composite_types(
t(),
constraints
) :: Enumerable.t()
def composite_types(type, constraints) do
type = get_type(type)
type.composite_types(constraints)
end
defp item_constraints(constraints) do
item_constraints = Keyword.get(constraints, :items) || []
case Keyword.fetch(constraints, :__source__) do
{:ok, source} -> Keyword.put(item_constraints, :__source__, source)
:error -> item_constraints
end
end
@doc "Returns the StreamData generator for a given type"
@spec generator(
module | {:array, module},
constraints
) :: Enumerable.t()
def generator(type, constraints) do
do_generator(type, constraints)
end
defp do_generator({:array, type}, constraints) do
generator = do_generator(type, item_constraints(constraints))
generator =
if constraints[:nil_items?] do
StreamData.one_of([StreamData.constant(nil), generator])
else
generator
end
StreamData.list_of(generator, Keyword.take(constraints, [:max_length, :min_length]))
end
defp do_generator(type, constraints) do
type = get_type(type)
Code.ensure_compiled!(type)
if Ash.Type.embedded_type?(type) do
action =
constraints[:create_action] || Ash.Resource.Info.primary_action!(type, :create).name
Ash.Generator.action_input(type, action)
else
type.generator(constraints)
end
end
@doc """
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.
"""
def handle_change({:array, {:array, _type}}, _, new_value, _) do
{:ok, new_value}
end
def handle_change({:array, type}, old_value, new_value, constraints) do
type = get_type(type)
type.handle_change_array(old_value, new_value, item_constraints(constraints))
end
def handle_change(type, old_value, new_value, constraints) do
type = get_type(type)
type.handle_change(old_value, new_value, constraints)
end
@doc """
Process the old casted values alongside the new *un*casted values.
This is leveraged by embedded types to know if something is being updated
or destroyed. This is not called on creates.
"""
# Callback does not currently support this
def prepare_change({:array, {:array, _type}}, _, new_value, _) do
{:ok, new_value}
end
def prepare_change({:array, type}, old_value, new_value, constraints) do
type = get_type(type)
type.prepare_change_array(old_value, new_value, item_constraints(constraints))
end
def prepare_change(type, old_value, new_value, constraints) do
type = get_type(type)
type.prepare_change(old_value, new_value, constraints)
end
@doc """
Initializes the constraints according to the underlying type
"""
@spec init(t(), constraints) :: {:ok, constraints} | {:error, Ash.Error.t()}
def init({:array, type}, constraints) do
item_constraints = item_constraints(constraints)
case init(type, item_constraints) do
{:ok, new_item_constraints} ->
{:ok, Keyword.put(constraints, :items, new_item_constraints)}
{:error, error} ->
{:error, error}
end
end
def init(type, constraints) do
type = get_type(type)
type.init(constraints)
end
@doc """
Returns the *underlying* storage type (the underlying type of the *ecto type* of the *ash type*)
"""
@spec storage_type(t()) :: Ecto.Type.t()
def storage_type(type, constraints \\ [])
def storage_type({:array, type}, constraints), do: {:array, storage_type(type, constraints)}
def storage_type(type, constraints), do: type.storage_type(constraints)
@doc """
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)
"""
@spec ecto_type(t) :: Ecto.Type.t()
def ecto_type({:array, type}), do: {:array, ecto_type(type)}
for {name, mod} <- @short_names do
def ecto_type(unquote(name)), do: ecto_type(unquote(mod))
end
def ecto_type(type) do
if Ash.Resource.Info.resource?(type) do
Module.concat(type, EctoType)
else
type.ecto_type()
end
end
@spec ash_type?(term) :: boolean
@doc "Returns true if the value is a builtin type or adopts the `Ash.Type` behaviour"
def ash_type?({:array, value}), do: ash_type?(value)
def ash_type?(module) when is_atom(module) do
ash_type_module?(module)
end
def ash_type?(_), do: false
@doc """
Casts input (e.g. unknown) data to an instance of the type, or errors
Maps to `Ecto.Type.cast/2`
"""
@spec cast_input(t(), term, constraints | nil) :: {:ok, term} | {:error, Keyword.t()} | :error
def cast_input(type, term, constraints \\ nil)
def cast_input({:array, _type}, term, _)
when not (is_list(term) or is_map(term) or is_nil(term)) do
{:error, "is invalid"}
end
def cast_input(type, term, nil) do
with {:ok, constraints} <- Spark.Options.validate([], Ash.Type.constraints(type)),
{:ok, constraints} <- Ash.Type.init(type, constraints) do
cast_input(type, term, constraints)
end
end
def cast_input({:array, {:array, type}}, term, constraints) do
cond do
is_nil(term) ->
{:ok, nil}
empty?(term, constraints) ->
{:ok, []}
is_list(term) ->
map_while_ok(term, &cast_input({:array, type}, &1, item_constraints(constraints)))
end
end
def cast_input({:array, type}, term, constraints) do
type = get_type(type)
cond do
empty?(term, constraints) ->
{:ok, []}
is_nil(term) ->
{:ok, nil}
true ->
term =
if is_map(term) and not is_struct(term) do
term
|> Enum.sort_by(&elem(&1, 1))
|> Enum.map(&elem(&1, 0))
else
term
end
type.cast_input_array(term, item_constraints(constraints))
end
end
def cast_input(_, nil, _), do: {:ok, nil}
def cast_input(type, %type{__metadata__: _} = value, _), do: {:ok, value}
def cast_input(type, term, constraints) do
type = get_type(type)
case type.cast_input(term, constraints) do
{:ok, value} ->
{:ok, value}
:error ->
case term do
"" ->
cast_input(type, nil, constraints)
_ ->
{:error, "is invalid"}
end
{:error, other} ->
case term do
"" ->
cast_input(type, nil, constraints)
_ ->
{:error, other}
end
end
end
defp empty?(value, constraints) do
value in List.wrap(constraints[:empty_values])
end
defp map_while_ok(term, func) do
Enum.reduce_while(term, {:ok, []}, fn item, {:ok, acc} ->
case func.(item) do
{:ok, result} -> {:cont, {:ok, [result | acc]}}
other -> {:halt, other}
end
end)
|> case do
{:ok, result} -> {:ok, Enum.reverse(result)}
other -> other
end
end
@doc """
Casts a value from the data store to an instance of the type, or errors
Maps to `Ecto.Type.load/2`
"""
@spec cast_stored(t(), term, constraints | nil) :: {:ok, term} | {:error, keyword()} | :error
def cast_stored(type, term, constraints \\ [])
def cast_stored({:array, {:array, type}}, term, constraints) do
if is_nil(term) do
{:ok, nil}
else
map_while_ok(term, &cast_stored({:array, type}, &1, item_constraints(constraints)))
end
end
def cast_stored({:array, type}, term, constraints) do
type = get_type(type)
type.cast_stored_array(term, item_constraints(constraints))
end
def cast_stored(type, term, constraints) do
type = get_type(type)
type.cast_stored(term, constraints)
end
@doc """
Confirms if a casted value matches the provided constraints.
"""
@spec apply_constraints(t(), term, constraints()) :: {:ok, term} | {:error, String.t()}
def apply_constraints({:array, {:array, type}}, term, constraints) do
type = get_type(type)
map_while_ok(term, &apply_constraints({:array, type}, &1, item_constraints(constraints)))
end
def apply_constraints({:array, type}, term, constraints) when is_list(term) do
type = get_type(type)
list_constraint_errors = list_constraint_errors(term, constraints)
item_constraints = item_constraints(constraints)
case list_constraint_errors do
[] ->
nil_items? = Keyword.get(constraints, :nil_items?, false)
term
|> Enum.with_index()
|> Enum.reduce({[], []}, fn {item, index}, {items, errors} ->
if is_nil(item) && not nil_items? do
{[item | items], [[message: "no nil values", index: index] | errors]}
else
if type.custom_apply_constraints_array?() do
{[item | items], errors}
else
case apply_constraints(type, item, item_constraints) do
{:ok, value} ->
{[value | items], errors}
{:error, new_errors} ->
new_errors =
new_errors
|> List.wrap()
|> Ash.Helpers.flatten_preserving_keywords()
|> Enum.map(fn
string when is_binary(string) ->
[message: string, index: index]
vars ->
Keyword.put(vars, :index, index)
end)
{[item | items], List.wrap(new_errors) ++ errors}
end
end
end
end)
|> case do
{terms, []} ->
if type.custom_apply_constraints_array?() do
case type.apply_constraints_array(Enum.reverse(terms), constraints) do
:ok -> {:ok, term}
other -> other
end
else
{:ok, Enum.reverse(terms)}
end
{_, errors} ->
{:error, errors}
end
errors ->
{:error, errors}
end
end
def apply_constraints({:array, _}, nil, _), do: {:ok, nil}
def apply_constraints({:array, _}, _, _) do
{:error, "must be a list"}
end
def apply_constraints(type, term, constraints) do
type = get_type(type)
case type.apply_constraints(term, constraints) do
:ok -> {:ok, term}
other -> other
end
end
@doc false
def list_constraint_errors(term, constraints) do
length =
if Keyword.has_key?(constraints, :max_length) ||
Keyword.has_key?(constraints, :min_length) do
length(term)
else
0
end
constraints
|> Enum.reduce([], fn
{:min_length, min_length}, errors ->
if length < min_length do
[message: "must have %{min} or more items", min: min_length]
else
errors
end
{:max_length, max_length}, errors ->
if length > max_length do
[message: "must have %{max} or fewer items", max: max_length]
else
errors
end
_, errors ->
errors
end)
end
@doc "Returns the constraint schema for a type"
@spec constraints(t()) :: constraints()
def constraints({:array, _type}) do
@array_constraints
end
def constraints(type) do
type = get_type(type)
type.constraints()
end
@doc "Returns `true` if the type should be cast in underlying queries"
def cast_in_query?(type, constraints \\ [])
def cast_in_query?({:array, type}, constraints) do
cast_in_query?(type, item_constraints(constraints))
end
def cast_in_query?(type, constraints) do
type = get_type(type)
type.cast_in_query?(constraints)
end
@doc """
Casts a value from the Elixir type to a value that the data store can persist
Maps to `Ecto.Type.dump/2`
"""
@spec dump_to_native(t(), term, constraints | nil) ::
{:ok, term} | {:error, keyword()} | :error
def dump_to_native(type, term, constraints \\ [])
def dump_to_native({:array, {:array, type}}, term, constraints) do
map_while_ok(term, &dump_to_native({:array, type}, &1, item_constraints(constraints)))
end
def dump_to_native({:array, type}, term, constraints) do
type = get_type(type)
type.dump_to_native_array(term, item_constraints(constraints))
end
def dump_to_native(type, term, constraints) do
type = get_type(type)
type.dump_to_native(term, constraints)
end
@spec cast_atomic(t(), term, constraints()) ::
{:atomic, Ash.Expr.t()} | {:error, Ash.Error.t()} | {:not_atomic, String.t()}
def cast_atomic({:array, {:array, _}}, _term, _constraints),
do: {:not_atomic, "cannot currently atomically update doubly nested arrays"}
def cast_atomic({:array, type}, term, constraints) do
type = get_type(type)
with {:ok, value} <- maybe_cast_input({:array, type}, term, constraints) do
type.cast_atomic_array(value, item_constraints(constraints))
end
end
def cast_atomic(type, term, constraints) do
type = get_type(type)
with {:ok, value} <-
maybe_cast_input(type, term, constraints) do
type.cast_atomic(value, constraints)
end
end
defp maybe_cast_input(type, term, constraints) do
if Ash.Expr.expr?(term) do
{:ok, term}
else
Ash.Type.cast_input(type, term, constraints)
end
end
@doc """
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_embedded(t(), term, constraints | nil) ::
{:ok, term} | {:error, keyword()} | :error
def dump_to_embedded(type, term, constraints \\ [])
def dump_to_embedded({:array, {:array, type}}, term, constraints) do
map_while_ok(term, &dump_to_embedded({:array, type}, &1, item_constraints(constraints)))
end
def dump_to_embedded({:array, type}, term, constraints) do
type = Ash.Type.get_type(type)
type.dump_to_embedded_array(term, item_constraints(constraints))
end
def dump_to_embedded(type, term, constraints) do
type = get_type(type)
type.dump_to_embedded(term, constraints)
end
@doc """
Determines if two values of a given type are equal.
Maps to `Ecto.Type.equal?/3`
"""
@spec equal?(t(), term, term) :: boolean
def equal?({:array, type}, [nil | xs], [nil | ys]), do: equal?({:array, type}, xs, ys)
def equal?({:array, type}, [x | xs], [y | ys]),
do: equal?(type, x, y) && equal?({:array, type}, xs, ys)
def equal?({:array, _}, [], []), do: true
def equal?({:array, _}, _, _), do: false
def equal?(type, left, right) do
type.equal?(left, right)
end
@spec include_source(
t(),
Ash.Changeset.t() | Ash.Query.t() | Ash.ActionInput.t(),
constraints()
) :: constraints()
def include_source({:array, type}, changeset_or_query, constraints) do
Keyword.put(
constraints,
:items,
include_source(type, changeset_or_query, constraints[:items] || [])
)
end
def include_source(type, changeset_or_query, constraints) do
type = get_type(type)
type.include_source(constraints, changeset_or_query)
end
@spec merge_load(
type :: Ash.Type.t(),
left :: term(),
right :: term(),
constraints :: Keyword.t(),
context :: merge_load_context() | nil
) ::
{:ok, list(term)} | :error | {:error, Ash.Error.t()}
def merge_load({:array, type}, left, right, constraints, context) do
merge_load(type, left, right, constraints[:items] || [], context)
end
def merge_load(
type,
left,
right,
constraints,
context
) do
type = Ash.Type.get_type(type)
type.merge_load(left, right, constraints, context)
end
@spec loaded?(
type :: Ash.Type.t(),
value_or_values :: term,
path_to_load :: list(atom),
constraints :: Keyword.t(),
opts :: Keyword.t()
) :: boolean
def loaded?(type, values, load, constraints, opts \\ [])
def loaded?({:array, type}, values, loads, constraints, opts) do
loaded?(type, values, loads, constraints, opts)
end
def loaded?(type, values, loads, constraints, opts) when is_list(values) do
case Keyword.get(opts, :lists, :all) do
:all ->
Enum.all?(values, &loaded?(type, &1, loads, constraints, opts))
:any ->
Enum.any?(values, &loaded?(type, &1, loads, constraints, opts))
end
end
def loaded?(type, value, load_path, constraints, opts) do
type = get_type(type)
type.loaded?(value, load_path, constraints, opts)
end
@spec load(
type :: Ash.Type.t(),
values :: list(term),
load :: Keyword.t(),
constraints :: Keyword.t(),
context :: load_context()
) ::
{:ok, list(term)} | {:error, Ash.Error.t()}
def load(_, [], _, _, _), do: {:ok, []}
def load(_, nil, _, _, _), do: {:ok, nil}
def load(_, %Ash.ForbiddenField{} = value, _, _, _), do: {:ok, value}
def load({:array, type}, values, loads, constraints, context) do
load(type, values, loads, item_constraints(constraints), context)
end
def load(
type,
values,
loads,
constraints,
context
) do
splicing_nil_values(values, fn values ->
type = get_type(type)
if can_load?(type, constraints) do
type.load(values, loads, constraints, context)
else
{:error, Ash.Error.Query.InvalidLoad.exception(load: loads)}
end
end)
end
@doc """
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.
"""
def get_rewrites({:array, type}, merged_load, calculation, path, constraints) do
get_rewrites(type, merged_load, calculation, path, constraints[:items] || [])
end
def get_rewrites(type, merged_load, calculation, path, constraints) do
type = get_type(type)
type.get_rewrites(merged_load, calculation, path, constraints)
end
@doc """
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.
"""
def rewrite(_type, nil, _rewrites, _constraints), do: nil
def rewrite(_type, [], _rewrites, _constraints), do: []
def rewrite({:array, type}, value, rewrites, constraints) when is_list(value) do
item_constraints = constraints[:items] || []
Enum.map(value, fn value ->
rewrite(type, value, rewrites, item_constraints)
end)
end
def rewrite(type, item, rewrites, constraints) when not is_list(item) do
type.rewrite(item, rewrites, constraints)
end
@doc false
def splicing_nil_values(values, callback) when is_list(values) do
values
|> Stream.flat_map(fn value ->
if is_list(value) do
value
else
[value]
end
end)
|> Stream.with_index()
|> Enum.reduce({[], []}, fn
{nil, index}, {acc, nil_indices} ->
{acc, [index | nil_indices]}
{value, _index}, {acc, nil_indices} ->
{[value | acc], nil_indices}
end)
|> then(fn {list, nil_indices} ->
case callback.(list) do
{:ok, new_list} ->
nil_indices = Enum.reverse(nil_indices)
new_list = Enum.reverse(new_list)
{:ok, Enum.reduce(nil_indices, new_list, &List.insert_at(&2, &1, nil))}
{:error, error} ->
{:error, error}
end
end)
end
def splicing_nil_values(value, callback), do: callback.(value)
@doc "Returns true if the type supports nested loads"
@spec can_load?(t(), Keyword.t()) :: boolean
def can_load?(type, constraints \\ [])
def can_load?({:array, type}, constraints), do: can_load?(type, item_constraints(constraints))
def can_load?(type, constraints) do
type = get_type(type)
type.can_load?(constraints)
end
@doc "Prepares a given array of values for an attribute change. Runs before casting."
@spec prepare_change_array?(t()) :: boolean
def prepare_change_array?({:array, type}),
do: prepare_change_array?(type)
def prepare_change_array?(type) do
type = get_type(type)
type.prepare_change_array?()
end
@doc "Handles the change of a given array of values for an attribute change. Runs after casting."
@spec handle_change_array?(t()) :: boolean
def handle_change_array?({:array, type}),
do: handle_change_array?(type)
def handle_change_array?(type) do
type = get_type(type)
type.handle_change_array?()
end
@doc """
Determines if a type can be compared using the `==` operator.
"""
@spec simple_equality?(t()) :: boolean
def simple_equality?({:array, type}), do: simple_equality?(type)
def simple_equality?(type) do
type = get_type(type)
type.simple_equality?()
end
defmacro __using__(opts) do
quote location: :keep, generated: true do
@behaviour Ash.Type
@before_compile Ash.Type
parent = __MODULE__
@doc false
def ash_type?, do: true
defmodule EctoType do
@moduledoc false
@parent parent
@compile {:no_warn_undefined, @parent}
use Ecto.ParameterizedType
@impl true
def init(opts) do
constraints = @parent.constraints()
{:ok, opts} =
opts
|> Keyword.take(Keyword.keys(constraints))
|> @parent.init()
opts
end
@impl true
def type(constraints) do
@parent.storage_type(constraints)
end
@impl true
def cast(term, params) do
@parent.cast_input(term, params)
end
@impl true
def load(term, _, params) do
parent = @parent
case parent.cast_stored(term, params) do
{:ok, value} ->
{:ok, value}
_ ->
:error
end
end
@impl true
def dump(term, _dumper, params) do
parent = @parent
case parent.dump_to_native(term, params) do
{:ok, value} ->
{:ok, value}
_ ->
:error
end
end
@impl true
def equal?(left, right, _params) do
@parent.equal?(left, right)
end
@impl true
def embed_as(_, _), do: :self
end
@impl true
def ecto_type, do: EctoType
@impl true
def constraints, do: []
@impl true
def describe([]), do: String.trim_leading(inspect(__MODULE__), "Ash.Type.")
def describe(constraints) do
"#{String.trim_leading(inspect(__MODULE__), "Ash.Type.")} | #{inspect(constraints)}"
end
@impl true
def apply_constraints(value, _), do: {:ok, value}
@impl true
def cast_in_query?(_), do: true
@impl true
def composite?(_constraints), do: false
@impl true
def composite_types(_constraints), do: []
@impl true
def handle_change(_old_value, new_value, _constraints), do: {:ok, new_value}
@impl true
def prepare_change(_old_value, new_value, _constraints), do: {:ok, new_value}
@impl true
def include_source(constraints, _), do: constraints
@impl true
def array_constraints do
unquote(@array_constraints)
end
@impl true
def merge_load(_, _, _, _), do: :error
@impl true
def embedded? do
unquote(opts[:embedded?] || false)
end
@impl true
def init(constraints), do: {:ok, constraints}
@impl true
def dump_to_embedded(value, constraints) do
dump_to_native(value, constraints)
end
@impl true
def loaded?(_, _, _, _), do: false
@impl true
def cast_input_array(nil, _), do: {:ok, nil}
def cast_input_array(term, single_constraints) do
term
|> Stream.with_index()
|> Enum.reduce_while({:ok, []}, fn {item, index}, {:ok, casted} ->
case Ash.Type.cast_input(__MODULE__, item, single_constraints) do
:error ->
{:halt, {:error, message: "invalid value at %{index}", index: index, path: [index]}}
{:error, keyword} ->
errors =
keyword
|> List.wrap()
|> Ash.Helpers.flatten_preserving_keywords()
|> Enum.map(fn
message when is_binary(message) ->
[message: message, index: index, path: [index]]
error when is_exception(error) ->
error
|> Ash.Error.to_ash_error()
|> Ash.Error.set_path([index])
keyword ->
keyword
|> Keyword.put(:index, index)
|> Keyword.update(:path, [index], &[index | &1])
end)
{:halt, {:error, errors}}
{:ok, value} ->
{:cont, {:ok, [value | casted]}}
end
end)
|> case do
{:ok, result} ->
{:ok, Enum.reverse(result)}
{:error, error} ->
{:error, error}
end
end
@impl true
def cast_stored_array(term, single_constraints) do
if is_nil(term) do
{:ok, nil}
else
term
|> Enum.with_index()
|> Enum.reverse()
|> Enum.reduce_while({:ok, []}, fn {item, index}, {:ok, casted} ->
case Ash.Type.cast_stored(__MODULE__, item, single_constraints) do
:error ->
{:halt, {:error, index: index}}
{:error, keyword} ->
errors =
keyword
|> List.wrap()
|> Ash.Helpers.flatten_preserving_keywords()
|> Enum.map(fn
string when is_binary(string) ->
[message: string, index: index]
vars ->
Keyword.put(vars, :index, index)
end)
{:halt, {:error, errors}}
{:ok, value} ->
{:cont, {:ok, [value | casted]}}
end
end)
end
end
@impl true
def dump_to_native_array(term, single_constraints) do
if is_nil(term) do
{:ok, nil}
else
term
|> Enum.reverse()
|> Enum.reduce_while({:ok, []}, fn item, {:ok, dumped} ->
case Ash.Type.dump_to_native(__MODULE__, item, single_constraints) do
:error ->
{:halt, :error}
{:ok, value} ->
{:cont, {:ok, [value | dumped]}}
end
end)
end
end
@impl true
def dump_to_embedded_array(term, single_constraints) do
if is_nil(term) do
{:ok, nil}
else
term
|> Enum.reverse()
|> Enum.reduce_while({:ok, []}, fn item, {:ok, dumped} ->
case Ash.Type.dump_to_embedded(__MODULE__, item, single_constraints) do
:error ->
{:halt, :error}
{:ok, value} ->
{:cont, {:ok, [value | dumped]}}
end
end)
end
end
@impl true
def cast_atomic(_new_value, _constraints) do
{:not_atomic, "Type `#{inspect(__MODULE__)}` does not support atomic updates"}
end
@impl true
def cast_atomic_array(new_value, constraints) do
new_value
|> Enum.reduce_while({:atomic, []}, fn val, {:atomic, vals} ->
case cast_atomic(val, constraints) do
{:atomic, atomic} ->
{:cont, {:atomic, [atomic | vals]}}
{:not_atomic, reason} ->
{:halt, {:not_atomic, reason}}
end
end)
|> case do
{:atomic, vals} -> {:atomic, Enum.reverse(vals)}
{:not_atomic, reason} -> {:not_atomic, reason}
end
end
@impl true
def generator(constraints) do
raise "generator/1 unimplemented for #{inspect(__MODULE__)}"
end
defoverridable constraints: 0,
init: 1,
include_source: 2,
describe: 1,
generator: 1,
cast_atomic: 2,
cast_atomic_array: 2,
cast_input_array: 2,
dump_to_native_array: 2,
dump_to_embedded: 2,
dump_to_embedded_array: 2,
embedded?: 0,
ecto_type: 0,
merge_load: 4,
array_constraints: 0,
apply_constraints: 2,
cast_stored_array: 2,
handle_change: 3,
loaded?: 4,
composite?: 1,
composite_types: 1,
prepare_change: 3,
cast_in_query?: 1
end
end
defp ash_type_module?(module) do
module.ash_type?()
rescue
_ -> false
end
@doc """
Determine types for a given function or operator.
"""
def determine_types(types, values) do
Enum.map(types, fn types ->
case types do
:same ->
types =
for _ <- values do
:same
end
closest_fitting_type(types, values)
:any ->
for _ <- values do
:any
end
types ->
closest_fitting_type(types, values)
end
end)
|> Enum.filter(fn types ->
Enum.all?(types, &(vagueness(&1) == 0))
end)
|> Enum.map(fn
:any ->
nil
{:array, :any} ->
nil
type ->
type
end)
|> Enum.filter(& &1)
end
defp closest_fitting_type(types, values) do
types_with_values = Enum.zip(types, values)
types_with_values
|> fill_in_known_types()
|> clarify_types()
end
defp clarify_types(types) do
basis =
types
|> Enum.map(&elem(&1, 0))
|> Enum.min_by(&vagueness(&1))
Enum.map(types, fn {type, _value} ->
replace_same(type, basis)
end)
end
defp replace_same({:array, type}, basis) do
{:array, replace_same(type, basis)}
end
defp replace_same(:same, :same) do
:any
end
defp replace_same(:same, {:array, :same}) do
{:array, :any}
end
defp replace_same(:same, basis) do
basis
end
defp replace_same(other, _basis) do
other
end
defp fill_in_known_types(types) do
Enum.map(types, &fill_in_known_type/1)
end
defp fill_in_known_type({vague_type, %Ash.Query.Ref{attribute: %{type: type}}} = ref)
when vague_type in [:any, :same] do
if Ash.Type.ash_type?(type) do
{type || :any, ref}
else
{type, ref}
end
end
defp fill_in_known_type(
{{:array, type}, %Ash.Query.Ref{attribute: %{type: {:array, type}} = attribute} = ref}
) do
{:array, fill_in_known_type({type, %{ref | attribute: %{attribute | type: type}}})}
end
defp fill_in_known_type({type, value}), do: {array_to_in(type), value}
defp array_to_in({:array, v}), do: {:array, array_to_in(v)}
defp array_to_in(v), do: v
defp vagueness({:array, type}), do: vagueness(type)
defp vagueness(:same), do: 2
defp vagueness(:any), do: 1
defp vagueness(_), do: 0
@doc false
def set_type_transformation(%{type: original_type, constraints: constraints} = thing) do
type = get_type(original_type)
ash_type? = Ash.Type.ash_type?(type)
unless ash_type? do
raise """
#{inspect(original_type)} is not a valid type.
Valid types include any custom types, or the following short codes (alongside the types they map to):
#{Enum.map_join(@short_names, "\n", fn {name, type} -> " #{inspect(name)} -> #{inspect(type)}" end)}
"""
end
with {:ok, constraints} <- validate_constraints(type, constraints),
{:ok, constraints} <- Ash.Type.init(type, constraints),
{:ok, thing} <- set_default(thing, type, constraints),
{:ok, thing} <- set_update_default(thing, type, constraints) do
{:ok, %{thing | type: type, constraints: constraints}}
end
end
defp set_default(%{default: {_m, _f, _a}} = thing, _type, _constraints), do: {:ok, thing}
defp set_default(%{default: default} = thing, type, constraints)
when not is_nil(default) and not is_function(default) do
case Ash.Type.cast_input(type, default, constraints) do
{:ok, value} ->
{:ok, %{thing | default: value}}
:error ->
{:error, "Could not cast #{inspect(default)} to #{inspect(type)}"}
{:error, error} ->
{:error, "Could not cast #{inspect(default)} to #{inspect(type)}: #{inspect(error)}"}
end
end
defp set_default(thing, _type, _constraints), do: {:ok, thing}
defp set_update_default(%{update_default: {_m, _f, _a}} = thing, _type, _constraints),
do: {:ok, thing}
defp set_update_default(%{update_default: update_default} = thing, type, constraints)
when not is_nil(update_default) and not is_function(update_default) do
case Ash.Type.cast_input(type, update_default, constraints) do
{:ok, value} ->
{:ok, %{thing | update_default: value}}
:error ->
{:error, "Could not cast #{inspect(update_default)} to #{inspect(type)}"}
{:error, error} ->
{:error,
"Could not cast #{inspect(update_default)} to #{inspect(type)}: #{inspect(error)}"}
end
end
defp set_update_default(thing, _type, _constraints), do: {:ok, thing}
@doc false
def validate_constraints(type, constraints) do
case type do
{:array, type} ->
array_constraints = array_constraints(type)
with {:ok, new_constraints} <-
Spark.Options.validate(
Keyword.delete(constraints || [], :items),
Keyword.delete(array_constraints, :items)
),
{:ok, item_constraints} <- validate_constraints(type, item_constraints(constraints)) do
{:ok, Keyword.put(new_constraints, :items, item_constraints)}
end
type ->
schema = constraints(type)
case Spark.Options.validate(constraints, schema) do
{:ok, constraints} ->
validate_none_reserved(constraints, type)
{:error, error} ->
{:error, error}
end
end
end
@reserved ~w(default source autogenerate read_after_writes virtual primary_key load_in_query redact)a
defp validate_none_reserved(constraints, type) do
case Enum.find(@reserved, &Keyword.has_key?(constraints, &1)) do
nil ->
{:ok, constraints}
key ->
{:error,
"Invalid constraint key #{key} in type #{inspect(type)}. This name is reserved due to the underlying ecto implementation."}
end
end
# Credit to @immutable from elixir discord for the idea
defmacro __before_compile__(_env) do
quote generated: true do
if Module.defines?(__MODULE__, {:equal?, 2}, :def) do
unless Module.defines?(__MODULE__, {:simple_equality, 0}, :def) do
@impl true
def simple_equality?, do: false
end
else
unless Module.defines?(__MODULE__, {:simple_equality, 0}, :def) do
@impl true
def simple_equality?, do: true
end
@impl true
def equal?(left, right), do: left == right
end
if Module.defines?(__MODULE__, {:handle_change_array, 3}, :def) do
@impl true
def handle_change_array?, do: true
else
@impl true
def handle_change_array(_old_value, new_value, _constraints) do
{:ok, new_value}
end
@impl true
def handle_change_array?, do: false
end
if Module.defines?(__MODULE__, {:prepare_change_array, 3}, :def) do
@impl true
def prepare_change_array?, do: true
else
@impl true
def prepare_change_array(_old_value, new_value, _constraints) do
{:ok, new_value}
end
@impl true
def prepare_change_array?, do: false
end
cond do
Module.defines?(__MODULE__, {:storage_type, 0}) &&
Module.defines?(__MODULE__, {:storage_type, 1}) ->
raise "Must only define storage_type/0 or storage_type/1 but not both"
Module.defines?(__MODULE__, {:storage_type, 0}) ->
@impl Ash.Type
def storage_type(_constraints), do: storage_type()
true ->
:ok
end
@impl Ash.Type
if Module.defines?(__MODULE__, {:apply_constraints_array, 2}, :def) do
def custom_apply_constraints_array?, do: true
else
def custom_apply_constraints_array?, do: false
end
unless Module.defines?(__MODULE__, {:can_load?, 1}, :def) do
@impl Ash.Type
if Module.defines?(__MODULE__, {:load, 4}, :def) do
def can_load?(_), do: true
else
def can_load?(_), do: false
end
end
end
end
end