Repository: polygraph-python/polygraph

Issue:
Typemap builder

Discussion:

Patch:
diff --git a/polygraph/types/schema.py b/polygraph/types/schema.py
new file mode 100644
index 0000000..6c98fed
--- /dev/null
+++ b/polygraph/types/schema.py
@@ -0,0 +1,61 @@
+from polygraph.exceptions import PolygraphSchemaError
+from polygraph.types.api import typedef
+from polygraph.types.definitions import TypeKind
+from polygraph.types.object_type import ObjectType
+from polygraph.utils.strict_dict import StrictDict
+
+
+def typename(type_):
+    return typedef(type_).name
+
+
+def build_type_map(type_list):
+    type_map = StrictDict()
+
+    for type_ in type_list:
+        typedef = type_.__type
+        kind = typedef.kind
+        type_map[typename(type_)] = type_
+        if kind in (TypeKind.SCALAR, TypeKind.ENUM):
+            continue
+        elif kind in (TypeKind.LIST, TypeKind.NON_NULL):
+            of_type = typedef.of_type
+            subtype_map = build_type_map([of_type])
+            type_map.update(subtype_map)
+
+        elif kind == TypeKind.UNION:
+            possible_types = typedef.possible_types
+            subtype_map = build_type_map(possible_types)
+            type_map.update(subtype_map)
+
+        elif kind == TypeKind.OBJECT:
+            field_types = []
+            for field in typedef.fields:
+                field_types.append(field.return_type)
+                field_types.extend(field.arg_types.values() if field.arg_types else [])
+            subtype_map = build_type_map(field_types)
+            type_map.update(subtype_map)
+
+        elif kind == TypeKind.INPUT_OBJECT:
+            # FIXME
+            pass
+
+        elif kind == TypeKind.INTERFACE:
+            # FIXME
+            pass
+
+    return type_map
+
+
+class Schema:
+    def __init__(self, query: ObjectType, mutation=None, directives=None, additional_types=None):
+        if not issubclass(query, ObjectType):
+            raise PolygraphSchemaError("Query must be an ObjectType")
+        self.query = query
+        self.mutation = mutation
+        self.directives = directives
+        initial_types = additional_types or []
+        initial_types.append(query)
+        if mutation:
+            initial_types.append(mutation)
+        self.type_map = build_type_map(initial_types)
diff --git a/polygraph/types/type_builder.py b/polygraph/types/type_builder.py
index 943b40d..2b20577 100644
--- a/polygraph/types/type_builder.py
+++ b/polygraph/types/type_builder.py
@@ -77,7 +77,7 @@ class Union(PolygraphOutputType, PolygraphType):
         return type(name, bases, attrs)
 
 
-class List(PolygraphType):
+class List(PolygraphOutputType, PolygraphType):
     """
     A GraphQL list is a special collection type which declares the
     type of each item in the List (referred to as the item type of
diff --git a/polygraph/utils/strict_dict.py b/polygraph/utils/strict_dict.py
new file mode 100644
index 0000000..98f7dc7
--- /dev/null
+++ b/polygraph/utils/strict_dict.py
@@ -0,0 +1,11 @@
+class StrictDict(dict):
+    """
+    A dictionary that raises an exception if attempting to associate a known
+    key with a different value
+    """
+    def __setitem__(self, key, item):
+        if key in self and self[key] != item:
+            raise ValueError(
+                "Attempting to set {} to a different value {}".format(key, item)
+            )
+        return super().__setitem__(key, item)


Test patch:
diff --git a/polygraph/types/tests/test_type_map.py b/polygraph/types/tests/test_type_map.py
new file mode 100644
index 0000000..78476f0
--- /dev/null
+++ b/polygraph/types/tests/test_type_map.py
@@ -0,0 +1,41 @@
+from unittest import TestCase
+
+from polygraph.types.decorators import field
+from polygraph.types.object_type import ObjectType
+from polygraph.types.scalar import ID, Boolean, Float, Int, String
+from polygraph.types.schema import Schema
+from polygraph.types.type_builder import List, NonNull, Union
+
+
+class Person(ObjectType):
+    @field()
+    def name(self) -> NonNull(String):
+        pass
+
+    @field()
+    def age(year: Int) -> String:
+        pass
+
+
+class Animal(ObjectType):
+    @field()
+    def can_walk(self) -> Boolean:
+        pass
+
+
+class Query(ObjectType):
+    @field()
+    def characters(self) -> List(Union(Animal, Person)):
+        pass
+
+
+class TypeMapTest(TestCase):
+    def test_type_map_builder(self):
+        schema = Schema(query=Query, additional_types=[ID])
+        type_map = schema.type_map
+        self.assertEqual(type_map["Animal"], Animal)
+        self.assertEqual(type_map["Person"], Person)
+        self.assertEqual(type_map["Animal|Person"], Union(Animal, Person))
+        self.assertEqual(type_map["Boolean"], Boolean)
+        self.assertEqual(type_map["ID"], ID)
+        self.assertNotIn(Float, type_map.values())  # Float was not defined anywhere
diff --git a/polygraph/utils/tests/test_strict_dict.py b/polygraph/utils/tests/test_strict_dict.py
new file mode 100644
index 0000000..b000dab
--- /dev/null
+++ b/polygraph/utils/tests/test_strict_dict.py
@@ -0,0 +1,20 @@
+from unittest import TestCase
+
+from polygraph.utils.strict_dict import StrictDict
+
+
+class StrictDictTest(TestCase):
+    def test_cannot_update_same_key_with_different_value(self):
+        d = StrictDict()
+        d["George"] = "Washington"
+        d["John"] = "Adams"
+        with self.assertRaises(ValueError):
+            d["George"] = "Bush"
+        self.assertEqual(d["George"], "Washington")
+
+    def test_can_update_same_key_with_same_value(self):
+        d = StrictDict()
+        d["George"] = "Bush"
+        d["Bill"] = "Clinton"
+        d["George"] = "Bush"
+        self.assertEqual(d["George"], "Bush")