defmodule Z.Struct do
@moduledoc """
A module for defining Z structs
"""
defmacro __using__(_) do
quote do
import Z.Struct, only: [schema: 1]
Module.register_attribute(__MODULE__, :z_fields, accumulate: true)
Module.register_attribute(__MODULE__, :z_enforced_fields, accumulate: true)
Module.register_attribute(__MODULE__, :z_struct_fields, accumulate: true)
end
end
defmacro schema(do: block) do
__schema__(__CALLER__, block)
end
defp __schema__(caller, block) do
quote do
unquote(define_schema(caller, block))
unquote(define_struct())
end
end
defp define_schema(caller, block) do
quote do
if line = Module.get_attribute(__MODULE__, :z_schema_defined) do
raise "schema already defined for #{inspect(__MODULE__)} on line #{line}"
end
@z_schema_defined unquote(caller.line)
try do
import Z.Struct, only: [field: 2, field: 3]
unquote(block)
after
:ok
end
end
end
defp define_struct() do
quote unquote: false do
fields = Macro.escape(@z_fields) |> Enum.reverse()
@enforce_keys Enum.reverse(@z_enforced_fields)
defstruct Enum.reverse(@z_struct_fields)
use Z.Type, options: unquote(Z.Any.__z__(:options) ++ [:cast])
def __z__(:fields), do: unquote(fields)
def new(enum \\ []) do
struct(__MODULE__, enum)
|> validate()
end
def new!(enum \\ []) do
case new(enum) do
{:ok, value} -> value
{:error, error} -> raise error
end
end
def check(result, :conversions, rules, context) do
result
|> Z.Any.check(:conversions, rules, context)
|> maybe_check(:cast, rules, context)
end
def check(result, :mutations, rules, context) do
result
|> Z.Any.check(:mutations, rules, context)
end
def check(result, :assertions, rules, context) do
result
|> Z.Any.check(:assertions, rules, context)
|> check(:fields, rules, context)
end
def check(result, rule, options, context) do
Z.Struct.__check__(result, rule, options, context)
end
end
end
def __check__(result, rule, options, context) do
check(result, rule, options, context)
end
defp check(result, _rule, _options, _context) when result.value == nil do
result
end
defp check(result, :cast, _enabled, _context) when not is_map(result.value) do
result
end
defp check(result, :cast, _enabled, context) when is_struct(result.value, context.type) do
result
end
defp check(result, :cast, false, _context) do
result
end
defp check(result, :cast, true, context) when is_struct(result.value) do
result
|> Z.Result.set_value(struct(context.type, Map.from_struct(result.value)))
end
defp check(result, :cast, true, context) do
result
|> Z.Result.set_value(struct(context.type, result.value))
end
defp check(result, :type, options, context) when not is_struct(result.value, context.type) do
message = Keyword.get(options, :message, "input is not a #{inspect(context.type)}")
result
|> Z.Result.add_issue(
Z.Issue.new(
Z.Error.Codes.invalid_type(),
message,
context
)
)
end
defp check(result, :type, _options, _context) do
result
end
defp check(result, _rule, _options, context)
when not is_struct(result.value, context.type) do
result
end
defp check(result, :fields, _options, context) do
Enum.reduce(context.type.__z__(:fields), result, fn {name, {type, rules}}, res ->
check_field(res, name, type, rules, context)
end)
end
defp check_field(result, name, type, rules, context) do
case type.validate(Map.get(result.value, name), rules, Z.Context.new(type, name, context)) do
{:ok, value} ->
result |> Z.Result.set_value(Map.replace(result.value, name, value))
{:error, error} ->
result |> Z.Result.add_issues(error.issues)
end
end
defmacro field(name, type, rules \\ []) do
quote do
Z.Struct.__field__(__MODULE__, unquote(name), unquote(type), unquote(rules))
end
end
def __field__(mod, name, type, rules) do
rules = Z.Rule.to_keyword_list(rules)
type = check_field_type!(name, type)
check_rules!(name, type, rules)
validate_default!(name, type, rules)
define_field(mod, name, type, rules)
end
defp check_field_type!(name, type) do
case Z.Type.resolve(type) do
{:ok, type} ->
type
_ ->
raise ArgumentError, "invalid type #{inspect(type)} for field #{inspect(name)}"
end
end
defp check_rules!(name, type, rules) do
case Enum.find(rules, fn {rule, _} -> rule not in type.__z__(:options) end) do
nil ->
:ok
{rule, _} ->
raise ArgumentError,
"invalid rule #{inspect(rule)} for field #{name}"
end
end
defp validate_default!(name, type, rules) do
if Keyword.has_key?(rules, :default) do
value = Keyword.fetch!(rules, :default)
if !is_function(value, 0) do
case type.validate(value, Keyword.delete(rules, :default)) do
{:ok, _} ->
:ok
_ ->
raise ArgumentError,
"default value #{inspect(value)} is invalid for type #{inspect(type)} of field #{inspect(name)}"
end
end
end
end
defp define_field(mod, name, type, rules) do
put_struct_field(mod, name, Keyword.get(rules, :default))
put_enforced_field(mod, name, Keyword.get(rules, :required))
Module.put_attribute(mod, :z_fields, {name, {type, rules}})
end
defp put_struct_field(mod, name, default) do
fields = Module.get_attribute(mod, :z_struct_fields)
if List.keyfind(fields, name, 0) do
raise ArgumentError,
"field #{inspect(name)} already exists on schema, you must either remove the duplication or choose a different name"
end
if is_function(default, 0) do
Module.put_attribute(mod, :z_struct_fields, {name, nil})
else
Module.put_attribute(mod, :z_struct_fields, {name, default})
end
end
defp put_enforced_field(mod, name, options) when is_list(options) do
if options[:enforced] != false do
put_enforced_field(mod, name, true)
end
end
defp put_enforced_field(mod, name, true) do
fields = Module.get_attribute(mod, :z_enforced_fields)
if List.keyfind(fields, name, 0) do
raise ArgumentError,
"field #{inspect(name)} already exists on schema, you must either remove the duplication or choose a different name"
end
Module.put_attribute(mod, :z_enforced_fields, name)
end
defp put_enforced_field(_mod, _name, _options) do
end
end