Current section
Files
Jump to
Current section
Files
lib/defparser.ex
defmodule Defparser do
@moduledoc """
Provides a way to define a parser for an arbitrary map with atom or string keys.
Works with `Ecto.Schema`s and `Ecto.Type`s.
## Example
iex(1)> defmodule Test do
...(1)> import Defparser
...(1)> defparser :user, %{
...(1)> username: %{first_name: :string, last_name: :string},
...(1)> birthdate: :date,
...(1)> favourite_numbers: [%{value: :integer}]
...(1)> }
...(1)> end
iex(2)> Test.parse_user(%{
...(2)> username: %{first_name: "User", last_name: "Name"},
...(2)> birthdate: "1990-01-01",
...(2)> favourite_numbers: [%{value: 1}, %{value: 2}]
...(2)> })
{:ok,
%{
__struct__: DefparserTest.Test.User,
username: %{
__struct__: DefparserTest.Test.User.Username,
first_name: "User",
last_name: "Name"
},
birthdate: ~D[1990-01-01],
favourite_numbers: [
%{
__struct__: DefparserTest.Test.User.FavouriteNumbers,
value: 1
},
%{
__struct__: DefparserTest.Test.User.FavouriteNumbers,
value: 2
}
]
}}
## Ecto.Type
You can provide an `Ecto.Type` as a field
iex(1)> defmodule AtomEctoType do
...(1)> @behaviour Ecto.Type
...(1)> def type, do: :string
...(1)> def cast(str), do: {:ok, String.to_atom(str)}
...(1)> def load(_), do: :error
...(1)> def dump(_), do: :error
...(1)> def embed_as(_), do: :error
...(1)> def equal?(a, b), do: a == b
...(1)> end
iex(2)> defmodule Parser do
...(2)> import Defparser
...(2)> defparser :data, %{atom: AtomEctoType}
...(2)> end
iex(3)> Parser.parse_data(%{atom: "atom"})
{:ok,
%{
__struct__: DefparserTest.Parser.Data,
atom: :atom
}}
## Ecto.Schema
You may want to pass already existing ecto schema
iex(1)> defmodule Schema do
...(1)> use Ecto.Schema
...(1)> @primary_key false
...(1)> embedded_schema do
...(1)> field :x, :integer
...(1)> end
...(1)> def changeset(schema, attrs) do
...(1)> Ecto.Changeset.cast(schema, attrs, [:x])
...(1)> end
...(1)> end
iex(2)> defmodule SchemaParser do
...(2)> import Defparser
...(2)> defparser :data, %{schema: {:embeds_one, Schema}}
...(2)> end
iex(3)> SchemaParser.parse_data(%{schema: %{x: "1"}})
{:ok,
%{
__struct__: DefparserTest.SchemaParser.Data,
schema: %{
__struct__: DefparserTest.Schema,
x: 1
}
}}
Besides `:embeds_one` it supports `:embeds_many` in case you expect
an array with the provided schema.
"""
@doc """
Defines a parser for arbitrary map.
"""
defmacro defparser(name, schema) do
{schema_definition, _} = Code.eval_quoted(schema, [], __CALLER__)
[{root, _} | _] =
schemas =
__CALLER__.module
|> base_namespace(name)
|> fetch_schemas(schema_definition)
schemas
|> Enum.map(&define_module/1)
|> Enum.each(&Code.compile_quoted(&1))
quote do
def unquote(:"parse_#{name}")(attrs) do
struct!(unquote(root))
|> unquote(root).changeset(attrs)
|> Ecto.Changeset.apply_action(:insert)
end
end
end
defp base_namespace(module, name) do
Module.concat([module, "#{Macro.camelize(to_string(name))}"])
end
defp fetch_schemas(namespace, map) do
{array_keys, rest} = fetch_arrays(map)
{schema_keys, value_keys} = fetch_maps(rest)
arrays =
fetch_all_schemas(
namespace,
Enum.map(array_keys, fn {k, [s]} -> {k, s} end)
)
schemas = fetch_all_schemas(namespace, schema_keys)
values = {namespace, schema_with_embeds(value_keys, arrays, schemas)}
[values] ++
Enum.flat_map(arrays, fn {_, _, s} -> s end) ++
Enum.flat_map(schemas, fn {_, _, s} -> s end)
end
defp fetch_arrays(map) do
Enum.split_with(map, fn
{_, [_]} -> true
{_, _} -> false
end)
end
defp fetch_maps(map) do
Enum.split_with(map, fn {_, x} -> is_map(x) end)
end
defp fetch_all_schemas(namespace, schemas) do
Enum.map(schemas, fn {k, s} ->
{
k,
namespace,
fetch_schemas(modulename_for_embed(namespace, k), s)
}
end)
end
defp schema_with_embeds(values, arrays, schemas) do
values ++
build_embeds_map(arrays, :embeds_many) ++
build_embeds_map(schemas, :embeds_one)
end
defp build_embeds_map(list, as) do
for {key, namespace, _schema} <- list do
{key, {as, modulename_for_embed(namespace, key)}}
end
end
defp modulename_for_embed(namespace, key) do
Module.concat(namespace, Macro.camelize("#{key}"))
end
defp define_module({module, schema}) do
quote do
defmodule unquote(module) do
use Ecto.Schema
@primary_key false
embedded_schema do
unquote(schema_body(schema))
end
def changeset(%__MODULE__{} = schema, attrs) do
unquote(__MODULE__).__schema_changeset__(
schema,
attrs,
unquote(schema_fields(schema)),
unquote(schema_assocs(schema))
)
end
end
end
end
defp schema_body(schema) do
for {key, type} <- schema do
case type do
{:embeds_one, ref} ->
quote do
embeds_one unquote(key), unquote(ref)
end
{:embeds_many, ref} ->
quote do
embeds_many unquote(key), unquote(ref)
end
type when is_atom(type) ->
quote do
field unquote(key), unquote(type)
end
end
end
end
defp schema_fields(schema) do
for {key, type} <- schema, is_atom(type), do: key
end
defp schema_assocs(schema) do
for {key, type} <- schema, assoc?(type), do: key
end
defp assoc?({_, _}), do: true
defp assoc?(_), do: false
@doc false
def __schema_changeset__(schema, attrs, fields, assocs) do
changeset = Ecto.Changeset.cast(schema, attrs, fields)
Enum.reduce(assocs, changeset, fn assoc, changeset ->
Ecto.Changeset.cast_embed(changeset, assoc)
end)
end
end