Skip to content

Commit d1ba78d

Browse files
authored
DSL serialize complex arguments to literals (#255)
1 parent 37f1917 commit d1ba78d

File tree

3 files changed

+368
-8
lines changed

3 files changed

+368
-8
lines changed

gql/dsl.py

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
import logging
2+
import re
23
from abc import ABC, abstractmethod
4+
from math import isfinite
35
from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, Union, cast
46

57
from graphql import (
68
ArgumentNode,
9+
BooleanValueNode,
710
DocumentNode,
11+
EnumValueNode,
812
FieldNode,
13+
FloatValueNode,
914
FragmentDefinitionNode,
1015
FragmentSpreadNode,
1116
GraphQLArgument,
17+
GraphQLError,
1218
GraphQLField,
19+
GraphQLID,
1320
GraphQLInputObjectType,
1421
GraphQLInputType,
1522
GraphQLInterfaceType,
@@ -20,6 +27,7 @@
2027
GraphQLSchema,
2128
GraphQLWrappingType,
2229
InlineFragmentNode,
30+
IntValueNode,
2331
ListTypeNode,
2432
ListValueNode,
2533
NamedTypeNode,
@@ -31,25 +39,76 @@
3139
OperationDefinitionNode,
3240
OperationType,
3341
SelectionSetNode,
42+
StringValueNode,
3443
TypeNode,
3544
Undefined,
3645
ValueNode,
3746
VariableDefinitionNode,
3847
VariableNode,
3948
assert_named_type,
49+
is_enum_type,
4050
is_input_object_type,
51+
is_leaf_type,
4152
is_list_type,
4253
is_non_null_type,
4354
is_wrapping_type,
4455
print_ast,
4556
)
46-
from graphql.pyutils import FrozenList
47-
from graphql.utilities import ast_from_value as default_ast_from_value
57+
from graphql.pyutils import FrozenList, inspect
4858

4959
from .utils import to_camel_case
5060

5161
log = logging.getLogger(__name__)
5262

63+
_re_integer_string = re.compile("^-?(?:0|[1-9][0-9]*)$")
64+
65+
66+
def ast_from_serialized_value_untyped(serialized: Any) -> Optional[ValueNode]:
67+
"""Given a serialized value, try our best to produce an AST.
68+
69+
Anything ressembling an array (instance of Mapping) will be converted
70+
to an ObjectFieldNode.
71+
72+
Anything ressembling a list (instance of Iterable - except str)
73+
will be converted to a ListNode.
74+
75+
In some cases, a custom scalar can be serialized differently in the query
76+
than in the variables. In that case, this function will not work."""
77+
78+
if serialized is None or serialized is Undefined:
79+
return NullValueNode()
80+
81+
if isinstance(serialized, Mapping):
82+
field_items = (
83+
(key, ast_from_serialized_value_untyped(value))
84+
for key, value in serialized.items()
85+
)
86+
field_nodes = (
87+
ObjectFieldNode(name=NameNode(value=field_name), value=field_value)
88+
for field_name, field_value in field_items
89+
if field_value
90+
)
91+
return ObjectValueNode(fields=FrozenList(field_nodes))
92+
93+
if isinstance(serialized, Iterable) and not isinstance(serialized, str):
94+
maybe_nodes = (ast_from_serialized_value_untyped(item) for item in serialized)
95+
nodes = filter(None, maybe_nodes)
96+
return ListValueNode(values=FrozenList(nodes))
97+
98+
if isinstance(serialized, bool):
99+
return BooleanValueNode(value=serialized)
100+
101+
if isinstance(serialized, int):
102+
return IntValueNode(value=f"{serialized:d}")
103+
104+
if isinstance(serialized, float) and isfinite(serialized):
105+
return FloatValueNode(value=f"{serialized:g}")
106+
107+
if isinstance(serialized, str):
108+
return StringValueNode(value=serialized)
109+
110+
raise TypeError(f"Cannot convert value to AST: {inspect(serialized)}.")
111+
53112

54113
def ast_from_value(value: Any, type_: GraphQLInputType) -> Optional[ValueNode]:
55114
"""
@@ -60,15 +119,21 @@ def ast_from_value(value: Any, type_: GraphQLInputType) -> Optional[ValueNode]:
60119
VariableNode when value is a DSLVariable
61120
62121
Produce a GraphQL Value AST given a Python object.
122+
123+
Raises a GraphQLError instead of returning None if we receive an Undefined
124+
of if we receive a Null value for a Non-Null type.
63125
"""
64126
if isinstance(value, DSLVariable):
65127
return value.set_type(type_).ast_variable
66128

67129
if is_non_null_type(type_):
68130
type_ = cast(GraphQLNonNull, type_)
69-
ast_value = ast_from_value(value, type_.of_type)
131+
inner_type = type_.of_type
132+
ast_value = ast_from_value(value, inner_type)
70133
if isinstance(ast_value, NullValueNode):
71-
return None
134+
raise GraphQLError(
135+
"Received Null value for a Non-Null type " f"{inspect(inner_type)}."
136+
)
72137
return ast_value
73138

74139
# only explicit None, not Undefined or NaN
@@ -77,7 +142,7 @@ def ast_from_value(value: Any, type_: GraphQLInputType) -> Optional[ValueNode]:
77142

78143
# undefined
79144
if value is Undefined:
80-
return None
145+
raise GraphQLError(f"Received Undefined value for type {inspect(type_)}.")
81146

82147
# Convert Python list to GraphQL list. If the GraphQLType is a list, but the value
83148
# is not a list, convert the value using the list's item type.
@@ -108,7 +173,32 @@ def ast_from_value(value: Any, type_: GraphQLInputType) -> Optional[ValueNode]:
108173
)
109174
return ObjectValueNode(fields=FrozenList(field_nodes))
110175

111-
return default_ast_from_value(value, type_)
176+
if is_leaf_type(type_):
177+
# Since value is an internally represented value, it must be serialized to an
178+
# externally represented value before converting into an AST.
179+
serialized = type_.serialize(value) # type: ignore
180+
181+
# if the serialized value is a string, then we should use the
182+
# type to determine if it is an enum, an ID or a normal string
183+
if isinstance(serialized, str):
184+
# Enum types use Enum literals.
185+
if is_enum_type(type_):
186+
return EnumValueNode(value=serialized)
187+
188+
# ID types can use Int literals.
189+
if type_ is GraphQLID and _re_integer_string.match(serialized):
190+
return IntValueNode(value=serialized)
191+
192+
return StringValueNode(value=serialized)
193+
194+
# Some custom scalars will serialize to dicts or lists
195+
# Providing here a default conversion to AST using our best judgment
196+
# until graphql-js issue #1817 is solved
197+
# https://github.com/graphql/graphql-js/issues/1817
198+
return ast_from_serialized_value_untyped(serialized)
199+
200+
# Not reachable. All possible input types have been considered.
201+
raise TypeError(f"Unexpected input type: {inspect(type_)}.")
112202

113203

114204
def dsl_gql(

0 commit comments

Comments
 (0)