Repository: mirumee/ariadne

Issue:
Provide gql() utility function for wrapping/validating SDL on spot
Currently, syntax errors in SDL are raised by `graphql()`, which itself is called deep in GraphQL server initialization logic. This makes it trickier to trace back to invalid type definitions - especially in complex projects.

Ariadne could provide `gql()` utility function that would allow developers to mark pieces of code as SDL, and validate those SDL strings on scripts execution, raising execetions with traceback pointing to point of definition, making those easier to find and debug:

```python
from ariadne import gql

type_defs = gql("""
    type User {
        username: String!
        posts: [Post!]
        threads: [Thread!]
        follows [User!]
        likes: [Post!]
    }
""")
```

Above code would raise `GraphQLSyntaxError` pointing to `type_defs`, instead of somewhere in GraphQL server initialization logic.

This approach adds one more advantage: If we know that strings wrapped with `gql()` are SDL, we can develop a pattern for GraphQL syntax highlighters for IDEs to use to color GraphQL syntax in Python projects.

Lastly, `gql()` should pass through the strings, because our current modularization implementation is very simple: internally it does string joining using `\n` as glue. We'll rather avoid reimplementing it to create custom mechanism operating on `ast`'s.

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 857e135..7e569de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 - Removed support for Python 3.5 and added support for 3.7.
 - Moved to `GraphQL-core-next` that supports `async` resolvers, query execution and implements more recent version of GraphQL spec. If you are updating existing project, you will need to uninstall `graphql-core` before installing `graphql-core-next`, as both libraries use `graphql` namespace.
+- Added `gql()` utility that provides GraphQL string validation on declaration time, and enables use of [Apollo-GraphQL](https://marketplace.visualstudio.com/items?itemName=apollographql.vscode-apollo) plugin in Python code.
 - `Boolean` built-in scalar now checks the type of each serialized value. Returning values of type other than `bool`, `int` or `float` from a field resolver will result in a `Boolean cannot represent a non boolean value` error.
 - Redefining type in `type_defs` will now result in `TypeError` being raised. This is breaking change from previous behaviour where old type was simply replaced with new one.
-- Returning `None` from scalar `parse_literal` and `parse_value` function no longer results in GraphQL API producing default error message. Instead `None` will be passed further down to resolver or producing "value is required" error if its marked as such with `!` For old behaviour raise either `ValueError` or `TypeError`. See documentation for more details.
+- Returning `None` from scalar `parse_literal` and `parse_value` function no longer results in GraphQL API producing default error message. Instead `None` will be passed further down to resolver or producing "value is required" error if its marked as such with `!` For old behaviour raise either `ValueError` or `TypeError`. See documentation for more details.
\ No newline at end of file
diff --git a/README.md b/README.md
index d2c0e09..bea59e0 100644
--- a/README.md
+++ b/README.md
@@ -27,10 +27,11 @@ Ariadne can be installed with pip:
 Following example creates API defining `Person` type and single query field `people` returning list of two persons. It also starts local dev server with [GraphQL Playground](https://github.com/prisma/graphql-playground) available on the `http://127.0.0.1:8888` address.
 
 ```python
-from ariadne import GraphQLMiddleware
+from ariadne import GraphQLMiddleware, gql
 
 # Define types using Schema Definition Language (https://graphql.org/learn/schema/)
-type_defs = """
+# Wrapping string in gql function provides validation and better error traceback
+type_defs = gql("""
     type Query {
         people: [Person!]!
     }
@@ -41,7 +42,7 @@ type_defs = """
         age: Int
         fullName: String
     }
-"""
+""")
 
 
 # Resolvers are simple python functions
diff --git a/ariadne/__init__.py b/ariadne/__init__.py
index d9fdbf1..5784542 100644
--- a/ariadne/__init__.py
+++ b/ariadne/__init__.py
@@ -1,5 +1,6 @@
 from .executable_schema import make_executable_schema
 from .resolvers import add_resolve_functions_to_schema, default_resolver, resolve_to
+from .utils import gql
 from .wsgi_middleware import GraphQLMiddleware
 
 __all__ = [
@@ -8,4 +9,5 @@ __all__ = [
     "default_resolver",
     "make_executable_schema",
     "resolve_to",
+    "gql",
 ]
diff --git a/ariadne/utils.py b/ariadne/utils.py
new file mode 100644
index 0000000..d10576e
--- /dev/null
+++ b/ariadne/utils.py
@@ -0,0 +1,6 @@
+from graphql import parse
+
+
+def gql(value: str) -> str:
+    parse(value)
+    return value
diff --git a/docs/introduction.rst b/docs/introduction.rst
index 528881d..fd766f1 100644
--- a/docs/introduction.rst
+++ b/docs/introduction.rst
@@ -30,6 +30,32 @@ Using the SDL, our ``Query`` type definition will look like this::
 The ``type Query { }`` block declares the type, ``hello`` is the field definition, ``String`` is the return value type, and the exclamation mark following it means that returned value will never be ``null``.
 
 
+Validating schema
+-----------------
+
+Ariadne provides tiny ``gql`` utility function that takes single argument: GraphQL string, validates it and raises descriptive ``GraphQLSyntaxError``, or returns original unmodified string if its correct::
+
+    from ariadne import gql
+
+    type_defs = gql("""
+        type Query {
+            hello String!
+        }
+    """)
+
+If we try to run above code now, we will get an error pointing to our incorrect syntax within our ``type_defs`` declaration::
+
+    graphql.error.syntax_error.GraphQLSyntaxError: Syntax Error: Expected :, found Name
+
+    GraphQL request (3:19)
+        type Query {
+            hello String!
+                  ^
+        }
+
+Using ``gql`` is optional, but without it above error would occur during your server's initialization and point to somewhere inside Ariadne's GraphQL initialization logic, making tracking down incorrect place trickier if your API is large and spread across many modules.
+
+
 Resolvers
 ---------
 
@@ -105,13 +131,13 @@ Completed code
 
 For reference here is complete code of the API from this guide::
 
-    from ariadne import GraphQLMiddleware
+    from ariadne import GraphQLMiddleware, gql
 
-    type_defs = """
+    type_defs = gql("""
         type Query {
             hello: String!
         }
-    """
+    """)
 
 
     def resolve_hello(_, info):


Test patch:
diff --git a/tests/test_gql_util.py b/tests/test_gql_util.py
new file mode 100644
index 0000000..ab66580
--- /dev/null
+++ b/tests/test_gql_util.py
@@ -0,0 +1,54 @@
+import pytest
+from graphql.error import GraphQLSyntaxError
+
+from ariadne import gql
+
+
+def test_valid_graphql_schema_string_is_returned_unchanged():
+    sdl = """
+        type User {
+            username: String!
+        }
+    """
+    result = gql(sdl)
+    assert sdl == result
+
+
+def test_invalid_graphql_schema_string_causes_syntax_error():
+    with pytest.raises(GraphQLSyntaxError):
+        gql(
+            """
+                type User {
+                    username String!
+                }
+            """
+        )
+
+
+def test_valid_graphql_query_string_is_returned_unchanged():
+    query = """
+        query TestQuery {
+            auth
+            users {
+                id
+                username
+            }
+        }
+    """
+    result = gql(query)
+    assert query == result
+
+
+def test_invalid_graphql_query_string_causes_syntax_error():
+    with pytest.raises(GraphQLSyntaxError):
+        gql(
+            """
+                query TestQuery {
+                    auth
+                    users
+                        id
+                        username
+                    }
+                }
+            """
+        )