Skip to content

Commit

Permalink
Merge pull request #112 from evo-company/add-interface-support
Browse files Browse the repository at this point in the history
add interface support (restricted to 1 interface in this implementation)
  • Loading branch information
kindermax committed Jul 29, 2023
2 parents 6eea610 + ba25727 commit 9ae0788
Show file tree
Hide file tree
Showing 23 changed files with 1,129 additions and 71 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ RUN pdm sync -G docs

FROM base as tests

RUN pdm sync -G test
RUN pdm sync -G test -G dev
RUN python3 -m pip install tox tox-pdm

FROM base as examples
Expand Down
1 change: 1 addition & 0 deletions docs/changelog/changes_07.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Changes in 0.7
- Added support for custom schema directives :ref:`Check directives documentation <directives-doc>`
- Added `ID` type.
- Added support for unions :ref:`Check unions documentation <unions-doc>`
- Added support for interfaces :ref:`Check interfaces documentation <interfaces-doc>`

Backward-incompatible changes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ User's Guide
asyncio
graphql
protobuf
interfaces
unions
directives
federation
Expand Down
129 changes: 129 additions & 0 deletions docs/interfaces.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
Interfaces
==========

.. _interfaces-doc:

Interfaces are a special types that other types can implement.

Interfaces are useful when you want to define a common set of fields.

In graphql you can use interfaces types like this:

.. code-block::
interface Media {
id: ID!
duration: String!
}
type Audio implements Media {
id: ID!
duration: String!
album: String!
}
type Video implements Media {
id: ID!
duration: String!
thumbnailUrl: String!
}
type Query {
search(text: String!): [Media!]!
}
In `hiku` you can define interface types like this:

.. code-block:: python
from hiku.graph import Field, Graph, Link, Node, Root, Interface
from hiku.types import ID, Integer, String, TypeRef, Sequence, Optional, InterfaceRef
from hiku.utils import empty_field
def search_resolver():
return [
(1, TypeRef['Audio']),
(2, TypeRef['Video']),
]
interfaces = [
Interface('Media', [
Field('id', ID, empty_field),
Field('duration', String, empty_field),
]),
]
GRAPH = Graph([
Node('Audio', [
Field('id', ID, audio_fields_resolver),
Field('duration', String, audio_fields_resolver),
Field('album', String, audio_fields_resolver),
], implements=['Media']),
Node('Video', [
Field('id', ID, video_fields_resolver),
Field('duration', String, video_fields_resolver),
Field('thumbnailUrl', String, video_fields_resolver),
], implements=['Media']),
Root([
Link('search', Sequence(UnionRef['Media']), search_resolver, requires=None),
]),
], interfaces=interfaces)
Lets look at the example above:

- `Interface` type is defined with a name and a list of fields that any implementation type must contain.
- `Audio` and `Video` types implement `Media` interface - they have `id` and `duration` field because `Media` interface declares them, and in adition to those shared fields each type has its own fields.
- `Link` type is defined with a return type of `Sequence[InterfaceRef['Media']]`
- `search_resolver` returns a list of tuples with an id as a first tuple element and type as a second tuple element
- note that interface fields does need to have a resolver function, but currently this function is not used by hiku engine so you can pass `empty_field` as a resolver function (it may change in the future)

.. note::

`InterfaceRef` is a special type that is used to reference interface types. It is used in the example above to define
the return type of the `search` link. `TypeRef` will not work in this case.

Now lets look at the query:

.. code-block:: python
query {
search(text: "test") {
__typename
id
duration
... on Audio {
album
}
... on Video {
thumbnailUrl
}
}
}
As a result of the query above you will get a list of objects with `__typename`, `id` and `duration` fields and fields that are specific
to the type of the object.

.. code-block::
[
{
'__typename': 'Audio',
'id': 1,
'duration': '1:20',
'album': 'Cool album',
},
{
'__typename': 'Video',
'id': 2,
'duration': '1:40',
'thumbnailUrl': 'http://example.com/thumbnail.jpg',
},
]
Type narrowing
--------------

Unlike other graphql implementations `hiku` supports type narrowing without
`__resolveType` function. It is possible because `hiku` knows all possible types
at the link resolution time.
2 changes: 2 additions & 0 deletions docs/unions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Unions

Union types are special types used to represent a value that could be one of types.

.. note:: Unlike interfaces, unions do not define any common fields between types.

