Skip to content

Commit

Permalink
Merge pull request #999 from maartenvanvliet/add-repeatable-directives
Browse files Browse the repository at this point in the history
Add support for repeatable directives
  • Loading branch information
benwilson512 committed Dec 22, 2020
2 parents 72ff9f9 + cf76e23 commit 9cb0900
Show file tree
Hide file tree
Showing 19 changed files with 219 additions and 13 deletions.
1 change: 1 addition & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ locals_without_parens = [
object: 3,
on: 1,
parse: 1,
repeatable: 1,
resolve: 1,
resolve_type: 1,
scalar: 2,
Expand Down
3 changes: 3 additions & 0 deletions lib/absinthe/blueprint/schema/directive_definition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule Absinthe.Blueprint.Schema.DirectiveDefinition do
directives: [],
arguments: [],
locations: [],
repeatable: false,
source_location: nil,
expand: nil,
errors: [],
Expand All @@ -24,6 +25,7 @@ defmodule Absinthe.Blueprint.Schema.DirectiveDefinition do
description: nil,
arguments: [Blueprint.Schema.InputValueDefinition.t()],
locations: [String.t()],
repeatable: boolean(),
source_location: nil | Blueprint.SourceLocation.t(),
errors: [Absinthe.Phase.Error.t()]
}
Expand All @@ -36,6 +38,7 @@ defmodule Absinthe.Blueprint.Schema.DirectiveDefinition do
args: Blueprint.Schema.ObjectTypeDefinition.build_args(type_def, schema),
locations: type_def.locations |> Enum.sort(),
definition: type_def.module,
repeatable: type_def.repeatable,
expand: type_def.expand
}
end
Expand Down
1 change: 1 addition & 0 deletions lib/absinthe/language.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule Absinthe.Language do
Language.Argument.t()
| Language.BooleanValue.t()
| Language.Directive.t()
| Language.DirectiveDefinition.t()
| Language.Document.t()
| Language.EnumTypeDefinition.t()
| Language.EnumValue.t()
Expand Down
7 changes: 5 additions & 2 deletions lib/absinthe/language/directive_definition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ defmodule Absinthe.Language.DirectiveDefinition do
arguments: [],
directives: [],
locations: [],
loc: %{line: nil}
loc: %{line: nil},
repeatable: false

@type t :: %__MODULE__{
name: String.t(),
description: nil | String.t(),
directives: [Language.Directive.t()],
arguments: [Language.Argument.t()],
locations: [String.t()],
loc: Language.loc_t()
loc: Language.loc_t(),
repeatable: boolean()
}

defimpl Blueprint.Draft do
Expand All @@ -28,6 +30,7 @@ defmodule Absinthe.Language.DirectiveDefinition do
arguments: Absinthe.Blueprint.Draft.convert(node.arguments, doc),
directives: Absinthe.Blueprint.Draft.convert(node.directives, doc),
locations: node.locations,
repeatable: node.repeatable,
source_location: source_location(node)
}
end
Expand Down
1 change: 1 addition & 0 deletions lib/absinthe/lexer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ defmodule Absinthe.Lexer do
on
ON
query
repeatable
scalar
schema
subscription
Expand Down
86 changes: 86 additions & 0 deletions lib/absinthe/phase/document/validation/repeatable_directives.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
defmodule Absinthe.Phase.Document.Validation.RepeatableDirectives do
@moduledoc false

alias Absinthe.{Blueprint, Phase}

use Absinthe.Phase
use Absinthe.Phase.Validation

@doc """
Run the validation.
"""
@spec run(Blueprint.t(), Keyword.t()) :: Phase.result_t()
def run(input, _options \\ []) do
result = Blueprint.postwalk(input, &handle_node/1)
{:ok, result}
end

defp handle_node(%Blueprint.Directive{} = node) do
node
end

defp handle_node(%{directives: []} = node) do
node
end

defp handle_node(%{directives: _} = node) do
node
|> check_directives
|> inherit_invalid(node.directives, :bad_directive)
end

defp handle_node(node) do
node
end

defp check_directives(node) do
directives =
for directive <- node.directives do
case directive do
%{schema_node: nil} ->
directive

%{schema_node: %{repeatable: true}} ->
directive

directive ->
check_duplicates(
directive,
Enum.filter(
node.directives,
&compare_directive_schema_node(directive.schema_node, &1.schema_node)
)
)
end
end

%{node | directives: directives}
end

defp compare_directive_schema_node(_, nil), do: false

defp compare_directive_schema_node(%{identifier: identifier}, %{identifier: identifier}),
do: true

defp compare_directive_schema_node(_, _), do: false

# Generate the error for the node
@spec error_repeated(Blueprint.node_t()) :: Phase.Error.t()
defp error_repeated(node) do
%Phase.Error{
phase: __MODULE__,
message: "Directive `#{node.name}' cannot be applied repeatedly.",
locations: [node.source_location]
}
end

