Skip to content

Commit ade73fa

Browse files
authored
Union query requests in client code with conditional fragments #164 (#222)
1 parent 0b49dcc commit ade73fa

15 files changed

+389
-49
lines changed

src/main/java/com/kobylynskyi/graphql/codegen/GraphQLCodegen.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ private List<File> processDefinitions(ExtendedDocument document) {
213213
generatedFiles.add(generateEnum(context, extendedEnumTypeDefinition));
214214
}
215215
for (ExtendedUnionTypeDefinition extendedUnionTypeDefinition : document.getUnionDefinitions()) {
216-
generatedFiles.add(generateUnion(context, extendedUnionTypeDefinition));
216+
generatedFiles.addAll(generateUnion(context, extendedUnionTypeDefinition));
217217
}
218218
for (ExtendedInterfaceTypeDefinition extendedInterfaceTypeDefinition : document.getInterfaceDefinitions()) {
219219
generatedFiles.add(generateInterface(context, extendedInterfaceTypeDefinition));
@@ -227,9 +227,16 @@ private List<File> processDefinitions(ExtendedDocument document) {
227227
return generatedFiles;
228228
}
229229

230-
private File generateUnion(MappingContext mappingContext, ExtendedUnionTypeDefinition definition) {
230+
private List<File> generateUnion(MappingContext mappingContext, ExtendedUnionTypeDefinition definition) {
231+
List<File> generatedFiles = new ArrayList<>();
231232
Map<String, Object> dataModel = UnionDefinitionToDataModelMapper.map(mappingContext, definition);
232-
return GraphQLCodegenFileCreator.generateFile(FreeMarkerTemplatesRegistry.unionTemplate, dataModel, outputDir);
233+
generatedFiles.add(GraphQLCodegenFileCreator.generateFile(FreeMarkerTemplatesRegistry.unionTemplate, dataModel, outputDir));
234+
235+
if (Boolean.TRUE.equals(mappingConfig.getGenerateClient())) {
236+
Map<String, Object> responseProjDataModel = RequestResponseDefinitionToDataModelMapper.mapResponseProjection(mappingContext, definition);
237+
generatedFiles.add(GraphQLCodegenFileCreator.generateFile(FreeMarkerTemplatesRegistry.responseProjectionTemplate, responseProjDataModel, outputDir));
238+
}
239+
return generatedFiles;
233240
}
234241

235242
private File generateInterface(MappingContext mappingContext, ExtendedInterfaceTypeDefinition definition) {

src/main/java/com/kobylynskyi/graphql/codegen/mapper/FieldDefinitionToParameterMapper.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,9 @@ private static ProjectionParameterDefinition mapProjectionField(MappingContext m
8989
ExtendedDefinition<?, ?> parentTypeDef) {
9090
ProjectionParameterDefinition parameter = new ProjectionParameterDefinition();
9191
parameter.setName(MapperUtils.capitalizeIfRestricted(fieldDef.getName()));
92+
parameter.setMethodName(parameter.getName());
9293
String nestedType = getNestedTypeName(fieldDef.getType());
93-
if (mappingContext.getTypeNames().contains(nestedType)) {
94+
if (mappingContext.getTypeAndUnionNames().contains(nestedType)) {
9495
parameter.setType(nestedType + mappingContext.getResponseProjectionSuffix());
9596
}
9697
if (!Utils.isEmpty(fieldDef.getInputValueDefinitions())) {

src/main/java/com/kobylynskyi/graphql/codegen/mapper/RequestResponseDefinitionToDataModelMapper.java

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import com.kobylynskyi.graphql.codegen.model.MappingContext;
44
import com.kobylynskyi.graphql.codegen.model.ProjectionParameterDefinition;
5+
import com.kobylynskyi.graphql.codegen.model.definitions.ExtendedDefinition;
56
import com.kobylynskyi.graphql.codegen.model.definitions.ExtendedFieldDefinition;
67
import com.kobylynskyi.graphql.codegen.model.definitions.ExtendedObjectTypeDefinition;
8+
import com.kobylynskyi.graphql.codegen.model.definitions.ExtendedUnionTypeDefinition;
79
import com.kobylynskyi.graphql.codegen.utils.Utils;
810

911
import java.util.Collection;
@@ -41,17 +43,17 @@ private RequestResponseDefinitionToDataModelMapper() {
4143
* Map type definition to a Freemarker data model of Response Projection.
4244
*
4345
* @param mappingContext Global mapping context
44-
* @param typeDefinition GraphQL type definition
46+
* @param definition GraphQL definition (type or union)
4547
* @return Freemarker data model of the GraphQL Response Projection
4648
*/
4749
public static Map<String, Object> mapResponseProjection(MappingContext mappingContext,
48-
ExtendedObjectTypeDefinition typeDefinition) {
50+
ExtendedDefinition<?, ?> definition) {
4951
Map<String, Object> dataModel = new HashMap<>();
5052
// ResponseProjection classes are sharing the package with the model classes, so no imports are needed
5153
dataModel.put(PACKAGE, MapperUtils.getModelPackageName(mappingContext));
52-
dataModel.put(CLASS_NAME, Utils.capitalize(typeDefinition.getName()) + mappingContext.getResponseProjectionSuffix());
53-
dataModel.put(JAVA_DOC, Collections.singletonList("Response projection for " + typeDefinition.getName()));
54-
dataModel.put(FIELDS, getProjectionFields(mappingContext, typeDefinition));
54+
dataModel.put(CLASS_NAME, Utils.capitalize(definition.getName()) + mappingContext.getResponseProjectionSuffix());
55+
dataModel.put(JAVA_DOC, Collections.singletonList("Response projection for " + definition.getName()));
56+
dataModel.put(FIELDS, getProjectionFields(mappingContext, definition));
5557
dataModel.put(BUILDER, mappingContext.getGenerateBuilder());
5658
dataModel.put(EQUALS_AND_HASH_CODE, mappingContext.getGenerateEqualsAndHashCode());
5759
dataModel.put(GENERATED_INFO, mappingContext.getGeneratedInformation());
@@ -167,23 +169,39 @@ private static String getClassName(ExtendedFieldDefinition operationDef,
167169
* Get merged attributes from the type and attributes from the interface.
168170
*
169171
* @param mappingContext Global mapping context
170-
* @param typeDefinition GraphQL type definition
172+
* @param definition GraphQL definition (type or union)
171173
* @return Freemarker data model of the GraphQL type
172174
*/
173175
private static Collection<ProjectionParameterDefinition> getProjectionFields(MappingContext mappingContext,
174-
ExtendedObjectTypeDefinition typeDefinition) {
176+
ExtendedDefinition<?, ?> definition) {
175177
// using the map to exclude duplicate fields from the type and interfaces
176178
Map<String, ProjectionParameterDefinition> allParameters = new LinkedHashMap<>();
177179

178-
// includes parameters from the base definition and extensions
179-
FieldDefinitionToParameterMapper.mapProjectionFields(mappingContext, typeDefinition.getFieldDefinitions(), typeDefinition)
180-
.forEach(p -> allParameters.put(p.getName(), p));
181-
// includes parameters from the interface
182-
MapperUtils.getInterfacesOfType(typeDefinition, mappingContext.getDocument()).stream()
183-
.map(i -> FieldDefinitionToParameterMapper.mapProjectionFields(mappingContext, i.getFieldDefinitions(), i))
184-
.flatMap(Collection::stream)
185-
.filter(paramDef -> !allParameters.containsKey(paramDef.getName()))
186-
.forEach(paramDef -> allParameters.put(paramDef.getName(), paramDef));
180+
if (definition instanceof ExtendedObjectTypeDefinition) {
181+
ExtendedObjectTypeDefinition typeDefinition = (ExtendedObjectTypeDefinition) definition;
182+
// includes parameters from the base definition and extensions
183+
FieldDefinitionToParameterMapper.mapProjectionFields(mappingContext, typeDefinition.getFieldDefinitions(), typeDefinition)
184+
.forEach(p -> allParameters.put(p.getName(), p));
185+
// includes parameters from the interface
186+
MapperUtils.getInterfacesOfType(typeDefinition, mappingContext.getDocument()).stream()
187+
.map(i -> FieldDefinitionToParameterMapper.mapProjectionFields(mappingContext, i.getFieldDefinitions(), i))
188+
.flatMap(Collection::stream)
189+
.filter(paramDef -> !allParameters.containsKey(paramDef.getName()))
190+
.forEach(paramDef -> allParameters.put(paramDef.getName(), paramDef));
191+
} else if (definition instanceof ExtendedUnionTypeDefinition) {
192+
ExtendedUnionTypeDefinition unionDefinition = (ExtendedUnionTypeDefinition) definition;
193+
for (String memberTypeName : unionDefinition.getMemberTypeNames()) {
194+
ProjectionParameterDefinition parameter = new ProjectionParameterDefinition();
195+
parameter.setName("...on " + memberTypeName);
196+
parameter.setMethodName("on" + memberTypeName);
197+
parameter.setType(memberTypeName + mappingContext.getResponseProjectionSuffix());
198+
allParameters.put(parameter.getName(), parameter);
199+
}
200+
ProjectionParameterDefinition typeNameProjParamDef = new ProjectionParameterDefinition();
201+
typeNameProjParamDef.setName("__typename");
202+
typeNameProjParamDef.setMethodName("typename");
203+
allParameters.put(typeNameProjParamDef.getName(), typeNameProjParamDef);
204+
}
187205
return allParameters.values();
188206
}
189207

src/main/java/com/kobylynskyi/graphql/codegen/model/MappingContext.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public class MappingContext implements GraphQLCodegenConfiguration {
99

1010
private final MappingConfig config;
1111
private final ExtendedDocument document;
12-
private final Set<String> typeNames;
12+
private final Set<String> typeAndUnionNames;
1313
private final Set<String> interfaceNames;
1414
private final GeneratedInformation generatedInformation;
1515

@@ -18,7 +18,7 @@ public MappingContext(MappingConfig mappingConfig,
1818
GeneratedInformation generatedInformation) {
1919
this.config = mappingConfig;
2020
this.document = document;
21-
this.typeNames = document.getTypeNames();
21+
this.typeAndUnionNames = document.getTypeAndUnionNames();
2222
this.interfaceNames = document.getInterfaceNames();
2323
this.generatedInformation = generatedInformation;
2424
}
@@ -207,8 +207,8 @@ public ExtendedDocument getDocument() {
207207
return document;
208208
}
209209

210-
public Set<String> getTypeNames() {
211-
return typeNames;
210+
public Set<String> getTypeAndUnionNames() {
211+
return typeAndUnionNames;
212212
}
213213

214214
public Set<String> getInterfaceNames() {

src/main/java/com/kobylynskyi/graphql/codegen/model/ProjectionParameterDefinition.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public class ProjectionParameterDefinition {
99

1010
private String type;
1111
private String name;
12+
private String methodName;
1213
private boolean deprecated;
1314
private String parametrizedInputClassName;
1415

@@ -28,6 +29,14 @@ public void setName(String name) {
2829
this.name = name;
2930
}
3031

32+
public String getMethodName() {
33+
return methodName;
34+
}
35+
36+
public void setMethodName(String methodName) {
37+
this.methodName = methodName;
38+
}
39+
3140
public boolean isDeprecated() {
3241
return deprecated;
3342
}

src/main/java/com/kobylynskyi/graphql/codegen/model/definitions/ExtendedDocument.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.kobylynskyi.graphql.codegen.model.definitions;
22

33
import java.util.Collection;
4+
import java.util.LinkedHashSet;
45
import java.util.Set;
56
import java.util.stream.Collectors;
67

@@ -33,10 +34,15 @@ public ExtendedDocument(Collection<ExtendedObjectTypeDefinition> operationDefini
3334
this.unionDefinitions = unionDefinitions;
3435
}
3536

36-
public Set<String> getTypeNames() {
37-
return typeDefinitions.stream()
37+
public Set<String> getTypeAndUnionNames() {
38+
Set<String> typeAndUnions = new LinkedHashSet<>();
39+
typeDefinitions.stream()
3840
.map(ExtendedDefinition::getName)
39-
.collect(Collectors.toSet());
41+
.forEach(typeAndUnions::add);
42+
unionDefinitions.stream()
43+
.map(ExtendedDefinition::getName)
44+
.forEach(typeAndUnions::add);
45+
return typeAndUnions;
4046
}
4147

4248
public Set<String> getInterfaceNames() {

src/main/java/com/kobylynskyi/graphql/codegen/model/definitions/ExtendedUnionTypeDefinition.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public boolean isDefinitionPartOfUnion(ExtendedDefinition<?, ?> definition) {
2525
return memberTypeNames.contains(definition.getName());
2626
}
2727

28-
private Set<String> getMemberTypeNames() {
28+
public Set<String> getMemberTypeNames() {
2929
Set<String> allTypeNames = new HashSet<>();
3030
if (definition != null) {
3131
definition.getMemberTypes().stream()

src/main/resources/templates/javaClassGraphqlResponseProjection.ftl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,21 @@ public class ${className} extends GraphQLResponseProjection {
3838
<#if field.deprecated>
3939
@Deprecated
4040
</#if>
41-
public ${className} ${field.name}(<#if field.type?has_content>${field.type} subProjection</#if>) {
42-
return ${field.name}(<#if field.parametrizedInputClassName?has_content>(String)</#if>null<#if field.type?has_content>, subProjection</#if>);
41+
public ${className} ${field.methodName}(<#if field.type?has_content>${field.type} subProjection</#if>) {
42+
return ${field.methodName}(<#if field.parametrizedInputClassName?has_content>(String)</#if>null<#if field.type?has_content>, subProjection</#if>);
4343
}
4444

45-
public ${className} ${field.name}(String alias<#if field.type?has_content>, ${field.type} subProjection</#if>) {
45+
public ${className} ${field.methodName}(String alias<#if field.type?has_content>, ${field.type} subProjection</#if>) {
4646
fields.add(new GraphQLResponseField("${field.name}").alias(alias)<#if field.type?has_content>.projection(subProjection)</#if>);
4747
return this;
4848
}
4949

5050
<#if field.parametrizedInputClassName?has_content>
51-
public ${className} ${field.name}(${field.parametrizedInputClassName} input<#if field.type?has_content>, ${field.type} subProjection</#if>) {
51+
public ${className} ${field.methodName}(${field.parametrizedInputClassName} input<#if field.type?has_content>, ${field.type} subProjection</#if>) {
5252
return ${field.name}(null, input<#if field.type?has_content>, subProjection</#if>);
5353
}
5454

55-
public ${className} ${field.name}(String alias, ${field.parametrizedInputClassName} input<#if field.type?has_content>, ${field.type} subProjection</#if>) {
55+
public ${className} ${field.methodName}(String alias, ${field.parametrizedInputClassName} input<#if field.type?has_content>, ${field.type} subProjection</#if>) {
5656
fields.add(new GraphQLResponseField("${field.name}").alias(alias).parameters(input)<#if field.type?has_content>.projection(subProjection)</#if>);
5757
return this;
5858
}

src/test/java/com/kobylynskyi/graphql/codegen/GraphQLCodegenGitHubTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,20 @@ void generate_NoValidationAnnotation() throws Exception {
9191
commitFile);
9292
}
9393

94+
@Test
95+
void generate_Client_ConditionalFragments() throws Exception {
96+
mappingConfig.setGenerateClient(true);
97+
mappingConfig.setGenerateApis(false);
98+
99+
generator.generate();
100+
101+
File[] files = Objects.requireNonNull(outputJavaClassesDir.listFiles());
102+
assertSameTrimmedContent(new File("src/test/resources/expected-classes/response/SearchResultItemConnectionResponseProjection.java.txt"),
103+
getFileByName(files, "SearchResultItemConnectionResponseProjection.java"));
104+
assertSameTrimmedContent(new File("src/test/resources/expected-classes/response/SearchResultItemResponseProjection.java.txt"),
105+
getFileByName(files, "SearchResultItemResponseProjection.java"));
106+
}
107+
94108
private static String getFileContent(File[] files, String fileName) throws IOException {
95109
return Utils.getFileContent(getFileByName(files, fileName).getPath());
96110
}

0 commit comments

Comments
 (0)