Skip to content

Commit de08b9e

Browse files
authored
Support fields resolvers #25 (#31)
* Support fields resolvers #25 * Code coverage for MappingConfig #25
1 parent 510a61c commit de08b9e

20 files changed

+750
-60
lines changed

README.md

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,26 @@ Please refer to:
1313

1414
## Supported Options
1515

16-
| Key | Data Type | Default value | Description |
17-
| ------------------------- | ------------------ | ----------------------------------------- | ----------- |
18-
| graphqlSchemaPaths | List(String) | None | GraphQL schema locations. You can supply multiple paths to GraphQL schemas. |
19-
| packageName | String | Empty | Java package for generated classes. |
20-
| outputDir | String | None | The output target directory into which code will be generated. |
21-
| apiPackage | String | Empty | Java package for generated api classes (Query, Mutation, Subscription). |
22-
| modelPackage | String | Empty | Java package for generated model classes (type, input, interface, enum, union). |
23-
| generateApis | Boolean | True | Specifies whether api classes should be generated as well as model classes. |
24-
| customTypesMapping | Map(String,String) | Empty | Can be used to supply custom mappings for scalars. <br/> Supports:<br/> * Map of (GraphqlObjectName.fieldName) to (JavaType) <br/> * Map of (GraphqlType) to (JavaType) |
25-
| customAnnotationsMapping | Map(String,String) | Empty | Can be used to supply custom annotations (serializers) for scalars. <br/> Supports:<br/> * Map of (GraphqlObjectName.fieldName) to (JavaType) <br/> * Map of (GraphqlType) to (JavaType) |
26-
| modelValidationAnnotation | String | @javax.validation.<br>constraints.NotNull | Annotation for mandatory (NonNull) fields. Can be null/empty. |
27-
| modelNamePrefix | String | Empty | Sets the prefix for GraphQL model classes (type, input, interface, enum, union). |
28-
| modelNameSuffix | String | Empty | Sets the suffix for GraphQL model classes (type, input, interface, enum, union). |
29-
| subscriptionReturnType | String | Empty | Return type for subscription methods. For example: `org.reactivestreams.Publisher`, `io.reactivex.Observable`, etc. |
30-
| generateEqualsAndHashCode | Boolean | False | Specifies whether generated model classes should have equals and hashCode methods defined. |
31-
| generateToString | Boolean | False | Specifies whether generated model classes should have toString method defined. |
32-
| generateAsyncApi | Boolean | False | If true, then wrap type into `java.util.concurrent.CompletableFuture` or `subscriptionReturnType` |
33-
| jsonConfigurationFile | String | Empty | Path to an external mapping configuration. |
16+
| Key | Data Type | Default value | Description |
17+
| ------------------------------------ | ------------------ | ----------------------------------------- | ----------- |
18+
| graphqlSchemaPaths | List(String) | None | GraphQL schema locations. You can supply multiple paths to GraphQL schemas. |
19+
| packageName | String | Empty | Java package for generated classes. |
20+
| outputDir | String | None | The output target directory into which code will be generated. |
21+
| apiPackage | String | Empty | Java package for generated api classes (Query, Mutation, Subscription). |
22+
| modelPackage | String | Empty | Java package for generated model classes (type, input, interface, enum, union). |
23+
| generateApis | Boolean | True | Specifies whether api classes should be generated as well as model classes. |
24+
| customTypesMapping | Map(String,String) | Empty | Can be used to supply custom mappings for scalars. <br/> Supports:<br/> * Map of (GraphqlObjectName.fieldName) to (JavaType) <br/> * Map of (GraphqlType) to (JavaType) |
25+
| customAnnotationsMapping | Map(String,String) | Empty | Can be used to supply custom annotations (serializers) for scalars. <br/> Supports:<br/> * Map of (GraphqlObjectName.fieldName) to (JavaType) <br/> * Map of (GraphqlType) to (JavaType) |
26+
| modelValidationAnnotation | String | @javax.validation.<br>constraints.NotNull | Annotation for mandatory (NonNull) fields. Can be null/empty. |
27+
| modelNamePrefix | String | Empty | Sets the prefix for GraphQL model classes (type, input, interface, enum, union). |
28+
| modelNameSuffix | String | Empty | Sets the suffix for GraphQL model classes (type, input, interface, enum, union). |
29+
| subscriptionReturnType | String | Empty | Return type for subscription methods. For example: `org.reactivestreams.Publisher`, `io.reactivex.Observable`, etc. |
30+
| generateEqualsAndHashCode | Boolean | False | Specifies whether generated model classes should have equals and hashCode methods defined. |
31+
| generateToString | Boolean | False | Specifies whether generated model classes should have toString method defined. |
32+
| generateAsyncApi | Boolean | False | If true, then wrap type into `java.util.concurrent.CompletableFuture` or `subscriptionReturnType` |
33+
| generateParameterizedFieldsResolvers | Boolean | True | If true, then generate separate `Resolver` interface for parametrized fields. If false, then add field to the type definition and ignore field parameters. |
34+
| fieldsResolvers | Set(String) | Empty | Fields that require Resolvers should be defined here in format: `TypeName.fieldName`. |
35+
| jsonConfigurationFile | String | Empty | Path to an external mapping configuration. |
3436

