Current section
Files
Jump to
Current section
Files
lib/parameters.ex
defmodule Parameters do
alias Parameters.{
ParamsNode,
FieldNode
}
defmacro __using__(_) do
quote do
import unquote(__MODULE__)
@on_definition Parameters
@before_compile Parameters
Module.register_attribute(__MODULE__, :parameters, accumulate: true)
end
end
def __on_definition__(%{module: module}, _kind, name, _args, _guards, _body) do
if ast = Module.get_attribute(module, :parameters_block) do
Module.put_attribute(module, :parameters, ParamsNode.parse(name, ast))
Module.delete_attribute(module, :parameters_block)
end
end
defmacro __before_compile__(env) do
for schema <- Module.get_attribute(env.module, :parameters) do
parent = Module.concat(Parameters, env.module)
quote do
unquote(define_schema(parent, schema))
end
end
end
defmacro params(do: block) do
quote do
@parameters_block unquote(Macro.escape(block, prune_metadata: true))
end
end
def params_for(module, fun, params) do
module
|> changeset_for(fun, params)
|> params_for()
end
def params_for(%Ecto.Changeset{} = changeset) do
with {:ok, schema} <- Ecto.Changeset.apply_action(changeset, :insert) do
{:ok, schema_to_map(schema)}
end
end
def params_for(%{
private: %{phoenix_controller: module, phoenix_action: fun},
params: params
}) do
params_for(module, fun, params)
end
def changeset_for(module, fun, params) do
module = Module.safe_concat([Parameters, module, Macro.camelize("#{fun}")])
apply(module, :changeset, [struct(module), params])
end
def changeset_for(%{
private: %{phoenix_controller: module, phoenix_action: fun},
params: params
}) do
changeset_for(module, fun, params)
end
defp define_schema(parent, node) do
module = Module.concat(parent, Macro.camelize("#{node.id}"))
quote do
defmodule unquote(module) do
use Ecto.Schema
@primary_key false
embedded_schema do
unquote(define_fields(node.fields))
end
unquote(define_changeset(node.fields))
unquote(define_embeds(module, node.fields))
end
end
end
defp define_embeds(parent, fields) do
for field <- fields, not is_nil(field.fields) do
define_schema(parent, field)
end
end
defp define_fields(fields) do
for field <- fields do
options = Keyword.take(field.options, [:default])
case field do
%FieldNode{type: :map, fields: fields} when is_list(fields) ->
quote do
embeds_one unquote(field.id),
Module.concat(__MODULE__, Macro.camelize("#{unquote(field.id)}")),
unquote(options)
end
%FieldNode{type: :array, fields: fields} when is_list(fields) ->
quote do
embeds_many unquote(field.id),
Module.concat(__MODULE__, Macro.camelize("#{unquote(field.id)}")),
unquote(options)
end
%FieldNode{fields: nil} ->
quote do
field unquote(field.id), unquote(field.type), unquote(options)
end
end
end
end
defp define_changeset(fields) do
permitted_fields =
fields
|> Enum.filter(fn field -> is_nil(field.fields) end)
|> Enum.map(fn field -> field.id end)
required_fields =
fields
|> Enum.filter(fn field ->
is_nil(field.fields) and Keyword.fetch!(field.options, :required)
end)
|> Enum.map(fn field -> field.id end)
optional_embeds =
fields
|> Enum.filter(fn field ->
not is_nil(field.fields) and not Keyword.fetch!(field.options, :required)
end)
|> Enum.map(fn field -> field.id end)
required_embeds =
fields
|> Enum.filter(fn field ->
not is_nil(field.fields) and Keyword.fetch!(field.options, :required)
end)
|> Enum.map(fn field -> field.id end)
quote do
def changeset(schema, params) do
changeset =
schema
|> Ecto.Changeset.cast(params, unquote(permitted_fields))
|> Ecto.Changeset.validate_required(unquote(required_fields))
changeset =
Enum.reduce(unquote(optional_embeds), changeset, fn item, acc ->
Ecto.Changeset.cast_embed(acc, item)
end)
changeset =
Enum.reduce(unquote(required_embeds), changeset, fn item, acc ->
Ecto.Changeset.cast_embed(acc, item, required: true)
end)
changeset
end
end
end
defp schema_to_map(nil), do: nil
defp schema_to_map(schemas) when is_list(schemas), do: Enum.map(schemas, &schema_to_map/1)
defp schema_to_map(%module{} = schema) do
embeds = module.__schema__(:embeds)
mapper = fn {key, val} ->
if key in embeds do
{key, schema_to_map(val)}
else
{key, val}
end
end
schema
|> Map.from_struct()
|> Enum.map(mapper)
|> Enum.into(%{})
end
end