In graphql you can use union types like this:

.. code-block::
Expand Down
13 changes: 6 additions & 7 deletions hiku/denormalize/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import typing as t
from collections import deque

from ..graph import Graph, Union
from ..graph import Graph, Interface, Union
from ..query import (
QueryVisitor,
Link,
Expand All @@ -12,10 +12,9 @@
from ..types import (
Record,
RecordMeta,
TypeRefMeta,
RefMeta,
OptionalMeta,
SequenceMeta,
UnionRefMeta,
get_type,
)

Expand All @@ -25,7 +24,7 @@ def __init__(self, graph: Graph, result: Proxy) -> None:
self._types = graph.__types__
self._unions = graph.unions_map
self._result = result
self._type: t.Deque[t.Union[t.Type[Record], Union]] = deque(
self._type: t.Deque[t.Union[t.Type[Record], Union, Interface]] = deque(
[self._types["__root__"]]
)
self._data = deque([result])
Expand All @@ -49,7 +48,7 @@ def visit_link(self, obj: Link) -> None:
else:
raise AssertionError(repr(self._type[-1]))

if isinstance(type_, (TypeRefMeta, UnionRefMeta)):
if isinstance(type_, RefMeta):
self._type.append(get_type(self._types, type_))
self._res.append({})
self._data.append(self._data[-1][obj.result_key])
Expand All @@ -61,7 +60,7 @@ def visit_link(self, obj: Link) -> None:
type_ref = type_.__item_type__
if isinstance(type_.__item_type__, OptionalMeta):
type_ref = type_.__item_type__.__type__
assert isinstance(type_ref, (TypeRefMeta, UnionRefMeta))
assert isinstance(type_ref, RefMeta)
self._type.append(get_type(self._types, type_ref))
items = []
for item in self._data[-1][obj.result_key]:
Expand All @@ -76,7 +75,7 @@ def visit_link(self, obj: Link) -> None:
if self._data[-1][obj.result_key] is None:
self._res[-1][obj.result_key] = None
else:
assert isinstance(type_.__type__, (TypeRefMeta, UnionRefMeta))
assert isinstance(type_.__type__, RefMeta)
self._type.append(get_type(self._types, type_.__type__))
self._res.append({})
self._data.append(self._data[-1][obj.result_key])
Expand Down
15 changes: 7 additions & 8 deletions hiku/denormalize/graphql.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
from collections import deque

from ..graph import Graph, Union
from ..graph import Graph, Interface, Union
from ..query import Field, Link
from ..result import Proxy
from ..types import (
RecordMeta,
TypeRefMeta,
RefMeta,
SequenceMeta,
OptionalMeta,
GenericMeta,
UnionRefMeta,
)

from .base import Denormalize
Expand All @@ -25,11 +24,11 @@ def __init__(
def visit_field(self, obj: Field) -> None:
if obj.name == "__typename":
type_name = self._type_name[-1]
if isinstance(self._type[-1], Union):
if isinstance(self._type[-1], (Union, Interface)):
type_name = self._data[-1].__ref__.node
self._res[-1][obj.result_key] = type_name
else:
if isinstance(self._type[-1], Union):
if isinstance(self._type[-1], (Union, Interface)):
type_name = self._data[-1].__ref__.node

if obj.name not in self._types[type_name].__field_types__:
Expand All @@ -46,16 +45,16 @@ def visit_link(self, obj: Link) -> None:
raise AssertionError(repr(self._type[-1]))

type_ref: GenericMeta
if isinstance(type_, (TypeRefMeta, UnionRefMeta)):
if isinstance(type_, RefMeta):
type_ref = type_
elif isinstance(type_, SequenceMeta):
type_ref = type_.__item_type__
if isinstance(type_ref, OptionalMeta):
type_ref = type_ref.__type__
assert isinstance(type_ref, (TypeRefMeta, UnionRefMeta)), type_ref
assert isinstance(type_ref, RefMeta), type_ref
elif isinstance(type_, OptionalMeta):
type_ref = type_.__type__
assert isinstance(type_ref, (TypeRefMeta, UnionRefMeta)), type_ref
assert isinstance(type_ref, RefMeta), type_ref
else:
raise AssertionError(repr(type_))
self._type_name.append(type_ref.__type_name__)
Expand Down

0 comments on commit 9ae0788

Please sign in to comment.