Repository: mirumee/ariadne

Issue:
Provide utility for loading types from file/directory
We could provide a utility function that would take a path to the filesystem, and:

- if its a file, read it, validate that its correct file, and return SDL string
- if its a directory, glob `.graphql` files, validate each file, and concat them into single SDL string

That way users could easily use single schema definition for both graphql API and frontend tools.


Discussion:

Resolving patch:
diff --git a/ariadne/__init__.py b/ariadne/__init__.py
--- a/ariadne/__init__.py
+++ b/ariadne/__init__.py
@@ -1,4 +1,5 @@
 from .executable_schema import make_executable_schema
+from .load_schema import load_schema_from_path
 from .resolvers import (
     FallbackResolversSetter,
     ResolverMap,
@@ -13,6 +14,7 @@
 from .utils import convert_camel_case_to_snake, gql
 from .wsgi_middleware import GraphQLMiddleware
 
+
 __all__ = [
     "FallbackResolversSetter",
     "GraphQLMiddleware",
@@ -23,6 +25,7 @@
     "default_resolver",
     "fallback_resolvers",
     "gql",
+    "load_schema_from_path",
     "make_executable_schema",
     "resolve_to",
     "snake_case_fallback_resolvers",
diff --git a/ariadne/exceptions.py b/ariadne/exceptions.py
--- a/ariadne/exceptions.py
+++ b/ariadne/exceptions.py
@@ -15,3 +15,15 @@ class HttpBadRequestError(HttpError):
 
 class HttpMethodNotAllowedError(HttpError):
     status = HTTP_STATUS_405_METHOD_NOT_ALLOWED
+
+
+class GraphQLFileSyntaxError(Exception):
+    def __init__(self, schema_file, message):
+        super().__init__()
+        self.message = self.format_message(schema_file, message)
+
+    def format_message(self, schema_file, message):
+        return f"Could not load {schema_file}:\n{message}"
+
+    def __str__(self):
+        return self.message
diff --git a/ariadne/load_schema.py b/ariadne/load_schema.py
new file mode 100644
--- /dev/null
+++ b/ariadne/load_schema.py
@@ -0,0 +1,31 @@
+import os
+from typing import Generator
+
+from graphql import parse
+from graphql.error import GraphQLSyntaxError
+
+from .exceptions import GraphQLFileSyntaxError
+
+
+def load_schema_from_path(path: str) -> str:
+    if os.path.isdir(path):
+        schema_list = [read_graphql_file(f) for f in walk_graphql_files(path)]
+        return "\n".join(schema_list)
+    return read_graphql_file(os.path.abspath(path))
+
+
+def walk_graphql_files(path: str) -> Generator:
+    def abs_path(f):
+        return os.path.abspath(os.path.join(path, f))
+
+    return (abs_path(f) for f in sorted(os.listdir(path)) if f.endswith(".graphql"))
+
+
+def read_graphql_file(path: str) -> str:
+    with open(path, "r") as graphql_file:
+        schema = graphql_file.read()
+    try:
+        parse(schema)
+    except GraphQLSyntaxError as e:
+        raise GraphQLFileSyntaxError(path, str(e))
+    return schema


Test patch:
diff --git a/tests/test_schema_file_load.py b/tests/test_schema_file_load.py
new file mode 100644
--- /dev/null
+++ b/tests/test_schema_file_load.py
@@ -0,0 +1,71 @@
+import pytest
+from ariadne import exceptions, load_schema_from_path
+
+FIRST_SCHEMA = """
+    type Query {
+        test: Custom
+    }
+
+    type Custom {
+        node: String
+        default: String
+    }
+"""
+
+
+@pytest.fixture(scope="session")
+def single_file_schema(tmpdir_factory):
+    f = tmpdir_factory.mktemp("schema").join("schema.graphql")
+    f.write(FIRST_SCHEMA)
+    return f
+
+
+def test_load_schema_from_single_file(single_file_schema):
+    assert load_schema_from_path(str(single_file_schema)) == FIRST_SCHEMA
+
+
+INCORRECT_SCHEMA = """
+    type Query {
+        test: Custom
+
+    type Custom {
+        node: String
+        default: String
+    }
+"""
+
+
+@pytest.fixture(scope="session")
+def incorrect_schema_file(tmpdir_factory):
+    f = tmpdir_factory.mktemp("schema").join("schema.graphql")
+    f.write(INCORRECT_SCHEMA)
+    return f
+
+
+def test_loading_schema_fails_on_bad_syntax(incorrect_schema_file):
+    with pytest.raises(exceptions.GraphQLFileSyntaxError) as e:
+        load_schema_from_path(str(incorrect_schema_file))
+    assert str(incorrect_schema_file) in str(e)
+
+
+SECOND_SCHEMA = """
+    type User {
+        name: String
+    }
+"""
+
+
+@pytest.fixture
+def schema_directory(tmpdir_factory):
+    directory = tmpdir_factory.mktemp("schema")
+    first_file = directory.join("base.graphql")
+    first_file.write(FIRST_SCHEMA)
+    second_file = directory.join("user.graphql")
+    second_file.write(SECOND_SCHEMA)
+    return directory
+
+
+def test_loading_schema_from_directory(schema_directory):
+    assert load_schema_from_path(str(schema_directory)) == "\n".join(
+        [FIRST_SCHEMA, SECOND_SCHEMA]
+    )