如何在Elixir中扩展多个宏?

时间:2018-01-17 22:57:16

标签: macros 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和宏世界都很陌生。

1 个答案:

答案 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而导致的唯一故障,但这绝对是一个合适的解决方案。