Skip to content

Commit 94c6097

Browse files
committed
Extract check for unique directive names into separate rule
Replicates graphql/graphql-js@c174522
1 parent ceabd24 commit 94c6097

File tree

7 files changed

+167
-68
lines changed

7 files changed

+167
-68
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ a query language for APIs created by Facebook.
1313
[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
1414

1515
The current version 1.0.1 of GraphQL-core-next is up-to-date with GraphQL.js version
16-
14.0.2. All parts of the API are covered by an extensive test suite of currently 1668
16+
14.0.2. All parts of the API are covered by an extensive test suite of currently 1675
1717
unit tests.
1818

1919

graphql/utilities/extend_schema.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,6 @@ def extend_schema(
124124
check_extension_node(existing_type, def_)
125125
type_extensions_map[extended_type_name].append(def_)
126126
elif isinstance(def_, DirectiveDefinitionNode):
127-
directive_name = def_.name.value
128-
existing_directive = schema.get_directive(directive_name)
129-
if existing_directive:
130-
raise GraphQLError(
131-
f"Directive '{directive_name}' already exists"
132-
" in the schema. It cannot be redefined.",
133-
[def_],
134-
)
135127
directive_definitions.append(def_)
136128

137129
# If this document contains no new types, extensions, or directives then return the
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from typing import Dict
2+
3+
from ...error import GraphQLError
4+
from ...language import NameNode, DirectiveDefinitionNode
5+
from . import SDLValidationContext, SDLValidationRule
6+
7+
__all__ = [
8+
"UniqueDirectiveNamesRule",
9+
"duplicate_directive_name_message",
10+
"existed_directive_name_message",
11+
]
12+
13+
14+
def duplicate_directive_name_message(directive_name: str) -> str:
15+
return f"There can be only one directive named '{directive_name}'."
16+
17+
18+
def existed_directive_name_message(directive_name: str) -> str:
19+
return (
20+
f"Directive '{directive_name}' already exists in the schema."
21+
" It cannot be redefined."
22+
)
23+
24+
25+
class UniqueDirectiveNamesRule(SDLValidationRule):
26+
"""Unique directive names
27+
28+
A GraphQL document is only valid if all defined directives have unique names.
29+
"""
30+
31+
def __init__(self, context: SDLValidationContext) -> None:
32+
super().__init__(context)
33+
self.known_directive_names: Dict[str, NameNode] = {}
34+
self.schema = context.schema
35+
36+
def enter_directive_definition(self, node: DirectiveDefinitionNode, *_args):
37+
directive_name = node.name.value
38+
39+
if self.schema and self.schema.get_directive(directive_name):
40+
self.report_error(
41+
GraphQLError(
42+
existed_directive_name_message(directive_name), [node.name]
43+
)
44+
)
45+
else:
46+
if directive_name in self.known_directive_names:
47+
self.report_error(
48+
GraphQLError(
49+
duplicate_directive_name_message(directive_name),
50+
[self.known_directive_names[directive_name], node.name],
51+
)
52+
)
53+
else:
54+
self.known_directive_names[directive_name] = node.name
55+
return self.SKIP

graphql/validation/rules/unique_type_names.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
from ...language import NameNode, TypeDefinitionNode
55
from . import SDLValidationContext, SDLValidationRule
66

7-
__all__ = ["UniqueTypeNamesRule", "existed_type_name_message"]
7+
__all__ = [
8+
"UniqueTypeNamesRule",
9+
"duplicate_type_name_message",
10+
"existed_type_name_message",
11+
]
812

913

1014
def duplicate_type_name_message(type_name: str) -> str:

graphql/validation/specified_rules.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
# Schema definition language:
8484
from .rules.lone_schema_definition import LoneSchemaDefinitionRule
8585
from .rules.unique_type_names import UniqueTypeNamesRule
86+
from .rules.unique_directive_names import UniqueDirectiveNamesRule
8687
from .rules.known_argument_names import KnownArgumentNamesOnDirectivesRule
8788
from .rules.provided_required_arguments import ProvidedRequiredArgumentsOnDirectivesRule
8889

@@ -126,6 +127,7 @@
126127
specified_sdl_rules: List[RuleType] = [
127128
LoneSchemaDefinitionRule,
128129
UniqueTypeNamesRule,
130+
UniqueDirectiveNamesRule,
129131
KnownDirectivesRule,
130132
UniqueDirectivesPerLocationRule,
131133
KnownArgumentNamesOnDirectivesRule,

tests/utilities/test_extend_schema.py

Lines changed: 1 addition & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,70 +1049,13 @@ def does_not_allow_replacing_a_default_directive():
10491049
sdl = """
10501050
directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD
10511051
"""
1052-
with raises(GraphQLError) as exc_info:
1052+
with raises(TypeError) as exc_info:
10531053
extend_test_schema(sdl)
10541054
assert str(exc_info.value).startswith(
10551055
"Directive 'include' already exists in the schema."
10561056
" It cannot be redefined."
10571057
)
10581058

1059-
def does_not_allow_replacing_a_custom_directive():
1060-
extended_schema = extend_test_schema(
1061-
"""
1062-
directive @meow(if: Boolean!) on FIELD | FRAGMENT_SPREAD
1063-
"""
1064-
)
1065-
1066-
replacement_ast = parse(
1067-
"""
1068-
directive @meow(if: Boolean!) on FIELD | QUERY
1069-
"""
1070-
)
1071-
1072-
with raises(GraphQLError) as exc_info:
1073-
extend_schema(extended_schema, replacement_ast)
1074-
assert str(exc_info.value).startswith(
1075-
"Directive 'meow' already exists in the schema. It cannot be redefined."
1076-
)
1077-
1078-
def does_not_allow_replacing_an_existing_field():
1079-
def existing_field_error(type_, field):
1080-
return (
1081-
f"Field '{type_}.{field}' already exists in the schema."
1082-
" It cannot also be defined in this type extension."
1083-
)
1084-
1085-
type_sdl = """
1086-
extend type Bar {
1087-
foo: Foo
1088-
}
1089-
"""
1090-
with raises(GraphQLError) as exc_info:
1091-
extend_test_schema(type_sdl)
1092-
assert str(exc_info.value).startswith(existing_field_error("Bar", "foo"))
1093-
1094-
interface_sdl = """
1095-
extend interface SomeInterface {
1096-
some: Foo
1097-
}
1098-
"""
1099-
with raises(GraphQLError) as exc_info:
1100-
extend_test_schema(interface_sdl)
1101-
assert str(exc_info.value).startswith(
1102-
existing_field_error("SomeInterface", "some")
1103-
)
1104-
1105-
input_sdl = """
1106-
extend input SomeInput {
1107-
fooArg: String
1108-
}
1109-
"""
1110-
with raises(GraphQLError) as exc_info:
1111-
extend_test_schema(input_sdl)
1112-
assert str(exc_info.value).startswith(
1113-
existing_field_error("SomeInput", "fooArg")
1114-
)
1115-
11161059
def does_not_allow_replacing_an_existing_enum_value():
11171060
sdl = """
11181061
extend enum SomeEnum {
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from functools import partial
2+
3+
from graphql.utilities import build_schema
4+
from graphql.validation.rules.unique_directive_names import (
5+
UniqueDirectiveNamesRule,
6+
duplicate_directive_name_message,
7+
existed_directive_name_message,
8+
)
9+
10+
from .harness import assert_sdl_validation_errors
11+
12+
assert_errors = partial(assert_sdl_validation_errors, UniqueDirectiveNamesRule)
13+
14+
assert_valid = partial(assert_errors, errors=[])
15+
16+
17+
def describe_validate_unique_directive_names():
18+
def no_directive():
19+
assert_valid(
20+
"""
21+
type Foo
22+
"""
23+
)
24+
25+
def one_directive():
26+
assert_valid(
27+
"""
28+
directive @foo on SCHEMA
29+
"""
30+
)
31+
32+
def many_directives():
33+
assert_valid(
34+
"""
35+
directive @foo on SCHEMA
36+
directive @bar on SCHEMA
37+
directive @baz on SCHEMA
38+
"""
39+
)
40+
41+
def directive_and_non_directive_definitions_named_the_same():
42+
assert_valid(
43+
"""
44+
query foo { __typename }
45+
fragment foo on foo { __typename }
46+
type foo
47+
48+
directive @foo on SCHEMA
49+
"""
50+
)
51+
52+
def directives_named_the_same():
53+
assert_errors(
54+
"""
55+
directive @foo on SCHEMA
56+
57+
directive @foo on SCHEMA
58+
""",
59+
[
60+
{
61+
"message": duplicate_directive_name_message("foo"),
62+
"locations": [(2, 24), (4, 24)],
63+
}
64+
],
65+
)
66+
67+
def adding_new_directive_to_existing_schema():
68+
schema = build_schema("directive @foo on SCHEMA")
69+
70+
assert_valid("directive @bar on SCHEMA", schema=schema)
71+
72+
def adding_new_directive_with_standard_name_to_existing_schema():
73+
schema = build_schema("type foo")
74+
75+
assert_errors(
76+
"directive @skip on SCHEMA",
77+
[
78+
{
79+
"message": existed_directive_name_message("skip"),
80+
"locations": [(1, 12)],
81+
}
82+
],
83+
schema,
84+
)
85+
86+
def adding_new_directive_to_existing_schema_with_same_named_type():
87+
schema = build_schema("type foo")
88+
89+
assert_valid("directive @foo on SCHEMA", schema=schema)
90+
91+
def adding_conflicting_directives_to_existing_schema():
92+
schema = build_schema("directive @foo on SCHEMA")
93+
94+
assert_errors(
95+
"directive @foo on SCHEMA",
96+
[
97+
{
98+
"message": existed_directive_name_message("foo"),
99+
"locations": [(1, 12)],
100+
}
101+
],
102+
schema,
103+
)

0 commit comments

Comments
 (0)