Repository: mirumee/ariadne (python)

Issue:
`add_resolve_functions_to_schema` should support Scalars parse_value and parse_literal
Currently Ariadne Scalar support is limited to serializing python types to JSON before returning them to client, but we also want to support using custom scalars for input.

Our `add_resolve_functions_to_scalar` utility could support following use-cases:

Code below results in one-way only scalar:

- `type_defs = {'Scalar': {'serialize': callable}}`

And this code results in two-way scalar:

- `type_defs = {'Scalar': {'serialize': callable, 'parse_value': callable, 'parse_literal': callable}}` - explicit syntax for two-directional scalar.


Pull request:
Bidirectional scalars
This PR updates `add_resolve_functions_to_schema` to pass `serializer`, `parse_literal` and `parse_value` functions to `Scalar`.

Fixes #13 

### TODO

* [x] Test read-only scalar
* [x] Test input scalar

Patch:
diff --git a/ariadne/resolvers.py b/ariadne/resolvers.py
index 388e3ad..8f7bf77 100644
--- a/ariadne/resolvers.py
+++ b/ariadne/resolvers.py
@@ -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)
-    obj.serialize = serializer
+def add_resolve_functions_to_scalar(name: str, obj: GraphQLObjectType, resolvers: dict):
+    scalar_resolvers = resolvers.get(name, {})
+
+    serialize = scalar_resolvers.get("serialize", obj.serialize)
+    obj.serialize = serialize
+
+    parse_literal = scalar_resolvers.get("parse_literal", obj.parse_literal)
+    obj.parse_literal = parse_literal
+
+    parse_value = scalar_resolvers.get("parse_value", obj.parse_value)
+    obj.parse_value = parse_value


Test patch:
diff --git a/tests/test_custom_scalars.py b/tests/test_custom_scalars.py
new file mode 100644
index 0000000..954933b
--- /dev/null
+++ b/tests/test_custom_scalars.py
@@ -0,0 +1,128 @@
+from datetime import date, datetime
+
+from graphql import graphql
+from graphql.language.ast import StringValue
+
+from ariadne import make_executable_schema
+
+TEST_DATE = date(2006, 9, 13)
+TEST_DATE_SERIALIZED = TEST_DATE.strftime("%Y-%m-%d")
+
+type_defs = """
+    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 not isinstance(ast, StringValue):
+        return None
+
+    formatted_date = ast.value
+    try:
+        parsed_datetime = datetime.strptime(formatted_date, "%Y-%m-%d")
+        return parsed_datetime.date()
+    except ValueError:
+        pass
+
+
+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 TEST_DATE
+
+
+def resolve_test_input(*_, value):
+    assert value == TEST_DATE
+    return True
+
+
+resolvers = {
+    "Query": {"testSerialize": resolve_test_serialize, "testInput": resolve_test_input},
+    "DateReadOnly": {"serialize": 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": TEST_DATE_SERIALIZED}
+
+
+def test_parse_literal_valid_str_ast_to_date_instance():
+    test_input = TEST_DATE_SERIALIZED
+    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():
+    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": TEST_DATE_SERIALIZED}
+    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.',
+    ]
diff --git a/tests/test_queries.py b/tests/test_queries.py
index cfff208..8d98648 100644
--- a/tests/test_queries.py
+++ b/tests/test_queries.py
@@ -1,4 +1,3 @@
-from datetime import date
 from unittest.mock import Mock
 
 from graphql import graphql
@@ -6,7 +5,7 @@ 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
@@ -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 {