Skip to content

Commit

Permalink
Support dynamic Enum value (#1023)
Browse files Browse the repository at this point in the history
* Support dynamic Enum values

* Hack - attempt at support for dynamic Enum values

* Handle values via attribute
  • Loading branch information
binaryseed authored Jan 14, 2021
1 parent 97a281e commit 70d7073
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 112 deletions.
3 changes: 2 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ locals_without_parens = [
types: 1,
union: 3,
value: 1,
value: 2
value: 2,
values: 1
]

[
Expand Down
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
- Feature: Support for the [`repeatable` directive](https://github.com/absinthe-graphql/absinthe/pull/999)
- Feature: Enable [rendering](https://github.com/absinthe-graphql/absinthe/pull/1010) of Type System Directives in SDL based schemas.
- Feature: Correctly match [Introspection type specs](https://github.com/absinthe-graphql/absinthe/pull/1017)
- Bug Fix: Restore dynamic description support [comprehensively fixed](https://github.com/absinthe-graphql/absinthe/pull/1005) (Note: the `description`s are evaluated once --- at compile time)
- Bug Fix: Restore dynamic default_value support [comprehensively fixed](https://github.com/absinthe-graphql/absinthe/pull/1026) (Note: the `default_value`s evaluated once --- at compile time)
- Bug Fix: Restore dynamic [description support](https://github.com/absinthe-graphql/absinthe/pull/1005) (Note: the `description`s are evaluated once --- at compile time)
- Bug Fix: Restore dynamic [default_value support](https://github.com/absinthe-graphql/absinthe/pull/1026) (Note: the `default_value`s evaluated once --- at compile time)
- Bug Fix: Restore dynamic [Enum value support](https://github.com/absinthe-graphql/absinthe/pull/1023) (Note: the `value` is evaluated once --- at compile time)
- Bug Fix: [Interface nullability](https://github.com/absinthe-graphql/absinthe/pull/1009) corrections
- Bug Fix: Fix [field listing for Inputs](https://github.com/absinthe-graphql/absinthe/pull/1015) that import fields
- Bug Fix: Properly [trim all descriptions](https://github.com/absinthe-graphql/absinthe/pull/1014) no matter the mechanism used to specify them
Expand Down
2 changes: 1 addition & 1 deletion lib/absinthe/blueprint/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ defmodule Absinthe.Blueprint.Schema do
end

defp build_types([{:values, values} | rest], [enum | stack], buff) do
enum = Map.update!(enum, :values, &(values ++ &1))
enum = Map.update!(enum, :values, &(List.wrap(values) ++ &1))
build_types(rest, [enum | stack], buff)
end

Expand Down
45 changes: 34 additions & 11 deletions lib/absinthe/blueprint/schema/enum_type_definition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,40 @@ defmodule Absinthe.Blueprint.Schema.EnumTypeDefinition do
end

def values_by(type_def, key) do
for value_def <- type_def.values, into: %{} do
value = %Absinthe.Type.Enum.Value{
name: value_def.name,
value: value_def.value,
enum_identifier: type_def.identifier,
__reference__: value_def.__reference__,
description: value_def.description,
deprecation: value_def.deprecation
}

{Map.fetch!(value_def, key), value}
for value_def <- List.flatten(type_def.values), into: %{} do
case value_def do
%Blueprint.Schema.EnumValueDefinition{} ->
value = %Absinthe.Type.Enum.Value{
name: value_def.name,
value: value_def.value,
enum_identifier: type_def.identifier,
__reference__: value_def.__reference__,
description: value_def.description,
deprecation: value_def.deprecation
}

{Map.fetch!(value_def, key), value}

# Values defined via dynamic function calls don't yet get converted
# into proper Blueprints, but it works for now. This will get refactored
# in the future as we build out a general solution for dynamic values.
raw_value ->
name = raw_value |> to_string() |> String.upcase()

value_def = %{
name: name,
value: raw_value,
identifier: raw_value
}

value = %Absinthe.Type.Enum.Value{
name: name,
value: raw_value,
enum_identifier: type_def.identifier
}

{Map.fetch!(value_def, key), value}
end
end
end

Expand Down
34 changes: 34 additions & 0 deletions lib/absinthe/introspection/directive_location.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule Absinthe.Introspection.DirectiveLocation do
@moduledoc false

# https://spec.graphql.org/draft/#sec-Schema-Introspection

@executable_directive_locations [
:query,
:mutation,
:subscription,
:field,
:fragment_definition,
:fragment_spread,
:inline_fragment,
:variable_definition
]
@type_system_directive_locations [
:schema,
:scalar,
:object,
:field_definition,
:argument_definition,
:interface,
:union,
:enum,
:enum_value,
:input_object,
:input_field_definition
]

def values do
@executable_directive_locations ++
@type_system_directive_locations
end
end
13 changes: 13 additions & 0 deletions lib/absinthe/introspection/type_kind.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,17 @@ defmodule Absinthe.Introspection.TypeKind do
| :non_null

@callback kind() :: type_kind()

def values do
[
:scalar,
:object,
:interface,
:union,
:enum,
:input_object,
:list,
:non_null
]
end
end
26 changes: 1 addition & 25 deletions lib/absinthe/phase/schema/validation/directives_must_be_valid.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Absinthe.Phase.Schema.Validation.DirectivesMustBeValid do
alias Absinthe.Blueprint

@spec_link "https://spec.graphql.org/draft/#sec-Type-System.Directives"
@directive_locations Absinthe.Introspection.DirectiveLocation.values()

@doc """
Run the validation.
Expand Down Expand Up @@ -33,31 +34,6 @@ defmodule Absinthe.Phase.Schema.Validation.DirectivesMustBeValid do
end)
end

@executable_directive_locations [
:query,
:mutation,
:subscription,
:field,
:fragment_definition,
:fragment_spread,
:inline_fragment,
:variable_definition
]
@type_system_directive_locations [
:schema,
:scalar,
:object,
:field_definition,
:argument_definition,
:interface,
:union,
:enum,
:enum_value,
:input_object,
:input_field_definition
]
@directive_locations @executable_directive_locations ++ @type_system_directive_locations

defp validate_location(directive, location) when location in @directive_locations do
directive
end
Expand Down
25 changes: 4 additions & 21 deletions lib/absinthe/schema/notation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1101,12 +1101,7 @@ defmodule Absinthe.Schema.Notation do
defp handle_enum_attrs(attrs, env) do
attrs
|> expand_ast(env)
|> Keyword.update(:values, [], fn values ->
Enum.map(values, fn ident ->
value_attrs = handle_enum_value_attrs(ident, [], env)
struct!(Schema.EnumValueDefinition, value_attrs)
end)
end)
|> Keyword.update(:values, [], &[wrap_in_unquote(&1)])
|> Keyword.update(:description, nil, &wrap_in_unquote/1)
end

Expand Down Expand Up @@ -1471,21 +1466,12 @@ defmodule Absinthe.Schema.Notation do
end

def handle_enum_value_attrs(identifier, raw_attrs, env) do
value =
case Keyword.get(raw_attrs, :as, identifier) do
value when is_tuple(value) ->
raise Absinthe.Schema.Notation.Error,
"Invalid Enum value for #{inspect(identifier)}. " <>
"Must be a literal term, dynamic values must use `hydrate`"

value ->
value
end
value = Keyword.get(raw_attrs, :as, identifier)

raw_attrs
|> expand_ast(env)
|> Keyword.put(:identifier, identifier)
|> Keyword.put(:value, value)
|> Keyword.put(:value, wrap_in_unquote(value))
|> Keyword.put_new(:name, String.upcase(to_string(identifier)))
|> Keyword.delete(:as)
|> Keyword.update(:description, nil, &wrap_in_unquote/1)
Expand All @@ -1505,10 +1491,7 @@ defmodule Absinthe.Schema.Notation do
values =
values
|> expand_ast(env)
|> Enum.map(fn ident ->
value_attrs = handle_enum_value_attrs(ident, [], env)
struct!(Schema.EnumValueDefinition, value_attrs)
end)
|> wrap_in_unquote

put_attr(env.module, {:values, values})
end
Expand Down
33 changes: 2 additions & 31 deletions lib/absinthe/type/built_ins/introspection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -99,39 +99,10 @@ defmodule Absinthe.Type.BuiltIns.Introspection do
end

enum :__type_kind,
values: [
:scalar,
:object,
:interface,
:union,
:enum,
:input_object,
:list,
:non_null
]
values: Absinthe.Introspection.TypeKind.values()

enum :__directive_location,
values: [
:query,
:mutation,
:subscription,
:field,
:fragment_definition,
:fragment_spread,
:inline_fragment,
:variable_definition,
:schema,
:scalar,
:object,
:field_definition,
:argument_definition,
:interface,
:union,
:enum,
:enum_value,
:input_object,
:input_field_definition
]
values: Absinthe.Introspection.DirectiveLocation.values()

object :__type do
description "Represents scalars, interfaces, object types, unions, enums in the system"
Expand Down
20 changes: 0 additions & 20 deletions test/absinthe/schema/hydrate_dynamic_values_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,4 @@ defmodule HydrateDynamicValuesTest do
test "can hydrate enum values dynamically" do
assert {:ok, %{data: %{"all" => ["RED", "BLUE", "GREEN"]}}} == Absinthe.run("{ all }", Schema)
end

test "can't call functions to configure enum values dynamically" do
schema = """
defmodule KeywordExtend do
use Absinthe.Schema
enum :color do
value :red
value :blue, as: color_map(:blue)
value :green
end
end
"""

error = ~r/Invalid Enum value/

assert_raise(Absinthe.Schema.Notation.Error, error, fn ->
Code.eval_string(schema)
end)
end
end
32 changes: 32 additions & 0 deletions test/absinthe/type/enum_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ defmodule Absinthe.Type.EnumTest do
value :zero, as: 0
value :negative_one, as: -1
end

enum :dynamic_color do
value :red, as: color(:red)
value :green, as: color(:green)
value :blue, as: color(:blue)
end

enum :dynamic_color_list do
values color_list()
end

def color_list, do: [:purple, :orange, :yellow]

def color(:red), do: {255, 0, 0}
def color(:green), do: {0, 255, 0}
def color(:blue), do: {0, 0, 255}
end

describe "enums" do
Expand All @@ -71,6 +87,22 @@ defmodule Absinthe.Type.EnumTest do
assert %Type.Enum{} = type
assert %Type.Enum.Value{name: "RED", value: :red, description: nil} = type.values[:red]
end

test "value can be defined dynamically!" do
type = TestSchema.__absinthe_type__(:dynamic_color)

assert %Type.Enum.Value{name: "RED", value: {255, 0, 0}} = type.values[:red]
assert %Type.Enum.Value{name: "GREEN", value: {0, 255, 0}} = type.values[:green]
assert %Type.Enum.Value{name: "BLUE", value: {0, 0, 255}} = type.values[:blue]
end

test "values can be defined dynamically too" do
type = TestSchema.__absinthe_type__(:dynamic_color_list)

assert %Type.Enum.Value{name: "YELLOW"} = type.values[:yellow]
assert %Type.Enum.Value{name: "PURPLE"} = type.values[:purple]
assert %Type.Enum.Value{name: "ORANGE"} = type.values[:orange]
end
end

describe "enum value description evaluation" do
Expand Down

0 comments on commit 70d7073

Please sign in to comment.