Macros not seeing passed-in attributes

How do I make this work in a defenum macro?

  defenum EmailStatus, :email_status, @email_states

Right now, I’m getting:

warning: undefined module attribute @email_states, please remove access to @email_states or explicitly set it before access

It’s making no difference whether I’m using Macro.expand/2 or not:

values: {:@, [line: 122], [{:email_states, [line: 122], nil}]}
values-expanded: {{:., [],
  [
    {:__aliases__, [counter: {MyApp.Enum, 9}, alias: false],
     [:Module]},
    :get_attribute
  ]}, [],
 [
   {:__MODULE__, [counter: {MyApp.Enum, 9}], Kernel},
   :email_states,
   122
 ]}

I actually want to pass in a value that’s closer to:

  @a ~w(a b c d)
  @b ~w(e f g h)
  @c ~w(I j k l)
  defenum EmailStatus, :email_status, @a ++ @b ++ @c

The macro in question is complex, and may eventually be released as a fork of an already existing package. But for the purposes of this test, I believe that this will do:

  defmacro defenum(module, type, values) do
    quote location: :keep do
      defmodule unquote(module) do
        @enum_type unquote(type)
        @values Enum.map(unquote(values), &to_string/1)

        def type, do: @enum_type
        def values, do: @values
      end
    end
  end

Ideas?

-a

@halostatue First of all I do not recommend creating an module (except nested ones) inside macro for example:

# instead of
import Example
@values [:values, :list]
defenum(MyApp.MyEnum, :type, @values)

# you could do
defmodule MyApp.MyEnum do
  import Example
  @values [:values, :list]
  defenum(:type, @values)
  # you can now access here all attributes you specify
end

Here is how usually I’m working with such macros:

defmodule Example do
  defmacro defenum(type, values) do
    quote bind_quoted: [type: type, values: values], unquote: false do
      @enum_type type
      @values Enum.map(values, &to_string/1)

      def type, do: @enum_type
      def values, do: @values
    end
  end
end

You can take a look at code of my last project:

Feel free in case of any questions.

Looks interesting and has some of the same features that I was building in myself, although organized differently and with some features that (a) I wouldn’t implement myself (your type conversion SQL) and (b) I hadn’t thought of (generating the Absinthe enum type module directly). One feature that I’m specifically building in what I’m doing is that the Absinthe enum type may have extra fields/values that aren’t part of the database enum, so I have ways of setting that, and that I put @desc attributes on each Absinthe enum.

I’m not in a position to change the enumeration code we use right now, even if your library were yet available, but there are some things I may have adapt from your concepts later.

I’ve put your answer as the solution to the problem, because it does solve the explicit example given…but it doesn’t answer the ultimate problem: it doesn’t appear possible to properly pass module attributes to macros where there’s any complexity to the module attribute (as I gave above), even when using Module.expand/2.

Not sure if you noticed it (I would need to add more documentation), but my project supports such extra absinthe options. Instead of :values option you need to call value/2 macro in do … end block. :values option is only for defining values in shortest possible way. Also it’s possible to pass :absinthe extra options to enum module.

For more information please see:

If you would need some features then please let me know. I will at least comment each proposition.

Sorry, I did not get what do you mean by that.

I just tried my suggested code i.e.:

defmodule Example do
  defmacro defenum(type, values) do
    quote bind_quoted: [type: type, values: values], unquote: false do
      @enum_type type
      @values Enum.map(values, &to_string/1)

      def type, do: @enum_type
      def values, do: @values
    end
  end
end

and used it with multiple attributes without any problem:

defmodule MyEnum do                  
  @a ~w(a b c d)                       
  @b ~w(e f g h)                       
  @c ~w(I j k l)                       
  import Example                                    
  defenum :email_status, @a ++ @b ++ @c
end

and here is a result of MyEnum.values():

["a", "b", "c", "d", "e", "f", "g", "h", "I", "j", "k", "l"]

Did I missed something?

I’ll try the bind_quoted solution when I have a bit of time; that’s the one thing that I haven’t tried. I’m still not in a position to switch from generating modules (which are nested inside my enum namespace, but I understand the difference of what you’re talking about).

I did. Part of what I can do (although not as nicely as what you’ve got) is have something like:

defmodule MyApp.Enums do
  import EnumMacros, only: [defenum: 3]

  defenum Region, :region, ~w(east central west)
end

defmodule MyApp.Schema do
  # Absinthe preamble…
  import EnumMacros, only: [defgraphqlenum: 2]

  defgraphqlenum :regions, from: MyApp.Region do
    @desc "View all regions"
    value :all    
  end
end

It’s not that nice, right now, but that’s the idea. :all is not a permitted value in the database, but it is permitted in the GraphQL because the GraphQL enum can be used as a filter.

Not sure what permissions stops you from doing something like:

defmodule MyApp.Enums do
  import EnumMacros, only: [defenum: 3]

  defmodule Region do
    defenum :region, ~w(east central west)
  end
end

If it’s really a problem then nothing really stops you from having it + add an extra macro just to generate module like:

defmodule Example do
  defmacro defenum(type, values) do
    quote bind_quoted: [type: type, values: values], unquote: false do
      @enum_type type
      @values Enum.map(values, &to_string/1)

      def type, do: @enum_type
      def values, do: @values
    end
  end

  defmacro defenum(module, type, values) do
    # …
  end
end

This option is really useful. I recommend to read documentation:

https://hexdocs.pm/elixir/Kernel.SpecialForms.html#quote/2-quote-and-macros

That’s a nice argument! I would probably do it in one of such ways:

  1. Add :only option to value/2 macro with supported values: [:raw, :absinthe]
defmodule MyApp.MyEnum do
  import Enumex

  defenum do
    value(:first)
    value(:second, only: :raw)
    value(:third, only: :absinthe)
  end
end
  1. Add an extra enum macro to Enumex.Absinthe. This would be called inside normal Enumex.enum macro, but also it would optionally support be called separately. For example:
defmodule MyApp.MyEnum do
  import Enumex

  enum(values:  [:first, :second, :third])
end

defmodule MyApp.MyAbsintheEnum do
  import Enumex.Absinthe

  enum(values:  [:first, :second, :third])
end

In that example MyApp.MyEnum.Absinthe would have exactly same absinthe type as a MyApp.MyAbsintheEnum.

Added to my private TODO list, thanks :smiley:

It’s time available, because it’s a relatively large change to how my enums are currently created (it’s derived from ecto_enum but primarily switches to a string representation for the enums for a number of reasons). I like your overall approach better, but it’s going to take time that I don’t actually have to change how I manage the ~20 different enums used in our app.