Skip to content

Commit 7df80ab

Browse files
Support enum-field-as-literal in GraphQL parser (#2677)
* initial idea * update dispatch * clean unused * fix required * add tests * tweak name / docstring * cleanup unused
1 parent 8a0d375 commit 7df80ab

5 files changed

Lines changed: 178 additions & 0 deletions

File tree

src/datamodel_code_generator/parser/graphql.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,8 +404,40 @@ def parse_scalar(self, scalar_graphql_object: graphql.GraphQLScalarType) -> None
404404
)
405405
)
406406

407+
def should_parse_enum_as_literal(self, obj: graphql.GraphQLEnumType) -> bool:
408+
"""Determine if an enum should be parsed as a literal type."""
409+
return self.enum_field_as_literal == LiteralType.All or (
410+
self.enum_field_as_literal == LiteralType.One and len(obj.values) == 1
411+
)
412+
407413
def parse_enum(self, enum_object: graphql.GraphQLEnumType) -> None:
408414
"""Parse a GraphQL enum type and add it to results."""
415+
if self.should_parse_enum_as_literal(enum_object):
416+
return self.parse_enum_as_literal(enum_object)
417+
return self.parse_enum_as_enum_class(enum_object)
418+
419+
def parse_enum_as_literal(self, enum_object: graphql.GraphQLEnumType) -> None:
420+
"""Parse enum values as a Literal type."""
421+
data_type = self.data_type(literals=list(enum_object.values.keys()))
422+
data_model_type = self._create_data_model(
423+
model_type=self.data_model_root_type,
424+
reference=self.references[enum_object.name],
425+
fields=[
426+
self.data_model_field_type(
427+
required=True,
428+
data_type=data_type,
429+
)
430+
],
431+
custom_base_class=self.base_class,
432+
custom_template_dir=self.custom_template_dir,
433+
extra_template_data=self.extra_template_data,
434+
path=self.current_source_path,
435+
description=enum_object.description,
436+
)
437+
self.results.append(data_model_type)
438+
439+
def parse_enum_as_enum_class(self, enum_object: graphql.GraphQLEnumType) -> None:
440+
"""Parse enum values as an Enum class."""
409441
enum_fields: list[DataModelFieldBase] = []
410442
exclude_field_names: set[str] = set()
411443

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# generated by datamodel-codegen:
2+
# filename: enums.graphql
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Literal
8+
9+
from pydantic import BaseModel
10+
from typing_extensions import TypeAlias
11+
12+
Boolean: TypeAlias = bool
13+
"""
14+
The `Boolean` scalar type represents `true` or `false`.
15+
"""
16+
17+
18+
String: TypeAlias = str
19+
"""
20+
The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
21+
"""
22+
23+
24+
class Color(BaseModel):
25+
__root__: Literal['BLUE', 'GREEN', 'RED']
26+
27+
28+
class EmployeeShiftStatus(BaseModel):
29+
"""
30+
Employee shift status
31+
"""
32+
33+
__root__: Literal['NOT_ON_SHIFT', 'ON_SHIFT']
34+
35+
36+
class EnumWithOneField(BaseModel):
37+
__root__: Literal['FIELD']
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# generated by datamodel-codegen:
2+
# filename: enums.graphql
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from enum import Enum
8+
from typing import Literal
9+
10+
from pydantic import BaseModel
11+
from typing_extensions import TypeAlias
12+
13+
Boolean: TypeAlias = bool
14+
"""
15+
The `Boolean` scalar type represents `true` or `false`.
16+
"""
17+
18+
19+
String: TypeAlias = str
20+
"""
21+
The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
22+
"""
23+
24+
25+
class Color(Enum):
26+
BLUE = 'BLUE'
27+
GREEN = 'GREEN'
28+
RED = 'RED'
29+
30+
31+
class EmployeeShiftStatus(Enum):
32+
"""
33+
Employee shift status
34+
"""
35+
36+
NOT_ON_SHIFT = 'NOT_ON_SHIFT'
37+
ON_SHIFT = 'ON_SHIFT'
38+
39+
40+
class EnumWithOneField(BaseModel):
41+
__root__: Literal['FIELD']
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# generated by datamodel-codegen:
2+
# filename: enums.graphql
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Literal
8+
9+
from typing_extensions import TypeAlias
10+
11+
Boolean: TypeAlias = bool
12+
"""
13+
The `Boolean` scalar type represents `true` or `false`.
14+
"""
15+
16+
17+
String: TypeAlias = str
18+
"""
19+
The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
20+
"""
21+
22+
23+
Color: TypeAlias = Literal['BLUE', 'GREEN', 'RED']
24+
25+
26+
EmployeeShiftStatus: TypeAlias = Literal['NOT_ON_SHIFT', 'ON_SHIFT']
27+
"""
28+
Employee shift status
29+
"""
30+
31+
32+
EnumWithOneField: TypeAlias = Literal['FIELD']

tests/main/graphql/test_main_graphql.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,42 @@ def test_main_graphql_specialized_enums(output_file: Path) -> None:
165165
)
166166

167167

168+
def test_main_graphql_enums_as_literals_all(output_file: Path) -> None:
169+
"""Test GraphQL code generation with all enum fields as literals."""
170+
run_main_and_assert(
171+
input_path=GRAPHQL_DATA_PATH / "enums.graphql",
172+
output_path=output_file,
173+
input_file_type="graphql",
174+
assert_func=assert_file_content,
175+
expected_file="enum_literals_all.py",
176+
extra_args=["--enum-field-as-literal", "all"],
177+
)
178+
179+
180+
def test_main_graphql_enums_as_literals_one(output_file: Path) -> None:
181+
"""Test GraphQL code generation with single-field enums as literals."""
182+
run_main_and_assert(
183+
input_path=GRAPHQL_DATA_PATH / "enums.graphql",
184+
output_path=output_file,
185+
input_file_type="graphql",
186+
assert_func=assert_file_content,
187+
expected_file="enum_literals_one.py",
188+
extra_args=["--enum-field-as-literal", "one"],
189+
)
190+
191+
192+
def test_main_graphql_enums_to_typed_dict(output_file: Path) -> None:
193+
"""Test GraphQL code generation paired with typing.TypedDict output which forces enums as literals."""
194+
run_main_and_assert(
195+
input_path=GRAPHQL_DATA_PATH / "enums.graphql",
196+
output_path=output_file,
197+
input_file_type="graphql",
198+
assert_func=assert_file_content,
199+
expected_file="enums_typed_dict.py",
200+
extra_args=["--output-model-type", "typing.TypedDict"],
201+
)
202+
203+
168204
@pytest.mark.skipif(
169205
black.__version__.split(".")[0] == "22",
170206
reason="Installed black doesn't support the old style",

0 commit comments

Comments
 (0)