Packages

ash

2.17.24
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
ash lib ash type union.ex
Raw

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,
tag: :type,
tag_value: "my_object"
],
other_object: [
type: MyOtherObjectType,
tag: :type,
tag_value: "my_other_object"
],
other_object_without_type: [
type: MyOtherObjectTypeWithoutType,
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.
"""
]
]
@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.OptionsHelpers.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.OptionsHelpers.docs(@constraints)}
"""
use Ash.Type
@impl true
def constraints, do: @constraints
## Find the minimal supported type?
@impl true
def storage_type(_), do: :map
@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 true
def cast_input(nil, _), do: {:ok, nil}
def cast_input(%Ash.Union{value: value, type: type_name}, constraints) do
type = constraints[:types][type_name][:type]
inner_constraints = constraints[:types][type_name][:constraints] || []
case Ash.Type.cast_input(type, value, inner_constraints) do
{:ok, value} ->
{:ok, %Ash.Union{value: value, type: type_name}}
error ->
error
end
end
def cast_input(value, constraints) do
types = constraints[:types] || []
types
|> Enum.sort_by(fn {_type_name, config} -> config[:tag] end)
|> Enum.reverse()
|> Enum.reduce_while({:error, []}, fn {type_name, config}, {:error, errors} ->
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
Keyword.values(value)
else
value
end
end)
else
_ ->
value
end
end)
else
value
end
their_tag_value = get_tag(value, config[:tag])
tags_equal? = tags_equal?(tag_value, their_tag_value)
if tags_equal? do
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} ->
{: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)
|> 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 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
|> 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, _} ->
value
other ->
other
end)
type = constraints[:types][name][:type]
result =
Ash.Type.prepare_change(
{:array, type},
old_values_by_type[name] || [],
new_values,
items: constraints[:types][name][: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 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
|> 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]
result =
Ash.Type.handle_change(
{:array, type},
old_values_by_type[name] || [],
new_values,
items: constraints[:types][name][: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
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 <- 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