Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schema for macro definitions #319

Merged
merged 14 commits into from
May 29, 2019
28 changes: 23 additions & 5 deletions graphql_compiler/macros/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright 2019-present Kensho Technologies, LLC.
from collections import namedtuple
from copy import copy
import six

from graphql.language.ast import (FieldDefinition, Name, NamedType,
Expand All @@ -14,7 +15,9 @@
from ..ast_manipulation import safe_parse_graphql
from .macro_edge import make_macro_edge_descriptor
from .macro_edge.helpers import get_type_at_macro_edge_target
from .macro_edge.directives import MacroEdgeDirective
from .macro_edge.directives import (MacroEdgeDirective, DIRECTIVES_ALLOWED_IN_MACRO_EDGE_DEFINITION,
DIRECTIVES_REQUIRED_IN_MACRO_EDGE_DEFINITION)
from ..schema import _check_for_nondefault_directives
from .macro_expansion import expand_macros_in_query_ast
from ..exceptions import GraphQLInvalidMacroError, GraphQLValidationError

Expand Down Expand Up @@ -167,16 +170,31 @@ def get_schema_for_macro_definition(schema):
"""Returns a schema with macro directives.

This returned schema can be used to validate macro definitions, and support GraphQL
macro editors, enabling them to autocomplete one the @macro_edge_definition and
@macro_edge_target directives.
macro editors, enabling them to autocomplete on the @macro_edge_definition and
@macro_edge_target directives. Some directives that are disallowed in macro edge definitions,
like @output and @output_source, will be removed from the directives list.

Args:
schema: GraphQLSchema over which we want to write macros

Returns:
GraphQLSchema usable for writing macros
GraphQLSchema usable for writing macros. Modifying this schema is undefined behavior.

Raises:
AssertionError, if the schema contains directives that are non-default.
"""
raise NotImplementedError() # TODO(bojanserafimov): Implement
macro_definition_schema = copy(schema)
pmantica1 marked this conversation as resolved.
Show resolved Hide resolved
macro_definition_schema_directives = schema.get_directives()
_check_for_nondefault_directives(macro_definition_schema_directives)
macro_definition_schema_directives += DIRECTIVES_REQUIRED_IN_MACRO_EDGE_DEFINITION
# Remove disallowed directives from directives list
macro_definition_schema_directives = list(set(macro_definition_schema_directives) &
set(DIRECTIVES_ALLOWED_IN_MACRO_EDGE_DEFINITION))

# pylint: disable=protected-access
macro_definition_schema._directives = macro_definition_schema_directives
# pylint: enable=protected-access
return macro_definition_schema


def perform_macro_expansion(macro_registry, graphql_with_macro, graphql_args):
Expand Down
4 changes: 1 addition & 3 deletions graphql_compiler/macros/macro_edge/directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@
DIRECTIVES_ALLOWED_IN_MACRO_EDGE_DEFINITION = frozenset({
FoldDirective,
FilterDirective,
MacroEdgeDefinitionDirective,
MacroEdgeTargetDirective,
OptionalDirective,
TagDirective,
RecurseDirective,
})
}.union(DIRECTIVES_REQUIRED_IN_MACRO_EDGE_DEFINITION))
14 changes: 14 additions & 0 deletions graphql_compiler/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,17 @@ def insert_meta_fields_into_existing_schema(graphql_schema):
.format(meta_field_name))

type_obj.fields[meta_field_name] = meta_field


def _check_for_nondefault_directives(directives):
"""Check if any user-created directives are present"""
deprecated_directive_names = {'deprecated', 'skip', 'include'}
vmaksimovski marked this conversation as resolved.
Show resolved Hide resolved
default_directive_names = {directive.name for directive in DIRECTIVES}
vmaksimovski marked this conversation as resolved.
Show resolved Hide resolved
expected_directive_names = deprecated_directive_names | default_directive_names
nondefault_directive_names_found = {
directive.name
for directive in directives
} - expected_directive_names
if nondefault_directive_names_found != set():
raise AssertionError(u'Unexpected non-default directives found: {}'.format(
nondefault_directive_names_found))
119 changes: 118 additions & 1 deletion graphql_compiler/tests/test_macro_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@

from graphql.type import GraphQLList
from graphql.utils.schema_printer import print_schema
from graphql.validation import validate

