You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I assume this has to do with document symbols in some way.
Triggered by the following file
defmoduleSchematicdo@external_resource"README.md"@moduledoc"README.md"|>File.read!()|>String.split("<!-- MDOC !-->")|>Enum.fetch!(1)defstruct[:unify,:kind,:message,:meta]@typedoc""" The Schematic data structure. This data structure is meant to be opaque to the user, but you can create your own for super niche use cases. But backwards compatiblility of this data structure is not guaranteed. """@opaquet::%__MODULE__{unify: (term(),:up|:down->{:ok,term()}|{:error,String.t()|[String.t()]}),kind: String.t(),message: function()|nil}@typedoc""" A lazy reference to a schematic, used to define recursive schematics. """@typelazy_schematic::{atom(),atom(),list(any())}@typedoc""" A literal schematic. """@typeliteral::any()unlessmacro_exported?(Kernel,:then,2)dodefmacropthen(value,fun)doquotedounquote(fun).(unquote(value))endendenddefmoduleOptionalKeydo@enforce_keys[:key]defstruct[:key]@opaquet::%__MODULE__{key: {any(),any()}|any()}enddefmoduleErrordo@moduledocfalsedefstruct[]end
@doc """
Specifies that the data can be **any**thing.
## Usage
```elixir
iex> schematic = any()
iex> {:ok, "hi!"} = unify(schematic, "hi!")
iex> {:ok, [:one, :two, :three]} = unify(schematic, [:one, :two, :three])
iex> {:ok, true} = unify(schematic, true)
@doc """
Shortcut for specifiying that a schematic can be either null or the schematic.
Usage
iex>schematic=nullable(str())iex>{:ok,nil}=unify(schematic,nil)iex>{:ok,"hi!"}=unify(schematic,"hi!")iex>{:error,"expected either null or a string"}=unify(schematic,:boom)
"""
@spec nullable(t() | lazy_schematic() | literal()) :: t()
def nullable(schematic) do
oneof([nil, schematic])
end
@doc """
Specifies that the data is a boolean or a specific boolean.
Usage
Any boolean.
iex>schematic=bool()iex>{:ok,true}=unify(schematic,true)iex>{:ok,false}=unify(schematic,false)iex>{:error,"expected a boolean"}=unify(schematic,:boom)
"""
@spec float() :: t()
def float() do
message = fn ->
"a float"
end
%Schematic{
kind: "float",
message: message,
unify:
telemetry_wrap(:float, %{}, fn input, _dir ->
if is_float(input) do
{:ok, input}
else
{:error, "expected #{message.()}"}
end
end)
}
end
@doc """
Specifies that the data is a list of any size and contains anything.
Usage
iex>schematic=list()iex>{:ok,["one",2,:three]}=unify(schematic,["one",2,:three])iex>{:error,"expected a list"}=unify(schematic,:hi)
"""
@spec list() :: t()
def list() do
message = fn -> "a list" end
%Schematic{
kind: "list",
message: message,
unify:
telemetry_wrap(:list, %{}, fn input, _dir ->
if is_list(input) do
{:ok, input}
else
{:error, ~s|expected #{message.()}|}
end
end)
}
end
@doc """
Specifies that the data is a list whose items unify to the given schematic.
Lists whose elements do not unify return a list of :ok and :error tuples.
Usage
iex>schematic=list(oneof([str(),int()]))iex>{:ok,["one",2,"three"]}=unify(schematic,["one",2,"three"])iex>{:error,[ok: "one",ok: 2,error: "expected either a string or an integer"]}=unify(schematic,["one",2,:three])
"""
@spec list(t() | lazy_schematic() | literal()) :: t()
def list(schematic) do
schematic = fn ->
case schematic do
{mod, func, args} ->
apply(mod, func, args)
schematic ->
schematic
end
end
message = fn -> "a list of #{schematic.().message.()}" end
%Schematic{
kind: "list",
message: message,
unify:
telemetry_wrap(:list, %{}, fn input, dir ->
if is_list(input) do
Enum.map_reduce(input, [], fn el, acc ->
case schematic.().unify.(el, dir) do
{:ok, output} -> {output, [{:ok, output} | acc]}
{:error, error} -> {%Error{}, [{:error, error} | acc]}
end
end)
|> then(fn {result, errors} ->
if %Error{} in result do
{:error, Enum.reverse(errors)}
else
{:ok, result}
end
end)
else
{:error, ~s|expected a list|}
end
end)
}
end
@doc """
Specifies that the data is a tuple of the given length where each element unifies to the schematic in the same position.
Usage
iex>schematic=tuple([str(),int()])iex>{:ok,{"one",2}}=unify(schematic,{"one",2})iex>{:error,"expected a tuple of {a string, an integer}"}=unify(schematic,{1,"two"})
Options
:from - Either :tuple or :list. Defaults to :tuple.
iex>schematic=tuple([str(),int()],from: :list)iex>{:ok,{"one",2}}=unify(schematic,["one",2])iex>{:error,"expected a list of {a string, an integer}"}=unify(schematic,[1,"two"])
message = fn ->
"a #{from} of {#{Enum.map_join(schematics, ", ", &Schematic.Unification.message/1)}}"
end
{condition, to_list, length} =
case from do
:list ->
{&is_list/1, &Function.identity/1, &Enum.count/1}
:tuple ->
{&is_tuple/1, &Tuple.to_list/1, &tuple_size/1}
end
%Schematic{
kind: "tuple",
message: message,
unify: fn input, dir ->
if condition.(input) and length.(input) == Enum.count(schematics) do
input
|> to_list.()
|> Enum.with_index()
|> Enum.reduce_while({:ok, []}, fn {el, idx}, {:ok, acc} ->
case Schematic.Unification.unify(Enum.at(schematics, idx), el, dir) do
{:ok, output} ->
{:cont, {:ok, [output | acc]}}
{:error, _error} ->
{:halt, {:error, ~s|expected #{message.()}|}}
end
end)
|> then(fn
{:ok, result} ->
{:ok, result |> Enum.reverse() |> List.to_tuple()}
error ->
error
end)
else
{:error, ~s|expected #{message.()}|}
end
end
}
end
@doc """
Specifies that the data is a map with the given keys (literal values) that unify to the provided blueprint.
Unification errors for keys are returned in a map with the key as the key and the value as the error.
Map schematics serve as a way to permit certain keys and discard all others.
Keys are non-nullable unless the value schematic is marked with nullable/1. This allows the value of the key to be nil as well as the key to be absent from the source data.
Keys are considered required unless tagged with optional/1. This allows the entire key to be absent from the source data. If the key is present, it must unify according to the given schematic.
Basic Usage
The most basic map schematic can look like the following.
iex>schematic=map(%{...>"league"=>oneof(["NBA","MLB","NFL"]),...>})iex># ignores the `"team"` keyiex>{:ok,%{"league"=>"NBA"}}==unify(schematic,%{"league"=>"NBA","team"=>"Chicago Bulls"})trueiex>{:error,...>%{...>"league"=>...>~s|expected either "NBA", "MLB", or "NFL"|
...>}}=unify(schematic,%{"league"=>"NHL"})
With a permissive amp
If you want to only check that the data is a map, but not the shape, you can use map/0.
Instead of passing a blueprint, which specifies keys and values, you can pass a :keys and :values options which provide schematic that all keys and values in the input must unify to.
iex>schematic=map(keys: str(),values: oneof([str(),int()]))iex>{:ok,%{"type"=>"big","quantity"=>99}}=unify(schematic,%{"type"=>"big","quantity"=>99})iex>{:error,%{"quantity"=>"expected either a string or an integer"}}=unify(schematic,%{"type"=>"big","quantity"=>[99]})
Transforming Keys
During unification, key transformation can be performed if it is specified in the schematic.
You can specify a key as a 2-tuple with the first element being the input key and the second element being the output key. When calling dump/2, the key will be turned from the output key back to the input key (and will also be revalidated).
This is useful for transforming string keys to atom keys as well as camelCase keys to snake_case keys.
Key transformation can also be used when declaring an optional key with optional/1.
One can define schematics that specify keys whose values are themselves.
For this to be possible, recursive schematics must terminate some way. This can be achienved by specifying those keys as optional/1 or within a oneof/1 schematic.
Recursive schematics are specified as a MFA tuple, t:lazy_schematic/0.
iex> defmoduleTreedo...>importSchematic...>...> defschematic()do...>map(%{values: list(Tree.branch())})...>end...>...> defbranch()do...>map(%{...>values: list(oneof([Tree.leaf(),{__MODULE__,:branch,[]}]))
...>})...>end...>...> defleaf()do...>map(%{...>value: str()
...>})...>end...>endiex>input=%{...>type: "root",...>values: [...>%{...>type: "branch",...>values: [...>%{...>type: "leaf",...>value: "i'm a leaf"
...>},...>%{...>type: "branch",...>values: [...>%{...>type: "leaf",...>value: "i'm another leaf"
...>}
...>]
...>}...>]...>}
...>]
...>}iex>unify(SchematicTest.Tree.schematic(),input){:ok,%{values: [%{values: [%{value: "i'm a leaf"},%{values: [%{value: "i'm another leaf"}]}]}]}}
def map(blueprint) when is_map(blueprint) do
%Schematic{
kind: "map",
message: fn -> "a map" end,
meta: %{blueprint: blueprint},
unify:
telemetry_wrap(:map, %{style: :blueprint}, fn input, dir ->
if is_map(input) do
bp_keys = Map.keys(blueprint)
Enum.reduce(
bp_keys,
[ok: %{}, errors: %{}],
fn bpk, [{:ok, acc}, {:errors, errors}] ->
schematic =
case blueprint[bpk] do
{mod, func, args} ->
apply(mod, func, args)
schematic ->
schematic
end
key = with %OptionalKey{key: key} <- bpk, do: key
{from_key, to_key} = with key when not is_tuple(key) <- key, do: {key, key}
{from_key, to_key} =
case dir do
:to -> {from_key, to_key}
:from -> {to_key, from_key}
end
if not Map.has_key?(input, from_key) and match?(%OptionalKey{}, bpk) do
[{:ok, acc}, {:errors, errors}]
else
case Schematic.Unification.unify(schematic, input[from_key], dir) do
{:ok, output} ->
acc =
acc
|> Map.put(to_key, output)
[{:ok, acc}, {:errors, errors}]
{:error, error} ->
# NOTE: in the case of schemas, an optional key will exist because structs always
# have all of their fields. So if they don't unify **and**, the key is optional, and
# the value is nil, we can assume the schematic did not allow nil, and we can omit
# the key from the dump.
if input[from_key] == nil and match?(%OptionalKey{}, bpk) do
[{:ok, acc}, {:errors, errors}]
else
[{:ok, acc}, {:errors, Map.put(errors, from_key, error)}]
end
end
end
end
)
|> then(fn
[ok: output, errors: e] when map_size(e) == 0 ->
{:ok, output}
[ok: _output, errors: errors] ->
{:error, errors}
end)
else
{:error, "expected a map"}
end
end)
}
end
def map(opts) when is_list(opts) do
key_schematic = Keyword.get(opts, :keys, any())
value_schematic = Keyword.get(opts, :values, any())
%Schematic{
kind: "map",
message: fn -> "a map" end,
unify:
telemetry_wrap(:map, %{style: :open}, fn input, dir ->
if is_map(input) do
Enum.reduce(
Map.keys(input),
[ok: %{}, errors: %{}],
fn input_key, [{:ok, acc}, {:errors, errors}] ->
case key_schematic.unify.(input_key, dir) do
{:ok, key_output} ->
case value_schematic.unify.(input[input_key], dir) do
{:ok, value_output} ->
[{:ok, Map.put(acc, key_output, value_output)}, {:errors, errors}]
{:error, error} ->
[{:ok, acc}, {:errors, Map.put(errors, input_key, error)}]
end
{:error, _error} ->
# NOTE: we pass just ignore keys which non conforming keys
[{:ok, acc}, {:errors, errors}]
end
end
)
|> then(fn
[ok: output, errors: e] when map_size(e) == 0 ->
{:ok, output}
[ok: _output, errors: errors] ->
{:error, errors}
end)
else
{:error, "expected a map"}
end
end)
}
end
@doc """
Map schematics can be extended by using map/2.
"""
@spec map(t(), map()) :: t()
def map(schematic, blueprint) do
map(Map.merge(schematic.meta.blueprint, blueprint))
end
@doc """
Specifies a map/1 schematic that is then hydrated into a struct.
Works the same as the map/1 schematic, but will also automatically transform all keys from string keys to atom keys if a key conversion is not already specified.
Since this schematic hydrates a struct, it is also only capable of having atom keys in the output, whereas a normal map can have arbitrary terms as the key.
{k, v} when is_atom(k) ->
{{to_string(k), k}, v}
kv ->
kv
end)
)
%Schematic{
kind: "#{mod}",
message: fn -> "a %#{String.replace(to_string(mod), "Elixir.", "")}{}" end,
unify:
telemetry_wrap(:schema, %{mod: mod}, fn input, dir ->
case dir do
:to ->
with {:ok, output} <- schematic.unify.(input, :to) do
{:ok, struct(mod, output)}
end
:from ->
with {:ok, input} <- struct?(input, mod),
{:ok, output} <- schematic.unify.(Map.from_struct(input), :from) do
{:ok, output}
end
end
end)
}
end
defp struct?(%struct{} = input, mod) when struct == mod do
{:ok, input}
end
defp struct?(_input, mod) do
{:error, "expected a #{mod} struct"}
end
@doc """
A utility for creating custom schematics.
The raw/1 schematic is useful for creating schematics that unify the values of the inputs, rather than just the shape.
Options
:message - a custom error message. Defaults to "is invalid".
:transformer - a function that takes the input and the unification direction and must return the desired value. Defaults to fn input, _dir -> input end.
Basic Usage
iex>schematic=all([int(),raw(fni->i>10end,message: "must be greater than 10")])iex>{:ok,11}=unify(schematic,11)iex>{:error,["must be greater than 10"]}=unify(schematic,9)
Advanced Usage
If your data requires different validations for unification and dumping, then you can pass a 2-arity function (instead of a 1-arity function) and the second parameter will be the direction.
This concept also applies to the :transform option.
iex>schematic =
...> raw(...> fn...>n,:to->is_list(n)andlength(n)==3...>n,:from->is_tuple(n)andtuple_size(n)==3...>end,...> message: "must be a tuple of size 3",...> transform: fn...>input,:to->...>List.to_tuple(input)...>input,:from->...>Tuple.to_list(input)...>end...>)iex>{:ok,{"one","two",3}}=unify(schematic,["one","two",3])iex>{:error,"must be a tuple of size 3"}=unify(schematic,["not","big"])iex>{:ok,["one","two",3]}=dump(schematic,{"one","two",3})
%Schematic{
kind: "function",
message: message,
unify:
telemetry_wrap(:raw, %{}, fn input, dir ->
if convert_to_two_arity(function).(input, dir) do
{:ok, convert_to_two_arity(transformer).(input, dir)}
else
{:error, message.()}
end
end)
}
end
defp convert_to_two_arity(f) when is_function(f, 1) do
fn a, _ -> f.(a) end
end
defp convert_to_two_arity(f) when is_function(f, 2) do
f
end
@doc """
Specifies that the data must unify with all of the given schematics.
On error, returns a list of validation messages.
If a schematic raises an exception, it is caught and the error "is invalid" is returned.
iex>schematic=all([int(),raw(&Kernel.<(&1,10),message: "must be less than 10"),raw(&(Kernel.rem(&1,2)==0),message: "must be divisible by 2")])iex>{:ok,8}=unify(schematic,8)iex>{:error,["must be less than 10","must be divisible by 2"]}=unify(schematic,15)iex>{:error,["expected an integer","must be less than 10","is invalid"]}=unify(schematic,"15")
"""
@spec all([t()]) :: t()
def all(schematics) when is_list(schematics) do
message = fn -> Enum.map(schematics, & &1.message) end
%Schematic{
kind: "all",
message: message,
unify:
telemetry_wrap(:all, %{}, fn input, dir ->
errors =
for schematic <- schematics,
{result, message} =
__try__(fn -> Schematic.Unification.unify(schematic, input, dir) end),
result == :error do
message
end
if Enum.empty?(errors) do
{:ok, input}
else
{:error, errors}
end
end)
}
end
defp try(callback) do
callback.()
rescue
_ ->
{:error, "is invalid"}
end
@doc """
Specifies that the data unifies to one of the given schematics.
Can be called with a list of schematics or a function.
With a list
When called with a list of schematics, they will be traversed during unification and the first one to unify will be returned. If none of them unify, then an error is returned.
iex>team=map(%{name: str(),league: str()})iex>player=map(%{name: str(),team: str()})iex>schematic=oneof([team,player])iex>{:ok,%{name: "Indiana Pacers",league: "NBA"}}=unify(schematic,%{name: "Indiana Pacers",league: "NBA"})iex>{:ok,%{name: "George Hill",team: "Indiana Pacers"}}=unify(schematic,%{name: "George Hill",team: "Indiana Pacers"})iex>{:error,"expected either a map or a map"}=unify(schematic,%{name: "NBA",sport: "basketball"})
With a function
When called with a function, the input is passed as the only parameter. This can be used to dispach to a specific schematic. This is a performance optimization, as you can dispatch to a specific schematic rather than traversing all of them.
iex>schematic=oneof(fn...>%{type: "team"}->map(%{name: str(),league: str()})...>%{type: "player"}->map(%{name: str(),team: str()})...>_->{:error,"expected either a player or a team"}...>end)iex>{:ok,%{name: "Indiana Pacers",league: "NBA"}}=unify(schematic,%{type: "team",name: "Indiana Pacers",league: "NBA"})iex>{:ok,%{name: "George Hill",team: "Indiana Pacers"}}=unify(schematic,%{type: "player",name: "George Hill",team: "Indiana Pacers"})iex>{:error,"expected either a player or a team"}=unify(schematic,%{name: "NBA",sport: "basketball"})
"""
@spec oneof([t() | lazy_schematic() | literal()] | (any -> t() | literal())) :: t()
def oneof(schematics) when is_list(schematics) do
message = fn ->
"either #{sentence_join(schematics, "or", &Schematic.Unification.message/1)}"
end
%Schematic{
kind: "oneof",
message: message,
unify:
telemetry_wrap(:oneof, %{style: :sequential}, fn input, dir ->
inquiry =
Enum.find_value(schematics, fn schematic ->
schematic =
case schematic do
{mod, func, args} -> apply(mod, func, args)
schematic -> schematic
end
with {:error, _} <- Schematic.Unification.unify(schematic, input, dir), do: false
end)
with nil <- inquiry, do: {:error, ~s|expected #{message.()}|}
end)
}
end
def oneof(dispatch) when is_function(dispatch) do
%Schematic{
kind: "oneof:dispatch",
unify:
telemetry_wrap(:oneof, %{style: :dispatch}, fn input, dir ->
with %Schematic{} = schematic <- dispatch.(input) do
Schematic.Unification.unify(schematic, input, dir)
end
end)
}
end
defp sentence_join(items, joiner, mapper) do
length = length(items)
item_joiner = if length > 2, do: ", ", else: " "
@doc """
Unify external data with your internal data structures.
See all the other functions for information on how to create schematics.
"""
@spec unify(t() | literal(), any()) :: any()
def unify(schematic, input) do
Schematic.Unification.unify(schematic, input, :to)
end
@doc """
Dump your internal data to their external data structures.
See all the other functions for information on how to create schematics.
"""
@spec dump(t() | literal(), any()) :: any()
def dump(schematic, input) do
Schematic.Unification.unify(schematic, input, :from)
end
defp telemetry_wrap(type, metadata, func) do
fn input, dir ->
metadata = Map.merge(%{kind: type, dir: dir}, metadata)
:telemetry.span([:schematic, :unify], metadata, fn ->
result = func.(input, dir)
{result, metadata}
end)
end
end
@doc """
See map/1 for examples and explanation.
"""
@spec optional(any()) :: OptionalKey.t()
def optional(key) do
%OptionalKey{key: key}
end
end
</pre>
</details>
The text was updated successfully, but these errors were encountered:
I assume this has to do with document symbols in some way.
Triggered by the following file
:from
- Either:tuple
or:list
. Defaults to:tuple
."""
@spec tuple([t() | lazy_schematic() | literal()], Keyword.t()) :: t()
def tuple(schematics, opts \ []) do
from = Keyword.get(opts, :from, :tuple)
end
@doc """
Specifies that the data is a map with the given keys (literal values) that unify to the provided blueprint.
Unification errors for keys are returned in a map with the key as the key and the value as the error.
nullable/1
. This allows the value of the key to be nil as well as the key to be absent from the source data.optional/1
. This allows the entire key to be absent from the source data. If the key is present, it must unify according to the given schematic.Basic Usage
The most basic map schematic can look like the following.
With a permissive amp
If you want to only check that the data is a map, but not the shape, you can use
map/0
.With
nullable/1
Marking a key as nullable using
nullable/1
.This means the value of the key can be nil as well as omitting the key entirely. The unified output will always contain the key.
With
optional/1
Marking a key as optional using
optional/1
.This means that you can omit the key from the input and that the unified output will not contain the key if it wasn't in the input.
If the key is provided, it must unify according to the given schematic.
Likewise, using
dump/2
will also omit that key.With
:keys
and:values
Instead of passing a blueprint, which specifies keys and values, you can pass a
:keys
and:values
options which provide schematic that all keys and values in the input must unify to.Transforming Keys
During unification, key transformation can be performed if it is specified in the schematic.
You can specify a key as a 2-tuple with the first element being the input key and the second element being the output key. When calling
dump/2
, the key will be turned from the output key back to the input key (and will also be revalidated).This is useful for transforming string keys to atom keys as well as camelCase keys to snake_case keys.
Key transformation can also be used when declaring an optional key with
optional/1
.Recursive Schematics
One can define schematics that specify keys whose values are themselves.
For this to be possible, recursive schematics must terminate some way. This can be achienved by specifying those keys as
optional/1
or within aoneof/1
schematic.Recursive schematics are specified as a MFA tuple,
t:lazy_schematic/0
."""
@typedoc "Map blueprint key."
@type map_blueprint_key :: OptionalKey.t() | any()
@typedoc "Map blueprint value."
@type map_blueprint_value :: t() | lazy_schematic() | literal()
@typedoc """
The blueprint used to specify a map schematic.
"""
@type map_blueprint :: %{map_blueprint_key() => map_blueprint_value()}
@spec map(%{map_blueprint_key() => map_blueprint_value()} | Keyword.t()) :: t()
def map(blueprint_or_opts \ [])
def map(blueprint) when is_map(blueprint) do
%Schematic{
kind: "map",
message: fn -> "a map" end,
meta: %{blueprint: blueprint},
unify:
telemetry_wrap(:map, %{style: :blueprint}, fn input, dir ->
if is_map(input) do
bp_keys = Map.keys(blueprint)
end
def map(opts) when is_list(opts) do
key_schematic = Keyword.get(opts, :keys, any())
value_schematic = Keyword.get(opts, :values, any())
end
@doc """
Map schematics can be extended by using
map/2
."""
@spec map(t(), map()) :: t()
def map(schematic, blueprint) do
map(Map.merge(schematic.meta.blueprint, blueprint))
end
@doc """
Specifies a
map/1
schematic that is then hydrated into a struct.Works the same as the
map/1
schematic, but will also automatically transform all keys from string keys to atom keys if a key conversion is not already specified.Since this schematic hydrates a struct, it is also only capable of having atom keys in the output, whereas a normal map can have arbitrary terms as the key.
"""
@typedoc "Schema blueprint key."
@type schema_blueprint_key :: OptionalKey.t() | atom()
@typedoc "Schema blueprint value."
@type schema_blueprint_value :: t() | lazy_schematic() | literal()
@typedoc """
The blueprint used to specify a schema schematic.
"""
@type schema_blueprint :: %{schema_blueprint_key() => schema_blueprint_value()}
@spec schema(atom(), schema_blueprint()) :: t()
def schema(mod, blueprint) do
schematic =
map(
Map.new(blueprint, fn
{%OptionalKey{key: k}, v} when is_atom(k) ->
{%OptionalKey{key: {to_string(k), k}}, v}
end
defp struct?(%struct{} = input, mod) when struct == mod do
{:ok, input}
end
defp struct?(_input, mod) do
{:error, "expected a #{mod} struct"}
end
@doc """
A utility for creating custom schematics.
The
raw/1
schematic is useful for creating schematics that unify the values of the inputs, rather than just the shape.Options
:message
- a custom error message. Defaults to"is invalid"
.:transformer
- a function that takes the input and the unification direction and must return the desired value. Defaults tofn input, _dir -> input end
.Basic Usage
Advanced Usage
If your data requires different validations for unification and dumping, then you can pass a 2-arity function (instead of a 1-arity function) and the second parameter will be the direction.
This concept also applies to the
:transform
option."""
@spec raw((any() -> boolean()) | (any(), :up | :down -> boolean()), [tuple()]) :: t()
def raw(function, opts \ []) do
message = fn -> Keyword.get(opts, :message, "is invalid") end
transformer = Keyword.get(opts, :transform, fn input, _dir -> input end)
end
defp convert_to_two_arity(f) when is_function(f, 1) do
fn a, _ -> f.(a) end
end
defp convert_to_two_arity(f) when is_function(f, 2) do
f
end
@doc """
Specifies that the data must unify with all of the given schematics.
On error, returns a list of validation messages.
If a schematic raises an exception, it is caught and the error
"is invalid"
is returned."""
@spec all([t()]) :: t()
def all(schematics) when is_list(schematics) do
message = fn -> Enum.map(schematics, & &1.message) end
end
defp try(callback) do
callback.()
rescue
_ ->
{:error, "is invalid"}
end
@doc """
Specifies that the data unifies to one of the given schematics.
Can be called with a list of schematics or a function.
With a list
When called with a list of schematics, they will be traversed during unification and the first one to unify will be returned. If none of them unify, then an error is returned.
With a function
When called with a function, the input is passed as the only parameter. This can be used to dispach to a specific schematic. This is a performance optimization, as you can dispatch to a specific schematic rather than traversing all of them.
"""
@spec oneof([t() | lazy_schematic() | literal()] | (any -> t() | literal())) :: t()
def oneof(schematics) when is_list(schematics) do
message = fn ->
"either #{sentence_join(schematics, "or", &Schematic.Unification.message/1)}"
end
end
def oneof(dispatch) when is_function(dispatch) do
%Schematic{
kind: "oneof:dispatch",
unify:
telemetry_wrap(:oneof, %{style: :dispatch}, fn input, dir ->
with %Schematic{} = schematic <- dispatch.(input) do
Schematic.Unification.unify(schematic, input, dir)
end
end)
}
end
defp sentence_join(items, joiner, mapper) do
length = length(items)
item_joiner = if length > 2, do: ", ", else: " "
end
@doc """
Unify external data with your internal data structures.
See all the other functions for information on how to create schematics.
"""
@spec unify(t() | literal(), any()) :: any()
def unify(schematic, input) do
Schematic.Unification.unify(schematic, input, :to)
end
@doc """
Dump your internal data to their external data structures.
See all the other functions for information on how to create schematics.
"""
@spec dump(t() | literal(), any()) :: any()
def dump(schematic, input) do
Schematic.Unification.unify(schematic, input, :from)
end
defp telemetry_wrap(type, metadata, func) do
fn input, dir ->
metadata = Map.merge(%{kind: type, dir: dir}, metadata)
end
@doc """
See
map/1
for examples and explanation."""
@spec optional(any()) :: OptionalKey.t()
def optional(key) do
%OptionalKey{key: key}
end
end
The text was updated successfully, but these errors were encountered: