Packages
ash
3.29.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/map.ex
# SPDX-FileCopyrightText: 2019 ash contributors <https://github.com/ash-project/ash/graphs/contributors>
#
# SPDX-License-Identifier: MIT
defmodule Ash.Type.Map do
@constraints [
preserve_nil_values?: [
type: :boolean,
default: false,
doc: """
If set to true, nil values will be preserved both when storing and in the casted map.
Otherwise, keys whose values are `nil` will be omitted.
preserved_nil_values? is false by default
"""
],
fields: [
type: :keyword_list,
keys: [
*: [
type: :keyword_list,
keys: [
type: [
type: Ash.OptionsHelpers.ash_type(),
required: true
],
allow_nil?: [
type: :boolean,
default: true
],
description: [
type: :string
],
constraints: [
type: :keyword_list,
default: []
],
init?: [
type: :boolean,
default: true,
doc: """
If false, the field's type constraints are not initialised at compile time. \
Allows for recursive map fields.
"""
]
]
]
],
doc: """
The types of the fields in the map, and their constraints.
If constraints are specified, only those fields will be in the casted map.
For example:
fields: [
amount: [
type: :integer,
description: "The amount of the transaction",
constraints: [
max: 10
]
],
currency: [
type: :string,
allow_nil?: false,
description: "The currency code of the transaction",
constraints: [
max_length: 3
]
]
]
allow_nil? is true by default
"""
]
]
@moduledoc """
Represents a map stored in the database.
In postgres, for example, this represents binary encoded json
A builtin type that can be referenced via `:map`
### Constraints
#{Spark.Options.docs(@constraints)}
"""
use Ash.Type
import Ash.Gettext
@impl true
def constraints, do: @constraints
@impl true
def storage_type(_), do: :map
@impl true
def referenced_types(constraints), do: Ash.Type.field_referenced_types(constraints[:fields])
@impl true
def init(constraints) do
if is_list(constraints[:fields]) do
constraints[:fields]
|> List.wrap()
|> Enum.reduce_while({:ok, []}, fn {name, config}, {:ok, fields} ->
type = Ash.Type.get_type(config[:type])
constraints = config[:constraints] || []
if Keyword.get(config, :init?, true) do
case Ash.Type.init(type, constraints) do
{:ok, constraints} ->
{:cont,
{:ok,
[{name, Keyword.merge(config, constraints: constraints, type: type)} | fields]}}
{:error, error} ->
{:halt, {:error, error}}
end
else
{:cont, {:ok, [{name, config} | fields]}}
end
end)
|> case do
{:ok, fields} ->
{:ok, Keyword.put(constraints, :fields, Enum.reverse(fields))}
{:error, error} ->
{:error, error}
end
else
if is_nil(constraints[:fields]) do
{:ok, constraints}
else
{:error, "fields must be a list, got `#{constraints[:fields]}`"}
end
end
end
@impl true
def matches_type?(v, _constraints) do
is_map(v)
end
@impl true
def can_load?(constraints) do
case constraints[:fields] do
fields when is_list(fields) ->
Enum.any?(fields, fn {_name, config} ->
inner_type = config[:type]
inner_constraints = config[:constraints] || []
inner_type && Ash.Type.can_load?(inner_type, inner_constraints)
end)
_ ->
false
end
end
@impl true
def load(values, load, constraints, context) when is_list(values) do
fields = constraints[:fields] || []
fields
|> effective_load(load)
|> Enum.reduce_while({:ok, values}, fn {key, sub_load}, {:ok, values} ->
config = Keyword.fetch!(fields, key)
inner_type = config[:type]
inner_constraints = config[:constraints] || []
load_field_across(values, key, inner_type, inner_constraints, sub_load, context)
end)
end
# For non-array inner types we batch the slice across all records and
# let the inner type's load callback see the whole batch at once. For
# `{:array, _}` fields the value at this key is already a list per
# record, so we have to iterate per-record — otherwise the dispatcher's
# `splicing_nil_values` would flatten the per-record arrays into one
# and lose record grouping.
defp load_field_across(
values,
key,
{:array, _} = inner_type,
inner_constraints,
sub_load,
context
) do
values
|> Enum.reduce_while({:ok, []}, fn
%{} = map, {:ok, acc} ->
case Map.get(map, key) do
nil ->
{:cont, {:ok, [map | acc]}}
list when is_list(list) ->
case Ash.Type.load(inner_type, list, sub_load, inner_constraints, context) do
{:ok, loaded} -> {:cont, {:ok, [Map.put(map, key, loaded) | acc]}}
{:error, error} -> {:halt, {:error, error}}
end
other ->
{:cont, {:ok, [Map.put(map, key, other) | acc]}}
end
other, {:ok, acc} ->
{:cont, {:ok, [other | acc]}}
end)
|> case do
{:ok, reversed} -> {:cont, {:ok, Enum.reverse(reversed)}}
{:error, error} -> {:halt, {:error, error}}
end
end
defp load_field_across(values, key, inner_type, inner_constraints, sub_load, context) do
slice =
Enum.map(values, fn
%{} = v -> Map.get(v, key)
_ -> nil
end)
case Ash.Type.load(inner_type, slice, sub_load, inner_constraints, context) do
{:ok, loaded_slice} ->
updated =
values
|> Enum.zip(loaded_slice)
|> Enum.map(fn
{%{} = m, v} -> Map.put(m, key, v)
{other, _v} -> other
end)
{:cont, {:ok, updated}}
{:error, error} ->
{:halt, {:error, error}}
end
end
# Caller's explicit sub-loads union with auto-recursion into any
# declared field whose type is loadable. The auto-added sub-load is
# `[]` — telling inner load callbacks to do their default work
# (e.g. `Ash.Type.Struct.load/4` applies field-policy scrubbing on
# populated values).
defp effective_load(fields, load) do
explicit =
load
|> List.wrap()
|> Enum.flat_map(fn
{k, sub} when is_atom(k) -> [{k, sub}]
k when is_atom(k) -> [{k, []}]
_ -> []
end)
|> Map.new()
Enum.reduce(fields, explicit, fn {field_name, config}, acc ->
cond do
Map.has_key?(acc, field_name) ->
acc
loadable_inner?(config) ->
Map.put(acc, field_name, [])
true ->
acc
end
end)
end
defp loadable_inner?(config) do
type = config[:type]
constraints = config[:constraints] || []
type && Ash.Type.can_load?(type, constraints)
end
@impl true
def cast_input("", _), do: {:ok, nil}
def cast_input(nil, _), do: {:ok, nil}
def cast_input(value, constraints) when is_binary(value) do
case Ash.Helpers.json_module().decode(value) do
{:ok, value} ->
cast_input(value, constraints)
_ ->
:error
end
end
def cast_input(value, _) when is_map(value), do: {:ok, value}
def cast_input(_, _), do: :error
@impl true
def cast_stored(nil, _), do: {:ok, nil}
# stored fields are in the embedded format (see `dump_to_native/2`),
# so they are loaded with `cast_from_embedded`
def cast_stored(value, constraints) when is_map(value) do
cast_fields(value, constraints, &Ash.Type.cast_from_embedded/3)
end
def cast_stored(_, _), do: :error
@impl true
def dump_to_native(nil, _), do: {:ok, nil}
# fields are dumped with `dump_to_embedded` because the dumped map is
# JSON-encoded by data layers, and `dump_to_native` may produce values
# that are not JSON-safe (e.g. raw binaries for `:binary`/`:uuid_v7`)
def dump_to_native(value, constraints) when is_map(value) do
dump_fields(value, constraints, &Ash.Type.dump_to_embedded/3)
end
def dump_to_native(_, _), do: :error
@impl true
def dump_to_embedded(nil, _), do: {:ok, nil}
def dump_to_embedded(value, constraints) when is_map(value) do
dump_fields(value, constraints, &Ash.Type.dump_to_embedded/3)
end
def dump_to_embedded(_, _), do: :error
@impl true
def cast_from_embedded(nil, _), do: {:ok, nil}
def cast_from_embedded(value, constraints) when is_map(value) do
cast_fields(value, constraints, &Ash.Type.cast_from_embedded/3)
end
def cast_from_embedded(_, _), do: :error
defp dump_fields(value, constraints, dump_fn) do
if fields = constraints[:fields] do
nil_values = constraints[:preserve_nil_values?] == true
Enum.reduce_while(fields, {:ok, %{}}, fn {key, config}, {:ok, acc} ->
case dump_field(value, key, config, dump_fn) do
{:ok, nil} when not nil_values -> {:cont, {:ok, acc}}
{:ok, dumped} -> {:cont, {:ok, Map.put(acc, key, dumped)}}
:skip -> {:cont, {:ok, acc}}
other -> {:halt, other}
end
end)
else
{:ok, value}
end
end
defp dump_field(value, key, config, dump_fn) do
case fetch_field(value, key) do
{:ok, field_value} -> dump_fn.(config[:type], field_value, config[:constraints] || [])
:error -> :skip
end
end
defp cast_fields(value, constraints, cast_fn) do
if fields = constraints[:fields] do
nil_values = constraints[:preserve_nil_values?] == true
Enum.reduce_while(fields, {:ok, %{}}, fn {key, config}, {:ok, acc} ->
case fetch_field(value, key) do
{:ok, value} ->
case cast_fn.(config[:type], value, config[:constraints] || []) do
{:ok, value} ->
if is_nil(value) && !nil_values do
{:cont, {:ok, acc}}
else
{:cont, {:ok, Map.put(acc, key, value)}}
end
other ->
{:halt, other}
end
:error ->
{:cont, {:ok, acc}}
end
end)
else
{:ok, value}
end
end
@impl true
def apply_constraints(nil, _constraints), do: {:ok, nil}
def apply_constraints(value, constraints) do
Enum.reduce(constraints, {:ok, value}, fn
{:fields, fields}, {:ok, value} ->
check_fields(value, fields)
{:preserve_nil_values?, _}, errors ->
errors
{_, _}, {:error, errors} ->
{:error, errors}
end)
end
@impl true
def generator(constraints) do
if constraints[:fields] do
optional =
constraints[:fields]
|> Enum.filter(fn {_, value} ->
value[:allow_nil?]
end)
|> Keyword.keys()
constraints[:fields]
|> Map.new(fn {key, config} ->
type = config[:type]
constraints = config[:constraints] || []
generator =
type
|> Ash.Type.generator(constraints)
|> StreamData.filter(fn item ->
with {:ok, value} <- Ash.Type.cast_input(config[:type], item, config[:constraints]),
{:ok, nil} <-
Ash.Type.apply_constraints(config[:type], value, config[:constraints]) do
false
else
_ ->
true
end
end)
{key, generator}
end)
|> Ash.Generator.mixed_map(optional)
else
StreamData.constant(%{})
end
end
defp check_fields(value, fields) do
{errors, result} =
Enum.reduce(fields, {[], %{}}, fn {field, field_constraints}, {errors_acc, result_acc} ->
case fetch_field(value, field) do
{:ok, field_value} ->
case check_field(result_acc, field, field_value, field_constraints) do
{:ok, updated_result} ->
{errors_acc, updated_result}
{:error, field_errors} ->
{errors_acc ++ field_errors, result_acc}
end
:error ->
if field_constraints[:allow_nil?] == false do
field_error = [message: error_message("field must be present"), field: field]
{errors_acc ++ [field_error], result_acc}
else
{errors_acc, result_acc}
end
end
end)
case errors do
[] -> {:ok, result}
_ -> {:error, errors}
end
end
defp check_field(result, field, field_value, field_constraints) do
case Ash.Type.cast_input(
field_constraints[:type],
field_value,
field_constraints[:constraints] || []
) do
{:ok, field_value} ->
case Ash.Type.apply_constraints(
field_constraints[:type],
field_value,
field_constraints[:constraints] || []
) do
{:ok, nil} ->
if field_constraints[:allow_nil?] == false do
{:error, [[message: error_message("value must not be nil"), field: field]]}
else
{:ok, Map.put(result, field, nil)}
end
{:ok, field_value} ->
{:ok, Map.put(result, field, field_value)}
{:error, errors} ->
{:error,
Ash.Type.CompositeTypeHelpers.convert_constraint_errors_to_keyword_lists(
errors,
field
)}
end
{:error, error} ->
{:error, [error]}
:error ->
{:error, [[message: error_message("invalid value"), field: field]]}
end
end
defp fetch_field(map, atom) when is_atom(atom) do
case Map.fetch(map, atom) do
{:ok, value} -> {:ok, value}
:error -> fetch_field(map, to_string(atom))
end
end
defp fetch_field(map, key), do: Map.fetch(map, key)
end