from ..macros import get_schema_with_macros
from ..ast_manipulation import safe_parse_graphql
from ..macros import get_schema_for_macro_definition, get_schema_with_macros
from ..macros.macro_edge.directives import (
DIRECTIVES_ALLOWED_IN_MACRO_EDGE_DEFINITION, DIRECTIVES_REQUIRED_IN_MACRO_EDGE_DEFINITION
)
from .test_helpers import get_empty_test_macro_registry, get_test_macro_registry


Expand Down Expand Up @@ -32,3 +37,115 @@ def test_get_schema_with_macros_basic(self):
'Animal').fields['out_Animal_RelatedFood'].type
self.assertTrue(isinstance(related_food_target_type, GraphQLList))
self.assertEqual('Food', related_food_target_type.of_type.name)

def test_get_schema_for_macro_definition_addition(self):
original_schema = self.macro_registry.schema_without_macros
macro_definition_schema = get_schema_for_macro_definition(original_schema)
for directive in DIRECTIVES_REQUIRED_IN_MACRO_EDGE_DEFINITION:
self.assertTrue(directive in macro_definition_schema.get_directives())

def test_get_schema_for_macro_definition_retain(self):
original_schema = self.macro_registry.schema_without_macros
macro_definition_schema = get_schema_for_macro_definition(original_schema)
for directive in original_schema.get_directives():
if directive in DIRECTIVES_ALLOWED_IN_MACRO_EDGE_DEFINITION:
self.assertTrue(directive in macro_definition_schema.get_directives())

def test_get_schema_for_macro_definition_removal(self):
schema_with_macros = get_schema_with_macros(self.macro_registry)
macro_definition_schema = get_schema_for_macro_definition(schema_with_macros)
for directive in macro_definition_schema.get_directives():
self.assertTrue(directive.name != '@output')
self.assertTrue(directive.name != '@output_source')
vmaksimovski marked this conversation as resolved.
Show resolved Hide resolved
vmaksimovski marked this conversation as resolved.
Show resolved Hide resolved

def test_get_schema_for_macro_definition_validation(self):
macro_definition_schema = get_schema_for_macro_definition(
self.macro_registry.schema_without_macros)
valid_macro_definitions = [
'''{
Entity @macro_edge_definition(name: "out_Entity_AlmostRelated") {
out_Entity_Related {
out_Entity_Related @macro_edge_target{
uuid
}
}
}
}''',
'''{
Animal @macro_edge_definition(name: "out_Animal_GrandparentOf") {
out_Animal_ParentOf {
out_Animal_ParentOf @macro_edge_target {
uuid
}
}
}
}''',
'''{
Animal @macro_edge_definition(name: "out_Animal_GrandchildrenCalledNate") {
out_Animal_ParentOf {
out_Animal_ParentOf @filter(op_name: "name_or_alias", value: ["$wanted"])
@macro_edge_target {
uuid
}
}
}
}''',
'''{
Animal @macro_edge_definition(name: "out_Animal_RichSiblings") {
in_Animal_ParentOf {
net_worth @tag(tag_name: "parent_net_worth")
out_Animal_ParentOf @macro_edge_target {
net_worth @filter(op_name: ">", value: ["%parent_net_worth"])
}
}
}
}''',
'''{
Location @macro_edge_definition(name: "out_Location_Orphans") {
in_Animal_LivesIn @macro_edge_target {
in_Animal_ParentOf @filter(op_name: "has_edge_degree",
value: ["$num_parents"])
@optional {
uuid
}
}
}
}''',
'''{
Animal @macro_edge_definition(name: "out_Animal_RichYoungerSiblings") {
net_worth @tag(tag_name: "net_worth")
out_Animal_BornAt {
event_date @tag(tag_name: "birthday")
}
in_Animal_ParentOf {
out_Animal_ParentOf @macro_edge_target {
net_worth @filter(op_name: ">", value: ["%net_worth"])
out_Animal_BornAt {
event_date @filter(op_name: "<", value: ["%birthday"])
}
}
}
}
}''',
'''{
Animal @macro_edge_definition(name: "out_Animal_RelatedFood") {
in_Entity_Related {
... on Food @macro_edge_target {
uuid
}
}
}
}''',
'''{
Animal @macro_edge_definition(name: "out_Animal_RelatedEntity") {
in_Entity_Related {
... on Entity @macro_edge_target {
uuid
}
}
}
}''']
vmaksimovski marked this conversation as resolved.
Show resolved Hide resolved

for macro in valid_macro_definitions:
macro_edge_definition_ast = safe_parse_graphql(macro)
validate(macro_definition_schema, macro_edge_definition_ast)