defmodule Rubbergloves.Structure do defmacro __using__(opts \\ []) do validator = Keyword.get(opts, :validator, Rubbergloves.Validator.Justify) quote do require Rubbergloves.Structure import Rubbergloves.Structure @properties [] @validator unquote(validator) @before_compile {unquote(__MODULE__), :__before_compile__} end end defmacro defstructure(block) do quote do unquote(block) end end defmacro field(name, type, opts \\ []) do quote do Module.put_attribute( __MODULE__, :properties, @properties ++ [ {unquote(name), unquote(type), unquote(opts)} ] ) end end defmacro one(name, type, opts \\ []) do quote do Module.put_attribute( __MODULE__, :properties, @properties ++ [ {unquote(name), {:one, unquote(type)}, unquote(opts)} ] ) end end defmacro many(name, type, opts \\ []) do quote do Module.put_attribute( __MODULE__, :properties, @properties ++ [ {unquote(name), {:many, unquote(type)}, unquote(opts)} ] ) end end defmacro __before_compile__(_env) do quote do @properties |> Enum.map(&struct_key/1) |> defstruct def validate(struct = %__MODULE__{}) do Enum.reduce(@properties, struct, &validations(@validator, &1, &2)) end end end def validations(validator, {key, type, options}, struct) do result = case options do required: true -> validator.required(struct, key) _ -> struct end |> validator.type(key, type) case Rubbergloves.Validation.valid?(result) do true -> data = Map.get(struct, :data, %{}) nested = Map.get(struct, key, Map.get(data, key)) case {type, nested} do {_, nil} -> result {{:one, type}, _} -> maybe_put_nested_errors(result, key, type.validate(nested)) {{:many, type}, _} -> maybe_put_nested_errors(result, key, Enum.map(nested, fn item -> type.validate(item) end)) _ -> result end false -> result end end defp maybe_put_nested_errors(result, key, nested) when is_list(nested) do errors = nested |> Enum.filter(fn item -> !Rubbergloves.Validation.valid?(item) end) case length(errors) do 0 -> result _ -> Map.put(result, :errors, [{key, Enum.map(errors, &Rubbergloves.Validation.errors/1)}]) end end defp maybe_put_nested_errors(result, key, nested) do case Rubbergloves.Validation.valid?(nested) do true -> result false -> Map.put(result, :errors, [{key, Rubbergloves.Validation.errors(nested)}]) end end def struct_key({name, _, default: default}), do: {name, default} def struct_key({name, _, _}), do: {name, nil} end