Repository: mirumee/ariadne

Issue:
`make_executable_schema`'s  omits default enum values in args and inputs
GraphQL enums are represented in Python via the [`GraphQLEnumType`](https://github.com/graphql-python/graphql-core-next/blob/master/src/graphql/type/definition.py#L996). If those types are created from the SDL, they define no Python representation for GraphQL enum items. Developers can use Ariadne-provided `EnumType` to specify how enum items should be represented in Python themselves, but we also include "fallback" step that fills enum values with their `str` representations.

However neither of those approaches takes care of enum items defined as default values for field arguments. Eg:

```
type Query {
    users(role: UserRole = ADMIN): [User]
}

input FooBarInput {
    foo: FooEnum! = FOO
}
```

We will need to add additional step to `make_executable_schema` that gathers all enums from schema together with their Python representations, and then introspects schema's fields values for places where field or input has default value type of enum. If this is the case, we should set correct `default_value` on [`GraphQLArgument`](https://github.com/graphql-python/graphql-core-next/blob/b320331faf2fc2f4f1f6a1366f07109d1bdd44f1/src/graphql/type/definition.py#L576) in the schema.




Discussion:
Is there a workaround for this ?
Any update on this? The issue was created quite a while ago and feels like a pretty common use case. I've only been playing with ariadne for a couple days and hit the bug immediately.
PR's welcome :)

In the meantime workaround for this issue is to define default value in field's resolver, eg:

```
def resolve(_, info, *, role=Roles.ADMIN)
```
It's unclear to me why this is not already done by `graphql.parse()`
This is the function that interprets a raw schema string and converts it into python objects like `GraphQLEnumType` and `GraphQLArgument`, correct?
Why isn't the population of `GraphQLArgument.default_value` happening as part of that step?
`graphql.parse()` doesn't known about custom enum bindings defined in `ariadne.EnumType`.
The use of custom `ariadne.EnumType` bindings is optional, I thought. According to https://ariadnegraphql.org/docs/enums#mapping-to-internal-values - the default behaviour is for enums to be represented as strings. So if someone is not making use of custom enum bindings, can `graphql.parse()` take care of it?
That may be the case, but please remember that this issue is mainly about `EnumType` that Ariadne provides.

This is not the case as long as `EnumType` is not used. For this schema:

```graphql
enum Role {
  ADMIN
  USER
}

type Query {
  hello(r: Role = USER): String
}
```

And this query:

```graphql
{
  default: hello
  custom: hello(r: ADMIN)
}
```

Resolver that returns `repr(r)` gives this result:

```json
{
  "default": "'USER'",
  "custom": "'ADMIN'"
}
```

However as soon as `EnumType` is used, python types stop working out:

```json
{
  "default": "'USER'",
  "custom": "<Role.ADMIN: 'ADMIN'>"
}
```
Turns out there's also a gotcha with Schema validation in GraphQL core which skips default values validation against enum definition:

https://github.com/graphql-python/graphql-core/issues/122

Without this its going to be super troublesome to offer useful error messages. I'll wait now to see what response to above issue will be given. If there's no, we may have to implement schema validator on our own for this case.
I don't know if this is the right place to discuss this issue (it seems related to enums and default values):

There seems to be a regression in ariadne 0.13.0. Or at least an unexpected change in behaviour when I upgraded from 0.12.0.

If you run the following:
```
import ariadne
import enum
import pprint

type_defs = """
    enum Role {
        ADMIN
        USER
    }

    type Query {
        hello(r: Role = USER): String
    }
"""

class Role(enum.Enum):
    ADMIN = "admin"
    USER = "user"

RoleGraphQLType = ariadne.EnumType("Role", Role)

QueryGraphQLType = ariadne.QueryType()

schema = ariadne.make_executable_schema(
    type_defs,
    QueryGraphQLType,
    RoleGraphQLType,
)

query = "{__schema{types{name,fields{name,args{name,defaultValue}}}}}"

result = ariadne.graphql_sync(schema, {"query": query}, debug=True)
pprint.pprint(result)
```

This works fine under the environment:
```
ariadne 0.12.0
graphql-core 3.0.5
```

But when run under the environment:
```
ariadne 0.13.0
graphql-core 3.1.3
```
It produces the error:
```
Traceback (most recent call last):
  File "venv/lib/python3.8/site-packages/graphql/execution/execute.py", line 678, in complete_value_catching_error
    completed = self.complete_value(
  File "venv/lib/python3.8/site-packages/graphql/execution/execute.py", line 745, in complete_value
    raise result
  File "venv/lib/python3.8/site-packages/graphql/execution/execute.py", line 637, in resolve_field_value_or_error
    result = resolve_fn(source, info, **args)
  File "venv/lib/python3.8/site-packages/graphql/type/introspection.py", line 399, in default_value
    value_ast = ast_from_value(item[1].default_value, item[1].type)
  File "venv/lib/python3.8/site-packages/graphql/utilities/ast_from_value.py", line 108, in ast_from_value
    serialized = type_.serialize(value)  # type: ignore
  File "venv/lib/python3.8/site-packages/graphql/type/definition.py", line 1146, in serialize
    raise GraphQLError(
graphql.error.graphql_error.GraphQLError: Enum 'Role' cannot represent value: 'USER'
```

Resolving patch:
diff --git a/ariadne/enums.py b/ariadne/enums.py
--- a/ariadne/enums.py
+++ b/ariadne/enums.py
@@ -1,12 +1,48 @@
 import enum
-
-from typing import Any, Dict, Optional, Union, cast
+from typing import (
+    Any,
+    Dict,
+    Generator,
+    List,
+    Mapping,
+    Optional,
+    Tuple,
+    TypeVar,
+    Union,
+    cast,
+)
+from functools import reduce, singledispatch
+import operator
 
 from graphql.type import GraphQLEnumType, GraphQLNamedType, GraphQLSchema
+from graphql.language.ast import (
+    EnumValueNode,
+    InputValueDefinitionNode,
+    ObjectValueNode,
+)
+from graphql.pyutils.undefined import Undefined
+from graphql.type.definition import (
+    GraphQLArgument,
+    GraphQLField,
+    GraphQLInputField,
+    GraphQLInputObjectType,
+    GraphQLInputType,
+    GraphQLNonNull,
+    GraphQLObjectType,
+    GraphQLScalarType,
+)
 
 from .types import SchemaBindable
 
 
+T = TypeVar("T")
+ArgumentWithKeys = Tuple[str, str, GraphQLArgument, Optional[List["str"]]]
+InputFieldWithKeys = Tuple[str, str, GraphQLInputField, Optional[List["str"]]]
+GraphQLNamedInputType = Union[
+    GraphQLScalarType, GraphQLEnumType, GraphQLInputObjectType
+]
+
+
 class EnumType(SchemaBindable):
     def __init__(
         self, name: str, values=Union[Dict[str, Any], enum.Enum, enum.IntEnum]
@@ -29,6 +65,28 @@ def bind_to_schema(self, schema: GraphQLSchema) -> None:
                 )
             graphql_type.values[key].value = value
 
+    def bind_to_default_values(self, schema: GraphQLSchema) -> None:
+        for _, _, arg, key_list in find_enum_values_in_schema(schema):
+            type_ = resolve_null_type(arg.type)
+            type_ = cast(GraphQLNamedInputType, type_)
+
+            if (
+                key_list is None
+                and arg.default_value in self.values
+                and type_.name == self.name
+            ):
+                type_ = resolve_null_type(arg.type)
+                arg.default_value = self.values[arg.default_value]
+
+            elif key_list is not None:
+                enum_value = get_value_from_mapping_value(arg.default_value, key_list)
+                type_ = cast(GraphQLEnumType, track_type_for_nested(arg, key_list))
+
+                if enum_value in self.values and type_.name == self.name:
+                    set_leaf_value_in_mapping(
+                        arg.default_value, key_list, self.values[enum_value]
+                    )
+
     def validate_graphql_type(self, graphql_type: Optional[GraphQLNamedType]) -> None:
         if not graphql_type:
             raise ValueError("Enum %s is not defined in the schema" % self.name)
@@ -49,3 +107,134 @@ def set_default_enum_values(graphql_type: GraphQLEnumType):
     for key in graphql_type.values:
         if graphql_type.values[key].value is None:
             graphql_type.values[key].value = key
+
+
+def validate_schema_enum_values(schema: GraphQLSchema) -> None:
+    for type_name, field_name, arg, _ in find_enum_values_in_schema(schema):
+        if is_invalid_enum_value(arg):
+            raise ValueError(
+                f"Value for type: <{arg.type}> is invalid. "
+                f"Check InputField/Arguments for <{field_name}> in <{type_name}> "
+                "(Undefined enum value)."
+            )
+
+
+def is_invalid_enum_value(field: Union[GraphQLInputField, GraphQLArgument]) -> bool:
+    if field.ast_node is None:
+        return False
+    return field.default_value is Undefined and field.ast_node.default_value is not None
+
+
+def find_enum_values_in_schema(
+    schema: GraphQLSchema,
+) -> Generator[Union[ArgumentWithKeys, InputFieldWithKeys], None, None]:
+
+    for name, type_ in schema.type_map.items():
+        result = enum_values_in_types(type_, name)
+        if result is not None:
+            yield from result
+
+
+@singledispatch
+def enum_values_in_types(
+    type_: Union[
+        GraphQLObjectType, GraphQLInputType
+    ],  # pylint: disable=unused-argument
+    name: str,  # pylint: disable=unused-argument
+) -> Optional[Generator[Union[ArgumentWithKeys, InputFieldWithKeys], None, None]]:
+    pass
+
+
+@enum_values_in_types.register(GraphQLObjectType)
+def enum_values_in_object_type(
+    type_: GraphQLObjectType,
+    field_name: str,
+) -> Generator[ArgumentWithKeys, None, None]:
+    for field in type_.fields.values():
+        yield from enum_values_in_field_args(field_name, field)
+
+
+@enum_values_in_types.register(GraphQLInputObjectType)
+def enum_values_in_input_type(
+    type_: GraphQLInputObjectType,
+    field_name,
+) -> Generator[InputFieldWithKeys, None, None]:
+    for input_name, field in type_.fields.items():
+        resolved_type = resolve_null_type(field.type)
+        if isinstance(resolved_type, GraphQLEnumType):
+            yield field_name, input_name, field, None
+
+        if isinstance(resolved_type, GraphQLInputObjectType):
+            if field.ast_node is not None:
+                routes = get_enum_keys_from_ast(field.ast_node)
+                for route in routes:
+                    yield field_name, input_name, field, route
+
+
+def enum_values_in_field_args(
+    field_name: str,
+    field: GraphQLField,
+) -> Generator[ArgumentWithKeys, None, None]:
+    args = [
+        (name, arg)
+        for name, arg in field.args.items()
+        if isinstance(
+            arg.type, (GraphQLInputObjectType, GraphQLEnumType, GraphQLNonNull)
+        )
+    ]
+
+    for arg_name, arg in args:
+        type_ = resolve_null_type(arg.type)
+        if isinstance(type_, GraphQLEnumType):
+            yield field_name, arg_name, arg, None
+
+        if isinstance(type_, GraphQLInputObjectType):
+            if arg.ast_node is not None and arg.ast_node.default_value is not None:
+                routes = get_enum_keys_from_ast(arg.ast_node)
+                for route in routes:
+                    yield field_name, arg_name, arg, route
+
+
+def get_enum_keys_from_ast(ast_node: InputValueDefinitionNode) -> List[List["str"]]:
+    routes = []
+    object_node = ast_node.default_value
+    object_node = cast(ObjectValueNode, object_node)
+    nodes = [([field.name.value], field) for field in object_node.fields]
+
+    while nodes:
+        key_list, field = nodes.pop()
+        if isinstance(field.value, EnumValueNode):
+            routes.append(key_list)
+
+        if isinstance(field.value, ObjectValueNode):
+            for new_field in field.value.fields:
+                new_route = key_list[:]
+                new_route.append(new_field.name.value)
+                nodes.append((new_route, new_field))
+
+    return routes
+
+
+def get_value_from_mapping_value(mapping: Mapping[T, Any], key_list: List[T]) -> Any:
+    return reduce(operator.getitem, key_list, mapping)
+
+
+def set_leaf_value_in_mapping(
+    mapping: Mapping[T, Any], key_list: List[T], value: Any
+) -> None:
+    get_value_from_mapping_value(mapping, key_list[:-1])[key_list[-1]] = value
+
+
+def track_type_for_nested(
+    arg: Union[GraphQLArgument, GraphQLInputField], key_list: List[str]
+) -> GraphQLInputType:
+    type_ = resolve_null_type(arg.type)
+
+    for elem in key_list:
+        if isinstance(type_, GraphQLInputObjectType):
+            type_ = type_.fields[elem].type
+    return type_
+
+
+def resolve_null_type(type_: GraphQLInputType) -> GraphQLInputType:
+    return type_.of_type if isinstance(type_, GraphQLNonNull) else type_
diff --git a/ariadne/executable_schema.py b/ariadne/executable_schema.py
--- a/ariadne/executable_schema.py
+++ b/ariadne/executable_schema.py
@@ -7,7 +7,11 @@
     parse,
 )
 
