Skip to content

Commit

Permalink
Merge pull request #7 from strollby/better-field-level-validator
Browse files Browse the repository at this point in the history
Better field level & non field level validations
  • Loading branch information
mak626 authored Jan 13, 2024
2 parents 1e98d6d + e7efbdd commit 88c6240
Show file tree
Hide file tree
Showing 19 changed files with 192 additions and 95 deletions.
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any

# Graphene Directives
Schema Directives implementation for graphene

Expand Down Expand Up @@ -144,21 +146,33 @@ schema = build_schema(
import graphene
from graphql import (
GraphQLArgument,
GraphQLDirective,
GraphQLInt,
GraphQLNonNull,
GraphQLString,
)

from graphene_directives import CustomDirective, DirectiveLocation, build_schema, directive_decorator
from graphene_directives import CustomDirective, DirectiveLocation, ValidatorLocation, build_schema, directive_decorator


def validate_input(_directive: GraphQLDirective, inputs: dict) -> bool:
def validate_non_field_input(_type: Any, inputs: dict) -> bool:
"""
def validator (type_: graphene type, inputs: Any) -> bool,
if validator returns False, library raises DirectiveCustomValidationError
"""
if inputs.get("max_age") > 2500:
return False
return True


def validate_field_input(_parent_type: Any, _field_type: Any, inputs: dict) -> bool:
"""
def validator (parent_type_: graphene_type, field_type_: graphene type, inputs: Any) -> bool,
if validator returns False, library raises DirectiveCustomValidationError
"""
if inputs.get("max_age") > 2500:
return False
return True

CacheDirective = CustomDirective(
name="cache",
locations=[DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT],
Expand All @@ -175,7 +189,8 @@ CacheDirective = CustomDirective(
),
},
description="Caching directive to control cache behavior of fields or fragments.",
validator=validate_input,
non_field_validator=validate_non_field_input,
field_validator=validate_field_input,
)

# This returns a partial of directive function
Expand Down
18 changes: 9 additions & 9 deletions example/complex_uses.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ directive @repeatable_directive(
serviceName: String!
) repeatable on OBJECT | FIELD_DEFINITION

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

