Skip to content

Commit

Permalink
GraphQL support now totally matches Hiku's functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
vmagamedov committed Nov 19, 2016
1 parent 5d75c9c commit b91f905
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 29 deletions.
12 changes: 6 additions & 6 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ Of course this is optional feature, and you are not required to rewrite
your code in order to use this feature later, code is initially written
in a way to make possible to run it concurrently.

★ Query your graph using `simple` queries or using GraphQL_ (in the future).
This is how client can express its needs and avoid data underfetching or data
overfetching. Client will load only what it currently needs using one query,
instead of multiple queries to different *resources* (as in RESTful APIs, for
example). `Simple` queries are basically a data structures in edn_ format. For
example:
★ Query your graph using `simple` queries or using GraphQL_ (partially
supported). This is how client can express its needs and avoid data
under-fetching or data over-fetching. Client will load only what it currently
needs using one query, instead of multiple queries to different *resources* (as
in RESTful APIs, for example). `Simple` queries are basically a data structures
in edn_ format. For example:

.. code-block:: clojure
Expand Down
54 changes: 42 additions & 12 deletions hiku/readers/graphql.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,56 @@
from __future__ import absolute_import

from graphql.language import ast
from graphql.language.parser import parse
from graphql.language.visitor import Visitor, visit

from ..query import Node, Field
from ..query import Node, Field, Link


class GraphQLTransformer(Visitor):
class NodeVisitor(object):

def __init__(self):
self._stack = [set()]
def visit(self, obj):
if not isinstance(obj, ast.Node):
raise TypeError('Unknown node type: {!r}'.format(obj))
visit_method = getattr(self, 'visit_{}'.format(obj.__class__.__name__),
None)
if visit_method is None:
raise TypeError('Not implemented node type: {!r}'.format(obj))
return visit_method(obj)


class GraphQLTransformer(NodeVisitor):

@classmethod
def transform(cls, document):
visitor = cls()
visit(document, visitor)
node = Node([Field(name) for name in visitor._stack[0]])
return node

def enter_SelectionSet(self, node, key, parent, path, ancestors):
for field in node.selections:
self._stack[-1].add(field.name.value)
return visitor.visit(document)

def visit_Document(self, obj):
if len(obj.definitions) != 1:
raise NotImplementedError('Only single operation per document is '
'supported, {} operations was provided'
.format(len(obj.definitions)))
definition, = obj.definitions
if definition.operation != 'query':
raise NotImplementedError('Only "query" operations are supported, '
'"{}" operation was provided'
.format(definition.operation))
assert definition.operation == 'query', definition.operation
return self.visit(definition.selection_set)

def visit_SelectionSet(self, obj):
return Node([self.visit(i) for i in obj.selections])

def visit_Field(self, obj):
if obj.alias is not None:
raise NotImplementedError('Field aliases are not supported: {!r}'
.format(obj))
options = {arg.name.value: arg.value.value for arg in obj.arguments}
if obj.selection_set is None:
return Field(obj.name.value, options or None)
else:
node = self.visit(obj.selection_set)
return Link(obj.name.value, node, options or None)


def read(src):
Expand Down
67 changes: 56 additions & 11 deletions tests/test_read_graphql.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,61 @@
from hiku.query import Node, Field
import pytest

from hiku.query import Node, Field, Link
from hiku.readers.graphql import read

from .base import TestCase, reqs_eq_patcher
from .base import reqs_eq_patcher


def check_read(source, query):
parsed_query = read(source)
with reqs_eq_patcher():
assert parsed_query == query


def test_field():
check_read(
'{ vaward }',
Node([Field('vaward')]),
)


def test_field_args():
check_read(
'{ ozone(auer: "spence") }',
Node([Field('ozone', options={'auer': 'spence'})]),
)


def test_field_alias():
with pytest.raises(NotImplementedError) as err:
read('{ glamors: foule }')
err.match('Field aliases are not supported')


def test_complex_field():
check_read(
'{ saale { slighty } }',
Node([Link('saale', Node([Field('slighty')]))]),
)


def test_complex_field_args():
check_read(
'{ saale(lammie: "nursy") { slighty } }',
Node([Link('saale', Node([Field('slighty')]),
options={'lammie': 'nursy'})]),
)


def test_multiple_operations():
with pytest.raises(NotImplementedError) as err:
read('{ gummed } { calce } { aaron }')
err.match('Only single operation per document is supported, '
'3 operations was provided')

class TestReadGraphQL(TestCase):

def test(self):
with reqs_eq_patcher():
self.assertEqual(
read("""
{ hello }
"""),
Node([Field('hello')]),
)
def test_mutation_operation():
with pytest.raises(NotImplementedError) as err:
read('mutation { doSomething(kokam: "screens") }')
err.match('Only "query" operations are supported, "mutation" operation '
'was provided')

0 comments on commit b91f905

Please sign in to comment.