Skip to content

Commit c320e57

Browse files
committed
Extract check for unique enum value names into separate rule
Replicates graphql/graphql-js@0737212
1 parent 7154b96 commit c320e57

File tree

8 files changed

+266
-65
lines changed

8 files changed

+266
-65
lines changed

graphql/type/validate.py

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from ..error import GraphQLError
55
from ..pyutils import inspect
66
from ..language import (
7-
EnumValueDefinitionNode,
87
FieldDefinitionNode,
98
InputValueDefinitionNode,
109
NamedTypeNode,
@@ -412,15 +411,6 @@ def validate_enum_values(self, enum_type: GraphQLEnumType):
412411
)
413412

414413
for value_name, enum_value in enum_values.items():
415-
# Ensure no duplicates.
416-
all_nodes = get_enum_value_nodes(enum_type, value_name)
417-
if all_nodes and len(all_nodes) > 1:
418-
self.report_error(
419-
f"Enum type {enum_type.name}"
420-
f" can include value {value_name} only once.",
421-
all_nodes,
422-
)
423-
424414
# Ensure valid name.
425415
self.validate_name(enum_value, value_name)
426416
if value_name in ("true", "false", "null"):
@@ -604,13 +594,3 @@ def get_union_member_type_nodes(
604594
return [
605595
union_node for union_node in union_nodes if union_node.name.value == type_name
606596
]
607-
608-
609-
def get_enum_value_nodes(
610-
enum_type: GraphQLEnumType, value_name: str
611-
) -> Optional[List[EnumValueDefinitionNode]]:
612-
enum_nodes = cast(
613-
List[EnumValueDefinitionNode],
614-
get_all_sub_nodes(enum_type, attrgetter("values")),
615-
)
616-
return [enum_node for enum_node in enum_nodes if enum_node.name.value == value_name]

graphql/utilities/extend_schema.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ def extend_input_field_map(type_: GraphQLInputObjectType) -> GraphQLInputFieldMa
228228
# If there are any extensions to the fields, apply those here.
229229
extensions = type_extensions_map.get(type_.name)
230230
if extensions:
231+
build_input_field = ast_builder.build_input_field
231232
for extension in extensions:
232233
for field in extension.fields:
233234
new_field_map[field.name.value] = build_input_field(field)
@@ -254,31 +255,23 @@ def extend_enum_type(type_: GraphQLEnumType) -> GraphQLEnumType:
254255
)
255256

256257
def extend_value_map(type_: GraphQLEnumType) -> GraphQLEnumValueMap:
257-
old_value_map = type_.values
258258
new_value_map = {
259259
value_name: GraphQLEnumValue(
260260
value.value,
261261
description=value.description,
262262
deprecation_reason=value.deprecation_reason,
263263
ast_node=value.ast_node,
264264
)
265-
for value_name, value in old_value_map.items()
265+
for value_name, value in type_.values.items()
266266
}
267267

268268
# If there are any extensions to the values, apply those here.
269269
extensions = type_extensions_map.get(type_.name)
270270
if extensions:
271+
build_enum_value = ast_builder.build_enum_value
271272
for extension in extensions:
272273
for value in extension.values:
273-
value_name = value.name.value
274-
if value_name in old_value_map:
275-
raise GraphQLError(
276-
f"Enum value '{type_.name}.{value_name}' already"
277-
" exists in the schema. It cannot also be defined"
278-
" in this type extension.",
279-
[value],
280-
)
281-
new_value_map[value_name] = ast_builder.build_enum_value(value)
274+
new_value_map[value.name.value] = build_enum_value(value)
282275

283276
return new_value_map
284277

@@ -389,7 +382,7 @@ def extend_possible_types(type_: GraphQLUnionType) -> List[GraphQLObjectType]:
389382
# typed values, that would throw immediately while type system
390383
# validation with `validate_schema()` will produce more actionable
391384
# results.
392-
possible_types.append(ast_builder.build_type(named_type))
385+
possible_types.append(build_type(named_type))
393386

394387
return cast(List[GraphQLObjectType], possible_types)
395388

@@ -434,6 +427,7 @@ def extend_field_map(
434427
}
435428

436429
# If there are any extensions to the fields, apply those here.
430+
build_field = ast_builder.build_field
437431
for extension in type_extensions_map[type_.name]:
438432
for field in extension.fields:
439433
new_field_map[field.name.value] = build_field(field)
@@ -458,8 +452,6 @@ def resolve_type(type_name: str) -> GraphQLNamedType:
458452
ast_builder = ASTDefinitionBuilder(
459453
type_definition_map, assume_valid=assume_valid, resolve_type=resolve_type
460454
)
461-
build_field = ast_builder.build_field
462-
build_input_field = ast_builder.build_input_field
463455
build_type = ast_builder.build_type
464456

