Skip to content

Commit

Permalink
Add support for custom validators
Browse files Browse the repository at this point in the history
Support extending the GraphQLCoreBackend class with custom validators by
adding a function \"get_validation_rules\" that can be overridden in
subclasses where needed.

This change in combination with setting the default graphql backend
allows for easy additions to validation rules. An example use case would
be if there's a need to perform query cost or depth analysis, one can
create a validator that restricts execution of the query based on it's
execution cost.

Of course this could also be used to remove validators if that is
necessary for some use case.

Resolves graphql-python#267
  • Loading branch information
JoaRiski committed Feb 25, 2020
1 parent fe703f0 commit 558378e
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 5 deletions.
17 changes: 13 additions & 4 deletions graphql/backend/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
from ..execution import execute, ExecutionResult
from ..language.base import parse, print_ast
from ..language import ast
from ..validation import validate
from ..validation import validate, specified_rules
from .base import GraphQLBackend, GraphQLDocument

# Necessary for static type checking
if False: # flake8: noqa
from typing import Any, Optional, Union
from typing import Any, Optional, Union, List, Type
from ..validation.rules import ValidationRule
from ..language.ast import Document
from ..type.schema import GraphQLSchema
from rx import Observable
Expand All @@ -24,8 +25,9 @@ def execute_and_validate(
):
# type: (...) -> Union[ExecutionResult, Observable]
do_validation = kwargs.get("validate", True)
validation_rules = kwargs.get("validation_rules", specified_rules)
if do_validation:
validation_errors = validate(schema, document_ast)
validation_errors = validate(schema, document_ast, validation_rules)
if validation_errors:
return ExecutionResult(errors=validation_errors, invalid=True)

Expand All @@ -38,7 +40,14 @@ class GraphQLCoreBackend(GraphQLBackend):

def __init__(self, executor=None):
# type: (Optional[Any]) -> None
self.execute_params = {"executor": executor}
self.execute_params = {
"executor": executor,
"validation_rules": self.get_validation_rules(),
}

def get_validation_rules(self):
# type: () -> List[Type[ValidationRule]]
return specified_rules

def document_from_string(self, schema, document_string):
# type: (GraphQLSchema, Union[Document, str]) -> GraphQLDocument
Expand Down
46 changes: 45 additions & 1 deletion graphql/backend/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
"""Tests for `graphql.backend.core` module."""

import pytest

from graphql import GraphQLError
from graphql.execution.executors.sync import SyncExecutor
from graphql.validation.rules.base import ValidationRule

from ..base import GraphQLBackend, GraphQLDocument
from ..core import GraphQLCoreBackend
from .schema import schema

if False:
from typing import Any
from pytest_mock import MockFixture
from typing import Any, List, Optional, Type
from graphql.language.ast import Document


def test_core_backend():
Expand Down Expand Up @@ -52,3 +57,42 @@ def test_backend_can_execute_custom_executor():
assert not result.errors
assert result.data == {"hello": "World"}
assert executor.executed


class AlwaysFailValidator(ValidationRule):
# noinspection PyPep8Naming
def enter_Document(self, node, key, parent, path, ancestors):
# type: (Document, Optional[Any], Optional[Any], List, List) -> None
self.context.report_error(GraphQLError("Test validator failure", [node]))


class CustomValidatorBackend(GraphQLCoreBackend):
def get_validation_rules(self):
# type: () -> List[Type[ValidationRule]]
return [AlwaysFailValidator]


def test_backend_custom_validators_result():
# type: () -> None
backend = CustomValidatorBackend()
assert isinstance(backend, CustomValidatorBackend)
document = backend.document_from_string(schema, "{ hello }")
assert isinstance(document, GraphQLDocument)
result = document.execute()
assert result.errors
assert len(result.errors) == 1
assert result.errors[0].message == "Test validator failure"


def test_backend_custom_validators_in_validation_args(mocker):
# type: (MockFixture) -> None
mocked_validate = mocker.patch("graphql.backend.core.validate")
backend = CustomValidatorBackend()
assert isinstance(backend, CustomValidatorBackend)
document = backend.document_from_string(schema, "{ hello }")
assert isinstance(document, GraphQLDocument)
mocked_validate.assert_not_called()
result = document.execute()
mocked_validate.assert_called_once()
(args, kwargs) = mocked_validate.call_args
assert [AlwaysFailValidator] in args

0 comments on commit 558378e

Please sign in to comment.