Skip to content

Commit 5de94cf

Browse files
committed
normalized operation to AST compiler
1 parent 08b498f commit 5de94cf

File tree

3 files changed

+360
-0
lines changed

3 files changed

+360
-0
lines changed

src/main/java/graphql/normalized/nf/NormalizedDocument.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package graphql.normalized.nf;
22

3+
import graphql.Assert;
34
import org.jetbrains.annotations.Nullable;
45

56
import java.util.List;
@@ -17,6 +18,11 @@ public List<NormalizedOperationWithAssumedSkipIncludeVariables> getNormalizedOpe
1718
return normalizedOperations;
1819
}
1920

21+
public NormalizedOperation getSingleNormalizedOperation() {
22+
Assert.assertTrue(normalizedOperations.size() == 1, "Expecting a single normalized operation");
23+
return normalizedOperations.get(0).getNormalizedOperation();
24+
}
25+
2026
public static class NormalizedOperationWithAssumedSkipIncludeVariables {
2127

2228
Map<String, Boolean> assumedSkipIncludeVariables;
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package graphql.normalized.nf;
2+
3+
import com.google.common.collect.ImmutableList;
4+
import graphql.Assert;
5+
import graphql.PublicApi;
6+
import graphql.introspection.Introspection;
7+
import graphql.language.Argument;
8+
import graphql.language.Document;
9+
import graphql.language.Field;
10+
import graphql.language.InlineFragment;
11+
import graphql.language.OperationDefinition;
12+
import graphql.language.Selection;
13+
import graphql.language.SelectionSet;
14+
import graphql.language.TypeName;
15+
import graphql.schema.GraphQLCompositeType;
16+
import graphql.schema.GraphQLFieldDefinition;
17+
import graphql.schema.GraphQLObjectType;
18+
import graphql.schema.GraphQLSchema;
19+
import graphql.schema.GraphQLUnmodifiedType;
20+
import org.jetbrains.annotations.NotNull;
21+
import org.jetbrains.annotations.Nullable;
22+
23+
import java.util.ArrayList;
24+
import java.util.LinkedHashMap;
25+
import java.util.List;
26+
import java.util.Map;
27+
28+
import static graphql.collect.ImmutableKit.emptyList;
29+
import static graphql.language.Field.newField;
30+
import static graphql.language.InlineFragment.newInlineFragment;
31+
import static graphql.language.SelectionSet.newSelectionSet;
32+
import static graphql.language.TypeName.newTypeName;
33+
import static graphql.schema.GraphQLTypeUtil.unwrapAll;
34+
35+
/**
36+
* This class can take a list of {@link NormalizedField}s and compiling out a
37+
* normalised operation {@link Document} that would represent how those fields
38+
* may be executed.
39+
* <p>
40+
* This is essentially the reverse of {@link NormalizedDocumentFactory} which takes
41+
* operation text and makes {@link NormalizedField}s from it, this takes {@link NormalizedField}s
42+
* and makes operation text from it.
43+
* <p>
44+
* You could for example send that operation text onto to some other graphql server if it
45+
* has the same schema as the one provided.
46+
*/
47+
@PublicApi
48+
public class NormalizedOperationToAstCompiler {
49+
50+
/**
51+
* The result is a {@link Document} and a map of variables
52+
* that would go with that document.
53+
*/
54+
public static class CompilerResult {
55+
private final Document document;
56+
private final Map<String, Object> variables;
57+
58+
public CompilerResult(Document document, Map<String, Object> variables) {
59+
this.document = document;
60+
this.variables = variables;
61+
}
62+
63+
public Document getDocument() {
64+
return document;
65+
}
66+
67+
public Map<String, Object> getVariables() {
68+
return variables;
69+
}
70+
}
71+
72+
public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema,
73+
NormalizedOperation normalizedOperation) {
74+
GraphQLObjectType operationType = getOperationType(schema, normalizedOperation.getOperation());
75+
76+
List<Selection<?>> selections = subSelectionsForNormalizedField(schema, operationType.getName(), normalizedOperation.getTopLevelFields());
77+
SelectionSet selectionSet = new SelectionSet(selections);
78+
79+
OperationDefinition.Builder definitionBuilder = OperationDefinition.newOperationDefinition()
80+
.name(normalizedOperation.getOperationName())
81+
.operation(normalizedOperation.getOperation())
82+
.selectionSet(selectionSet);
83+
84+
// definitionBuilder.variableDefinitions(variableAccumulator.getVariableDefinitions());
85+
86+
return new CompilerResult(
87+
Document.newDocument()
88+
.definition(definitionBuilder.build())
89+
.build(),
90+
null
91+
);
92+
}
93+
94+
private static List<Selection<?>> subSelectionsForNormalizedField(GraphQLSchema schema,
95+
@NotNull String parentOutputType,
96+
List<NormalizedField> normalizedFields
97+
) {
98+
ImmutableList.Builder<Selection<?>> selections = ImmutableList.builder();
99+
100+
// All conditional fields go here instead of directly to selections, so they can be grouped together
101+
// in the same inline fragment in the output
102+
Map<String, List<Field>> fieldsByTypeCondition = new LinkedHashMap<>();
103+
104+
for (NormalizedField nf : normalizedFields) {
105+
if (nf.isConditional(schema)) {
106+
selectionForNormalizedField(schema, nf)
107+
.forEach((objectTypeName, field) ->
108+
fieldsByTypeCondition
109+
.computeIfAbsent(objectTypeName, ignored -> new ArrayList<>())
110+
.add(field));
111+
} else {
112+
selections.add(selectionForNormalizedField(schema, parentOutputType, nf));
113+
}
114+
}
115+
116+
fieldsByTypeCondition.forEach((objectTypeName, fields) -> {
117+
TypeName typeName = newTypeName(objectTypeName).build();
118+
InlineFragment inlineFragment = newInlineFragment()
119+
.typeCondition(typeName)
120+
.selectionSet(selectionSet(fields))
121+
.build();
122+
selections.add(inlineFragment);
123+
});
124+
125+
return selections.build();
126+
}
127+
128+
/**
129+
* @return Map of object type names to list of fields
130+
*/
131+
private static Map<String, Field> selectionForNormalizedField(GraphQLSchema schema,
132+
NormalizedField normalizedField
133+
) {
134+
Map<String, Field> groupedFields = new LinkedHashMap<>();
135+
136+
for (String objectTypeName : normalizedField.getObjectTypeNames()) {
137+
groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, normalizedField));
138+
}
139+
140+
return groupedFields;
141+
}
142+
143+
/**
144+
* @return Map of object type names to list of fields
145+
*/
146+
private static Field selectionForNormalizedField(GraphQLSchema schema,
147+
String objectTypeName,
148+
NormalizedField normalizedField) {
149+
150+
final List<Selection<?>> subSelections;
151+
if (normalizedField.getChildren().isEmpty()) {
152+
subSelections = emptyList();
153+
} else {
154+
GraphQLFieldDefinition fieldDef = getFieldDefinition(schema, objectTypeName, normalizedField);
155+
GraphQLUnmodifiedType fieldOutputType = unwrapAll(fieldDef.getType());
156+
157+
subSelections = subSelectionsForNormalizedField(
158+
schema,
159+
fieldOutputType.getName(),
160+
normalizedField.getChildren()
161+
);
162+
}
163+
164+
SelectionSet selectionSet = selectionSetOrNullIfEmpty(subSelections);
165+
// List<Argument> arguments = createArguments(executableNormalizedField, variableAccumulator);
166+
List<Argument> arguments = normalizedField.getAstArguments();
167+
168+
169+
Field.Builder builder = newField()
170+
.name(normalizedField.getFieldName())
171+
.alias(normalizedField.getAlias())
172+
.selectionSet(selectionSet)
173+
.arguments(arguments);
174+
return builder.build();
175+
}
176+
177+
@Nullable
178+
private static SelectionSet selectionSetOrNullIfEmpty(List<Selection<?>> selections) {
179+
return selections.isEmpty() ? null : newSelectionSet().selections(selections).build();
180+
}
181+
182+
private static SelectionSet selectionSet(List<Field> fields) {
183+
return newSelectionSet().selections(fields).build();
184+
}
185+
186+
187+
@NotNull
188+
private static GraphQLFieldDefinition getFieldDefinition(GraphQLSchema schema,
189+
String parentType,
190+
NormalizedField nf) {
191+
return Introspection.getFieldDef(schema, (GraphQLCompositeType) schema.getType(parentType), nf.getName());
192+
}
193+
194+
195+
@Nullable
196+
private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema,
197+
@NotNull OperationDefinition.Operation operationKind) {
198+
switch (operationKind) {
199+
case QUERY:
200+
return schema.getQueryType();
201+
case MUTATION:
202+
return schema.getMutationType();
203+
case SUBSCRIPTION:
204+
return schema.getSubscriptionType();
205+
}
206+
207+
return Assert.assertShouldNeverHappen("Unknown operation kind " + operationKind);
208+
}
209+
210+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package graphql.normalized.nf
2+
3+
import graphql.GraphQL
4+
import graphql.TestUtil
5+
import graphql.language.AstPrinter
6+
import graphql.language.AstSorter
7+
import graphql.parser.Parser
8+
import graphql.schema.GraphQLSchema
9+
import spock.lang.Specification
10+
11+
import static graphql.ExecutionInput.newExecutionInput
12+
13+
class NormalizedOperationToAstCompilerTest extends Specification {
14+
15+
16+
def "test pet interfaces"() {
17+
String sdl = """
18+
type Query {
19+
animal: Animal
20+
}
21+
interface Animal {
22+
name: String
23+
friends: [Friend]
24+
}
25+
26+
union Pet = Dog | Cat
27+
28+
type Friend {
29+
name: String
30+
isBirdOwner: Boolean
31+
isCatOwner: Boolean
32+
pets: [Pet]
33+
}
34+
35+
type Bird implements Animal {
36+
name: String
37+
friends: [Friend]
38+
}
39+
40+
type Cat implements Animal {
41+
name: String
42+
friends: [Friend]
43+
breed: String
44+
mood: String
45+
}
46+
47+
type Dog implements Animal {
48+
name: String
49+
breed: String
50+
friends: [Friend]
51+
}
52+
"""
53+
54+
String query = """
55+
{
56+
animal {
57+
name
58+
otherName: name
59+
... on Animal {
60+
name
61+
}
62+
... on Cat {
63+
name
64+
mood
65+
friends {
66+
... on Friend {
67+
isCatOwner
68+
pets {
69+
... on Dog {
70+
name
71+
}
72+
}
73+
}
74+
}
75+
}
76+
... on Bird {
77+
friends {
78+
isBirdOwner
79+
}
80+
friends {
81+
name
82+
pets {
83+
... on Cat {
84+
breed
85+
}
86+
}
87+
}
88+
}
89+
... on Dog {
90+
name
91+
breed
92+
}
93+
}
94+
}
95+
"""
96+
GraphQLSchema schema = TestUtil.schema(sdl)
97+
assertValidQuery(schema, query)
98+
def normalizedDocument = NormalizedDocumentFactory.createNormalizedDocument(schema, Parser.parse(query))
99+
def normalizedOperation = normalizedDocument.getSingleNormalizedOperation()
100+
when:
101+
def result = NormalizedOperationToAstCompiler.compileToDocument(schema, normalizedOperation)
102+
def printed = AstPrinter.printAst(new AstSorter().sort(result.document))
103+
then:
104+
printed == '''{
105+
animal {
106+
name
107+
otherName: name
108+
... on Bird {
109+
friends {
110+
isBirdOwner
111+
name
112+
pets {
113+
... on Cat {
114+
breed
115+
}
116+
}
117+
}
118+
}
119+
... on Cat {
120+
friends {
121+
isCatOwner
122+
pets {
123+
... on Dog {
124+
name
125+
}
126+
}
127+
}
128+
mood
129+
}
130+
... on Dog {
131+
breed
132+
}
133+
}
134+
}
135+
'''
136+
}
137+
138+
private void assertValidQuery(GraphQLSchema graphQLSchema, String query, Map variables = [:]) {
139+
GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build()
140+
assert graphQL.execute(newExecutionInput().query(query).variables(variables)).errors.isEmpty()
141+
}
142+
143+
144+
}

0 commit comments

Comments
 (0)