Repository: polygraph-python/polygraph

Issue:
Union type
http://graphql.org/learn/schema/#union-types

Discussion:

Patch:
diff --git a/polygraph/types/basic_type.py b/polygraph/types/basic_type.py
index 6c6d393..8c5f336 100644
--- a/polygraph/types/basic_type.py
+++ b/polygraph/types/basic_type.py
@@ -1,6 +1,6 @@
 import types
 
-from polygraph.exceptions import PolygraphValueError
+from polygraph.exceptions import PolygraphSchemaError, PolygraphValueError
 from polygraph.utils.trim_docstring import trim_docstring
 
 
@@ -10,6 +10,7 @@ class PolygraphTypeMeta(type):
         meta = namespace.pop("Type", types.SimpleNamespace())
         meta.description = getattr(meta, "description", default_description)
         meta.name = getattr(meta, "name", name) or name
+        meta.possible_types = getattr(meta, "possible_types", None)
 
         namespace["_type"] = meta
 
@@ -18,6 +19,9 @@ class PolygraphTypeMeta(type):
     def __str__(self):
         return str(self._type.name)
 
+    def __or__(self, other):
+        return Union(self, other)
+
 
 class PolygraphType(metaclass=PolygraphTypeMeta):
     pass
@@ -61,6 +65,34 @@ class Union(PolygraphOutputType, PolygraphType):
     GraphQL Object types, but provides for no guaranteed fields between
     those types.
     """
+    def __new__(cls, *types):
+        types = set(types)
+        assert len(types) >= 2, "Unions must consist of more than 1 type"
+        bad_types = [t for t in types if not issubclass(t, PolygraphType)]
+        if bad_types:
+            message = "All types must be subclasses of PolygraphType. Invalid values: "\
+                      "{}".format(", ".join(bad_types))
+            raise PolygraphSchemaError(message)
+        type_names = [t._type.name for t in types]
+
+        class Unionable(PolygraphType):
+            def __new__(cls, value):
+                if type(value) not in cls._type.possible_types:
+                    valid_types = ", ".join(str(t) for t in cls._type.possible_types)
+                    message = "{} is an invalid value type. "\
+                              "Valid types: {}".format(type(value), valid_types)
+                    raise PolygraphValueError(message)
+                return value
+
+        class Type:
+            name = "|".join(type_names)
+            description = "One of {}".format(", ".join(type_names))
+            possible_types = types
+
+        name = "Union__" + "_".join(type_names)
+        bases = (Unionable, )
+        attrs = {"Type": Type}
+        return type(name, bases, attrs)
 
 
 class Listable(PolygraphType, list):


Test patch:
diff --git a/polygraph/types/tests/test_union.py b/polygraph/types/tests/test_union.py
new file mode 100644
index 0000000..0c60c7d
--- /dev/null
+++ b/polygraph/types/tests/test_union.py
@@ -0,0 +1,48 @@
+from unittest import TestCase, skip
+
+from polygraph.exceptions import PolygraphValueError
+from polygraph.types.basic_type import Union
+from polygraph.types.scalar import Float, Int, String
+
+
+@skip  # FIXME
+class UnionTypeTest(TestCase):
+    def test_commutativity(self):
+        self.assertEqual(Union(String, Int), Union(Int, String))
+
+    def test_associativity(self):
+        self.assertEqual(
+            Union(Union(String, Int), Float),
+            Union(String, Int, Float),
+        )
+
+    def test_pipe_operator(self):
+        self.assertEqual(
+            String | Int,
+            Union(String, Int),
+        )
+
+    def test_pipe_operator_with_more_than_two_types(self):
+        self.assertEqual(
+            String | Int | Float,
+            Union(String, Int, Float),
+        )
+
+
+class UnionValueTest(TestCase):
+    def test_valid_type(self):
+        union = String | Int
+        self.assertEqual(union(Int(32)), Int(32))
+        self.assertEqual(union(String("Test")), String("Test"))
+
+    def test_value_must_be_typed(self):
+        union = String | Int
+        with self.assertRaises(PolygraphValueError):
+            union(32)
+        with self.assertRaises(PolygraphValueError):
+            union("Test")
+
+    def test_value_must_have_right_type(self):
+        union = String | Int
+        with self.assertRaises(PolygraphValueError):
+            union(Float(32))