defp check_duplicates(directive, [_single]) do
directive
end

defp check_duplicates(directive, _multiple) do
directive
|> flag_invalid(:duplicate_directive)
|> put_error(error_repeated(directive))
end
end
1 change: 1 addition & 0 deletions lib/absinthe/pipeline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ defmodule Absinthe.Pipeline do
Phase.Document.Arguments.FlagInvalid,
# Validate Full Document
Phase.Document.Validation.KnownDirectives,
Phase.Document.Validation.RepeatableDirectives,
Phase.Document.Validation.ScalarLeafs,
Phase.Document.Validation.VariablesAreInputTypes,
Phase.Document.Validation.ArgumentsOfCorrectType,
Expand Down
22 changes: 22 additions & 0 deletions lib/absinthe/schema/notation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,23 @@ defmodule Absinthe.Schema.Notation do
|> record_expand!(func_ast)
end

@placement {:repeatable, [under: [:directive]]}
@doc """
Set whether the directive can be applied multiple times
an entity.
If omitted, defaults to `false`
## Placement
#{Utils.placement_docs(@placement)}
"""
defmacro repeatable(bool) do
__CALLER__
|> recordable!(:repeatable, @placement[:repeatable])
|> record_repeatable!(bool)
end

# INPUT OBJECTS

@placement {:input_object, [toplevel: true]}
Expand Down Expand Up @@ -1305,6 +1322,11 @@ defmodule Absinthe.Schema.Notation do
put_attr(env.module, {:expand, func_ast})
end

@doc false
def record_repeatable!(env, bool) do
put_attr(env.module, {:repeatable, bool})
end

@doc false
# Record directive AST nodes in the current scope
def record_locations!(env, locations) do
Expand Down
4 changes: 4 additions & 0 deletions lib/absinthe/schema/notation/sdl_render.ex
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ defmodule Absinthe.Schema.Notation.SDL.Render do
"directive ",
concat("@", string(directive.name)),
arguments(directive.arguments, type_definitions),
repeatable(directive.repeatable),
" on ",
join(locations, " | ")
])
Expand Down Expand Up @@ -441,6 +442,9 @@ defmodule Absinthe.Schema.Notation.SDL.Render do
defp block_string_line(["", _ | _]), do: nest(line(), :reset)
defp block_string_line(_), do: line()

defp repeatable(true), do: " repeatable"
defp repeatable(_), do: empty()

def join(docs, joiner) do
fold_doc(docs, fn doc, acc ->
concat([doc, concat(List.wrap(joiner)), acc])
Expand Down
4 changes: 4 additions & 0 deletions lib/absinthe/type/built_ins/directives.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ defmodule Absinthe.Type.BuiltIns.Directives do

on [:field, :fragment_spread, :inline_fragment]

repeatable false

expand fn
%{if: true}, node ->
Blueprint.put_flag(node, :include, __MODULE__)
Expand All @@ -27,6 +29,8 @@ defmodule Absinthe.Type.BuiltIns.Directives do
Directs the executor to skip this field or fragment when the `if` argument is true.
"""

repeatable false

arg :if, non_null(:boolean), description: "Skipped when true."

on [:field, :fragment_spread, :inline_fragment]
Expand Down
11 changes: 8 additions & 3 deletions lib/absinthe/type/built_ins/introspection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,17 @@ defmodule Absinthe.Type.BuiltIns.Introspection do
object :__directive do
description "Represents a directive"

field :name, :string
field :name, non_null(:string)

field :description, :string

field :is_repeatable, non_null(:boolean),
resolve: fn _, %{source: source} ->
{:ok, source.repeatable}
end

field :args,
type: list_of(:__inputvalue),
type: non_null(list_of(non_null(:__inputvalue))),
resolve: fn _, %{source: source} ->
args =
source.args
Expand Down Expand Up @@ -84,7 +89,7 @@ defmodule Absinthe.Type.BuiltIns.Introspection do
{:ok, Enum.member?(source.locations, :field)}
end

field :locations, list_of(:__directive_location)
field :locations, non_null(list_of(non_null(:__directive_location)))
end

enum :__directive_location,
Expand Down
3 changes: 3 additions & 0 deletions lib/absinthe/type/directive.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ defmodule Absinthe.Type.Directive do
* `:description` - A nice description for introspection.
* `:args` - A map of `Absinthe.Type.Argument` structs. See `Absinthe.Schema.Notation.arg/2`.
* `:locations` - A list of places the directives can be used.
* `:repeatable` - A directive may be defined as repeatable by including the “repeatable” keyword
The `:__reference__` key is for internal use.
"""
Expand All @@ -28,6 +29,7 @@ defmodule Absinthe.Type.Directive do
locations: [location],
expand: (map, Absinthe.Blueprint.node_t() -> atom),
definition: module,
repeatable: boolean,
__private__: Keyword.t(),
__reference__: Type.Reference.t()
}
Expand All @@ -42,6 +44,7 @@ defmodule Absinthe.Type.Directive do
locations: [],
expand: nil,
definition: nil,
repeatable: false,
__private__: [],
__reference__: nil

