DRY enums for Absinth macros

Absinth is a great GraphQL library for Elixir, but it brings a few challenges as it’s practically implemented using macros. One of these challenges is a DRY way of reusing enumerables in Absinth enums before v1.6.0.

The problem comes when you want to reuse a function that defines a list of values, like in the following case:

defmodule MyEnums do
  @currencies [
    :eur,
    :pln,
    :czk,
    :huf
  ]
  def currencies, do: @currencies
end

defmodule MyWeb.GQL.Types do
  @moduledoc false

  use Absinthe.Schema.Notation

  enum(:currencies_enum,
    values: MyEnums.currencies()
  )
  ...

That won’t work in Absinth v1.5.x because enum is a macro. Bummer.

Since I learned how to define my own macros, I ended up creating my own macros that would return the whole enum call:

defmodule MyMacros do
  # We create enum entries in Absinth as:
  #
  # enum :my_enum do
  #   value(:option_a, as: "option_a")
  #   value(:option_b, as: "option_b")
  # end
  #
  # Which translates to:
  #
  # {:enum, [],
  #  [
  #    :my_enum,
  #    [
  #      do: {:__block__, [],
  #       [
  #         {:value, [], [:option_a, [as: "option_a"]]},
  #         {:value, [], [:option_b, [as: "option_b"]]}
  #       ]}
  #    ]
  #  ]}
  defmacro currencies_enum() do
    {:__block__, [],
     {:enum, [],
      [
        :currencies_enum,
        [
          do:
            {:__block__, [],
             Enum.map(MyEnums.currencies(), fn value ->
               {:value, [], [value]}
             end)}
        ]
      ]}
  end
end

Then you include it as:

defmodule MyWeb.GQL.Types do
  @moduledoc false

  use Absinthe.Schema.Notation
  use MyMacros

  currencies_enum()
  ...

While this works and provides excellent flexibility to do anything on the Absinth side, it’s complete overkill if the only thing you need is to provide an already existing list of values.

Luckily, there is a nice and tidy way how to do this!

We’ll define a currencies_macro in the MyEnums module with the Macro.expand/2 call:

defmodule MyEnums do
  @currencies [
    :eur,
    :pln,
    :czk,
    :huf
  ]
  def currencies, do: @currencies
  defmacro currencies_macro, do: Macro.expand(@currencies, __CALLER__)
end

and then call this macro within the Absinth enum:

defmodule DigiWeb.Private.PacketsTypes do
  @moduledoc false

  use Absinthe.Schema.Notation

  require MyEnums

  enum(:currencies_enum,
    values: MyEnums.currencies_macro()
  )
  ...
end

The only thing we had to do is to use require to require the module beforehand.

If you are on Absinth v1.6.0 and higher, Absinth now supports dynamic enum values so the very first example should work.

← BUY THE PRE-RELEASE

I am writing a complete guide on web application deployment. Ruby with Puma, Python with Gunicorn, NGINX, PostgreSQL, Redis, networking, processes, systemd, backups, and all your usual suspects. Final release soon!

More →