Absinthe: Allow to specify dynamic (compile time) values for enum

Created on 16 Nov 2017  路  6Comments  路  Source: absinthe-graphql/absinthe

Compile Time values can not be provided for enum.

Example

use Absinthe.Schema.Notation

variable = [:foo, :bar]
enum :enum_variable, values: variable

@attribute [:foo, :bar]
enum :enum_attribute, values: @attribute

Error

== Compilation error in file lib/art_email/schema/content_types.ex ==
** (FunctionClauseError) no function clause matching in Absinthe.Type.Enum.Value.build/1

    The following arguments were given to Absinthe.Type.Enum.Value.build/1:

        # 1
        {:templates, [line: 8], nil}

    Attempted function clauses (showing 1 out of 1):

        def build(raw_values) when is_list(raw_values)

    lib/absinthe/type/enum/value.ex:31: Absinthe.Type.Enum.Value.build/1
    lib/absinthe/type/enum.ex:95: Absinthe.Type.Enum.build/1
    lib/absinthe/schema/notation/writer.ex:91: Absinthe.Schema.Notation.Writer.type_functions/1
    lib/absinthe/schema/notation/writer.ex:222: anonymous fn/2 in Absinthe.Schema.Notation.Writer.update_type_functions/3
    (elixir) lib/map.ex:727: Map.update!/3
    lib/absinthe/schema/notation/writer.ex:203: Absinthe.Schema.Notation.Writer.do_build_info/2
    (elixir) lib/enum.ex:1826: Enum."-reduce/3-lists^foldl/2-0-"/3
    expanding macro: Absinthe.Schema.Notation.Writer.__before_compile__/1
    lib/acme/schema/types.ex:1: Acme.Schema.Types (module)
    (elixir) lib/kernel/parallel_compiler.ex:121: anonymous fn/4 in Kernel.ParallelCompiler.spawn_compilers/1

Most helpful comment

Thank you very much @fertel.

Using expanded module attributes also works:

defmodule Enums do
  @channels [:red, :green, :blue]
  defmacro channels, do: Macro.expand(@channels, __CALLER__)
end

then

use Absinthe.Schema.Notation
require Enums

enum(:channel, values: Enum.channels())

You can also declare an EctoEnum with:

import EctoEnum
require Enums

defenum(Channel, :channel, Enums.channels())

If you need as values (or description) as well, the following macro works for me:

defmodule Help do
  defmacro enum_values(values) do
    expanded = Macro.expand(values, __CALLER__)

    values =
      Enum.map(expanded, fn {name, as} ->
        quote do
          value(unquote(name), as: unquote(as))
        end
      end)

    quote(do: (unquote_splicing(values)))
  end
end

You have to declare the enum values in an expanded module attribute:

defmodule Enums do
  @channels [red: :r, green: :g, blue: :b]
  defmacro channels, do: Macro.expand(@channels, __CALLER__)
end

Then you can declare your enum with:

use Absinthe.Schema.Notation
require Enums
require Help

enum :channel, do: Help.enum_values(Enums.channels())

or

enum :channel do
  Help.enum_values(Enums.channels())
end

A description could be added in the list by using maps instead of keywords.

All 6 comments

Hey @maennchen I'm going to include this under https://github.com/absinthe-graphql/absinthe/issues/410 I think.

Module attributes are actually incredibly tricky to work with in macros because while @foo :foo is itself a macro, it merely expands to Module.put_attribute which is a function, and the macro expansion tricks we've used heretofore won't work. Making module attributes work falls under Schema 1.5 goals.

Hi @benwilson512! I don't mean to rush you but when do you think we can see this in a new release? I use EctoEnum and it'd be great if I could do something like this:

enum :post_type, values: MyApp.Post.Type.__enum_map__()

I'm not sure if you've had a chance to look at how this should be done but I had a similar question for one of my own packages a while back. I can try going through the asbinthe code and send in a PR. Let me know what you think!

small workaround for this issue

  values = [:foo, :bar]
  enums = quote do
    enum :myenum, values: unquote(values)
  end

  Macro.expand_once(enums, __ENV__)

Thank you very much @fertel.

Using expanded module attributes also works:

defmodule Enums do
  @channels [:red, :green, :blue]
  defmacro channels, do: Macro.expand(@channels, __CALLER__)
end

then

use Absinthe.Schema.Notation
require Enums

enum(:channel, values: Enum.channels())

You can also declare an EctoEnum with:

import EctoEnum
require Enums

defenum(Channel, :channel, Enums.channels())

If you need as values (or description) as well, the following macro works for me:

defmodule Help do
  defmacro enum_values(values) do
    expanded = Macro.expand(values, __CALLER__)

    values =
      Enum.map(expanded, fn {name, as} ->
        quote do
          value(unquote(name), as: unquote(as))
        end
      end)

    quote(do: (unquote_splicing(values)))
  end
end

You have to declare the enum values in an expanded module attribute:

defmodule Enums do
  @channels [red: :r, green: :g, blue: :b]
  defmacro channels, do: Macro.expand(@channels, __CALLER__)
end

Then you can declare your enum with:

use Absinthe.Schema.Notation
require Enums
require Help

enum :channel, do: Help.enum_values(Enums.channels())

or

enum :channel do
  Help.enum_values(Enums.channels())
end

A description could be added in the list by using maps instead of keywords.

I've been using a similar macro:

  defmacro enum_from_ecto(name, module) when is_atom(name) do
    name   = Macro.expand(name,   __CALLER__)
    module = Macro.expand(module, __CALLER__)
    values = module.__enum_map__()

    quote do
      enum(unquote(name), values: unquote(values))
    end
  end

To call:

enum_from_ecto(:order_status, Order.StatusEnum)

This is still not working in Absinthe 1.5 and neither are the workarounds. Opened #946 to track this.

Was this page helpful?
0 / 5 - 0 ratings