465457
extend_type_cache: Dict[str, GraphQLNamedType] = {}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from typing import cast, Dict
2+
3+
from ...error import GraphQLError
4+
from ...language import NameNode, EnumTypeDefinitionNode
5+
from ...type import is_enum_type, GraphQLEnumType
6+
from . import SDLValidationContext, SDLValidationRule
7+
8+
__all__ = [
9+
"UniqueEnumValueNamesRule",
10+
"duplicate_enum_value_name_message",
11+
"existed_enum_value_name_message",
12+
]
13+
14+
15+
def duplicate_enum_value_name_message(type_name: str, value_name: str) -> str:
16+
return f"Enum value '{type_name}.{value_name}' can only be defined once."
17+
18+
19+
def existed_enum_value_name_message(type_name: str, value_name: str) -> str:
20+
return (
21+
f"Enum value '{type_name}.{value_name}' already exists in the schema."
22+
" It cannot also be defined in this type extension."
23+
)
24+
25+
26+
class UniqueEnumValueNamesRule(SDLValidationRule):
27+
"""Unique enum value names
28+
29+
A GraphQL enum type is only valid if all its values are uniquely named.
30+
"""
31+
32+
def __init__(self, context: SDLValidationContext) -> None:
33+
super().__init__(context)
34+
schema = context.schema
35+
self.existing_type_map = schema.type_map if schema else {}
36+
self.known_value_names: Dict[str, Dict[str, NameNode]] = {}
37+
38+
def check_value_uniqueness(self, node: EnumTypeDefinitionNode, *_args):
39+
type_name = node.name.value
40+
41+
known_value_names = self.known_value_names
42+
if type_name not in known_value_names:
43+
known_value_names[type_name] = {}
44+
45+
if node.values:
46+
value_names = known_value_names[type_name]
47+
existing_type_map = self.existing_type_map
48+
49+
for value_def in node.values:
50+
value_name = value_def.name.value
51+
52+
existing_type = existing_type_map.get(type_name)
53+
if (
54+
is_enum_type(existing_type)
55+
and value_name in cast(GraphQLEnumType, existing_type).values
56+
):
57+
self.report_error(
58+
GraphQLError(
59+
existed_enum_value_name_message(type_name, value_name),
60+
[value_def.name],
61+
)
62+
)
63+
elif value_name in value_names:
64+
self.report_error(
65+
GraphQLError(
66+
duplicate_enum_value_name_message(type_name, value_name),
67+
[value_names[value_name], value_def.name],
68+
)
69+
)
70+
else:
71+
value_names[value_name] = value_def.name
72+
73+
return self.SKIP
74+
75+
enter_enum_type_definition = check_value_uniqueness
76+
enter_enum_type_extension = check_value_uniqueness

graphql/validation/specified_rules.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
from .rules.lone_schema_definition import LoneSchemaDefinitionRule
8585
from .rules.unique_operation_types import UniqueOperationTypesRule
8686
from .rules.unique_type_names import UniqueTypeNamesRule
87+
from .rules.unique_enum_value_names import UniqueEnumValueNamesRule
8788
from .rules.unique_field_definition_names import UniqueFieldDefinitionNamesRule
8889
from .rules.unique_directive_names import UniqueDirectiveNamesRule
8990
from .rules.known_argument_names import KnownArgumentNamesOnDirectivesRule
@@ -130,6 +131,7 @@
130131
LoneSchemaDefinitionRule,
131132
UniqueOperationTypesRule,
132133
UniqueTypeNamesRule,
134+
UniqueEnumValueNamesRule,
133135
UniqueFieldDefinitionNamesRule,
134136
UniqueDirectiveNamesRule,
135137
KnownTypeNamesRule,

tests/type/test_validation.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -725,27 +725,6 @@ def rejects_an_enum_type_without_values():
725725
}
726726
]
727727

728-
def rejects_an_enum_type_with_duplicate_values():
729-
schema = build_schema(
730-
"""
731-
type Query {
732-
field: SomeEnum
733-
}
734-
735-
enum SomeEnum {
736-
SOME_VALUE
737-
SOME_VALUE
738-
}
739-
"""
740-
)
741-
assert validate_schema(schema) == [
742-
{
743-
"message": "Enum type SomeEnum can include value SOME_VALUE"
744-
" only once.",
745-
"locations": [(7, 15), (8, 15)],
746-
}
747-
]
748-
749728
def rejects_an_enum_type_with_incorrectly_named_values():
750729
def schema_with_enum(name):
751730
return schema_with_field_type(

tests/utilities/test_extend_schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1061,7 +1061,7 @@ def does_not_allow_replacing_an_existing_enum_value():
10611061
ONE
10621062
}
10631063
"""
1064-
with raises(GraphQLError) as exc_info:
1064+
with raises(TypeError) as exc_info:
10651065
extend_test_schema(sdl)
10661066
assert str(exc_info.value).startswith(
10671067
"Enum value 'SomeEnum.ONE' already exists in the schema."

0 commit comments

Comments
 (0)