Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support dynamic Enum value #1023

Merged
merged 3 commits into from
Jan 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the need to wrap these in a list?

Copy link
Contributor Author

@binaryseed binaryseed Jan 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, yes this is the sneaky bit. Now that values are wrap_in_unquote'd it can now be a list of atoms or AST for a function call - so I just wrap everything in a list here so that it can continue to collect in a list.

Then in the end after this all is injected into __absinthe_blueprint__ it'll be some form of nested list, and I can call List.flatten(type_def.values) to get the final resulting list....

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