Repository: mirumee/ariadne

Issue:
Support custom internal representation of enums
Currently Ariadne supports enums that are represented as strings that are mapped 1:1 to what they are in SDL. This should take care of majority of use cases, but there may be edge cases and legacy systems, where enum is defined for existing value that may be integer, and updating python implementation is out of question.

[Apollo doc for reference.](https://www.apollographql.com/docs/apollo-server/features/scalars-enums.html#internal-values)


Discussion:

Resolving patch:
diff --git a/ariadne/__init__.py b/ariadne/__init__.py
--- a/ariadne/__init__.py
+++ b/ariadne/__init__.py
@@ -1,3 +1,4 @@
+from .enums import Enum
 from .executable_schema import make_executable_schema
 from .load_schema import load_schema_from_path
 from .resolvers import (
@@ -16,6 +17,7 @@
 
 
 __all__ = [
+    "Enum",
     "FallbackResolversSetter",
     "GraphQLMiddleware",
     "ResolverMap",
diff --git a/ariadne/enums.py b/ariadne/enums.py
new file mode 100644
--- /dev/null
+++ b/ariadne/enums.py
@@ -0,0 +1,38 @@
+import enum
+
+from typing import Any, Dict, Union
+
+from graphql.type import GraphQLEnumType, GraphQLSchema
+
+from .types import Bindable
+
+
+class Enum(Bindable):
+    def __init__(
+        self, name: str, values=Union[Dict[str, Any], enum.Enum, enum.IntEnum]
+    ) -> None:
+        self.name = name
+        try:
+            self.values = values.__members__  # pylint: disable=no-member
+        except AttributeError:
+            self.values = values
+
+    def bind_to_schema(self, schema: GraphQLSchema) -> None:
+        graphql_type = schema.type_map.get(self.name)
+        self.validate_graphql_type(graphql_type)
+
+        for key, value in self.values.items():
+            if key not in graphql_type.values:
+                raise ValueError(
+                    "Value %s is not defined on enum %s" % (key, self.name)
+                )
+            graphql_type.values[key].value = value
+
+    def validate_graphql_type(self, graphql_type: str) -> None:
+        if not graphql_type:
+            raise ValueError("Enum %s is not defined in the schema" % self.name)
+        if not isinstance(graphql_type, GraphQLEnumType):
+            raise ValueError(
+                "%s is defined in the schema, but it is instance of %s (expected %s)"
+                % (self.name, type(graphql_type).__name__, GraphQLEnumType.__name__)
+            )


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,6 +1,10 @@
-from graphql import graphql_sync
+import enum
 
-from ariadne import ResolverMap, make_executable_schema
+import pytest
+
+from graphql import graphql_sync, build_schema
+
+from ariadne import Enum, ResolverMap, make_executable_schema
 
 enum_definition = """
     enum Episode {
@@ -45,7 +49,7 @@ def test_unsuccesfull_invalid_enum_value_evaluation():
 """
 
 
-def test_succesfull_enum_value_passed_as_argument():
+def test_successful_enum_value_passed_as_argument():
     query = ResolverMap("Query")
     query.field("testEnum")(lambda *_, value: True)
 
@@ -54,10 +58,112 @@ def test_succesfull_enum_value_passed_as_argument():
     assert result.errors is None, result.errors
 
 
-def test_unsuccesfull_invalid_enum_value_passed_as_argument():
+def test_unsuccessful_invalid_enum_value_passed_as_argument():
     query = ResolverMap("Query")
     query.field("testEnum")(lambda *_, value: True)
 
     schema = make_executable_schema([enum_definition, enum_param], query)
     result = graphql_sync(schema, "{ testEnum(value: %s) }" % INVALID_VALUE)
     assert result.errors is not None
+
+
+@pytest.fixture
+def schema_with_enum():
+    return build_schema("\n\n".join((enum_definition, enum_field)))
+
+
+def test_attempt_bind_custom_enum_to_undefined_type_raises_error(schema_with_enum):
+    graphql_enum = Enum("Undefined", {})
+    with pytest.raises(ValueError):
+        graphql_enum.bind_to_schema(schema_with_enum)
+
+
+def test_attempt_bind_custom_enum_to_wrong_schema_type_raises_error(schema_with_enum):
+    graphql_enum = Enum("Query", {})
+    with pytest.raises(ValueError):
+        graphql_enum.bind_to_schema(schema_with_enum)
+
+
+def test_attempt_bind_custom_enum_to_schema_enum_missing_value_raises_error(
+    schema_with_enum
+):
+    graphql_enum = Enum("Episode", {"JARJAR": 1999})
+    with pytest.raises(ValueError):
+        graphql_enum.bind_to_schema(schema_with_enum)  # pylint: disable=no-member
+
+
+dict_enum = Enum("Episode", {"NEWHOPE": 1977, "EMPIRE": 1980, "JEDI": 1983})
+
+TEST_INTERNAL_VALUE = 1977
+
+
+def test_dict_enum_is_resolved_from_internal_value():
+    query = ResolverMap("Query")
+    query.field("testEnum")(lambda *_: TEST_INTERNAL_VALUE)
+
+    schema = make_executable_schema([enum_definition, enum_field], [query, dict_enum])
+    result = graphql_sync(schema, "{ testEnum }")
+    assert result.data["testEnum"] == TEST_VALUE
+
+
+def test_dict_enum_arg_is_transformed_to_internal_value():
+    query = ResolverMap("Query")
+    query.field("testEnum")(lambda *_, value: value == TEST_INTERNAL_VALUE)
+
+    schema = make_executable_schema([enum_definition, enum_param], [query, dict_enum])
+    result = graphql_sync(schema, "{ testEnum(value: %s) }" % TEST_VALUE)
+    assert result.data["testEnum"] is True
+
+
+class PyEnum(enum.Enum):
+    NEWHOPE = 1977
+    EMPIRE = 1980
+    JEDI = 1983
+
+
+py_enum = Enum("Episode", PyEnum)
+
+
+def test_enum_is_resolved_from_internal_value():
+    query = ResolverMap("Query")
+    query.field("testEnum")(lambda *_: PyEnum.NEWHOPE)
+
+    schema = make_executable_schema([enum_definition, enum_field], [query, py_enum])
+    result = graphql_sync(schema, "{ testEnum }")
+    assert result.data["testEnum"] == TEST_VALUE
+
+
+def test_enum_arg_is_transformed_to_internal_value():
+    query = ResolverMap("Query")
+    query.field("testEnum")(lambda *_, value: value == PyEnum.NEWHOPE)
+
+    schema = make_executable_schema([enum_definition, enum_param], [query, py_enum])
+    result = graphql_sync(schema, "{ testEnum(value: %s) }" % TEST_VALUE)
+    assert result.data["testEnum"] is True
+
+
+class IntEnum(enum.IntEnum):
+    NEWHOPE = 1977
+    EMPIRE = 1980
+    JEDI = 1983
+
+
+int_enum = Enum("Episode", IntEnum)
+
+
+def test_int_enum_is_resolved_from_internal_value():
+    query = ResolverMap("Query")
+    query.field("testEnum")(lambda *_: IntEnum.NEWHOPE)
+
+    schema = make_executable_schema([enum_definition, enum_field], [query, int_enum])
+    result = graphql_sync(schema, "{ testEnum }")
+    assert result.data["testEnum"] == TEST_VALUE
+
+
+def test_int_enum_arg_is_transformed_to_internal_value():
+    query = ResolverMap("Query")
+    query.field("testEnum")(lambda *_, value: value == IntEnum.NEWHOPE)
+
+    schema = make_executable_schema([enum_definition, enum_param], [query, int_enum])
+    result = graphql_sync(schema, "{ testEnum(value: %s) }" % TEST_VALUE)
+    assert result.data["testEnum"] is True