Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bidirectional scalars #24

Merged
merged 13 commits into from
Sep 12, 2018
18 changes: 13 additions & 5 deletions ariadne/resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,24 @@ def add_resolve_functions_to_schema(schema: GraphQLSchema, resolvers: dict):
if isinstance(type_object, GraphQLObjectType):
add_resolve_functions_to_object(type_name, type_object, resolvers)
if isinstance(type_object, GraphQLScalarType):
add_resolve_function_to_scalar(type_name, type_object, resolvers)
add_resolve_functions_to_scalar(type_name, type_object, resolvers)


def add_resolve_functions_to_object(name: str, obj: GraphQLObjectType, resolvers: dict):
type_resolver = resolvers.get(name, {})
type_resolvers = resolvers.get(name, {})
for field_name, field_object in obj.fields.items():
field_resolver = type_resolver.get(field_name, default_resolver)
field_resolver = type_resolvers.get(field_name, default_resolver)
field_object.resolver = field_resolver


def add_resolve_function_to_scalar(name: str, obj: GraphQLObjectType, resolvers: dict):
serializer = resolvers.get(name, obj.serialize)
def add_resolve_functions_to_scalar(name: str, obj: GraphQLObjectType, resolvers: dict):
scalar_resolvers = resolvers.get(name, {})

serializer = scalar_resolvers.get("serializer", obj.serialize)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: parse_* are imperative verbs, serializer is a noun.

obj.serialize = serializer

parse_literal = scalar_resolvers.get("parse_literal", obj.parse_literal)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could default to calling parse_value with ast.value when only one function is provided?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This requires further study. IntValue, StringValue and friends are obvious to deal with, but but complex types like ListValue may require some extra unpacking magic.

Imho its worth pursuing in separate task.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@patrys this idea is now tracked in #26

obj.parse_literal = parse_literal

parse_value = scalar_resolvers.get("parse_value", obj.parse_value)
obj.parse_value = parse_value
124 changes: 124 additions & 0 deletions tests/test_custom_scalars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
from datetime import date, datetime

from graphql import graphql
from graphql.language.ast import StringValue

from ariadne import make_executable_schema

type_defs = """
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to use ALL_CAPITAL_WITH_UNDERSCORE convention for constants, just like pep8 suggests.
This is especially useful when dealing with module-scoped entities.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have strong opinion either way, but I've been looking at type_defs and resolvers more like Django's urlpatterns that are supposed to be defined and modified as it is comfortable to the dev. I know that people will look at our tests suite for examples and inspiration, and hence wanted this approach to be reflected in them.

scalar DateReadOnly
scalar DateInput

type Query {
testSerialize: DateReadOnly!
testInput(value: DateInput!): Boolean!
}
"""


def serialize(date):
return date.strftime("%Y-%m-%d")


def parse_literal(ast):
if isinstance(ast, StringValue):
salwator marked this conversation as resolved.
Show resolved Hide resolved
formatted_date = ast.value
try:
parsed_datetime = datetime.strptime(formatted_date, "%Y-%m-%d")
return parsed_datetime.date()
except ValueError:
pass
return None


def parse_value(formatted_date):
try:
parsed_datetime = datetime.strptime(formatted_date, "%Y-%m-%d")
return parsed_datetime.date()
except (TypeError, ValueError):
return None


def resolve_test_serialize(*_):
return date.today()


def resolve_test_input(*_, value):
assert value == date.today()
return True


resolvers = {
"Query": {"testSerialize": resolve_test_serialize, "testInput": resolve_test_input},
"DateReadOnly": {"serializer": serialize},
"DateInput": {"parse_literal": parse_literal, "parse_value": parse_value},
}

schema = make_executable_schema(type_defs, resolvers)


def test_serialize_date_obj_to_date_str():
result = graphql(schema, "{ testSerialize }")
assert result.errors is None
assert result.data == {"testSerialize": date.today().strftime("%Y-%m-%d")}


def test_parse_literal_valid_str_ast_to_date_instance():
test_input = date.today().strftime("%Y-%m-%d")
salwator marked this conversation as resolved.
Show resolved Hide resolved
result = graphql(schema, '{ testInput(value: "%s") }' % test_input)
assert result.errors is None
assert result.data == {"testInput": True}


def test_parse_literal_invalid_str_ast_to_date_instance():
salwator marked this conversation as resolved.
Show resolved Hide resolved
test_input = "invalid string"
result = graphql(schema, '{ testInput(value: "%s") }' % test_input)
assert result.errors is not None
assert str(result.errors[0]).splitlines() == [
'Argument "value" has invalid value "invalid string".',
'Expected type "DateInput", found "invalid string".',
]


def test_parse_literal_invalid_int_ast_errors():
test_input = 123
result = graphql(schema, "{ testInput(value: %s) }" % test_input)
assert result.errors is not None
assert str(result.errors[0]).splitlines() == [
'Argument "value" has invalid value 123.',
'Expected type "DateInput", found 123.',
]


parametrized_query = """
query parseValueTest($value: DateInput!) {
testInput(value: $value)
}
"""


def test_parse_value_valid_date_str_returns_date_instance():
variables = {"value": date.today().strftime("%Y-%m-%d")}
result = graphql(schema, parametrized_query, variables=variables)
assert result.errors is None
assert result.data == {"testInput": True}


def test_parse_value_invalid_str_errors():
variables = {"value": "invalid string"}
result = graphql(schema, parametrized_query, variables=variables)
assert result.errors is not None
assert str(result.errors[0]).splitlines() == [
'Variable "$value" got invalid value "invalid string".',
'Expected type "DateInput", found "invalid string".',
]


def test_parse_value_invalid_value_type_int_errors():
variables = {"value": 123}
result = graphql(schema, parametrized_query, variables=variables)
assert result.errors is not None
assert str(result.errors[0]).splitlines() == [
'Variable "$value" got invalid value 123.',
'Expected type "DateInput", found 123.',
]
24 changes: 1 addition & 23 deletions tests/test_queries.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from datetime import date
from unittest.mock import Mock

from graphql import graphql

from ariadne import make_executable_schema, resolve_to


def test_query_default_scalar():
def test_query_root_type_default_resolver():
type_defs = """
type Query {
test: String
Expand All @@ -22,27 +21,6 @@ def test_query_default_scalar():
assert result.data == {"test": "success"}


def test_query_custom_scalar():
type_defs = """
scalar Date

type Query {
test: Date
}
"""

resolvers = {
"Query": {"test": lambda *_: date.today()},
"Date": lambda date: date.strftime("%Y-%m-%d"),
}

schema = make_executable_schema(type_defs, resolvers)

result = graphql(schema, "{ test }")
assert result.errors is None
assert result.data == {"test": date.today().strftime("%Y-%m-%d")}


def test_query_custom_type_default_resolver():
type_defs = """
type Query {
Expand Down