Packages
ash
3.1.0
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/union.ex
defmodule Ash.Type.Union do
@constraints [
storage: [
type: {:one_of, [:type_and_value, :map_with_tag]},
default: :type_and_value,
doc: """
How the value will be stored when persisted.
`:type_and_value` will store the type and value in a map like so `{type: :type_name, value: the_value}`
`:map_with_tag` will store the value directly. This only works if all types have a `tag` and `tag_value` configured.
"""
],
types: [
type: {:custom, __MODULE__, :union_types, []},
doc: """
The types to be unioned, a map of an identifier for the enum value to its configuration.
When using `tag` and `tag_value` we are referring to a map key that must equal a certain value
in order for the value to be considered an instance of that type.
For example:
types: [
int: [
type: :integer,
constraints: [
max: 10
]
],
object: [
type: MyObjectType,
# The default value is `true`
# this passes the tag key/value to the nested type
# when casting input
cast_tag?: true,
tag: :type,
tag_value: "my_object"
],
other_object: [
type: MyOtherObjectType,
cast_tag?: true,
tag: :type,
tag_value: "my_other_object"
],
other_object_without_type: [
type: MyOtherObjectTypeWithoutType,
cast_tag?: false,
tag: :type,
tag_value: nil
]
]
IMPORTANT:
This is stored as a map under the hood. Filters over the data will need to take this into account.
Additionally, if you are not using a tag, a value will be considered to be of the given type if it successfully casts.
This means that, for example, if you try to cast `"10"` as a union of a string and an integer, it will end up as `"10"` because
it is a string. If you put the integer type ahead of the string type, it will cast first and `10` will be the value.
"""
]
]
@impl true
def init(constraints) do
constraints[:types]
|> List.wrap()
|> Enum.reduce_while({:ok, []}, fn {name, config}, {:ok, types} ->
type = config[:type]
constraints = config[:constraints] || []
case Ash.Type.init(type, constraints) do
{:ok, constraints} ->
{:cont, {:ok, [{name, Keyword.put(config, :constraints, constraints)} | types]}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
|> case do
{:ok, types} ->
{:ok, Keyword.put(constraints, :types, Enum.reverse(types))}
{:error, error} ->
{:error, error}
end
end
@doc false
def union_types(value) do
{:ok,
Enum.reduce(value, [], fn {name, config}, types ->
config =
Keyword.update!(config, :type, fn type ->
Ash.Type.get_type(type)
end)
if config[:type] == Ash.Union do
config
|> Keyword.update!(:constraints, fn constraints ->
Keyword.update!(constraints, :types, fn types ->
{:ok, types} = union_types(types)
types
end)
end)
|> Keyword.get(:constraints)
|> Keyword.get(:types)
|> Enum.reduce(types, fn {new_name, new_config}, types ->
if types[new_name] do
raise "Detected a conflict in nested union type names. They must be unique all the way down."
else
Keyword.put(types, new_name, new_config)
end
end)
else
Keyword.put(types, name, config)
end
end)
|> Keyword.new(fn {key, config} ->
type = Ash.Type.get_type(config[:type])
Code.ensure_compiled!(unwrap_type(type))
if !Ash.Type.ash_type?(type) do
raise """
Unknown type in union type \"#{key}\": #{inspect(config[:type])}
"""
end
schema = Ash.Type.constraints(type)
constraints = Spark.Options.validate!(config[:constraints] || [], schema)
{key, config |> Keyword.put(:constraints, constraints) |> Keyword.put(:type, type)}
end)}
end
defp unwrap_type({:array, type}), do: unwrap_type(type)
defp unwrap_type(type), do: type
@moduledoc """
A union between multiple types, distinguished with a tag or by attempting to validate.
## Constraints
#{Spark.Options.docs(@constraints)}
"""
use Ash.Type
@impl true
def constraints, do: @constraints
@impl true
def matches_type?(%Ash.Union{type: type, value: value}, constraints) do
Ash.Type.matches_type?(constraints[:types][type], value)
end
## Find the minimal supported type?
@impl true
def storage_type(_), do: :map
def loaded?(%Ash.Union{type: type, value: value}, path_to_load, constraints, opts) do
config = constraints[:types][type]
if opts[:type] == :request do
case path_to_load do
[:* | rest] ->
Ash.Type.loaded?(config[:type], value, rest, config[:constraints], opts)
[^type | rest] ->
Ash.Type.loaded?(config[:type], value, rest, config[:constraints], opts)
[first | _rest] ->
if Enum.any?(constraints[:types], fn {key, _} -> key == first end) do
true
else
false
end
[] ->
true
end
else
case path_to_load do
[:value | rest] ->
Ash.Type.loaded?(config[:type], value, rest, config[:constraints], opts)
[] ->
true
_ ->
false
end
end
end
@impl true
def load(unions, [], _, _), do: {:ok, unions}
@impl true
def load(unions, load, constraints, context) do
if Enum.any?(constraints[:types], fn {_name, config} ->
Ash.Type.can_load?(config[:type], config[:constraints])
end) do
unions
|> Stream.with_index()
|> Stream.map(fn {item, index} ->
Map.put(item, :__index__, index)
end)
|> Enum.group_by(& &1.type)
|> Enum.reduce_while({:ok, []}, fn {name, values}, {:ok, acc} ->
value_indexes_to_full_index =
values
|> Enum.with_index()
|> Map.new(fn {value, index} ->
{index, value.__index__}
end)
values = Enum.map(values, & &1.value)
our_load =
if load[:*] || load[name] do
List.wrap(load[:*]) ++ List.wrap(load[name])
end
result =
if our_load do
type = constraints[:types][name][:type]
Ash.Type.load(
type,
values,
our_load,
constraints[:types][name][:constraints],
context
)
else
type = constraints[:types][name][:type]
constraints = constraints[:types][name][:constraints]
if Ash.Type.can_load?(type, constraints) do
Ash.Type.load(
type,
values,
[],
constraints,
context
)
else
{:ok, values}
end
end
case result do
{:ok, values} ->
values_with_index =
values
|> Enum.with_index()
|> Enum.map(fn {value, index} ->
Map.put(
%Ash.Union{value: value, type: name},
:__index__,
Map.get(value_indexes_to_full_index, index)
)
end)
{:cont, {:ok, [values_with_index | acc]}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
|> case do
{:ok, batches} ->
{:ok,
batches
|> Stream.flat_map(& &1)
|> Enum.sort_by(& &1.__index__)
|> Enum.map(&Map.delete(&1, :__index__))}
{:error, error} ->
{:error, error}
end
else
{:ok, unions}
end
end
@impl Ash.Type
def merge_load(left, right, constraints, context) do
constraints[:types]
|> Enum.reduce_while({:ok, []}, fn {name, config}, {:ok, acc} ->
merged =
[left[name], right[name], left[:*], right[:*]]
|> Enum.reject(&is_nil/1)
|> case do
[] ->
{:ok, []}
load_statements ->
Enum.reduce_while(load_statements, {:ok, []}, fn load_statement, {:ok, merged} ->
if merged in [nil, [], %{}] do
{:cont, {:ok, load_statement}}
else
case Ash.Type.merge_load(
config[:type],
merged,
load_statement,
config[:constraints],
context
) do
{:ok, merged} -> {:cont, {:ok, merged}}
{:error, error} -> {:halt, {:error, error}}
end
end
end)
end
case merged do
{:ok, empty} when empty in [nil, []] ->
{:cont, {:ok, acc}}
{:ok, merged} ->
{:cont, {:ok, Keyword.put(acc, name, merged)}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
end
@impl Ash.Type
def get_rewrites(merged_load, calculation, path, constraints) do
merged_load
|> Enum.flat_map(fn {key, type_load} ->
if Ash.Type.can_load?(
constraints[:types][key][:type],
constraints[:types][key][:constraints] || []
) do
constraints[:types][key][:type]
|> Ash.Type.get_rewrites(
type_load,
calculation,
path,
constraints[:types][key][:constraints] || []
)
|> Enum.map(fn {{rewrite_path, data, name, load}, source} ->
{{[key | rewrite_path], data, name, load}, source}
end)
else
[]
end
end)
end
@impl Ash.Type
def rewrite(value, rewrites, constraints) do
type_rewrites =
Enum.flat_map(rewrites, fn
{:cleanup_field_auth, further} ->
[{:cleanup_field_auth, further[value.type] || []}]
{{[first | path_rest], data, name, load}, source} ->
if first == value.type do
[{{path_rest, data, name, load}, source}]
else
[]
end
_ ->
[]
end)
Map.update!(
value,
:value,
&Ash.Type.rewrite(
constraints[:types][value.type][:type],
&1,
type_rewrites,
constraints[:types][value.type][:type]
)
)
end
@impl true
def cast_atomic(_new_value, _constraints) do
{:not_atomic, "Unions do not support atomic updates"}
end
@impl true
def cast_input(nil, _), do: {:ok, nil}
def cast_input(%Ash.Union{value: value, type: type_name}, constraints) do
case try_cast_type(value, constraints, type_name, constraints[:types][type_name], [], false) do
{_, {:ok, value}} ->
{:ok, value}
{_, {:expose_error, errors}} ->
{:error, errors}
{_, {:error, error}} ->
{:error, List.flatten(Keyword.values(error))}
end
end
def cast_input(value, constraints) do
types = constraints[:types] || []
types
|> Enum.reduce_while({:error, []}, fn {type_name, config}, {:error, errors} ->
try_cast_type(value, constraints, type_name, config, errors)
end)
|> case do
{:error, errors} when is_binary(errors) ->
{:error, errors}
{:error, errors} ->
{:error, error_message(errors)}
{:expose_error, errors} ->
{:error, errors}
{:ok, value} ->
{:ok, value}
value ->
value
end
end
defp try_cast_type(value, constraints, type_name, config, errors, tags_must_match? \\ true) do
type = config[:type]
if is_map(value) && config[:tag] do
tag_value = config[:tag_value]
value =
if Ash.Type.embedded_type?(type) && !is_struct(value) do
Enum.reduce(Ash.Resource.Info.attributes(type), value, fn attr, value ->
with {:array, _nested_type} <- attr.type,
true <- has_key?(value, attr.name) do
update_key(value, attr.name, fn value ->
if is_map(value) && !is_struct(value) do
Map.values(value)
else
value
end
end)
else
_ ->
value
end
end)
else
value
end
if !tags_must_match? || tags_equal?(tag_value, get_tag(value, config[:tag])) do
value =
if Keyword.get(config, :cast_tag?, true) do
value
else
Map.drop(value, [config[:tag], to_string(config[:tag])])
end
config_constraints =
if Ash.Type.embedded_type?(config[:type]) do
Keyword.put(config[:constraints] || [], :__union_tag__, config[:tag])
else
config[:constraints] || []
end
constraints_with_source =
Ash.Type.include_source(
config[:type],
constraints[:__source__],
config_constraints
)
case Ash.Type.cast_input(
type,
value,
constraints_with_source
) do
{:ok, value} ->
case Ash.Type.apply_constraints(type, value, constraints_with_source) do
{:ok, value} ->
{:halt,
{:ok,
%Ash.Union{
value: value,
type: type_name
}}}
{:error, other} ->
{:halt, {:expose_error, other}}
end
{:error, other} ->
{:halt, {:expose_error, other}}
:error ->
{:halt, {:error, "is not a valid #{type_name}"}}
end
else
{:cont,
{:error,
Keyword.put(errors, type_name, "#{config[:tag]} does not equal #{config[:tag_value]}")}}
end
else
if config[:tag] do
{:cont, {:error, Keyword.put(errors, type_name, "is not a map")}}
else
case Ash.Type.cast_input(type, value, config[:constraints] || []) do
{:ok, value} ->
case Ash.Type.apply_constraints(type, value, config[:constraints] || []) do
{:ok, value} ->
{:halt,
{:ok,
%Ash.Union{
value: value,
type: type_name
}}}
{:error, other} ->
{:cont, {:error, Keyword.put(errors, type_name, other)}}
end
{:error, other} ->
{:cont, {:error, Keyword.put(errors, type_name, other)}}
:error ->
{:cont, {:error, Keyword.put(errors, type_name, "is invalid")}}
end
end
end
end
defp get_tag(map, tag) do
Map.get(map, tag, Map.get(map, to_string(tag)))
end
defp tags_equal?(tag_value, their_tag_value) do
cond do
is_atom(tag_value) ->
their_tag_value == tag_value || their_tag_value == to_string(tag_value)
is_binary(tag_value) && is_atom(their_tag_value) ->
their_tag_value == tag_value || to_string(their_tag_value) == tag_value
true ->
their_tag_value == tag_value
end
end
defp has_key?(map, key) do
Map.has_key?(map, key) || Map.has_key?(map, to_string(key))
end
defp update_key(map, key, func) do
cond do
Map.has_key?(map, key) ->
Map.update!(map, key, func)
Map.has_key?(map, to_string(key)) ->
Map.update!(map, to_string(key), func)
true ->
map
end
end
defp error_message(errors) do
"No union type matched\n" <>
Enum.map_join(errors, "\n", fn {key, errors} ->
" #{key}: #{inspect(errors)}"
end)
end
@impl true
def cast_stored(nil, _), do: {:ok, nil}
def cast_stored(value, constraints) when is_map(value) do
types = constraints[:types] || []
case constraints[:storage] do
:type_and_value ->
case value do
%{"type" => type, "value" => value} ->
type =
if is_binary(type) do
String.to_existing_atom(type)
else
type
end
case Keyword.fetch(types, type) do
{:ok, config} ->
case Ash.Type.cast_stored(config[:type], value, config[:constraints]) do
{:ok, casted_value} ->
{:ok,
%Ash.Union{
value: casted_value,
type: type
}}
other ->
other
end
other ->
other
end
_ ->
:error
end
:map_with_tag ->
case Enum.find(types, fn {_type_name, config} ->
unless config[:tag] && config[:tag_value] do
raise "Found a type without a tag when using the `:map_with_tag` storage constraint. Constraints: #{inspect(constraints)}"
end
their_tag_value = get_tag(value, config[:tag])
tags_equal?(config[:tag_value], their_tag_value)
end) do
nil ->
:error
{type_name, config} ->
case Ash.Type.cast_stored(config[:type], value, config[:constraints]) do
{:ok, casted_value} ->
{:ok,
%Ash.Union{
value: casted_value,
type: type_name
}}
other ->
other
end
end
end
end
def cast_stored(_, _), do: :error
@impl true
def dump_to_native(nil, _), do: {:ok, nil}
def dump_to_native(%Ash.Union{value: value, type: type_name}, union_constraints) do
type = union_constraints[:types][type_name][:type]
if type do
constraints = union_constraints[:types][type_name][:constraints] || []
case union_constraints[:storage] do
:type_and_value ->
case Ash.Type.dump_to_native(type, value, constraints) do
{:ok, value} ->
{:ok, %{"type" => type_name, "value" => value}}
other ->
other
end
:map_with_tag ->
config = union_constraints[:types][type_name]
unless config[:tag] && config[:tag_value] do
raise "Found a type without a tag when using the `:map_with_tag` storage constraint. Constraints: #{inspect(union_constraints)}"
end
Ash.Type.dump_to_native(type, value, constraints)
end
else
:error
end
end
@impl true
def handle_change(nil, %Ash.Union{type: type_name, value: new_value}, constraints),
do: do_handle_change(type_name, nil, new_value, constraints)
def handle_change(%Ash.Union{type: type_name, value: old_value}, nil, constraints),
do: do_handle_change(type_name, old_value, nil, constraints)
def handle_change(
%Ash.Union{type: type_name, value: old_value},
%Ash.Union{type: type_name, value: new_value},
constraints
),
do: do_handle_change(type_name, old_value, new_value, constraints)
def handle_change(
%Ash.Union{},
%Ash.Union{type: type_name, value: new_value},
constraints
),
do: do_handle_change(type_name, nil, new_value, constraints)
defp do_handle_change(type_name, old_value, new_value, constraints) do
with {:ok, type_configs} <- Keyword.fetch(constraints, :types),
{:ok, type_config} <- Keyword.fetch(type_configs, type_name),
{:ok, type} <- Keyword.fetch(type_config, :type),
type_constraints <- Keyword.get(type_config, :constraints, []),
type <- Ash.Type.get_type(type),
{:ok, new_value} <- Ash.Type.handle_change(type, old_value, new_value, type_constraints) do
{:ok, %Ash.Union{type: type_name, value: new_value}}
end
end
@impl true
def prepare_change_array(
old_values,
new_values,
constraints
) do
if Enum.any?(constraints[:types] || [], fn {_name, config} ->
Ash.Type.prepare_change_array?(config[:type])
end) do
old_values_by_type =
old_values
|> List.wrap()
|> Stream.with_index()
|> Stream.map(fn {item, index} ->
Map.put(item, :__index__, index)
end)
|> Enum.group_by(& &1.type, & &1.value)
new_values
|> Stream.with_index()
|> Stream.map(fn {item, index} ->
if is_map(item) do
Map.put(item, :__index__, index)
else
{:untagged, item, index}
end
end)
|> Enum.group_by(
fn
{:untagged, _item, _index} ->
:__ash_untagged_unions__
%Ash.Union{type: type} ->
type
item ->
Enum.find_value(
constraints[:types] || [],
:__ash_untagged_unions__,
fn {name, config} ->
field = config[:tag]
tag = config[:tag_value]
if field && tag && tags_equal?(get_tag(item, field), tag) do
name
end
end
)
end,
fn
%Ash.Union{value: value} = union ->
{:union_value, value, Map.get(union, :__index__)}
other ->
other
end
)
|> Enum.reduce_while({:ok, []}, fn
{:__ash_untagged_unions__, values}, {:ok, acc} ->
{:cont, {:ok, [values | acc]}}
{name, new_values}, {:ok, acc} ->
value_indexes_to_full_index =
new_values
|> Enum.with_index()
|> Map.new(fn
{{:union_value, _value, value_index}, index} ->
{index, value_index}
{value, index} ->
{index, value.__index__}
end)
new_values =
Enum.map(new_values, fn
{:union_value, value, _} ->
case value do
%{__index__: _} = value ->
Map.delete(value, :__index__)
value ->
value
end
value ->
case value do
%{__index__: _} = value ->
Map.delete(value, :__index__)
value ->
value
end
end)
type = constraints[:types][name][:type]
type_constraints = constraints[:types][name][:constraints] || []
type_constraints =
if union_tag = constraints[:types][name][:tag] do
Keyword.put(type_constraints, :__union_tag__, union_tag)
else
type_constraints
end
item_constraints =
Ash.Type.include_source({:array, type}, constraints[:__source__],
items: type_constraints
)
result =
Ash.Type.prepare_change(
{:array, type},
old_values_by_type[name] || [],
new_values,
item_constraints
)
case result do
{:ok, values} ->
values_with_index =
values
|> Enum.with_index()
|> Enum.map(fn {value, index} ->
Map.put(
%Ash.Union{value: value, type: name},
:__index__,
Map.get(value_indexes_to_full_index, index)
)
end)
{:cont, {:ok, [values_with_index | acc]}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
|> case do
{:ok, batches} ->
{:ok,
batches
|> Stream.flat_map(& &1)
|> Enum.sort_by(fn
{:untagged, _item, index} ->
index
item ->
item.__index__
end)
|> Enum.map(fn
{:untagged, item, _} ->
item
item ->
Map.delete(item, :__index__)
end)}
{:error, error} ->
{:error, error}
end
else
{:ok, new_values}
end
end
@impl true
def include_source(constraints, changeset) do
Keyword.put(constraints || [], :__source__, changeset)
end
@impl true
def handle_change_array(
old_values,
new_values,
constraints
) do
if Enum.any?(constraints[:types] || [], fn {_name, config} ->
Ash.Type.handle_change_array?(config[:type])
end) do
old_values_by_type =
old_values
|> List.wrap()
|> Stream.with_index()
|> Stream.map(fn {item, index} ->
Map.put(item, :__index__, index)
end)
|> Enum.group_by(& &1.type, & &1.value)
new_values
|> Stream.with_index()
|> Stream.map(fn {item, index} ->
Map.put(item, :__index__, index)
end)
|> Enum.group_by(& &1.type)
|> Enum.reduce_while({:ok, []}, fn {name, new_values}, {:ok, acc} ->
value_indexes_to_full_index =
new_values
|> Enum.with_index()
|> Map.new(fn {value, index} ->
{index, value.__index__}
end)
new_values = Enum.map(new_values, & &1.value)
type = constraints[:types][name][:type]
type_constraints = constraints[:types][name][:constraints] || []
type_constraints =
if union_tag = constraints[:types][name][:tag] do
Keyword.put(type_constraints, :__union_tag__, union_tag)
else
type_constraints
end
result =
Ash.Type.handle_change(
{:array, type},
old_values_by_type[name] || [],
new_values,
Keyword.put(constraints, :items, type_constraints)
)
case result do
{:ok, values} ->
values_with_index =
values
|> Enum.with_index()
|> Enum.map(fn {value, index} ->
Map.put(
%Ash.Union{value: value, type: name},
:__index__,
Map.get(value_indexes_to_full_index, index)
)
end)
{:cont, {:ok, [values_with_index | acc]}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
|> case do
{:ok, batches} ->
{:ok,
batches
|> Stream.flat_map(& &1)
|> Enum.sort_by(& &1.__index__)
|> Enum.map(&Map.delete(&1, :__index__))}
{:error, error} ->
{:error, error}
end
else
{:ok, new_values}
end
end
@impl true
def prepare_change(_old_value, nil, _constraints), do: {:ok, nil}
def prepare_change(nil, new_value, constraints) do
case cast_input(new_value, constraints) do
{:ok, %Ash.Union{type: type_name, value: value}} ->
do_prepare_change(type_name, nil, value, constraints)
{:error, _} ->
{:ok, new_value}
end
end
def prepare_change(
%Ash.Union{type: type_name, value: old_value},
%Ash.Union{type: type_name, value: new_value},
constraints
) do
with {:ok, type_configs} <- Keyword.fetch(constraints, :types),
{:ok, type_config} <- Keyword.fetch(type_configs, type_name),
{:ok, type} <- Keyword.fetch(type_config, :type),
type_constraints <- Keyword.get(type_config, :constraints, []),
type <- Ash.Type.get_type(type),
{:ok, value} <- type.prepare_change(old_value, new_value, type_constraints) do
{:ok, %Ash.Union{type: type_name, value: value}}
end
end
def prepare_change(
%Ash.Union{},
%Ash.Union{type: type_name, value: new_value},
constraints
) do
with {:ok, type_configs} <- Keyword.fetch(constraints, :types),
{:ok, type_config} <- Keyword.fetch(type_configs, type_name),
{:ok, type} <- Keyword.fetch(type_config, :type),
type_constraints <- Keyword.get(type_config, :constraints, []),
type <- Ash.Type.get_type(type),
{:ok, value} <- type.prepare_change(nil, new_value, type_constraints) do
{:ok, %Ash.Union{type: type_name, value: value}}
end
end
def prepare_change(%Ash.Union{type: type_name, value: old_value}, new_value, constraints)
when is_map(new_value) do
constraints
|> Keyword.get(:types, [])
|> Keyword.get(type_name, [])
|> Map.new()
|> case do
%{tag: field, tag_value: tag} ->
if tags_equal?(get_tag(new_value, field), tag),
do: do_prepare_change(type_name, old_value, new_value, constraints),
else: {:ok, new_value}
_ ->
{:ok, new_value}
end
end
def prepare_change(%Ash.Union{type: type_name, value: old_value}, new_value, constraints) do
case cast_input(new_value, constraints) do
{:ok, %Ash.Union{type: ^type_name, value: value}} ->
do_prepare_change(type_name, old_value, value, constraints)
{:ok, %Ash.Union{type: other_type, value: value}} ->
do_prepare_change(other_type, nil, value, constraints)
{:error, _} ->
{:ok, new_value}
end
end
@impl true
def can_load?(constraints) do
Enum.any?(constraints[:types], fn {_key, config} ->
Ash.Type.can_load?(config[:type], config[:constraints])
end)
end
defp do_prepare_change(type_name, old_value, new_value, constraints) do
with {:ok, type_configs} <- Keyword.fetch(constraints, :types),
{:ok, type_config} <- Keyword.fetch(type_configs, type_name),
{:ok, type} <- Keyword.fetch(type_config, :type),
type_constraints <- Keyword.get(type_config, :constraints, []),
type_constraints <-
Keyword.put(type_constraints, :__union_tag__, type_config[:tag]),
type <- Ash.Type.get_type(type),
{:ok, value} <- type.prepare_change(old_value, new_value, type_constraints) do
{:ok, %Ash.Union{type: type_name, value: value}}
end
end
end