Skip to content

Commit

Permalink
Support value transformation of input object types (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
Cito committed Jun 30, 2019
1 parent 06ed967 commit 6cfc9c7
Show file tree
Hide file tree
Showing 8 changed files with 92 additions and 3 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ a query language for APIs created by Facebook.
[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)

The current version 1.0.5 of GraphQL-core-next is up-to-date with GraphQL.js version
14.3.1. All parts of the API are covered by an extensive test suite of currently 1786
14.3.1. All parts of the API are covered by an extensive test suite of currently 1792
unit tests.


Expand Down
10 changes: 10 additions & 0 deletions graphql/type/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,7 @@ def is_deprecated(self) -> bool:


GraphQLInputFieldMap = Dict[str, "GraphQLInputField"]
GraphQLInputFieldOutType = Callable[[Dict[str, Any]], Any]


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

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

Expand All @@ -1123,6 +1129,7 @@ def __init__(
name: str,
fields: Thunk[GraphQLInputFieldMap],
description: str = None,
out_type: GraphQLInputFieldOutType = None,
ast_node: InputObjectTypeDefinitionNode = None,
extension_ast_nodes: Sequence[InputObjectTypeExtensionNode] = None,
) -> None:
Expand All @@ -1132,6 +1139,8 @@ def __init__(
ast_node=ast_node,
extension_ast_nodes=extension_ast_nodes,
)
if out_type is not None and not callable(out_type):
raise TypeError(f"The out type for {name} must be a function or a class.")
if ast_node and not isinstance(ast_node, InputObjectTypeDefinitionNode):
raise TypeError(
f"{name} AST node must be an InputObjectTypeDefinitionNode."
Expand All @@ -1144,6 +1153,7 @@ def __init__(
f"{name} extension AST nodes must be InputObjectTypeExtensionNode."
)
self._fields = fields
self.out_type = out_type or identity_func # type: ignore

def to_kwargs(self) -> Dict[str, Any]:
return dict(**super().to_kwargs(), fields=self.fields.copy())
Expand Down
6 changes: 5 additions & 1 deletion graphql/utilities/coerce_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,11 @@ def coerce_value(
),
)

return of_errors(errors) if errors else of_value(coerced_value_dict)
return (
of_errors(errors)
if errors
else of_value(type_.out_type(coerced_value_dict)) # type: ignore
)

# Not reachable. All possible input types have been considered.
raise TypeError(f"Unexpected input type: '{inspect(type_)}'.") # pragma: no cover
Expand Down
3 changes: 2 additions & 1 deletion graphql/utilities/value_from_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ def value_from_ast(
if is_invalid(field_value):
return INVALID
coerced_obj[field_name] = field_value
return coerced_obj

return type_.out_type(coerced_obj) # type: ignore

if is_enum_type(type_):
if not isinstance(value_node, EnumValueNode):
Expand Down
26 changes: 26 additions & 0 deletions tests/execution/test_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
GraphQLEnumType,
GraphQLEnumValue,
GraphQLField,
GraphQLFloat,
GraphQLInputField,
GraphQLInputObjectType,
GraphQLList,
Expand Down Expand Up @@ -40,6 +41,12 @@
},
)

TestCustomInputObject = GraphQLInputObjectType(
"TestCustomInputObject",
{"x": GraphQLInputField(GraphQLFloat), "y": GraphQLInputField(GraphQLFloat)},
out_type=lambda value: f"(x|y) = ({value['x']}|{value['y']})",
)


TestNestedInputObject = GraphQLInputObjectType(
"TestNestedInputObject",
Expand Down Expand Up @@ -81,6 +88,9 @@ def field_with_input_arg(input_arg: GraphQLArgument):
GraphQLArgument(GraphQLNonNull(TestEnum))
),
"fieldWithObjectInput": field_with_input_arg(GraphQLArgument(TestInputObject)),
"fieldWithCustomObjectInput": field_with_input_arg(
GraphQLArgument(TestCustomInputObject)
),
"fieldWithNullableStringInput": field_with_input_arg(
GraphQLArgument(GraphQLString)
),
Expand Down Expand Up @@ -135,6 +145,22 @@ def executes_with_complex_input():
None,
)

def executes_with_custom_input():
# This is an extension of GraphQL.js.
result = execute_query(
"""
{
fieldWithCustomObjectInput(
input: {x: -3.0, y: 4.5})
}
"""
)

assert result == (
{"fieldWithCustomObjectInput": "'(x|y) = (-3.0|4.5)'"},
None,
)

def properly_parses_single_value_to_list():
result = execute_query(
"""
Expand Down
23 changes: 23 additions & 0 deletions tests/type/test_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,18 @@ def accepts_an_input_object_type_with_a_field_function():
assert isinstance(input_field, GraphQLInputField)
assert input_field.type is ScalarType

def accepts_an_input_object_type_with_an_out_type_function():
# This is an extension of GraphQL.js.
input_obj_type = GraphQLInputObjectType(
"SomeInputObject", {}, out_type=dict
)
assert input_obj_type.out_type is dict

def provides_default_out_type_if_omitted():
# This is an extension of GraphQL.js.
input_obj_type = GraphQLInputObjectType("SomeInputObject", {})
assert input_obj_type.out_type is identity_func

def rejects_an_input_object_type_with_incorrect_fields():
input_obj_type = GraphQLInputObjectType("SomeInputObject", [])
with raises(TypeError) as exc_info:
Expand All @@ -493,6 +505,17 @@ def rejects_an_input_object_type_with_incorrect_fields_function():
" or a function which returns such an object."
)

def rejects_an_input_object_type_with_incorrect_out_type_function():
with raises(TypeError) as exc_info:
# noinspection PyTypeChecker
GraphQLInputObjectType(
"SomeInputObject", {}, out_type=[]
) # type: ignore
msg = str(exc_info.value)
assert msg == (
"The out type for SomeInputObject must be a function or a class."
)

def describe_type_system_input_objects_fields_must_not_have_resolvers():
def rejects_an_input_object_type_with_resolvers():
with raises(
Expand Down
13 changes: 13 additions & 0 deletions tests/utilities/test_coerce_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,19 @@ def returns_error_for_a_misspelled_field():
" Did you mean bar?"
]

def transforms_values_with_out_type():
# This is an extension of GraphQL.js.
ComplexInputObject = GraphQLInputObjectType(
"Complex",
{
"real": GraphQLInputField(GraphQLFloat),
"imag": GraphQLInputField(GraphQLFloat),
},
out_type=lambda value: complex(value["real"], value["imag"]),
)
result = coerce_value({"real": 1, "imag": 2}, ComplexInputObject)
assert expect_value(result) == 1 + 2j

def describe_for_graphql_list():
TestList = GraphQLList(GraphQLInt)

Expand Down
12 changes: 12 additions & 0 deletions tests/utilities/test_value_from_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,15 @@ def omits_input_object_fields_for_unprovided_variables():
"{ requiredBool: $foo }",
{"int": 42, "requiredBool": True},
)

def transforms_values_with_out_type():
# This is an extension of GraphQL.js.
complex_input_obj = GraphQLInputObjectType(
"Complex",
{
"real": GraphQLInputField(GraphQLFloat),
"imag": GraphQLInputField(GraphQLFloat),
},
out_type=lambda value: complex(value["real"], value["imag"]),
)
_test_case(complex_input_obj, "{ real: 1, imag: 2 }", 1 + 2j)

0 comments on commit 6cfc9c7

Please sign in to comment.