Packages

StructConstructor allows you to declare structs using Ecto.Schema and generate constructor functions that will take care of coercion and handling various input formats (maps, keyword lists with string or atom keys).

Current section

Files

Jump to
struct_constructor lib struct_constructor.ex
Raw

lib/struct_constructor.ex

defmodule StructConstructor do
@moduledoc """
StructConstructor makes it easy to define typed structures using `Ecto.Schema` DSL.
Such structures can later be easily initialized using function `new/1`.
See `Ecto.Schema` documentation for more info about DSL.
## Example
defmodule User do
use StructConstructor do
field :name, :string
field :age, :integer
end
end
iex> User.new(%{"name" => "Alex", "age" => "27"})
%User{age: 27, name: "Alex"}
iex> User.new(name: "Alex", age: "27"})
%User{age: 27, name: "Alex"}
"""
defmacro __using__(do: block) do
quote location: :keep do
use Ecto.Schema
@primary_key false
embedded_schema(do: unquote(block))
@doc """
Initialize `#{__MODULE__}` struct from map or keyword list
"""
@spec new(map | Keyword.t) :: struct
def new(kwlist) when is_list(kwlist) do
new(Map.new(kwlist))
end
def new(map) when is_map(map) do
StructConstructor.new(__MODULE__, map)
end
@doc """
Do something with `struct` after it's been instantiated
"""
@spec after_load(struct) :: struct
def after_load(struct),
do: struct
defoverridable [after_load: 1]
end
end
alias Ecto.Changeset
@doc false
def new(mod, map) do
mod
|> struct()
|> build_changeset(map)
|> Changeset.apply_changes()
|> after_load()
end
defp build_changeset(%mod{} = struct, map) do
struct
|> Changeset.cast(map, mod.__schema__(:fields) -- mod.__schema__(:embeds))
|> cast_embeds(mod.__schema__(:embeds))
end
defp cast_embeds(changeset, []),
do: changeset
defp cast_embeds(changeset, [embed | rest]) do
changeset
|> Changeset.cast_embed(embed, with: &build_changeset/2)
|> cast_embeds(rest)
end
defp after_load(%mod{} = struct) do
after_load(struct, mod.__schema__(:embeds))
end
defp after_load(%mod{} = struct, []) do
if function_exported?(mod, :after_load, 1) do
mod.after_load(struct)
else
struct
end
end
defp after_load(struct, [embed | rest]) do
case struct do
%{^embed => value} when not is_nil(value) ->
struct
|> Map.put(embed, after_load(value))
|> after_load(rest)
_ ->
after_load(struct, rest)
end
end
end