Skip to content

Commit 6cfc9c7

Browse files
committed
Support value transformation of input object types (#42)
1 parent 06ed967 commit 6cfc9c7

File tree

8 files changed

+92
-3
lines changed

8 files changed

+92
-3
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.5 of GraphQL-core-next is up-to-date with GraphQL.js version
16-
14.3.1. All parts of the API are covered by an extensive test suite of currently 1786
16+
14.3.1. All parts of the API are covered by an extensive test suite of currently 1792
1717
unit tests.
1818

1919

graphql/type/definition.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,6 +1091,7 @@ def is_deprecated(self) -> bool:
10911091

10921092

10931093
GraphQLInputFieldMap = Dict[str, "GraphQLInputField"]
1094+
GraphQLInputFieldOutType = Callable[[Dict[str, Any]], Any]
10941095

10951096

10961097
class GraphQLInputObjectType(GraphQLNamedType):
@@ -1113,8 +1114,13 @@ class GeoPoint(GraphQLInputObjectType):
11131114
'alt': GraphQLInputField(
11141115
GraphQLFloat(), default_value=0)
11151116
}
1117+
1118+
The outbound values will be Python dictionaries by default, but you can have them
1119+
converted to other types by specifying an `out_type` function or class.
11161120
"""
11171121

1122+
# Transforms values to different type (this is an extension of GraphQL.js).
1123+
out_type: GraphQLInputFieldOutType
11181124
ast_node: Optional[InputObjectTypeDefinitionNode]
11191125
extension_ast_nodes: Optional[Tuple[InputObjectTypeExtensionNode]]
11201126

@@ -1123,6 +1129,7 @@ def __init__(
11231129
name: str,
11241130
fields: Thunk[GraphQLInputFieldMap],
11251131
description: str = None,
1132+
out_type: GraphQLInputFieldOutType = None,
11261133
ast_node: InputObjectTypeDefinitionNode = None,
11271134
extension_ast_nodes: Sequence[InputObjectTypeExtensionNode] = None,
11281135
) -> None:
@@ -1132,6 +1139,8 @@ def __init__(
11321139
ast_node=ast_node,
11331140
extension_ast_nodes=extension_ast_nodes,
11341141
)
1142+
if out_type is not None and not callable(out_type):
1143+
raise TypeError(f"The out type for {name} must be a function or a class.")
11351144
if ast_node and not isinstance(ast_node, InputObjectTypeDefinitionNode):
11361145
raise TypeError(
11371146
f"{name} AST node must be an InputObjectTypeDefinitionNode."
@@ -1144,6 +1153,7 @@ def __init__(
11441153
f"{name} extension AST nodes must be InputObjectTypeExtensionNode."
11451154
)
11461155
self._fields = fields
1156+
self.out_type = out_type or identity_func # type: ignore
11471157

11481158
def to_kwargs(self) -> Dict[str, Any]:
11491159
return dict(**super().to_kwargs(), fields=self.fields.copy())

graphql/utilities/coerce_value.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,11 @@ def coerce_value(
172172
),
173173
)
174174

175-
return of_errors(errors) if errors else of_value(coerced_value_dict)
175+
return (
176+
of_errors(errors)
177+
if errors
178+
else of_value(type_.out_type(coerced_value_dict)) # type: ignore
179+
)
176180

177181
# Not reachable. All possible input types have been considered.
178182
raise TypeError(f"Unexpected input type: '{inspect(type_)}'.") # pragma: no cover

graphql/utilities/value_from_ast.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ def value_from_ast(
122122
if is_invalid(field_value):
123123
return INVALID
124124
coerced_obj[field_name] = field_value
125-
return coerced_obj
125+
126+
return type_.out_type(coerced_obj) # type: ignore
126127

127128
if is_enum_type(type_):
128129
if not isinstance(value_node, EnumValueNode):

tests/execution/test_variables.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
GraphQLEnumType,
99
GraphQLEnumValue,
1010
GraphQLField,
11+
GraphQLFloat,
1112
GraphQLInputField,
1213
GraphQLInputObjectType,
1314
GraphQLList,
@@ -40,6 +41,12 @@
4041
},
4142
)
4243

44+
TestCustomInputObject = GraphQLInputObjectType(
45+
"TestCustomInputObject",
46+
{"x": GraphQLInputField(GraphQLFloat), "y": GraphQLInputField(GraphQLFloat)},
47+
out_type=lambda value: f"(x|y) = ({value['x']}|{value['y']})",
48+
)
49+
4350

4451
TestNestedInputObject = GraphQLInputObjectType(
4552
"TestNestedInputObject",
@@ -81,6 +88,9 @@ def field_with_input_arg(input_arg: GraphQLArgument):
8188
GraphQLArgument(GraphQLNonNull(TestEnum))
8289
),
8390
"fieldWithObjectInput": field_with_input_arg(GraphQLArgument(TestInputObject)),
91+
"fieldWithCustomObjectInput": field_with_input_arg(
92+
GraphQLArgument(TestCustomInputObject)
93+
),
8494
"fieldWithNullableStringInput": field_with_input_arg(
8595
GraphQLArgument(GraphQLString)
8696
),
@@ -135,6 +145,22 @@ def executes_with_complex_input():
135145
None,
136146
)
137147

148+
def executes_with_custom_input():
149+
# This is an extension of GraphQL.js.
150+
result = execute_query(
151+
"""
152+
{
153+
fieldWithCustomObjectInput(
154+
input: {x: -3.0, y: 4.5})
155+
}
156+
"""
157+
)
158+
159+
assert result == (
160+
{"fieldWithCustomObjectInput": "'(x|y) = (-3.0|4.5)'"},
161+
None,
162+
)
163+
138164
def properly_parses_single_value_to_list():
139165
result = execute_query(
140166
"""