-from .enums import set_default_enum_values_on_schema
+from .enums import (
+    EnumType,
+    set_default_enum_values_on_schema,
+    validate_schema_enum_values,
+)
 from .schema_visitor import SchemaDirectiveVisitor
 from .types import SchemaBindable
 
@@ -22,13 +26,10 @@ def make_executable_schema(
 
     ast_document = parse(type_defs)
     schema = build_ast_schema(ast_document)
+    flat_bindables: List[SchemaBindable] = flatten_bindables(*bindables)
 
-    for bindable in bindables:
-        if isinstance(bindable, list):
-            for obj in bindable:
-                obj.bind_to_schema(schema)
-        else:
-            bindable.bind_to_schema(schema)
+    for bindable in flat_bindables:
+        bindable.bind_to_schema(schema)
 
     set_default_enum_values_on_schema(schema)
 
@@ -36,9 +37,31 @@ def make_executable_schema(
         SchemaDirectiveVisitor.visit_schema_directives(schema, directives)
 
     assert_valid_schema(schema)
+    validate_schema_enum_values(schema)
+    repair_default_enum_values(schema, flat_bindables)
 
     return schema
 
 
 def join_type_defs(type_defs: List[str]) -> str:
     return "\n\n".join(t.strip() for t in type_defs)
+
+
+def flatten_bindables(
+    *bindables: Union[SchemaBindable, List[SchemaBindable]]
+) -> List[SchemaBindable]:
+    new_bindables = []
+
+    for bindable in bindables:
+        if isinstance(bindable, list):
+            new_bindables.extend(bindable)
+        else:
+            new_bindables.append(bindable)
+
+    return new_bindables
+
+
+def repair_default_enum_values(schema, bindables) -> None:
+    for bindable in bindables:
+        if isinstance(bindable, EnumType):
+            bindable.bind_to_default_values(schema)


Test patch:
diff --git a/tests/test_enums.py b/tests/test_enums.py
--- a/tests/test_enums.py
+++ b/tests/test_enums.py
@@ -1,9 +1,15 @@
+import re
 from enum import Enum, IntEnum
 
 import pytest
-from graphql import graphql_sync, build_schema
+from graphql import graphql_sync, build_schema, parse
+from graphql.pyutils.undefined import Undefined
+from graphql.utilities.build_ast_schema import build_ast_schema
 
 from ariadne import EnumType, QueryType, make_executable_schema
+from ariadne import graphql_sync as ariadne_graphql_sync
+from ariadne.executable_schema import join_type_defs
+from ariadne.enums import find_enum_values_in_schema
 
 enum_definition = """
     enum Episode {
@@ -219,3 +225,233 @@ def test_int_enum_arg_is_transformed_to_internal_value():
     schema = make_executable_schema([enum_definition, enum_param], [query, int_enum])
     result = graphql_sync(schema, "{ testEnum(value: NEWHOPE) }")
     assert result.data["testEnum"] is True
+
+
+def test_int_enum_arg_default_python_value_is_set():
+    enum_param_default = """
+       type Query {
+           testEnum(value: Episode! = EMPIRE): Boolean!
+       }
+    """
+    query = QueryType()
+
+    def resolve_test_enum(*_, value):
+        return value == PyIntEnum.EMPIRE
+
+    query.set_field("testEnum", resolve_test_enum)
+    schema = make_executable_schema(
+        [enum_definition, enum_param_default], [query, int_enum]
+    )
+    result = graphql_sync(schema, "{ testEnum }")
+
+    assert result.data["testEnum"]
+    assert result.errors is None
+
+
+def test_int_enum_input_default_python_value_is_set():
+    input_param_default = """
+        type Query {
+            testEnum(inp: QueryInput): Boolean!
+        }
+         input QueryInput {
+            value: Episode! = EMPIRE
+        }
+    """
+    query = QueryType()
+
+    def resolve_test_enum(*_, inp):
+        return inp["value"] == PyIntEnum.EMPIRE
+
+    query.set_field("testEnum", resolve_test_enum)
+    schema = make_executable_schema(
+        [enum_definition, input_param_default], [query, int_enum]
+    )
+    result = graphql_sync(schema, "{ testEnum(inp: {}) }")
+
+    assert result.errors is None
+    assert result.data["testEnum"]
+
+
+def test_int_enum_input_nested_default_python_value_is_set():
+    input_schema = """
+        type Query {
+            complex(i: BetterTest = { test: { role: EMPIRE }}): Boolean 
+        }
+        input Test {
+            ignore: String 
+            role: Episode = EMPIRE  
+        }
+        input BetterTest {
+            newIgnore: String
+            test: Test = { role: NEWHOPE }
+        }
+    """
+    query = QueryType()
+
+    def resolve_test_enum(*_, i):
+        return i["test"]["role"] == PyIntEnum.EMPIRE
+
+    query.set_field("complex", resolve_test_enum)
+    schema = make_executable_schema([enum_definition, input_schema], [query, int_enum])
+    result = graphql_sync(schema, "{ complex(i: {test: {} }) }")
+
+    assert result.errors is None
+    assert result.data["complex"]
+
+
+def test_input_exc_schema_raises_exception_for_undefined_enum_value_in_flat_input():
+    input_schema = """
+         type Query {
+            complex(i: Test = { role: EMPIRE }): String
+        }
+        input Test {
+            ignore: String
+            role: Episode = TWO_TOWERS  
+        }
+    """
+    with pytest.raises(
+        ValueError,
+        match=re.escape(
+            "Value for type: <Episode> is invalid. "
+            "Check InputField/Arguments for <role> in <Test> (Undefined enum value)."
+        ),
+    ):
+        make_executable_schema([enum_definition, input_schema])
+
+
+def test_input_exc_schema_raises_exception_for_undefined_enum_value_in_nested_object():
+    input_schema = """
+        type Query {
+            complex(i: Test = { role: EMPIRE }): String
+        }
+        input Test {
+            ignore: String 
+            role: Episode = EMPIRE  
+        }
+        input BetterTest {
+            newIgnore: String
+            test: Test = { role: ANDRZEJU }
+        }
+    """
+
+    with pytest.raises(
+        ValueError,
+        match=re.escape(
+            "Value for type: <Test> is invalid. "
+            "Check InputField/Arguments for <test> in <BetterTest> (Undefined enum value)."
+        ),
+    ):
+        make_executable_schema([enum_definition, input_schema])
+
+
+def test_input_exc_schema_raises_exception_for_undefined_enum_value_in_nested_field_arg():
+    input_schema = """
+        type Query {
+            complex(i: BetterTest = { test: { role: TWO_TOWERS } }): String
+        }
+        input Test {
+            ignore: String 
+            role: Episode = EMPIRE  
+        }
+        input BetterTest {
+            newIgnore: String
+            test: Test = { role: NEW_HOPE }
+        }
+    """
+
+    with pytest.raises(
+        ValueError,
+        match=re.escape(
+            "Value for type: <BetterTest> is invalid. "
+            "Check InputField/Arguments for <i> in <Query> (Undefined enum value)."
+        ),
+    ):
+        make_executable_schema([enum_definition, input_schema])
+
+
+def test_find_enum_values_in_schema_for_undefined_and_invalid_values():
+    input_schema = """
+        type Query {
+            complex(hello: Episode = EMPIRE,
+                    hi: Test = { role: JEDI,  next_role: ENTERPRISE, ignore: "HI"}, 
+                    bonjour: BetterTest = {newIgnore: "Witam", test: { role: NEWHOPE }}): String
+        }
+    
+        input Test {
+            ignore: String 
+            role: Episode = EMPIRE  
+            next_role: Episode
+        }
+
+        input BetterTest {
+            newIgnore: String
+            test: Test = { role: NEWHOPE }
+        }
+    """
+    query_complex_keys = [["next_role"], ["role"], ["test", "role"]]
+    better_test_complex_keys = [["role"]]
+    number_of_defined_enum_values = 7
+
+    # 2 Undefined because of "ENTERPRISE" invalid value, and next_role in Test input
+    number_of_undefined_default_enum_values = 3
+
+    ast_document = parse(join_type_defs([enum_definition, input_schema]))
+    schema = build_ast_schema(ast_document)
+    enums_entities = list(find_enum_values_in_schema(schema))
+    keys_to_complex_inputs = [keys for *_, keys in enums_entities if keys is not None]
+
+    undefined = [
+        (*_, args, keys)
+        for *_, args, keys in enums_entities
+        if args.default_value is Undefined
+    ]
+
+    assert keys_to_complex_inputs == query_complex_keys + better_test_complex_keys
+    assert len(enums_entities) == number_of_defined_enum_values
+    assert len(undefined) == number_of_undefined_default_enum_values
+
+
+def test_enum_type_is_able_to_represent_enum_default_value_in_schema():
+    #  regression test for: https://github.com/mirumee/ariadne/issues/293
+
+    type_defs = """
+        enum Role {
+            ADMIN
+            USER
+        }
+
+        type Query {
+            hello(r: Role = USER): String
+        }
+    """
+
+    class Role(Enum):
+        ADMIN = "admin"
+        USER = "user"
+
+    def resolve_test_enum(*_, r):
+        return r == Role.USER
+
+    RoleGraphQLType = EnumType("Role", Role)
+    QueryGraphQLType = QueryType()
+
+    QueryGraphQLType.set_field("hello", resolve_test_enum)
+
+    schema = make_executable_schema(
+        type_defs,
+        QueryGraphQLType,
+        RoleGraphQLType,
+    )
+
+    query = "{__schema{types{name,fields{name,args{name,defaultValue}}}}}"
+
+    _, result = ariadne_graphql_sync(schema, {"query": query}, debug=True)
+    result_hello_query = graphql_sync(schema, "{hello}")
+    assert (
+        result["data"]["__schema"]["types"][-1]["fields"][0]["args"][0]["defaultValue"]
+        == "USER"
+    )
+
+    assert schema.type_map["Query"].fields["hello"].args["r"].default_value == Role.USER
+    assert result_hello_query.data["hello"]
+    assert result_hello_query.errors is None