Here’s some simplified code, maybe it’ll help.
Behavior module:
defmodule App.Contents.Element do
@doc """
Mandatory implementation of the embed data changeset.
"""
@callback data_changeset(struct(), map()) :: Changeset.t
@doc """
Optional implementation of the whole embed changeset, this is the wrapper
changeset that uses above data changeset but also handles files.
Only override if some special tweaking is required on this level.
"""
@callback embed_changeset(struct(), map()) :: Changeset.t
# other callbacks
@doc false
defmacro __using__(opts) do
quote [location: :keep] do
use Ecto.Schema
import Ecto.Changeset
@behaviour Element
@opts unquote(opts)
unless @opts[:data] do
raise("Please define the fields as data options key")
end
@doc """
Default implementation of the embed changeset.
"""
@impl true
@spec embed_changeset(struct(), map()) :: Changeset.t
def embed_changeset(struct, params) do
struct
|> data_changeset(params)
|> cast_uuids(params)
|> handle_uuids()
|> handle_files(params)
end
# other default callback implementations
defoverridable Element
schema "contents" do
field :order, :integer
field :type, :string
# other globally needed feidls
embeds_one :data, Data, [on_replace: :delete] do
# no @opts available since technically it's a declaration of another
# (child) module
el_opts = unquote(opts)
for {key, type} <- el_opts[:data] do
field(key, type)
end
# further things like files etc
end
timestamps()
end
# private (not overridable) functions go here
end
end
example module that uses it:
defmodule AppWeb.Elements.Headline do
@moduledoc """
A headline content element.
"""
alias AppWeb.ElementView
use App.Contents.Element, [
data: [
{:level, :integer},
{:text, :string}
]
]
@impl true
def data_changeset(struct, params) do
struct
|> cast(params, [:level, :text])
|> validate_required([:level, :text])
|> validate_inclusion(:level, 1..6)
end
# other callbacks go here
end
So basically each “using” module is rewritten to a whole schema / embedded schema with duplicated code, but you only have to write it once and let it duplicate itself