tests/type/test_definition.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,18 @@ def accepts_an_input_object_type_with_a_field_function():
471471
assert isinstance(input_field, GraphQLInputField)
472472
assert input_field.type is ScalarType
473473

474+
def accepts_an_input_object_type_with_an_out_type_function():
475+
# This is an extension of GraphQL.js.
476+
input_obj_type = GraphQLInputObjectType(
477+
"SomeInputObject", {}, out_type=dict
478+
)
479+
assert input_obj_type.out_type is dict
480+
481+
def provides_default_out_type_if_omitted():
482+
# This is an extension of GraphQL.js.
483+
input_obj_type = GraphQLInputObjectType("SomeInputObject", {})
484+
assert input_obj_type.out_type is identity_func
485+
474486
def rejects_an_input_object_type_with_incorrect_fields():
475487
input_obj_type = GraphQLInputObjectType("SomeInputObject", [])
476488
with raises(TypeError) as exc_info:
@@ -493,6 +505,17 @@ def rejects_an_input_object_type_with_incorrect_fields_function():
493505
" or a function which returns such an object."
494506
)
495507

508+
def rejects_an_input_object_type_with_incorrect_out_type_function():
509+
with raises(TypeError) as exc_info:
510+
# noinspection PyTypeChecker
511+
GraphQLInputObjectType(
512+
"SomeInputObject", {}, out_type=[]
513+
) # type: ignore
514+
msg = str(exc_info.value)
515+
assert msg == (
516+
"The out type for SomeInputObject must be a function or a class."
517+
)
518+
496519
def describe_type_system_input_objects_fields_must_not_have_resolvers():
497520
def rejects_an_input_object_type_with_resolvers():
498521
with raises(

tests/utilities/test_coerce_value.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,19 @@ def returns_error_for_a_misspelled_field():
241241
" Did you mean bar?"
242242
]
243243

244+
def transforms_values_with_out_type():
245+
# This is an extension of GraphQL.js.
246+
ComplexInputObject = GraphQLInputObjectType(
247+
"Complex",
248+
{
249+
"real": GraphQLInputField(GraphQLFloat),
250+
"imag": GraphQLInputField(GraphQLFloat),
251+
},
252+
out_type=lambda value: complex(value["real"], value["imag"]),
253+
)
254+
result = coerce_value({"real": 1, "imag": 2}, ComplexInputObject)
255+
assert expect_value(result) == 1 + 2j
256+
244257
def describe_for_graphql_list():
245258
TestList = GraphQLList(GraphQLInt)
246259

tests/utilities/test_value_from_ast.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,15 @@ def omits_input_object_fields_for_unprovided_variables():
174174
"{ requiredBool: $foo }",
175175
{"int": 42, "requiredBool": True},
176176
)
177+
178+
def transforms_values_with_out_type():
179+
# This is an extension of GraphQL.js.
180+
complex_input_obj = GraphQLInputObjectType(
181+
"Complex",
182+
{
183+
"real": GraphQLInputField(GraphQLFloat),
184+
"imag": GraphQLInputField(GraphQLFloat),
185+
},
186+
out_type=lambda value: complex(value["real"], value["imag"]),
187+
)
188+
_test_case(complex_input_obj, "{ real: 1, imag: 2 }", 1 + 2j)

0 commit comments

Comments
 (0)