简体   繁体   中英

How to expand multiple macros in Elixir?

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM