Skip to content

Commit

Permalink
Merge pull request #5 from strollby/feat-schema-directive
Browse files Browse the repository at this point in the history
feat: add support for schema directive
  • Loading branch information
abhinand-c committed Jan 12, 2024
2 parents de9dac2 + 5a67782 commit 848d7f1
Show file tree
Hide file tree
Showing 13 changed files with 173 additions and 25 deletions.
5 changes: 5 additions & 0 deletions example/complex_uses.graphql
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
extend schema
@compose(directiveName: "lowercase")
@compose(directiveName: "uppercase")
@compose(directiveName: "pascalcase")

"""Caching directive to control cache behavior of fields or fragments."""
directive @cache(
"""Specifies the maximum age for cache in seconds."""
Expand Down
26 changes: 26 additions & 0 deletions example/complex_uses.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from graphene_directives import (
CustomDirective,
DirectiveLocation,
SchemaDirective,
build_schema,
directive,
)
Expand Down Expand Up @@ -108,6 +109,19 @@ def validate_input(_directive: GraphQLDirective, inputs: dict) -> bool:
is_repeatable=True,
)

# A Schema directive
ComposeDirective = CustomDirective(
name="compose",
locations=[DirectiveLocation.SCHEMA],
description="A schema directive.",
args={
"directive_name": GraphQLArgument(
GraphQLNonNull(GraphQLString), description="Directive Name required"
)
},
is_repeatable=True,
)


@directive(target_directive=CacheDirective, max_age=100)
@directive(target_directive=AuthenticatedDirective, required=True)
Expand Down Expand Up @@ -257,6 +271,18 @@ class Query(graphene.ObjectType):
KeyDirective,
RepeatableDirective,
),
schema_directives=( # extend schema directives
SchemaDirective(
target_directive=ComposeDirective, arguments={"directive_name": "lowercase"}
),
SchemaDirective(
target_directive=ComposeDirective, arguments={"directive_name": "uppercase"}
),
SchemaDirective(
target_directive=ComposeDirective,
arguments={"directive_name": "pascalcase"},
),
),
)


Expand Down
2 changes: 2 additions & 0 deletions graphene_directives/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from .constants import DirectiveLocation
from .directive import ACCEPTED_TYPES
from .directive import CustomDirective, directive, directive_decorator
from .data_models import SchemaDirective
from .exceptions import DirectiveCustomValidationError, DirectiveValidationError
from .main import build_schema

