From 558378ee143fdcd056278610feac15d8c2a90a1b Mon Sep 17 00:00:00 2001 From: Joa Riski Date: Wed, 26 Feb 2020 00:22:38 +0200 Subject: [PATCH] Add support for custom validators 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 #267 --- graphql/backend/core.py | 17 ++++++++--- graphql/backend/tests/test_core.py | 46 +++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/graphql/backend/core.py b/graphql/backend/core.py index e8fc3099..1246da8b 100644 --- a/graphql/backend/core.py +++ b/graphql/backend/core.py @@ -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 @@ -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) @@ -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 diff --git a/graphql/backend/tests/test_core.py b/graphql/backend/tests/test_core.py index 257eb64b..39fb641c 100644 --- a/graphql/backend/tests/test_core.py +++ b/graphql/backend/tests/test_core.py @@ -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(): @@ -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