diff --git a/src/main/java/com/kobylynskyi/graphql/codegen/java/JavaGraphQLTypeMapper.java b/src/main/java/com/kobylynskyi/graphql/codegen/java/JavaGraphQLTypeMapper.java index 177e74fe5..223aa953a 100644 --- a/src/main/java/com/kobylynskyi/graphql/codegen/java/JavaGraphQLTypeMapper.java +++ b/src/main/java/com/kobylynskyi/graphql/codegen/java/JavaGraphQLTypeMapper.java @@ -2,6 +2,7 @@ import com.kobylynskyi.graphql.codegen.mapper.DataModelMapper; import com.kobylynskyi.graphql.codegen.mapper.GraphQLTypeMapper; +import com.kobylynskyi.graphql.codegen.model.MappingConfigConstants; import com.kobylynskyi.graphql.codegen.model.MappingContext; import com.kobylynskyi.graphql.codegen.model.NamedDefinition; import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperation; @@ -10,6 +11,8 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static java.util.Arrays.asList; @@ -19,6 +22,7 @@ public class JavaGraphQLTypeMapper extends GraphQLTypeMapper { public static final String JAVA_UTIL_LIST = "java.util.List"; + public static final Pattern JAVA_UTIL_LIST_ELEMENT_REGEX = Pattern.compile("java\\.util\\.List<(.+)>"); private static final String JAVA_UTIL_OPTIONAL = "java.util.Optional"; private static final Set JAVA_PRIMITIVE_TYPES = new HashSet<>(asList( "byte", "short", "int", "long", "float", "double", "char", "boolean")); @@ -65,11 +69,28 @@ public String wrapApiReturnTypeIfRequired(MappingContext mappingContext, if (computedTypeName.startsWith(JAVA_UTIL_LIST) && Utils.isNotBlank(mappingContext.getApiReturnListType())) { // in case it is query/mutation, return type is list and apiReturnListType is set - return computedTypeName.replace(JAVA_UTIL_LIST, mappingContext.getApiReturnListType()); + if (mappingContext.getApiReturnListType().contains(MappingConfigConstants.API_RETURN_NAME_PLACEHOLDER)) { + Matcher matcher = JAVA_UTIL_LIST_ELEMENT_REGEX.matcher(computedTypeName); + if (matcher.find()) { + String listElement = matcher.group(1); + return mappingContext.getApiReturnListType().replace( + MappingConfigConstants.API_RETURN_NAME_PLACEHOLDER, + listElement); + } else { + throw new IllegalStateException(); + } + } else { + return computedTypeName.replace(JAVA_UTIL_LIST, mappingContext.getApiReturnListType()); + } } if (Utils.isNotBlank(mappingContext.getApiReturnType())) { // in case it is query/mutation and apiReturnType is set - return getGenericsString(mappingContext, mappingContext.getApiReturnType(), computedTypeName); + if (mappingContext.getApiReturnType().contains(MappingConfigConstants.API_RETURN_NAME_PLACEHOLDER)) { + return mappingContext.getApiReturnType() + .replace(MappingConfigConstants.API_RETURN_NAME_PLACEHOLDER, computedTypeName); + } else { + return getGenericsString(mappingContext, mappingContext.getApiReturnType(), computedTypeName); + } } return getTypeConsideringPrimitive(mappingContext, namedDefinition, computedTypeName); } diff --git a/src/main/java/com/kobylynskyi/graphql/codegen/kotlin/KotlinGraphQLTypeMapper.java b/src/main/java/com/kobylynskyi/graphql/codegen/kotlin/KotlinGraphQLTypeMapper.java index 552d2c118..e98b4745c 100644 --- a/src/main/java/com/kobylynskyi/graphql/codegen/kotlin/KotlinGraphQLTypeMapper.java +++ b/src/main/java/com/kobylynskyi/graphql/codegen/kotlin/KotlinGraphQLTypeMapper.java @@ -2,6 +2,7 @@ import com.kobylynskyi.graphql.codegen.mapper.DataModelMapper; import com.kobylynskyi.graphql.codegen.mapper.GraphQLTypeMapper; +import com.kobylynskyi.graphql.codegen.model.MappingConfigConstants; import com.kobylynskyi.graphql.codegen.model.MappingContext; import com.kobylynskyi.graphql.codegen.model.NamedDefinition; import com.kobylynskyi.graphql.codegen.model.definitions.ExtendedFieldDefinition; @@ -10,6 +11,8 @@ import java.util.HashSet; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static java.util.Arrays.asList; @@ -22,6 +25,7 @@ public class KotlinGraphQLTypeMapper extends GraphQLTypeMapper { private static final String KOTLIN_UTIL_LIST = "List"; + public static final Pattern KOTLIN_UTIL_LIST_ELEMENT_REGEX = Pattern.compile("List<(.+)>"); private static final String KOTLIN_UTIL_NULLABLE = "?"; // Char Boolean are not primitive type, but non null equivalent jvm primitive types. private static final Set KOTLIN_PRIMITIVE_TYPES = new HashSet<>( @@ -93,11 +97,35 @@ public String wrapApiReturnTypeIfRequired(MappingContext mappingContext, if (computedTypeName.startsWith(KOTLIN_UTIL_LIST) && Utils.isNotBlank(mappingContext.getApiReturnListType())) { // in case it is query/mutation, return type is list and apiReturnListType is set - return computedTypeName.replace(KOTLIN_UTIL_LIST, mappingContext.getApiReturnListType()); + if (mappingContext.getApiReturnListType().contains(MappingConfigConstants.API_RETURN_NAME_PLACEHOLDER)) { + boolean isNullable = computedTypeName.endsWith(KOTLIN_UTIL_NULLABLE); + + Matcher matcher = KOTLIN_UTIL_LIST_ELEMENT_REGEX.matcher(computedTypeName); + if (matcher.find()) { + String listElement = matcher.group(1); + computedTypeName = mappingContext.getApiReturnListType() + .replace(MappingConfigConstants.API_RETURN_NAME_PLACEHOLDER, listElement); + + if (isNullable) { + return computedTypeName + "?"; + } else { + return computedTypeName; + } + } else { + throw new IllegalStateException(); + } + } else { + return computedTypeName.replace(KOTLIN_UTIL_LIST, mappingContext.getApiReturnListType()); + } } if (Utils.isNotBlank(mappingContext.getApiReturnType())) { // in case it is query/mutation and apiReturnType is set - return getGenericsString(mappingContext, mappingContext.getApiReturnType(), computedTypeName); + if (mappingContext.getApiReturnType().contains(MappingConfigConstants.API_RETURN_NAME_PLACEHOLDER)) { + return mappingContext.getApiReturnType() + .replace(MappingConfigConstants.API_RETURN_NAME_PLACEHOLDER, computedTypeName); + } else { + return getGenericsString(mappingContext, mappingContext.getApiReturnType(), computedTypeName); + } } return getTypeConsideringPrimitive(mappingContext, namedDefinition, computedTypeName); } diff --git a/src/main/java/com/kobylynskyi/graphql/codegen/model/MappingConfigConstants.java b/src/main/java/com/kobylynskyi/graphql/codegen/model/MappingConfigConstants.java index d940f0fab..8a0ef020e 100644 --- a/src/main/java/com/kobylynskyi/graphql/codegen/model/MappingConfigConstants.java +++ b/src/main/java/com/kobylynskyi/graphql/codegen/model/MappingConfigConstants.java @@ -9,6 +9,7 @@ public class MappingConfigConstants { public static final String DEFAULT_VALIDATION_ANNOTATION = "javax.validation.constraints.NotNull"; public static final String PARENT_INTERFACE_TYPE_PLACEHOLDER = "{{TYPE}}"; public static final String TYPE_NAME_PLACEHOLDER = "{{TYPE_NAME}}"; + public static final String API_RETURN_NAME_PLACEHOLDER = "{{TYPE}}"; public static final boolean DEFAULT_GENERATE_APIS = true; public static final String DEFAULT_GENERATE_APIS_STRING = "true"; diff --git a/src/main/java/com/kobylynskyi/graphql/codegen/scala/ScalaGraphQLTypeMapper.java b/src/main/java/com/kobylynskyi/graphql/codegen/scala/ScalaGraphQLTypeMapper.java index 2967d9a26..15b68d616 100644 --- a/src/main/java/com/kobylynskyi/graphql/codegen/scala/ScalaGraphQLTypeMapper.java +++ b/src/main/java/com/kobylynskyi/graphql/codegen/scala/ScalaGraphQLTypeMapper.java @@ -1,6 +1,7 @@ package com.kobylynskyi.graphql.codegen.scala; import com.kobylynskyi.graphql.codegen.mapper.GraphQLTypeMapper; +import com.kobylynskyi.graphql.codegen.model.MappingConfigConstants; import com.kobylynskyi.graphql.codegen.model.MappingContext; import com.kobylynskyi.graphql.codegen.model.NamedDefinition; import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperation; @@ -8,6 +9,8 @@ import java.util.HashSet; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static com.kobylynskyi.graphql.codegen.java.JavaGraphQLTypeMapper.JAVA_UTIL_LIST; import static java.util.Arrays.asList; @@ -18,6 +21,7 @@ public class ScalaGraphQLTypeMapper extends GraphQLTypeMapper { private static final String SCALA_UTIL_LIST = "scala.Seq"; + private static final Pattern SCALA_UTIL_LIST_ELEMENT_REGEX = Pattern.compile("scala\\.Seq\\[(.+)]"); private static final String SCALA_UTIL_OPTIONAL = "scala.Option"; private static final Set SCALA_PRIMITIVE_TYPES = new HashSet<>(asList( "Byte", "Short", "Int", "Long", "Float", "Double", "Char", "Boolean")); @@ -72,11 +76,28 @@ public String wrapApiReturnTypeIfRequired(MappingContext mappingContext, if (computedTypeName.startsWith(SCALA_UTIL_LIST) && Utils.isNotBlank(mappingContext.getApiReturnListType())) { // in case it is query/mutation, return type is list and apiReturnListType is set - return computedTypeName.replace(SCALA_UTIL_LIST, mappingContext.getApiReturnListType()); + if (mappingContext.getApiReturnListType().contains(MappingConfigConstants.API_RETURN_NAME_PLACEHOLDER)) { + Matcher matcher = SCALA_UTIL_LIST_ELEMENT_REGEX.matcher(computedTypeName); + if (matcher.find()) { + String listElement = matcher.group(1); + return mappingContext.getApiReturnListType().replace( + MappingConfigConstants.API_RETURN_NAME_PLACEHOLDER, + listElement); + } else { + throw new IllegalStateException(); + } + } else { + return computedTypeName.replace(SCALA_UTIL_LIST, mappingContext.getApiReturnListType()); + } } if (Utils.isNotBlank(mappingContext.getApiReturnType())) { // in case it is query/mutation and apiReturnType is set - return getGenericsString(mappingContext, mappingContext.getApiReturnType(), computedTypeName); + if (mappingContext.getApiReturnType().contains(MappingConfigConstants.API_RETURN_NAME_PLACEHOLDER)) { + return mappingContext.getApiReturnType() + .replace(MappingConfigConstants.API_RETURN_NAME_PLACEHOLDER, computedTypeName); + } else { + return getGenericsString(mappingContext, mappingContext.getApiReturnType(), computedTypeName); + } } return getTypeConsideringPrimitive(mappingContext, namedDefinition, computedTypeName); } diff --git a/src/test/java/com/kobylynskyi/graphql/codegen/GraphQLCodegenApiReturnTypeTest.java b/src/test/java/com/kobylynskyi/graphql/codegen/GraphQLCodegenApiReturnTypeTest.java new file mode 100644 index 000000000..1e1fce449 --- /dev/null +++ b/src/test/java/com/kobylynskyi/graphql/codegen/GraphQLCodegenApiReturnTypeTest.java @@ -0,0 +1,109 @@ +package com.kobylynskyi.graphql.codegen; + +import com.kobylynskyi.graphql.codegen.java.JavaGraphQLCodegen; +import com.kobylynskyi.graphql.codegen.model.GeneratedLanguage; +import com.kobylynskyi.graphql.codegen.model.MappingConfig; +import com.kobylynskyi.graphql.codegen.utils.Utils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; + +import static com.kobylynskyi.graphql.codegen.TestUtils.assertFileContainsElements; +import static java.util.Collections.singletonList; + +class GraphQLCodegenApiReturnTypeTest { + + private final File outputBuildDir = new File("build/generated"); + private final File outputJavaClassesDir = new File("build/generated/com/kobylynskyi/graphql/test1"); + + private MappingConfig mappingConfig; + + @BeforeEach + void init() { + mappingConfig = new MappingConfig(); + mappingConfig.setPackageName("com.kobylynskyi.graphql.test1"); + mappingConfig.setGeneratedLanguage(GeneratedLanguage.JAVA); + } + + @AfterEach + void cleanup() { + Utils.deleteDir(outputBuildDir); + } + + @Test + void generate_ApiReturnType_WithPlaceHolder() throws Exception { + mappingConfig.setApiReturnType( + "java.util.concurrent.CompletionStage>" + ); + + generate("src/test/resources/schemas/test.graphqls"); + + File[] files = Objects.requireNonNull(outputJavaClassesDir.listFiles()); + + String requireChildText = getChildText( + "java.util.concurrent.CompletionStage>>" + ); + assertFileContainsElements( + files, + "EventPropertyResolver.java", + requireChildText + ); + + String requireParentText = getParentText( + "java.util.concurrent.CompletionStage>" + ); + assertFileContainsElements(files, "EventPropertyResolver.java", + requireParentText + ); + } + + @Test + void generate_ApiReturnType_And_ApiReturnListType_WithPlaceHolder() throws Exception { + mappingConfig.setApiReturnType( + "java.util.concurrent.CompletionStage>" + ); + mappingConfig.setApiReturnListType( + "reactor.core.publisher.Mono>" + ); + + generate("src/test/resources/schemas/test.graphqls"); + + File[] files = Objects.requireNonNull(outputJavaClassesDir.listFiles()); + + assertFileContainsElements( + files, + "EventPropertyResolver.java", + getChildText( + "reactor.core.publisher.Mono>" + ) + ); + + assertFileContainsElements( + files, + "EventPropertyResolver.java", + getParentText( + "java.util.concurrent.CompletionStage>" + ) + ); + } + + private String getChildText(String returnType) { + return returnType + " child(EventProperty eventProperty, Integer first, Integer last) throws Exception;"; + } + + private String getParentText(String returnType) { + return returnType + + " parent(EventProperty eventProperty, EventStatus withStatus, String createdAfter) throws Exception;"; + } + + private void generate(String path) throws IOException { + new JavaGraphQLCodegen(singletonList(path), outputBuildDir, mappingConfig, + TestUtils.getStaticGeneratedInfo(mappingConfig)).generate(); + } + +} diff --git a/src/test/java/com/kobylynskyi/graphql/codegen/kotlin/GraphQLCodegenApiReturnTypeTest.java b/src/test/java/com/kobylynskyi/graphql/codegen/kotlin/GraphQLCodegenApiReturnTypeTest.java new file mode 100644 index 000000000..ca9df3c27 --- /dev/null +++ b/src/test/java/com/kobylynskyi/graphql/codegen/kotlin/GraphQLCodegenApiReturnTypeTest.java @@ -0,0 +1,111 @@ +package com.kobylynskyi.graphql.codegen.kotlin; + +import com.kobylynskyi.graphql.codegen.TestUtils; +import com.kobylynskyi.graphql.codegen.model.GeneratedLanguage; +import com.kobylynskyi.graphql.codegen.model.MappingConfig; +import com.kobylynskyi.graphql.codegen.utils.Utils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; + +import static com.kobylynskyi.graphql.codegen.TestUtils.assertFileContainsElements; +import static java.util.Collections.singletonList; + +class GraphQLCodegenApiReturnTypeTest { + + private final File outputBuildDir = new File("build/generated"); + private final File outputJavaClassesDir = new File("build/generated/com/kobylynskyi/graphql/test1"); + + private MappingConfig mappingConfig; + + @BeforeEach + void init() { + mappingConfig = new MappingConfig(); + mappingConfig.setPackageName("com.kobylynskyi.graphql.test1"); + mappingConfig.setGeneratedLanguage(GeneratedLanguage.KOTLIN); + } + + @AfterEach + void cleanup() { + Utils.deleteDir(outputBuildDir); + } + + @Test + void generate_ApiReturnType_WithPlaceHolder() throws Exception { + mappingConfig.setApiReturnType( + "java.util.concurrent.CompletionStage>" + ); + + generate("src/test/resources/schemas/test.graphqls"); + + File[] files = Objects.requireNonNull(outputJavaClassesDir.listFiles()); + + String requireChildText = getChildFunction( + "java.util.concurrent.CompletionStage?>>" + ); + assertFileContainsElements( + files, + "EventPropertyResolver.kt", + requireChildText + ); + + String requireParentText = getParentFunction( + "java.util.concurrent.CompletionStage>" + ); + assertFileContainsElements( + files, + "EventPropertyResolver.kt", + requireParentText + ); + } + + @Test + void generate_ApiReturnType_And_ApiReturnListType_WithPlaceHolder() throws Exception { + mappingConfig.setApiReturnType( + "java.util.concurrent.CompletionStage>" + ); + mappingConfig.setApiReturnListType( + "reactor.core.publisher.Mono>" + ); + + generate("src/test/resources/schemas/test.graphqls"); + + File[] files = Objects.requireNonNull(outputJavaClassesDir.listFiles()); + + assertFileContainsElements( + files, + "EventPropertyResolver.kt", + getChildFunction( + "reactor.core.publisher.Mono>?" + ) + ); + + assertFileContainsElements( + files, + "EventPropertyResolver.kt", + getParentFunction( + "java.util.concurrent.CompletionStage>" + ) + ); + } + + private String getChildFunction(String returnType) { + return "fun child(eventProperty: EventProperty, first: Int?, last: Int?)" + + ": " + returnType; + } + + private String getParentFunction(String returnType) { + return "fun parent(eventProperty: EventProperty, withStatus: EventStatus?, createdAfter: String?)" + + ": " + returnType; + } + + private void generate(String path) throws IOException { + new KotlinGraphQLCodegen(singletonList(path), outputBuildDir, mappingConfig, + TestUtils.getStaticGeneratedInfo(mappingConfig)).generate(); + } + +} diff --git a/src/test/java/com/kobylynskyi/graphql/codegen/scala/GraphQLCodegenApiReturnTypeTest.java b/src/test/java/com/kobylynskyi/graphql/codegen/scala/GraphQLCodegenApiReturnTypeTest.java new file mode 100644 index 000000000..70d054216 --- /dev/null +++ b/src/test/java/com/kobylynskyi/graphql/codegen/scala/GraphQLCodegenApiReturnTypeTest.java @@ -0,0 +1,111 @@ +package com.kobylynskyi.graphql.codegen.scala; + +import com.kobylynskyi.graphql.codegen.TestUtils; +import com.kobylynskyi.graphql.codegen.model.GeneratedLanguage; +import com.kobylynskyi.graphql.codegen.model.MappingConfig; +import com.kobylynskyi.graphql.codegen.utils.Utils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; + +import static com.kobylynskyi.graphql.codegen.TestUtils.assertFileContainsElements; +import static java.util.Collections.singletonList; + +class GraphQLCodegenApiReturnTypeTest { + + private final File outputBuildDir = new File("build/generated"); + private final File outputJavaClassesDir = new File("build/generated/com/kobylynskyi/graphql/test1"); + + private MappingConfig mappingConfig; + + @BeforeEach + void init() { + mappingConfig = new MappingConfig(); + mappingConfig.setPackageName("com.kobylynskyi.graphql.test1"); + mappingConfig.setGeneratedLanguage(GeneratedLanguage.SCALA); + } + + @AfterEach + void cleanup() { + Utils.deleteDir(outputBuildDir); + } + + @Test + void generate_ApiReturnType_WithPlaceHolder() throws Exception { + mappingConfig.setApiReturnType( + "java.util.concurrent.CompletionStage[graphql.execution.DataFetcherResult[{{TYPE}}]]" + ); + + generate("src/test/resources/schemas/test.graphqls"); + + File[] files = Objects.requireNonNull(outputJavaClassesDir.listFiles()); + + String childRequireText = getChildFunction( + "java.util.concurrent.CompletionStage[graphql.execution.DataFetcherResult[scala.Seq[EventProperty]]]" + ); + assertFileContainsElements( + files, + "EventPropertyResolver.scala", + childRequireText + ); + + String parentRequireText = getParentFunction( + "java.util.concurrent.CompletionStage[graphql.execution.DataFetcherResult[Event]]" + ); + assertFileContainsElements( + files, + "EventPropertyResolver.scala", + parentRequireText + ); + } + + @Test + void generate_ApiReturnType_And_ApiReturnListType_WithPlaceHolder() throws Exception { + mappingConfig.setApiReturnType( + "java.util.concurrent.CompletionStage[graphql.execution.DataFetcherResult[{{TYPE}}]]" + ); + mappingConfig.setApiReturnListType( + "reactor.core.publisher.Mono[graphql.execution.DataFetcherResult[{{TYPE}}]]" + ); + + generate("src/test/resources/schemas/test.graphqls"); + + File[] files = Objects.requireNonNull(outputJavaClassesDir.listFiles()); + + assertFileContainsElements( + files, + "EventPropertyResolver.scala", + getChildFunction( + "reactor.core.publisher.Mono[graphql.execution.DataFetcherResult[EventProperty]]" + ) + ); + + assertFileContainsElements( + files, + "EventPropertyResolver.scala", + getParentFunction( + "java.util.concurrent.CompletionStage[graphql.execution.DataFetcherResult[Event]]" + ) + ); + } + + private String getChildFunction(String returnType) { + return "def child(eventProperty: EventProperty, first: scala.Option[Int], last: scala.Option[Int])" + + ": " + returnType; + } + + private String getParentFunction(String returnType) { + return "def parent(eventProperty: EventProperty, withStatus: EventStatus, createdAfter: String)" + + ": " + returnType; + } + + private void generate(String path) throws IOException { + new ScalaGraphQLCodegen(singletonList(path), outputBuildDir, mappingConfig, + TestUtils.getStaticGeneratedInfo(mappingConfig)).generate(); + } + +}