__all__ = [
"build_schema",
"CustomDirective",
"SchemaDirective",
"directive_decorator",
"directive",
"ACCEPTED_TYPES",
Expand Down
4 changes: 4 additions & 0 deletions graphene_directives/data_models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .custom_directive_meta import CustomDirectiveMeta
from .schema_directive import SchemaDirective

__all__ = ["SchemaDirective", "CustomDirectiveMeta"]
16 changes: 16 additions & 0 deletions graphene_directives/data_models/custom_directive_meta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from dataclasses import dataclass
from typing import Any, Callable, Union

from graphql import DirectiveLocation as GrapheneDirectiveLocation, GraphQLDirective


@dataclass
class CustomDirectiveMeta:
allow_all_directive_locations: bool
add_definition_to_schema: bool
has_no_argument: bool
valid_types: set[GrapheneDirectiveLocation]
non_field_types: set[GrapheneDirectiveLocation]
supports_field_types: bool
supports_non_field_types: bool
validator: Union[Callable[[GraphQLDirective, Any], bool], None]
24 changes: 24 additions & 0 deletions graphene_directives/data_models/schema_directive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from dataclasses import dataclass
from typing import Any

from graphql import DirectiveLocation as GrapheneDirectiveLocation
from graphql import GraphQLDirective

from ..exceptions import DirectiveValidationError


@dataclass
class SchemaDirective:
target_directive: GraphQLDirective
arguments: dict[str, Any]

def __post_init__(self):
if GrapheneDirectiveLocation.SCHEMA not in self.target_directive.locations:
raise DirectiveValidationError(
". ".join(
[
f"{self.target_directive} cannot be used as schema directive",
"Missing DirectiveLocation.SCHEMA in locations",
]
)
)
23 changes: 3 additions & 20 deletions graphene_directives/directive.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
from dataclasses import dataclass
from functools import partial
from typing import Any, Callable, Collection, Dict, Optional, Union
from typing import Any, Callable, Collection, Dict, Optional

from graphene.utils.str_converters import to_camel_case
from graphql import (
DirectiveLocation as GrapheneDirectiveLocation,
GraphQLArgument,
GraphQLDirective,
GraphQLNonNull,
)
from graphql import GraphQLArgument, GraphQLDirective, GraphQLNonNull
from graphql.language import ast

from .constants import ACCEPTED_TYPES, FIELD_TYPES, LOCATION_NON_FIELD_VALIDATOR
from .constants import DirectiveLocation
from .data_models import CustomDirectiveMeta
from .exceptions import (
DirectiveCustomValidationError,
DirectiveInvalidArgTypeError,
Expand All @@ -22,18 +17,6 @@
from .utils import field_attribute_name, non_field_attribute_name, set_attribute_value


@dataclass
class CustomDirectiveMeta:
allow_all_directive_locations: bool
add_definition_to_schema: bool
has_no_argument: bool
valid_types: set[GrapheneDirectiveLocation]
non_field_types: set[GrapheneDirectiveLocation]
supports_field_types: bool
supports_non_field_types: bool
validator: Union[Callable[[GraphQLDirective, Any], bool], None]


def CustomDirective( # noqa
name: str,
locations: Collection[DirectiveLocation],
Expand Down
14 changes: 14 additions & 0 deletions graphene_directives/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from graphene import Schema as GrapheneSchema
from graphql import GraphQLDirective

from . import DirectiveValidationError
from .data_models import SchemaDirective
from .schema import Schema


Expand All @@ -14,12 +16,24 @@ def build_schema(
types: Collection[Union[graphene.ObjectType, Type[graphene.ObjectType]]] = None,
directives: Union[Collection[GraphQLDirective], None] = None,
auto_camelcase: bool = True,
schema_directives: Collection[SchemaDirective] = None,
) -> GrapheneSchema:
_schema_directive_set: set[str] = set()
for schema_directive in schema_directives or []:
if schema_directive.target_directive.name in _schema_directive_set:
if not schema_directive.target_directive.is_repeatable:
raise DirectiveValidationError(
f"{schema_directive.target_directive} is not repeatable on schema"
)
else:
_schema_directive_set.add(schema_directive.target_directive.name)

return Schema(
query=query,
mutation=mutation,
subscription=subscription,
types=types,
directives=directives,
auto_camelcase=auto_camelcase,
schema_directives=schema_directives,
)
24 changes: 23 additions & 1 deletion graphene_directives/parsers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from typing import Union
from typing import Collection, Union

from graphene.utils.str_converters import to_camel_case
from graphql import (
Expand All @@ -16,6 +16,8 @@
print_input_object,
)

from .data_models import SchemaDirective


def _remove_block(str_fields: str) -> str:
# Remove blocks added by `print_block`
Expand Down Expand Up @@ -62,3 +64,23 @@ def decorator_string(directive: GraphQLDirective, **kwargs: dict) -> str:

# Construct the directive string
return f"{directive_name}({', '.join(formatted_args)})"


def extend_schema_string(
string_schema: str, schema_directives: Collection[SchemaDirective]
) -> str:
schema_directives_strings = []
for schema_directive in schema_directives:
schema_directives_strings.append(
"\t"
+ decorator_string(
schema_directive.target_directive, **schema_directive.arguments
)
)

if len(schema_directives_strings) != 0:
string_schema += (
"extend schema\n" + "\n".join(schema_directives_strings) + "\n\n"
)

return string_schema
10 changes: 9 additions & 1 deletion graphene_directives/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@
print_input_value,
)

from .data_models import SchemaDirective
from .directive import CustomDirectiveMeta
from .exceptions import DirectiveValidationError
from .parsers import (
decorator_string,
entity_type_to_fields_string,
enum_type_to_fields_string,
extend_schema_string,
input_type_to_fields_string,
)
from .utils import (
Expand All @@ -54,9 +56,12 @@ def __init__(
types: list[graphene.ObjectType] = None,
directives: Union[Collection[GraphQLDirective], None] = None,
auto_camelcase: bool = True,
schema_directives: Collection[SchemaDirective] = None,
):
self.directives = directives or []
self.schema_directives = schema_directives or []
self.auto_camelcase = auto_camelcase
self.schema_directives = schema_directives or []
super().__init__(
query=query,
mutation=mutation,
Expand Down Expand Up @@ -406,7 +411,10 @@ def get_directive_applied_field_types(self) -> set:
return directives_fields

def __str__(self):
string_schema = print_schema(self.graphql_schema)
string_schema = ""
string_schema += extend_schema_string(string_schema, self.schema_directives)

string_schema += print_schema(self.graphql_schema)
regex = r"schema \{(\w|\!|\s|\:)*\}"
pattern = re.compile(regex)
string_schema = pattern.sub(" ", string_schema)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "graphene-directives"
version = "0.3.1"
version = "0.3.2"
packages = [{include = "graphene_directives"}]
description = "Schema Directives implementation for graphene"
authors = ["Strollby <developers@strollby.com>"]
Expand Down
11 changes: 10 additions & 1 deletion tests/schema_files/test_directive.graphql
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
extend schema
@link(url: "https://spec.graphql.org/v1.0")
@compose(directiveName: "lowercase")
@compose(directiveName: "uppercase")
@compose(directiveName: "lowercase")

"""Caching directive to control cache behavior of fields or fragments."""
directive @cache(
"""Specifies the maximum age for cache in seconds."""
Expand All @@ -20,7 +26,10 @@ directive @authenticated(
directive @hidden on OBJECT | ARGUMENT_DEFINITION

"""Schema directive to link files"""
directive @link on SCHEMA
directive @link(
"""Url required"""
url: String!
) on SCHEMA

union SearchResult @cache(maxAge: 500) @authenticated(required: True) = Human | Droid | Starship

Expand Down
37 changes: 36 additions & 1 deletion tests/test_directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from graphene_directives import (
CustomDirective,
DirectiveLocation,
SchemaDirective,
build_schema,
directive,
)
Expand Down Expand Up @@ -84,11 +85,30 @@ def validate_input(_directive: GraphQLDirective, inputs: dict) -> bool:
)


# No argument directive
# No Schema directive
LinkDirective = CustomDirective(
name="link",
locations=[DirectiveLocation.SCHEMA],
description="Schema directive to link files",
args={
"url": GraphQLArgument(
GraphQLNonNull(GraphQLString), description="Url required"
)
},
)


# A Schema directive
ComposeDirective = CustomDirective(
name="compose",
locations=[DirectiveLocation.SCHEMA],
description="A schema directive.",
args={
"directive_name": GraphQLArgument(
GraphQLNonNull(GraphQLString), description="Directive Name required"
)
},
is_repeatable=True,
)


Expand Down Expand Up @@ -220,6 +240,21 @@ class Query(graphene.ObjectType):
query=Query,
types=(SearchResult, Animal, Admin, HumanInput, TruthEnum, DateNewScalar, User),
directives=(CacheDirective, AuthenticatedDirective, HiddenDirective, LinkDirective),
schema_directives=( # extend schema directives
SchemaDirective(
target_directive=LinkDirective,
arguments={"url": "https://spec.graphql.org/v1.0"},
),
SchemaDirective(
target_directive=ComposeDirective, arguments={"directive_name": "lowercase"}
),
SchemaDirective(
target_directive=ComposeDirective, arguments={"directive_name": "uppercase"}
),
SchemaDirective(
target_directive=ComposeDirective, arguments={"directive_name": "lowercase"}
),
),
)


Expand Down

0 comments on commit 848d7f1

Please sign in to comment.