[英]How to expand multiple macros in Elixir?
我从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和宏世界还很陌生。
由于您未提供MCVE ,因此很难测试解决方案,但是乍一看,问题在于您希望Kernel.SpecialForms.quote/2
一些魔力,尽管它不会在任何地方隐式注入任何内容 ,但只会产生一个AST 。
你打电话时
Enum.each(...)
作为quote do
块的最后一行,此调用的结果从quote do
作为AST返回 。 也就是说,当前的__using__
实现会注入对quote do: :ok
的调用结果quote do: :ok
,显然是:ok
。 您需要的是构建要注入的子句的列表:
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)
]
通过使用Enum.map/2
我们为每个元素收集带引号的AST, 并将它们附加到已经构建的AST中以进行defstruct
创建。 我们返回一个包含许多子句的列表(这是一个适当的AST)。
不过,我不确定这是否是由于缺少MCVE而造成的唯一故障,但这绝对是开始的正确方法。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.