type Human @cache(maxAge: 60) {
name: String
Expand All @@ -48,7 +48,7 @@ type Starship @cache(maxAge: 200) {
length: Int @deprecated(reason: "Use another field") @cache(maxAge: 60)
}

interface Animal @cache(maxAge: 100) @authenticated(required: True) {
interface Animal @cache(maxAge: 100) @authenticated(required: true) {
age: Int!
kind: Int! @cache(maxAge: 60)
}
Expand All @@ -58,26 +58,26 @@ type Admin @internal @key {
password: String
}

input HumanInput @cache(maxAge: 60) @authenticated(required: True) {
input HumanInput @cache(maxAge: 60) @authenticated(required: true) {
bornIn: String
"""Test Description"""
name: String @deprecated(reason: "Deprecated use born in") @cache(maxAge: 300)
}

enum TruthEnum @cache(maxAge: 100) @authenticated(required: True) {
A @authenticated(required: True)
enum TruthEnum @cache(maxAge: 100) @authenticated(required: true) {
A @authenticated(required: true)
B
}

scalar DateNewScalar @cache(maxAge: 500) @authenticated(required: True)
scalar DateNewScalar @cache(maxAge: 500) @authenticated(required: true)

type User @authenticated(required: True) {
type User @authenticated(required: true) {
name: String
password: String
price(currency: Int @internal, country: Int @authenticated(required: True) @internal): String
price(currency: Int @internal, country: Int @authenticated(required: true) @internal): String
}

type Company @authenticated(required: True) @repeatable_directive(serviceName: "CompanyService") @repeatable_directive(serviceName: "ProductService") {
type Company @authenticated(required: true) @repeatable_directive(serviceName: "CompanyService") @repeatable_directive(serviceName: "ProductService") {
established: Int!
name: String! @deprecated(reason: "This field is deprecated and will be removed in future") @repeatable_directive(serviceName: "CompanyService Field") @repeatable_directive(serviceName: "ProductService Field")
}
Expand Down
21 changes: 18 additions & 3 deletions example/complex_uses.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import os
from typing import Any

import graphene
from graphql import (
GraphQLArgument,
GraphQLBoolean,
GraphQLDirective,
GraphQLInt,
GraphQLNonNull,
GraphQLString,
Expand All @@ -21,7 +21,21 @@
curr_dir = os.path.dirname(os.path.realpath(__file__))


def validate_input(_directive: GraphQLDirective, inputs: dict) -> bool:
def validate_non_field_input(_type: Any, inputs: dict) -> bool:
"""
def validator (type_: graphene type, inputs: Any) -> bool,
if validator returns False, library raises DirectiveCustomValidationError
"""
if inputs.get("max_age") > 2500:
return False
return True


def validate_field_input(_parent_type: Any, _field_type: Any, inputs: dict) -> bool:
"""
def validator (parent_type_: graphene_type, field_type_: graphene type, inputs: Any) -> bool,
if validator returns False, library raises DirectiveCustomValidationError
"""
if inputs.get("max_age") > 2500:
return False
return True
Expand Down Expand Up @@ -52,7 +66,8 @@ def validate_input(_directive: GraphQLDirective, inputs: dict) -> bool:
),
},
description="Caching directive to control cache behavior of fields or fragments.",
validator=validate_input,
non_field_validator=validate_non_field_input,
field_validator=validate_field_input,
)


Expand Down
2 changes: 1 addition & 1 deletion graphene_directives/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .constants import DirectiveLocation
from .data_models import SchemaDirective
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

Expand Down
5 changes: 3 additions & 2 deletions graphene_directives/data_models/custom_directive_meta.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from dataclasses import dataclass
from typing import Any, Callable, Union

from graphql import DirectiveLocation as GrapheneDirectiveLocation, GraphQLDirective
from graphql import DirectiveLocation as GrapheneDirectiveLocation


@dataclass
Expand All @@ -13,4 +13,5 @@ class CustomDirectiveMeta:
non_field_types: set[GrapheneDirectiveLocation]
supports_field_types: bool
supports_non_field_types: bool
validator: Union[Callable[[GraphQLDirective, Any], bool], None]
non_field_validator: Union[Callable[[Any, dict[str, Any]], bool], None]
field_validator: Union[Callable[[Any, Any, dict[str, Any]], bool], None]
25 changes: 16 additions & 9 deletions graphene_directives/directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ def CustomDirective( # noqa
ast_node: Optional[ast.DirectiveDefinitionNode] = None,
allow_all_directive_locations: bool = False,
add_definition_to_schema: bool = True,
validator: Callable[[GraphQLDirective, Any], bool] = None,
non_field_validator: Callable[[Any, dict[str, Any]], bool] = None,
field_validator: Callable[[Any, Any, dict[str, Any]], bool] = None,
) -> GraphQLDirective:
"""
Creates a GraphQLDirective
Expand All @@ -43,7 +44,11 @@ def CustomDirective( # noqa
:param locations: list[DirectiveLocation], if need to use unsupported locations, set allow_all_directive_locations True
:param allow_all_directive_locations: Allow other DirectiveLocation other than the ones supported by library
:param add_definition_to_schema: If false, the @directive definition is not added to the graphql schema
:param validator: a validator function def validator (directive: GraphQLDirective, inputs: Any) -> bool,
:param non_field_validator: a validator function
def validator (type_: graphene type, inputs: Any) -> bool,
if validator returns False, library raises DirectiveCustomValidationError
:param field_validator: a validator function
def validator (parent_type_: graphene_type, field_type_: graphene type, inputs: Any) -> bool,
if validator returns False, library raises DirectiveCustomValidationError


Expand All @@ -59,7 +64,7 @@ def CustomDirective( # noqa
f"directive @{name} add_definition_to_schema type invalid expected bool"
)

if not (isinstance(validator, Callable) or validator is None):
if not (isinstance(non_field_validator, Callable) or non_field_validator is None):
raise DirectiveInvalidArgTypeError(
f"directive @{name} validator type invalid expected Callable[[GraphQLDirective, Any], bool] "
)
Expand Down Expand Up @@ -109,7 +114,8 @@ def CustomDirective( # noqa
non_field_types=non_field_types,
supports_field_types=supports_field_types,
supports_non_field_types=supports_non_field_types,
validator=validator,
non_field_validator=non_field_validator,
field_validator=field_validator,
)

# Check if target_directive.locations have accepted types
Expand Down Expand Up @@ -148,13 +154,9 @@ def directive(

kwargs = {to_camel_case(field): value for (field, value) in _kwargs.items()}
directive_name = str(target_directive)
custom_validator = meta_data.validator
non_field_validator = meta_data.non_field_validator

kwargs = parse_argument_values(target_directive, kwargs)
if custom_validator is not None and not custom_validator(target_directive, _kwargs):
raise DirectiveCustomValidationError(
f"Custom Validation Failed for {directive_name} with args: ({kwargs})"
)

def decorator(type_: Any) -> Any:
if not meta_data.supports_non_field_types:
Expand All @@ -170,6 +172,11 @@ def decorator(type_: Any) -> Any:
f"{directive_name} cannot be used for {type_}, valid levels are: {[str(i) for i in meta_data.valid_types]}"
)

if non_field_validator is not None and not non_field_validator(type_, _kwargs):
raise DirectiveCustomValidationError(
f"Custom Validation Failed for {directive_name} with args: ({kwargs}) at non field level {type_}"
)

set_attribute_value(
type_=type_,
attribute_name=non_field_attribute_name(target_directive),
Expand Down
6 changes: 6 additions & 0 deletions graphene_directives/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ def build_schema(
else:
_schema_directive_set.add(schema_directive.target_directive.name)

_directive_set: set[str] = set()
for directive in directives or []:
if directive.name in _directive_set:
raise DirectiveValidationError(f"Duplicate {directive} found")
_directive_set.add(directive.name)

return Schema(
query=query,
mutation=mutation,
Expand Down
15 changes: 10 additions & 5 deletions graphene_directives/parsers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import re
from typing import Any, Collection, Dict, Union, cast

Expand Down Expand Up @@ -63,7 +64,7 @@ def decorator_string(directive: GraphQLDirective, **kwargs: dict) -> str:
formatted_args = [
(
f"{to_camel_case(key)}: "
+ (f'"{value}"' if isinstance(value, str) else str(value))
+ (f'"{value}"' if isinstance(value, str) else json.dumps(value))
)
for key, value in kwargs.items()
if value is not None and to_camel_case(key) in directive.args
Expand All @@ -78,11 +79,15 @@ def extend_schema_string(
) -> str:
schema_directives_strings = []
for schema_directive in schema_directives:
args = parse_argument_values(
schema_directive.target_directive,
{
to_camel_case(field): value
for (field, value) in schema_directive.arguments.items()
},
)
schema_directives_strings.append(
"\t"
+ decorator_string(
schema_directive.target_directive, **schema_directive.arguments
)
"\t" + decorator_string(schema_directive.target_directive, **args)
)

if len(schema_directives_strings) != 0:
Expand Down
Loading

0 comments on commit 88c6240

Please sign in to comment.