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