From 82ece57cf80dd8d9850b31ce1dda83474e0e563d Mon Sep 17 00:00:00 2001 From: Michael Chase <3686226+reallistic@users.noreply.github.com> Date: Wed, 16 Oct 2024 20:28:58 -0400 Subject: [PATCH 1/3] fix parsing of repeatable directives when cleaning up the sdl --- ariadne/contrib/federation/utils.py | 2 +- tests/federation/test_utils.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ariadne/contrib/federation/utils.py b/ariadne/contrib/federation/utils.py index bb6bc1d83..248b4bb30 100644 --- a/ariadne/contrib/federation/utils.py +++ b/ariadne/contrib/federation/utils.py @@ -29,7 +29,7 @@ f"{_i_token_delimiter}directive" f"(?:{_i_token_delimiter})?@({_i_token_name})" f"(?:(?:{_i_token_delimiter})?{_i_token_arguments})?" - f"{_i_token_delimiter}on" + f"{_i_token_delimiter}(?:repeatable)?(?:{_i_token_delimiter})?on" f"{_i_token_delimiter}(?:[|]{_i_token_delimiter})?{_i_token_location}" f"(?:{_i_token_delimiter}[|]{_i_token_delimiter}{_i_token_location})*" ")" diff --git a/tests/federation/test_utils.py b/tests/federation/test_utils.py index d6ef51ef5..faba0b10d 100644 --- a/tests/federation/test_utils.py +++ b/tests/federation/test_utils.py @@ -63,6 +63,8 @@ def test_purge_directives_remove_custom_directives(): directive @another on FIELD + directive @plural repeatable on FIELD + type Query { field1: String @custom field2: String @other From bf2b33090e0fe6cc2bd6e5ea0ca1ca0d66401cff Mon Sep 17 00:00:00 2001 From: Michael Chase <3686226+reallistic@users.noreply.github.com> Date: Thu, 17 Oct 2024 11:17:50 -0400 Subject: [PATCH 2/3] mutate and re-print the ast to remove custom directives from federation sdl --- ariadne/contrib/federation/utils.py | 76 ++++++++++++++--------------- tests/federation/test_utils.py | 60 +++++++++++++++++++++++ 2 files changed, 98 insertions(+), 38 deletions(-) diff --git a/ariadne/contrib/federation/utils.py b/ariadne/contrib/federation/utils.py index 248b4bb30..5f2970a55 100644 --- a/ariadne/contrib/federation/utils.py +++ b/ariadne/contrib/federation/utils.py @@ -1,9 +1,14 @@ # pylint: disable=cell-var-from-loop -import re from inspect import isawaitable -from typing import Any, List +from typing import Any, List, cast, Tuple +from graphql import ( + DirectiveDefinitionNode, + Node, + parse, + print_ast, +) from graphql.language import DirectiveNode from graphql.type import ( GraphQLNamedType, @@ -14,36 +19,6 @@ ) -_i_token_delimiter = r"(?:^|[\s]+|$)" -_i_token_name = "[_A-Za-z][_0-9A-Za-z]*" -_i_token_arguments = r"\([^)]*\)" -_i_token_location = "[_A-Za-z][_0-9A-Za-z]*" -_i_token_description_block_string = r"(?:\"{3}(?:[^\"]{1,}|[\s])\"{3})" -_i_token_description_single_line = r"(?:\"(?:[^\"\n\r])*?\")" - -_r_directive_definition = re.compile( - "(" - f"(?:{_i_token_delimiter}(?:" - f"{_i_token_description_block_string}|{_i_token_description_single_line}" - "))??" - f"{_i_token_delimiter}directive" - f"(?:{_i_token_delimiter})?@({_i_token_name})" - f"(?:(?:{_i_token_delimiter})?{_i_token_arguments})?" - f"{_i_token_delimiter}(?:repeatable)?(?:{_i_token_delimiter})?on" - f"{_i_token_delimiter}(?:[|]{_i_token_delimiter})?{_i_token_location}" - f"(?:{_i_token_delimiter}[|]{_i_token_delimiter}{_i_token_location})*" - ")" - f"(?={_i_token_delimiter})", -) - -_r_directive = re.compile( - "(" - f"(?:{_i_token_delimiter})?@({_i_token_name})" - f"(?:(?:{_i_token_delimiter})?{_i_token_arguments})?" - ")" - f"(?={_i_token_delimiter})", -) - _allowed_directives = [ "skip", # Default directive as per specs. "include", # Default directive as per specs. @@ -66,14 +41,39 @@ ] +def _purge_directive_nodes(nodes: Tuple[Node, ...]) -> Tuple[Node, ...]: + return tuple( + node + for node in nodes + if not isinstance(node, (DirectiveNode, DirectiveDefinitionNode)) + or node.name.value in _allowed_directives + ) + + +def _purge_type_directives(definition: Node): + # Recursively check every field defined on the Node definition + # and remove any directives found. + for key in definition.keys: + value = getattr(definition, key, None) + if isinstance(value, tuple): + # Remove directive nodes from the tuple + # e.g. doc -> definitions [DirectiveDefinitionNode] + next_value = _purge_directive_nodes(cast(Tuple[Node, ...], value)) + for item in next_value: + if isinstance(item, Node): + # Look for directive nodes on sub-nodes + # e.g. doc -> definitions [ObjectTypeDefinitionNode] -> fields -> directives + _purge_type_directives(item) + setattr(definition, key, next_value) + elif isinstance(value, Node): + _purge_type_directives(value) + + def purge_schema_directives(joined_type_defs: str) -> str: """Remove custom schema directives from federation.""" - joined_type_defs = _r_directive_definition.sub("", joined_type_defs) - joined_type_defs = _r_directive.sub( - lambda m: m.group(1) if m.group(2) in _allowed_directives else "", - joined_type_defs, - ) - return joined_type_defs + ast_document = parse(joined_type_defs) + _purge_type_directives(ast_document) + return print_ast(ast_document) def resolve_entities(_: Any, info: GraphQLResolveInfo, **kwargs) -> Any: diff --git a/tests/federation/test_utils.py b/tests/federation/test_utils.py index faba0b10d..9fccdab7d 100644 --- a/tests/federation/test_utils.py +++ b/tests/federation/test_utils.py @@ -109,6 +109,10 @@ def test_purge_directives_remove_custom_directives_with_single_line_description( "Any Description" directive @custom on FIELD + type Entity { + field: String @custom + } + type Query { rootField: String @custom } @@ -116,6 +120,10 @@ def test_purge_directives_remove_custom_directives_with_single_line_description( assert sic(purge_schema_directives(type_defs)) == sic( """ + type Entity { + field: String + } + type Query { rootField: String } @@ -129,6 +137,58 @@ def test_purge_directives_without_leading_whitespace(): assert sic(purge_schema_directives(type_defs)) == "" +def test_purge_directives_remove_custom_directives_from_interfaces(): + type_defs = """ + directive @custom on INTERFACE + + interface EntityInterface @custom { + field: String + } + + type Entity implements EntityInterface { + field: String + } + + type Query { + rootField: Entity + } + """ + + assert sic(purge_schema_directives(type_defs)) == sic( + """ + interface EntityInterface { + field: String + } + + type Entity implements EntityInterface { + field: String + } + + type Query { + rootField: Entity + } + """ + ) + + +def test_purge_directives_remove_custom_directive_with_arguments(): + type_defs = """ + directive @custom(arg: String) on FIELD + + type Query { + rootField: String @custom(arg: "value") + } + """ + + assert sic(purge_schema_directives(type_defs)) == sic( + """ + type Query { + rootField: String + } + """ + ) + + def test_get_entity_types_with_key_directive(): type_defs = """ type Query { From 0dec73ee8b9edaf31fc50b206e710f48eac4b52b Mon Sep 17 00:00:00 2001 From: Michael <3686226+reallistic@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:44:00 -0500 Subject: [PATCH 3/3] fix ordering of imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rafał Pitoń --- ariadne/contrib/federation/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ariadne/contrib/federation/utils.py b/ariadne/contrib/federation/utils.py index 5f2970a55..a93acc45e 100644 --- a/ariadne/contrib/federation/utils.py +++ b/ariadne/contrib/federation/utils.py @@ -1,7 +1,7 @@ # pylint: disable=cell-var-from-loop from inspect import isawaitable -from typing import Any, List, cast, Tuple +from typing import Any, List, Tuple, cast from graphql import ( DirectiveDefinitionNode,