defmodule Blunt.State do @moduledoc false alias Blunt.{Error, State} alias Blunt.Message.{Changeset, Schema, Schema.Fields} defmodule Error do defexception [:errors] def message(%{errors: errors}) do inspect(errors) end end defmacro __using__(_opts) do quote do use Blunt.Message.Compilation @primary_key_type false @require_all_fields? false @create_jason_encoders? false Module.register_attribute(__MODULE__, :schema_fields, accumulate: true) @before_compile Blunt.State import Blunt.State, only: :macros end end @spec field(name :: atom(), type :: atom(), keyword()) :: any() defmacro field(name, type, opts \\ []), do: Fields.record(name, type, opts) defmacro __before_compile__(env) do schema = Schema.generate(env) state_update = State.generate_update() field_access_functions = State.generate_field_access_functions(env) [schema, state_update] ++ field_access_functions end @doc false def generate_field_access_functions(%{module: module}) do module |> Module.get_attribute(:schema_fields) |> Enum.map(&generate_field_access/1) end @doc false defp generate_field_access({name, _type, _config}) do getter = String.to_atom("get_#{name}") putter = String.to_atom("put_#{name}") quote do def unquote(getter)(%__MODULE__{} = state) do Map.fetch!(state, unquote(name)) end def unquote(putter)(%__MODULE__{} = state, value) do State.put(__MODULE__, state, unquote(name), value) end end end @doc false def generate_update do quote do def update(%__MODULE__{} = state, values) do State.update(__MODULE__, state, values) end end end @doc false def update(state_module, state, values) do attrs = Blunt.Message.Input.normalize(values, state_module) types = :fields |> state_module.__schema__() |> Enum.into(%{}, fn field -> {field, state_module.__schema__(:type, field)} end) case Ecto.Changeset.cast({state, types}, attrs, Map.keys(types)) do %{valid?: true} = changeset -> Ecto.Changeset.apply_changes(changeset) changeset -> raise Error, errors: Changeset.format_errors(changeset) end end @doc false def put(state_module, state, key, value), do: update(state_module, state, Map.new([{key, value}])) end