I'm starting my adventure with Elixir and I need a little bit o help.
I'm trying to simplify my structs definition and validation by using macros. The goal is to automatically inject defstruct
and Vex library validators based on provided options in modules using it.
I've come up with the code as follows:
defmodule PdfGenerator.BibTypes.TypeDefinition do
@callback valid?(%{}) :: boolean
defmacro __using__(mod: mod, style: style, required: required, optional: optional) do
required_props = required |> Enum.map(&{:"#{&1}", nil})
optional_props = optional |> Enum.map(&{:"#{&1}", nil})
quote location: :keep do
defstruct unquote([{:style, style}] ++ required_props ++ optional_props)
@behaviour PdfGenerator.BibTypes.TypeDefinition
use Vex.Struct
def cast(%{} = map) do
styled_map = Map.put(map, :style, unquote(style))
struct_from_map(styled_map, as: %unquote(mod){})
end
defp struct_from_map(a_map, as: a_struct) do
keys =
Map.keys(a_struct)
|> Enum.filter(fn x -> x != :__struct__ end)
processed_map =
for key <- keys, into: %{} do
value = Map.get(a_map, key) || Map.get(a_map, to_string(key))
{key, value}
end
a_struct = Map.merge(a_struct, processed_map)
a_struct
end
validates(
:style,
presence: true,
inclusion: [unquote(style)]
)
end
Enum.each(required, fn prop ->
quote location: :keep do
validates(
unquote(prop),
presence: true
)
end
end)
end
end
And I'm using this macro in another module:
defmodule PdfGenerator.BibTypes.Booklet do
use PdfGenerator.BibTypes.TypeDefinition,
mod: __MODULE__,
style: "booklet",
required: [:title],
optional: [:author, :howpublished, :address, :month, :year, :note]
end
I want PdfGenerator.BibTypes.Booklet
module, after macro expansion, to look as follows:
defmodule PdfGenerator.BibTypes.Booklet do
defstruct style: "booklet",
title: nil,
author: nil,
howpublished: nil,
address: nil,
month: nil,
year: nil,
note: nil
@behaviour PdfGenerator.BibTypes.TypeDefinition
use Vex.Struct
def cast(%{} = map) do
styled_map = Map.put(map, :style, "booklet")
struct_from_map(styled_map, as: %PdfGenerator.BibTypes.Booklet{})
end
defp struct_from_map(a_map, as: a_struct) do
keys =
Map.keys(a_struct)
|> Enum.filter(fn x -> x != :__struct__ end)
processed_map =
for key <- keys, into: %{} do
value = Map.get(a_map, key) || Map.get(a_map, to_string(key))
{key, value}
end
a_struct = Map.merge(a_struct, processed_map)
a_struct
end
validates(
:style,
presence: true,
inclusion: ["booklet"]
)
validates(
:title,
presence: true
)
end
As you can see, based on required
option, I'm trying to expand to Vex
-specific macro (which in turn should be expanded further on in Vex.Struct
macro definition) validates(:<PROP_NAME>, presence: true)
for every value in required
list. This macro code works (but without these validators for required values) when I remove last block from __using__
macro:
Enum.each(required, fn prop ->
quote location: :keep do
validates(
unquote(prop),
presence: true
)
end
end)
But with it, when I'm trying to issue following command in the iex
console: %PdfGenerator.BibTypes.Booklet{}
I get:
** (CompileError) iex:1: PdfGenerator.BibTypes.Booklet.__struct__/1 is undefined, cannot expand struct PdfGenerator.BibTypes.Booklet
Any idea, what am I doing wrong? Any hint would be greatly appreciated as I'm pretty new to the whole Elixir and macros world.
Since you did not provide the MCVE , it's extremely hard to test the solution, but at the first glance the issue is you expect some magic from Kernel.SpecialForms.quote/2
, while it does not implicitly inject anything anywhere , it just produces an AST .
When you call
Enum.each(...)
as the last line of quote do
block, the result of this call is returned as AST from quote do
. That said, the current __using__
implementation injects the result of the call to quote do: :ok
, which is apparently :ok
. What you need, is to build the list of clauses to be injected:
defmacro __using__(mod: mod, ...) do
# preparation
ast_defstruct =
quote location: :keep do
# whole stuff for defstruct
end
# NB! last term will be returned from `__using__`!
[
ast_defstruct |
Enum.map(required, fn prop ->
quote location: :keep,
do: validates(unquote(prop), presence: true)
end)
]
By using Enum.map/2
we collect quoted ASTs for each element, and append them to already built AST for defstruct
creation. We return a list (which is a proper AST,) containing many clauses.
Still, I am unsure if this is the only glitch due to lack of MCVE, but this is definitely the proper fix to start with.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.