I have a protocol that is used on Ecto Schemas. The protocol requires that two fields are defined with specific defaults.
I have come up with a working solution that I’m pretty happy with, but I’m just looking for feedback as I’m unsure if this is the optimal way (and maybe I’ve looked past a much simpler solution). In short, I used __after_compile__
and Ecto’s reflection functions to ensure the keys are there.
Obviously one drawback of this is that it must be @derived
though I’m not so worried about that.
I’m also wondering about the line %{context_modules: [module]} <- env
and if that could have any ramifications. Could there ever be more than one context module in this scenario?
Here is the code:
defmodule MyApp.MyProtocolError do
defexception [:message]
end
defmodule MyApp.MyProtocolCompile do
@required_fields [
name: %{type: :string},
checked: %{type: :boolean, default: true}
]
def __after_compile__(env, _bytecode) do
with %{context_modules: [module]} <- env do
messages =
for {field, opts} <- required_fields, reduce: [] do
messages ->
if module.__schema__(:virtual_type, field) != opts.type do
message =
" * #{__MODULE__} must have a field `:#{field}` of type `:#{opts.type}`"
message =
message <>
((Map.has_key?(opts, :default) && " and default to `#{opts.default}`") || "")
[message | messages]
else
messages
end
end
if messages != [] do
raise MyApp.MyProtocolError,
message: "\n" <> Enum.join(messages, "\n")
end
end
end
end
defprotocol MyApp.MyProtocol do
def do_it(module)
@impl true
defmacro __deriving__(module, options) do
quote location: :keep do
@after_compile MyApp.MyProtocolCompile
defimpl MyApp.MyProtocol, for: unquote(module) do
def do_it(_module) do
unquote(options).key
end
end
end
end
end
The other thing I did was put the __after_compile__
in the __deriving__
so I could just unquote(module)
to get the module, but I like this way a bit better.