Expand Down
14 changes: 13 additions & 1 deletion src/absinthe_parser.yrl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Nonterminals

Terminals
'{' '}' '(' ')' '[' ']' '!' ':' '@' '$' '=' '|' '...'
'query' 'mutation' 'subscription' 'fragment' 'on' 'directive'
'query' 'mutation' 'subscription' 'fragment' 'on' 'directive' 'repeatable'
'type' 'implements' 'interface' 'union' 'scalar' 'enum' 'input' 'extend' 'schema'
name int_value float_value string_value block_string_value boolean_value null.

Expand Down Expand Up @@ -194,6 +194,18 @@ DirectiveDefinition -> 'directive' '@' Name 'on' DirectiveDefinitionLocations Di
DirectiveDefinition -> 'directive' '@' Name ArgumentsDefinition 'on' DirectiveDefinitionLocations Directives :
build_ast_node('DirectiveDefinition', #{'name' => extract_binary('$3'), 'arguments' => '$4', 'directives' => '$7', 'locations' => extract_directive_locations('$6')}, extract_location('$1')).

DirectiveDefinition -> 'directive' '@' Name 'repeatable' 'on' DirectiveDefinitionLocations :
build_ast_node('DirectiveDefinition', #{'name' => extract_binary('$3'), 'locations' => extract_directive_locations('$6'), 'repeatable' => true}, extract_location('$1')).
DirectiveDefinition -> 'directive' '@' Name ArgumentsDefinition 'repeatable' 'on' DirectiveDefinitionLocations :
build_ast_node('DirectiveDefinition', #{'name' => extract_binary('$3'), 'arguments' => '$4', 'locations' => extract_directive_locations('$7'), 'repeatable' => true}, extract_location('$1')).

DirectiveDefinition -> 'directive' '@' Name 'repeatable' 'on' DirectiveDefinitionLocations Directives :
build_ast_node('DirectiveDefinition', #{'name' => extract_binary('$3'), 'directives' => '$7', 'locations' => extract_directive_locations('$6'), 'repeatable' => true}, extract_location('$1')).
DirectiveDefinition -> 'directive' '@' Name ArgumentsDefinition 'repeatable' 'on' DirectiveDefinitionLocations Directives :
build_ast_node('DirectiveDefinition', #{'name' => extract_binary('$3'), 'arguments' => '$4', 'directives' => '$8', 'locations' => extract_directive_locations('$7'), 'repeatable' => true}, extract_location('$1')).



SchemaDefinition -> 'schema' : build_ast_node('SchemaDeclaration', #{}, extract_location('$1')).
SchemaDefinition -> 'schema' Directives : build_ast_node('SchemaDeclaration', #{'directives' => '$2'}, extract_location('$1')).
SchemaDefinition -> 'schema' '{' FieldDefinitionList '}' : build_ast_node('SchemaDeclaration', #{'fields' => '$3'}, extract_location('$1')).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule Elixir.Absinthe.Integration.Execution.Introspection.DirectivesTest do
name
args { name type { kind ofType { name kind } } }
locations
isRepeatable
onField
onFragment
onOperation
Expand Down Expand Up @@ -36,7 +37,8 @@ defmodule Elixir.Absinthe.Integration.Execution.Introspection.DirectivesTest do
"name" => "include",
"onField" => true,
"onFragment" => true,
"onOperation" => false
"onOperation" => false,
"isRepeatable" => false
},
%{
"args" => [
Expand All @@ -52,7 +54,8 @@ defmodule Elixir.Absinthe.Integration.Execution.Introspection.DirectivesTest do
"name" => "skip",
"onField" => true,
"onFragment" => true,
"onOperation" => false
"onOperation" => false,
"isRepeatable" => false
}
]
}
Expand Down
15 changes: 13 additions & 2 deletions test/absinthe/language/directive_definition_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,19 @@ defmodule Absinthe.Language.DirectiveDefinitionTest do

describe "blueprint conversion" do
test "works, given a Blueprint Schema 'directive' definition without arguments" do
assert %Blueprint.Schema.DirectiveDefinition{name: "thingy", locations: [:field, :object]} =
from_input("directive @thingy on FIELD | OBJECT")
assert %Blueprint.Schema.DirectiveDefinition{
name: "thingy",
locations: [:field, :object],
repeatable: false
} = from_input("directive @thingy on FIELD | OBJECT")
end

test "works, given a Blueprint Schema 'repeatable' 'directive' definition without arguments" do
assert %Blueprint.Schema.DirectiveDefinition{
name: "thingy",
locations: [:field, :object],
repeatable: true
} = from_input("directive @thingy repeatable on FIELD | OBJECT")
end

test "works, given a Blueprint Schema 'directive' definition without arguments and with directives" do
Expand Down
Loading

0 comments on commit 9cb0900

Please sign in to comment.