3537
### External mapping configuration
3638

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class FreeMarkerTemplatesRegistry {
1414
static Template unionTemplate;
1515
static Template interfaceTemplate;
1616
static Template operationsTemplate;
17+
static Template fieldsResolverTemplate;
1718

1819
static {
1920
Configuration configuration = new Configuration(Configuration.VERSION_2_3_28);
@@ -29,6 +30,7 @@ class FreeMarkerTemplatesRegistry {
2930
unionTemplate = configuration.getTemplate("templates/javaClassGraphqlUnion.ftl");
3031
interfaceTemplate = configuration.getTemplate("templates/javaClassGraphqlInterface.ftl");
3132
operationsTemplate = configuration.getTemplate("templates/javaClassGraphqlOperations.ftl");
33+
fieldsResolverTemplate = configuration.getTemplate("templates/javaClassGraphqlFieldsResolver.ftl");
3234
} catch (IOException e) {
3335
throw new UnableToLoadFreeMarkerTemplateException(e);
3436
}

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import java.util.List;
1414
import java.util.Map;
1515

16+
import static java.util.stream.Collectors.toList;
17+
1618
/**
1719
* Generator of:
1820
* - Interface for each GraphQL query
@@ -38,7 +40,6 @@ public GraphqlCodegen(List<String> schemas, File outputDir, MappingConfig mappin
3840
this(schemas, outputDir, mappingConfig, null);
3941
}
4042

41-
4243
public GraphqlCodegen(List<String> schemas, File outputDir, MappingConfig mappingConfig, MappingConfigSupplier externalMappingConfigSupplier) {
4344
this.schemas = schemas;
4445
this.outputDir = outputDir;
@@ -60,6 +61,9 @@ private void initDefaultValues(MappingConfig mappingConfig) {
6061
if (mappingConfig.getGenerateApis() == null) {
6162
mappingConfig.setGenerateApis(DefaultMappingConfigValues.DEFAULT_GENERATE_APIS);
6263
}
64+
if (mappingConfig.getGenerateParameterizedFieldsResolvers() == null) {
65+
mappingConfig.setGenerateParameterizedFieldsResolvers(DefaultMappingConfigValues.DEFAULT_GENERATE_PARAMETERIZED_FIELDS_RESOLVERS);
66+
}
6367
}
6468

6569

@@ -89,6 +93,7 @@ private void processDocument(Document document) throws IOException, TemplateExce
8993
break;
9094
case TYPE:
9195
generateType((ObjectTypeDefinition) definition, document);
96+
generateFieldResolvers((ObjectTypeDefinition) definition);
9297
break;
9398
case INTERFACE:
9499
generateInterface((InterfaceTypeDefinition) definition);
@@ -134,6 +139,16 @@ private void generateType(ObjectTypeDefinition definition, Document document) th
134139
GraphqlCodegenFileCreator.generateFile(FreeMarkerTemplatesRegistry.typeTemplate, dataModel, outputDir);
135140
}
136141

142+
private void generateFieldResolvers(ObjectTypeDefinition definition) throws IOException, TemplateException {
143+
List<FieldDefinition> fieldDefsWithResolvers = definition.getFieldDefinitions().stream()
144+
.filter(fieldDef -> FieldDefinitionToParameterMapper.generateResolversForField(mappingConfig, fieldDef, definition.getName()))
145+
.collect(toList());
146+
if (!fieldDefsWithResolvers.isEmpty()) {
147+
Map<String, Object> dataModel = FieldResolverDefinitionToDataModelMapper.map(mappingConfig, fieldDefsWithResolvers, definition.getName());
148+
GraphqlCodegenFileCreator.generateFile(FreeMarkerTemplatesRegistry.fieldsResolverTemplate, dataModel, outputDir);
149+
}
150+
}
151+
137152
private void generateInput(InputObjectTypeDefinition definition) throws IOException, TemplateException {
138153
Map<String, Object> dataModel = InputDefinitionToDataModelMapper.map(mappingConfig, definition);
139154
GraphqlCodegenFileCreator.generateFile(FreeMarkerTemplatesRegistry.typeTemplate, dataModel, outputDir);

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.kobylynskyi.graphql.codegen.model.MappingConfig;
44
import com.kobylynskyi.graphql.codegen.model.ParameterDefinition;
5+
import com.kobylynskyi.graphql.codegen.utils.Utils;
56
import graphql.language.FieldDefinition;
67

78
import java.util.Collections;
@@ -30,8 +31,17 @@ public static List<ParameterDefinition> map(MappingConfig mappingConfig,
3031
return Collections.emptyList();
3132
}
3233
return fieldDefinitions.stream()
33-
.map(fieldDefinition -> GraphqlTypeToJavaTypeMapper.map(mappingConfig, fieldDefinition, parentTypeName))
34+
.filter(fieldDef -> !generateResolversForField(mappingConfig, fieldDef, parentTypeName))
35+
.map(fieldDef -> GraphqlTypeToJavaTypeMapper.map(mappingConfig, fieldDef, parentTypeName))
3436
.collect(Collectors.toList());
3537
}
3638

39+
public static boolean generateResolversForField(MappingConfig mappingConfig,
40+
FieldDefinition fieldDef,
41+
String parentTypeName) {
42+
boolean resolverForParamField = mappingConfig.getGenerateParameterizedFieldsResolvers() && !Utils.isEmpty(fieldDef.getInputValueDefinitions());
43+
boolean resolverForSpecificField = mappingConfig.getFieldsResolvers().contains(parentTypeName + "." + fieldDef.getName());
44+
return resolverForParamField || resolverForSpecificField;
45+
}
46+
3747
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.kobylynskyi.graphql.codegen.mapper;
2+
3+
import com.kobylynskyi.graphql.codegen.model.FieldResolverDefinition;
4+
import com.kobylynskyi.graphql.codegen.model.MappingConfig;
5+
import com.kobylynskyi.graphql.codegen.model.ParameterDefinition;
6+
import com.kobylynskyi.graphql.codegen.utils.Utils;
7+
import graphql.language.FieldDefinition;
8+
import graphql.language.InputValueDefinition;
9+
10+
import java.util.ArrayList;
11+
import java.util.HashMap;
12+
import java.util.List;
13+
import java.util.Map;
14+
import java.util.stream.Collectors;
15+
16+
import static com.kobylynskyi.graphql.codegen.model.DataModelFields.*;
17+
import static java.util.Collections.emptyList;
18+
19+
/**
20+
* Map field definition resolver to a Freemarker data model
21+
*
22+
* @author kobylynskyi
23+
*/
24+
public class FieldResolverDefinitionToDataModelMapper {
25+
26+
/**
27+
* Map field definition to a Freemarker data model
28+
*
29+
* @param mappingConfig Global mapping configuration
30+
* @param fieldDefs GraphQL field definitions that require resolvers
31+
* @param typeName Name of the type for which Resolver will be generated
32+
* @return Freemarker data model of the GraphQL parametrized field
33+
*/
34+
public static Map<String, Object> map(MappingConfig mappingConfig, List<FieldDefinition> fieldDefs,
35+
String typeName) {
36+
Map<String, Object> dataModel = new HashMap<>();
37+
String packageName = MapperUtils.getApiPackageName(mappingConfig);
38+
dataModel.put(PACKAGE, packageName);
39+
dataModel.put(IMPORTS, MapperUtils.getImportsForFieldResolvers(mappingConfig, packageName));
40+
dataModel.put(CLASS_NAME, getClassName(typeName));
41+
dataModel.put(FIELDS, fieldDefs.stream()
42+
.map(fieldDef -> mapFieldDefinition(mappingConfig, fieldDef, typeName))
43+
.collect(Collectors.toList()));
44+
return dataModel;
45+
}
46+
47+
48+
/**
49+
* Map GraphQL's FieldDefinition to a Freemarker-understandable format of operation
50+
*
51+
* @param mappingConfig Global mapping configuration
52+
* @param typeName Name of the type for which Resolver will be generated
53+
* @param fieldDef GraphQL definition of the field which should have resolver
54+
* @return Freemarker-understandable format of Parametrized Field
55+
*/
56+
private static FieldResolverDefinition mapFieldDefinition(MappingConfig mappingConfig,
57+
FieldDefinition fieldDef,
58+
String typeName) {
59+
FieldResolverDefinition parametrizedFieldDef = new FieldResolverDefinition();
60+
parametrizedFieldDef.setName(fieldDef.getName());
61+
parametrizedFieldDef.setType(GraphqlTypeToJavaTypeMapper.getJavaType(mappingConfig, fieldDef.getType(), fieldDef.getName(), typeName));
62+
63+
List<ParameterDefinition> parameters = new ArrayList<>();
64+
// 1. Add type as a first method argument
65+
String typeNameParameterName = MapperUtils.capitalizeIfRestricted(Utils.uncapitalize(typeName)); // Commit -> commit
66+
parameters.add(new ParameterDefinition(typeName, typeNameParameterName, null, emptyList()));
67+
68+
// 2. Add each field parameter as a method argument
69+
for (InputValueDefinition parameterDef : fieldDef.getInputValueDefinitions()) {
70+
String parameterDefType = GraphqlTypeToJavaTypeMapper.getJavaType(mappingConfig, parameterDef.getType(), parameterDef.getName(), fieldDef.getName());
71+
String defaultValue = DefaultValueMapper.map(parameterDef.getDefaultValue(), parameterDef.getType());
72+
parameters.add(new ParameterDefinition(parameterDefType, parameterDef.getName(), defaultValue, emptyList()));
73+
}
74+
// 3. Add DataFetchingEnvironment as a last method argument
75+
parameters.add(new ParameterDefinition("DataFetchingEnvironment", "env", null, emptyList()));
76+
77+
parametrizedFieldDef.setParameters(parameters);
78+
return parametrizedFieldDef;
79+
}
80+
81+
/**
82+
* Examples:
83+
* - PersonResolver
84+
*/
85+
private static String getClassName(String typeName) {
86+
return Utils.capitalize(typeName) + "Resolver";
87+
}
88+
89+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package com.kobylynskyi.graphql.codegen.mapper;
22

3-
import static graphql.language.OperationDefinition.*;
4-
53
import com.kobylynskyi.graphql.codegen.model.MappingConfig;
64
import com.kobylynskyi.graphql.codegen.model.ParameterDefinition;
75
import com.kobylynskyi.graphql.codegen.utils.Utils;
@@ -11,6 +9,8 @@
119
import java.util.List;
1210
import java.util.Map;
1311

12+
import static graphql.language.OperationDefinition.Operation;
13+
1414
/**
1515
* Map GraphQL type to Java type
1616
*

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

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,10 @@ static Set<String> getImports(MappingConfig mappingConfig, String packageName) {
164164
* Returns imports required for the particular case of field definitions (operations), taking into account async
165165
* operations, etc.
166166
*
167-
* @param mappingConfig
168-
* @param packageName
169-
* @param objectTypeName
170-
* @return
167+
* @param mappingConfig Global mapping configuration
168+
* @param packageName Package name of the generated class which will be ignored
169+
* @param objectTypeName Object type: Query/Mutation/Subscription
170+
* @return all imports required for a generated class
171171
*/
172172
static Set<String> getImportsForFieldDefinition(MappingConfig mappingConfig, String packageName, String objectTypeName) {
173173
final Set<String> imports = getImports(mappingConfig, packageName);
@@ -179,15 +179,28 @@ static Set<String> getImportsForFieldDefinition(MappingConfig mappingConfig, Str
179179
return imports;
180180
}
181181

182+
/**
183+
* Returns imports required for the fields resolvers class
184+
*
185+
* @param mappingConfig Global mapping configuration
186+
* @param packageName Package name of the generated class which will be ignored
187+
* @return all imports required for a generated class
188+
*/
189+
static Set<String> getImportsForFieldResolvers(MappingConfig mappingConfig, String packageName) {
190+
Set<String> imports = getImports(mappingConfig, packageName);
191+
imports.add("graphql.schema");
192+
return imports;
193+
}
194+
182195
/**
183196
* Determines if the specified operation is an async query or mutation
184197
*
185-
* @param mappingConfig
186-
* @param objectTypeName
198+
* @param mappingConfig Global mapping configuration
199+
* @param objectTypeName Parent object type (Query/Mutation)
187200
* @return true if the given operation is an async query or mutation, false otherwise
188201
*/
189202
static boolean isAsyncQueryOrMutation(MappingConfig mappingConfig, String objectTypeName) {
190-
boolean isAsyncApi = mappingConfig.getGenerateAsyncApi() != null && mappingConfig.getGenerateAsyncApi().booleanValue();
203+
boolean isAsyncApi = mappingConfig.getGenerateAsyncApi() != null && mappingConfig.getGenerateAsyncApi();
191204

192205
return isAsyncApi && (Operation.QUERY.name().equalsIgnoreCase(objectTypeName) || Operation.MUTATION.name()
193206
.equalsIgnoreCase(objectTypeName));

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ public class DefaultMappingConfigValues {
66
public static final boolean DEFAULT_GENERATE_APIS = true;
77
public static final boolean DEFAULT_EQUALS_AND_HASHCODE = false;
88
public static final boolean DEFAULT_TO_STRING = false;
9+
public static final boolean DEFAULT_GENERATE_PARAMETERIZED_FIELDS_RESOLVERS = true;
910
}

0 commit comments

Comments
 (0)