[英]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.