我开始与Elixir一起冒险,我需要一些帮助。
我正在尝试使用宏来简化我的结构定义和验证。目标是根据使用它的模块中提供的选项自动注入defstruct
和Vex库验证器。
我提出了如下代码:
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
我在另一个模块中使用这个宏:
defmodule PdfGenerator.BibTypes.Booklet do
use PdfGenerator.BibTypes.TypeDefinition,
mod: __MODULE__,
style: "booklet",
required: [:title],
optional: [:author, :howpublished, :address, :month, :year, :note]
end
在宏扩展之后,我希望PdfGenerator.BibTypes.Booklet
模块看起来如下:
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
正如您所看到的,基于required
选项,我正在尝试扩展到Vex
特定的宏(反过来应该在Vex.Struct
宏定义中进一步扩展)对validates(:<PROP_NAME>, presence: true)
列表中的每个值required
。
当我从__using__
宏中移除最后一个块时,此宏代码可以工作(但没有这些验证器用于所需的值):
Enum.each(required, fn prop ->
quote location: :keep do
validates(
unquote(prop),
presence: true
)
end
end)
但有了它,当我尝试在iex
控制台中发出以下命令时:%PdfGenerator.BibTypes.Booklet{}
我明白了:
** (CompileError) iex:1: PdfGenerator.BibTypes.Booklet.__struct__/1 is undefined, cannot expand struct PdfGenerator.BibTypes.Booklet
任何想法,我做错了什么?任何提示都会受到高度赞赏,因为我对整个Elixir和宏世界都很陌生。
答案 0 :(得分:3)
由于你没有提供MCVE,所以测试解决方案非常困难,但乍一看问题是你期望来自Kernel.SpecialForms.quote/2
的一些魔法,而不是隐含地在任何地方注入任何东西,它只是产生一个AST 。
致电时
pip install -r requirements.txt
作为Enum.each(...)
块的最后一行,此调用的结果从quote do
返回为AST 。也就是说,当前quote do
实现会将调用结果注入__using__
,显然是quote do: :ok
。你需要的是建立要注入的子句列表:
:ok
使用Enum.map/2
我们收集引用的每个元素的AST,将它们附加到已构建的AST以创建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)
]
。我们返回一个包含许多子句的列表(这是一个合适的AST)。
尽管如此,我不确定这是否是由于缺乏MCVE而导致的唯一故障,但这绝对是一个合适的解决方案。