diff --git a/graphql-dgs-codegen-core/build.gradle b/graphql-dgs-codegen-core/build.gradle index 2792da501..5a3aa5386 100644 --- a/graphql-dgs-codegen-core/build.gradle +++ b/graphql-dgs-codegen-core/build.gradle @@ -43,6 +43,7 @@ dependencies { testImplementation 'org.jetbrains.kotlin:kotlin-compiler' integTestImplementation 'com.fasterxml.jackson.module:jackson-module-kotlin' + integTestImplementation 'tools.jackson.core:jackson-databind:3.2.0' // explicit version to keep tests stable } application { diff --git a/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/Kotlin2CodeGenTest.kt b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/Kotlin2CodeGenTest.kt index cbdcca3a4..4af71b5a4 100644 --- a/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/Kotlin2CodeGenTest.kt +++ b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/Kotlin2CodeGenTest.kt @@ -266,6 +266,38 @@ class Kotlin2CodeGenTest { assertThat(nameField.get(instance)).isEqualTo("John") } + @Test + fun `generated Jackson 3 annotations compile against Jackson 3 on the classpath`() { + val schema = + """ + type Query { + movies: [Movie] + } + + type Movie { + title: String + director: String + } + """.trimIndent() + + val result = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = "com.netflix.test.jackson3", + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + jacksonVersions = setOf(JacksonVersion.JACKSON_3), + ), + ).generate() + + val movie = result.kotlinDataTypes.first { it.name == "Movie" }.toString() + assertThat(movie).contains("tools.jackson.databind.`annotation`.JsonDeserialize") + assertThat(movie).contains("tools.jackson.databind.`annotation`.JsonPOJOBuilder") + + assertCompilesKotlin(result) + } + companion object { @Suppress("unused") @JvmStatic diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt index 29e5739e0..4122c4a91 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt @@ -584,6 +584,7 @@ class CodeGenConfig( var addDeprecatedAnnotation: Boolean = false, var trackInputFieldSet: Boolean = false, var generateJSpecifyAnnotations: Boolean = false, + var jacksonVersions: Set = emptySet(), ) { val packageNameClient: String = "$packageName.$subPackageNameClient" @@ -620,6 +621,24 @@ enum class Language { KOTLIN, } +enum class JacksonVersion { + JACKSON_2, + JACKSON_3, + ; + + companion object { + fun fromString(value: String): JacksonVersion = + when (value.trim()) { + "2" -> JACKSON_2 + "3" -> JACKSON_3 + else -> throw IllegalArgumentException( + "Invalid Jackson version '$value'. Supported values are \"2\" (com.fasterxml.jackson) " + + "and \"3\" (tools.jackson).", + ) + } + } +} + data class CodeGenResult( val javaDataTypes: List = listOf(), val javaInterfaces: List = listOf(), diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt index febec82bb..f06f436c2 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt @@ -22,10 +22,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder import com.netflix.graphql.dgs.codegen.CodeGen import com.netflix.graphql.dgs.codegen.CodeGenConfig +import com.netflix.graphql.dgs.codegen.JacksonVersion import com.netflix.graphql.dgs.codegen.generators.shared.CodeGeneratorUtils.capitalized import com.netflix.graphql.dgs.codegen.generators.shared.JAKARTA_GENERATED_ANNOTATION import com.netflix.graphql.dgs.codegen.generators.shared.PackageParserUtil @@ -127,32 +126,55 @@ fun jsonSubTypesAnnotation(subTypes: Collection): AnnotationSpec { .build() } +/** Configured Jackson versions based on compile dependencies, defaulting to Jackson 2 when none are set. */ +private fun jacksonVersionsOrDefault(config: CodeGenConfig): Set = + config.jacksonVersions.ifEmpty { setOf(JacksonVersion.JACKSON_2) } + +private fun jsonDeserializeClassName(version: JacksonVersion): ClassName = + when (version) { + JacksonVersion.JACKSON_2 -> ClassName("com.fasterxml.jackson.databind.annotation", "JsonDeserialize") + JacksonVersion.JACKSON_3 -> ClassName("tools.jackson.databind.annotation", "JsonDeserialize") + } + +private fun jsonPOJOBuilderClassName(version: JacksonVersion): ClassName = + when (version) { + JacksonVersion.JACKSON_2 -> ClassName("com.fasterxml.jackson.databind.annotation", "JsonPOJOBuilder") + JacksonVersion.JACKSON_3 -> ClassName("tools.jackson.databind.annotation", "JsonPOJOBuilder") + } + /** - * Generate a [JsonDeserialize] annotation for the builder class. + * Generate `@JsonDeserialize` annotations for the builder class, one per configured Jackson version. + * Jackson 2 lives in `com.fasterxml.jackson.databind.annotation`; Jackson 3 in `tools.jackson.databind.annotation`. * * Example generated annotation: * ``` * @JsonDeserialize(builder = Movie.Builder::class) * ``` */ -fun jsonDeserializeAnnotation(builderType: ClassName): AnnotationSpec = - AnnotationSpec - .builder(JsonDeserialize::class) - .addMember("builder = %T::class", builderType) - .build() +fun jsonDeserializeAnnotations( + config: CodeGenConfig, + builderType: ClassName, +): List = + jacksonVersionsOrDefault(config).map { version -> + AnnotationSpec + .builder(jsonDeserializeClassName(version)) + .addMember("builder = %T::class", builderType) + .build() + } /** - * Generate a [JsonPOJOBuilder] annotation for the builder class. + * Generate `@JsonPOJOBuilder` annotations for the builder class, one per configured Jackson version. + * Jackson 2 lives in `com.fasterxml.jackson.databind.annotation`; Jackson 3 in `tools.jackson.databind.annotation`. * * Example generated annotation: * ``` * @JsonPOJOBuilder * ``` */ -fun jsonBuilderAnnotation(): AnnotationSpec = - AnnotationSpec - .builder(JsonPOJOBuilder::class) - .build() +fun jsonBuilderAnnotations(config: CodeGenConfig): List = + jacksonVersionsOrDefault(config).map { version -> + AnnotationSpec.builder(jsonPOJOBuilderClassName(version)).build() + } /** * Generate a [JvmName] annotation for a kotlin property. diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataTypes.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataTypes.kt index c7adcf46a..9df488f1a 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataTypes.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataTypes.kt @@ -24,8 +24,8 @@ import com.netflix.graphql.dgs.codegen.generators.kotlin.ReservedKeywordFilter import com.netflix.graphql.dgs.codegen.generators.kotlin.addControlFlow import com.netflix.graphql.dgs.codegen.generators.kotlin.addOptionalGeneratedAnnotation import com.netflix.graphql.dgs.codegen.generators.kotlin.disableJsonTypeInfoAnnotation -import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonBuilderAnnotation -import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonDeserializeAnnotation +import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonBuilderAnnotations +import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonDeserializeAnnotations import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonIgnorePropertiesAnnotation import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonPropertyAnnotation import com.netflix.graphql.dgs.codegen.generators.kotlin.jvmNameAnnotation @@ -134,7 +134,7 @@ fun generateKotlin2DataTypes( TypeSpec .classBuilder("Builder") .addOptionalGeneratedAnnotation(config) - .addAnnotation(jsonBuilderAnnotation()) + .apply { jsonBuilderAnnotations(config).forEach { addAnnotation(it) } } .addAnnotation(jsonIgnorePropertiesAnnotation("__typename")) // add a backing property for each field .addProperties( @@ -193,11 +193,7 @@ fun generateKotlin2DataTypes( } // add jackson annotations .addAnnotation(disableJsonTypeInfoAnnotation()) - .addAnnotation( - jsonDeserializeAnnotation( - builderClassName, - ), - ) + .apply { jsonDeserializeAnnotations(config, builderClassName).forEach { addAnnotation(it) } } // add nested classes .addType(companionObject) .addType(builder) diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/JacksonVersionDetectionTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/JacksonVersionDetectionTest.kt new file mode 100644 index 000000000..6727584f2 --- /dev/null +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/JacksonVersionDetectionTest.kt @@ -0,0 +1,157 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.graphql.dgs.codegen + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class JacksonVersionDetectionTest { + private val schema = + """ + type Query { + movies: [Movie] + } + + type Movie { + title: String + director: String + } + """.trimIndent() + + @Test + fun `generates only Jackson 2 JsonDeserialize and JsonPOJOBuilder annotations when Jackson 2 is configured`() { + val result = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = "com.test", + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + jacksonVersions = setOf(JacksonVersion.JACKSON_2), + ), + ).generate() + + val movieType = result.kotlinDataTypes.first { it.name == "Movie" } + val fileContent = movieType.toString() + + assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize") + assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonDeserialize") + + assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonPOJOBuilder") + assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonPOJOBuilder") + } + + @Test + fun `generates only Jackson 3 JsonDeserialize and JsonPOJOBuilder annotations when Jackson 3 is configured`() { + val result = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = "com.test", + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + jacksonVersions = setOf(JacksonVersion.JACKSON_3), + ), + ).generate() + + val movieType = result.kotlinDataTypes.first { it.name == "Movie" } + val fileContent = movieType.toString() + + assertThat(fileContent).contains("tools.jackson.databind.`annotation`.JsonDeserialize") + assertThat(fileContent).doesNotContain("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize") + + assertThat(fileContent).contains("tools.jackson.databind.`annotation`.JsonPOJOBuilder") + assertThat(fileContent).doesNotContain("com.fasterxml.jackson.databind.`annotation`.JsonPOJOBuilder") + } + + @Test + fun `generates both Jackson 2 and 3 JsonDeserialize and JsonPOJOBuilder annotations when both are configured`() { + val result = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = "com.test", + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + jacksonVersions = setOf(JacksonVersion.JACKSON_2, JacksonVersion.JACKSON_3), + ), + ).generate() + + val movieType = result.kotlinDataTypes.first { it.name == "Movie" } + val fileContent = movieType.toString() + + assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize") + assertThat(fileContent).contains("tools.jackson.databind.`annotation`.JsonDeserialize") + + assertThat(fileContent).contains("tools.jackson.databind.`annotation`.JsonPOJOBuilder") + assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonPOJOBuilder") + + assertThat(fileContent).contains("@ToolsJacksonDatabindAnnotationJsonPOJOBuilder") + assertThat(fileContent).contains("@FasterxmlJacksonDatabindAnnotationJsonPOJOBuilder") + + assertThat(fileContent).contains("@ToolsJacksonDatabindAnnotationJsonDeserialize") + assertThat(fileContent).contains("@FasterxmlJacksonDatabindAnnotationJsonDeserialize") + } + + @Test + fun `defaults to Jackson 2 when no configuration is provided`() { + val result = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = "com.test", + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + ), + ).generate() + + val movieType = result.kotlinDataTypes.first { it.name == "Movie" } + val fileContent = movieType.toString() + + assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize") + assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonDeserialize") + + assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonPOJOBuilder") + assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonPOJOBuilder") + } + + @Test + fun `empty configuration defaults to Jackson 2`() { + val result = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = "com.test", + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + jacksonVersions = emptySet(), + ), + ).generate() + + val movieType = result.kotlinDataTypes.first { it.name == "Movie" } + val fileContent = movieType.toString() + + // Should default to Jackson 2 (backwards compatibility) + assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize") + assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonDeserialize") + + assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonPOJOBuilder") + assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonPOJOBuilder") + } +} diff --git a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/CodegenPlugin.kt b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/CodegenPlugin.kt index 241b5c188..983d218de 100644 --- a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/CodegenPlugin.kt +++ b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/CodegenPlugin.kt @@ -45,11 +45,11 @@ class CodegenPlugin : Plugin { val sourceSets = if (GradleVersion.current() >= - GradleVersion.version("7.1") + GradleVersion.version("7.4") ) { javaExtension.sourceSets } else { - throw RuntimeException("Gradle versions < 7.1 are no longer supported by DGS Codegen. Please upgrade your Gradle version.") + throw RuntimeException("Gradle versions < 7.4 are no longer supported by DGS Codegen. Please upgrade your Gradle version.") } val mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME) val outputDir = generateJavaTaskProvider.map(GenerateJavaTask::getOutputDir) diff --git a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt index cb44de469..50f7f779d 100644 --- a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt +++ b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt @@ -20,10 +20,14 @@ package com.netflix.graphql.dgs.codegen.gradle import com.netflix.graphql.dgs.codegen.CodeGen import com.netflix.graphql.dgs.codegen.CodeGenConfig +import com.netflix.graphql.dgs.codegen.JacksonVersion import com.netflix.graphql.dgs.codegen.Language import org.gradle.api.DefaultTask import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.model.ObjectFactory +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Provider import org.gradle.api.tasks.* import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper import java.io.File @@ -174,6 +178,27 @@ open class GenerateJavaTask project.configurations.findByName("dgsCodegen"), ) + @Input + val jacksonVersionOverride: ListProperty = + objectFactory.listProperty(String::class.java).convention(emptyList()) + + private val detectedJacksonVersions: Provider> = + project.configurations + .named(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME) + .flatMap { it.incoming.resolutionResult.rootComponent } + .map { JacksonVersionDetector.detect(it) } + + /** + * Effective Jackson versions for this run. Jackson annotations are only emitted and the + * override is only resolved when [generateKotlinNullableClasses] is enabled. + */ + private fun resolveJacksonVersions(): Set = + if (generateKotlinNullableClasses) { + JacksonVersionDetector.resolve(jacksonVersionOverride.get(), detectedJacksonVersions.get()) + } else { + emptySet() + } + @TaskAction fun generate() { val schemaJarFilesFromDependencies = dgsCodegenClasspath.files.toList() @@ -229,6 +254,7 @@ open class GenerateJavaTask javaGenerateAllConstructor = javaGenerateAllConstructor, trackInputFieldSet = trackInputFieldSet, generateJSpecifyAnnotations = generateJSpecifyAnnotations, + jacksonVersions = resolveJacksonVersions(), ) logger.info("Codegen config: {}", config) diff --git a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/JacksonVersionDetector.kt b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/JacksonVersionDetector.kt new file mode 100644 index 000000000..a18b6f01f --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/JacksonVersionDetector.kt @@ -0,0 +1,86 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.graphql.dgs.codegen.gradle + +import com.netflix.graphql.dgs.codegen.JacksonVersion +import org.gradle.api.InvalidUserDataException +import org.gradle.api.artifacts.component.ComponentIdentifier +import org.gradle.api.artifacts.result.ResolvedComponentResult +import org.gradle.api.artifacts.result.ResolvedDependencyResult + +object JacksonVersionDetector { + private const val JACKSON_2_GROUP = "com.fasterxml.jackson.core" + private const val JACKSON_3_GROUP = "tools.jackson.core" + private const val JACKSON_DATABIND_MODULE = "jackson-databind" + + /** + * Which Jackson major versions are present in the resolved dependency graph reachable from [root] + * (pass `resolutionResult.rootComponent.get()`). + */ + fun detect(root: ResolvedComponentResult): Set { + val detected = mutableSetOf() + val visited = mutableSetOf() + val queue = ArrayDeque().apply { addLast(root) } + + while (queue.isNotEmpty()) { + val component = queue.removeFirst() + if (!visited.add(component.id)) continue + + component.moduleVersion?.let { module -> + if (module.name == JACKSON_DATABIND_MODULE) { + when (module.group) { + JACKSON_2_GROUP -> detected.add(JacksonVersion.JACKSON_2) + JACKSON_3_GROUP -> detected.add(JacksonVersion.JACKSON_3) + } + } + } + // Nothing more the graph can tell us once every known major has been seen. + if (detected.size == JacksonVersion.entries.size) break + + component.dependencies + .filterIsInstance() + .forEach { queue.addLast(it.selected) } + } + + return detected + } + + /** + * Resolves the Jackson versions to target: the parsed [override] when non-empty, otherwise the + * auto-[detected] set. An empty override means "no override" and falls back to detection. + * + * @throws InvalidUserDataException (failing the build) if [override] contains a value other than `"2"` or `"3"`. + */ + fun resolve( + override: List, + detected: Set, + ): Set = + if (override.isEmpty()) { + detected + } else { + override + .map { raw -> + try { + JacksonVersion.fromString(raw) + } catch (e: IllegalArgumentException) { + throw InvalidUserDataException("Invalid 'jacksonVersionOverride' value. ${e.message}", e) + } + }.toSet() + } +} diff --git a/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt index 2c24c9c45..77976e7ae 100644 --- a/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt +++ b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt @@ -174,6 +174,121 @@ class CodegenGradlePluginTest { assertThat(File(EXPECTED_DEFAULT_PATH + "NotSchema.java").exists()).isFalse() } + @Test + fun jacksonVersionOverrideIsApplied() { + // A valid override (["2", "3"]) should be accepted and supersede classpath detection. + val result = + GradleRunner + .create() + .withProjectDir(File("src/test/resources/test-project/")) + .withPluginClasspath() + .withArguments( + "--stacktrace", + "-c", + "smoke_test_settings_jackson_override.gradle", + "-b", + "build_with_jackson_override.gradle", + "clean", + "generateJava", + ).forwardOutput() + .build() + + assertThat(result.task(":generateJava")).extracting { it?.outcome }.isEqualTo(SUCCESS) + } + + @Test + fun invalidJacksonVersionOverrideFailsTheBuild() { + // An unsupported override value should fail the build with a clear message. + val result = + GradleRunner + .create() + .withProjectDir(File("src/test/resources/test-project/")) + .withPluginClasspath() + .withArguments( + "--stacktrace", + "-c", + "smoke_test_settings_invalid_jackson.gradle", + "-b", + "build_with_invalid_jackson_version.gradle", + "clean", + "generateJava", + ).forwardOutput() + .buildAndFail() + + assertThat(result.output).contains("Invalid 'jacksonVersionOverride' value") + assertThat(result.output).contains("Invalid Jackson version '4'") + } + + @Test + fun detectsJackson3FromCompileClasspath() { + // End-to-end detection: Jackson 3 is the only Jackson on the compile classpath + // and the generated Kotlin must use the tools.jackson annotation packages. + val projectDir = File("src/test/resources/test-project/") + val result = + GradleRunner + .create() + .withProjectDir(projectDir) + .withPluginClasspath() + .withArguments( + "--stacktrace", + "-c", + "smoke_test_settings_jackson3.gradle", + "-b", + "build_with_jackson3.gradle", + "clean", + "generateJava", + ).forwardOutput() + .build() + + assertThat(result.task(":generateJava")).extracting { it?.outcome }.isEqualTo(SUCCESS) + + val generatedTypes = + File(projectDir, "build/graphql/generated/sources/dgs-codegen/com/netflix/testproject/graphql/types") + .walk() + .filter { it.extension == "kt" } + .joinToString("\n") { it.readText() } + + assertThat(generatedTypes).contains("tools.jackson.databind") + assertThat(generatedTypes).doesNotContain("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize") + } + + @Test + fun generateJavaIsConfigurationCacheCompatible() { + val projectDir = File("src/test/resources/test-project/") + + // generateKotlinNullableClasses is enabled so Jackson version detection (the lazy + // rootComponent classpath walk) actually runs under the configuration cache. + fun run() = + GradleRunner + .create() + .withProjectDir(projectDir) + .withPluginClasspath() + .withArguments( + "--stacktrace", + "--configuration-cache", + "--configuration-cache-problems=fail", + "-c", + "smoke_test_settings_nullable.gradle", + "-b", + "build_with_nullable_classes.gradle", + "clean", + "generateJava", + ).forwardOutput() + .build() + + // Clear Gradle configuration cache before test run. + File(projectDir, ".gradle/configuration-cache").deleteRecursively() + + // First run must store the configuration cache (a non-cacheable task field, e.g. a live Configuration, would fail here). + val first = run() + assertThat(first.task(":generateJava")).extracting { it?.outcome }.isEqualTo(SUCCESS) + assertThat(first.output).contains("Configuration cache entry stored.") + + // Second run must reload and reuse the stored entry. + val second = run() + assertThat(second.output).contains("Reusing configuration cache.") + } + companion object { const val EXPECTED_PATH = "src/test/resources/test-project/build/graphql/generated/sources/dgs-codegen/com/netflix/testproject/graphql/types/" diff --git a/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/JacksonVersionDetectorTest.kt b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/JacksonVersionDetectorTest.kt new file mode 100644 index 000000000..89e656f6e --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/JacksonVersionDetectorTest.kt @@ -0,0 +1,54 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.graphql.dgs + +import com.netflix.graphql.dgs.codegen.JacksonVersion +import com.netflix.graphql.dgs.codegen.gradle.JacksonVersionDetector +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.gradle.api.InvalidUserDataException +import org.junit.jupiter.api.Test + +class JacksonVersionDetectorTest { + @Test + fun `resolve falls back to the detected versions when no override is given`() { + val detected = setOf(JacksonVersion.JACKSON_2) + assertThat(JacksonVersionDetector.resolve(emptyList(), detected)).isEqualTo(detected) + } + + @Test + fun `resolve uses the override and ignores detection when an override is given`() { + assertThat(JacksonVersionDetector.resolve(listOf("3"), setOf(JacksonVersion.JACKSON_2))) + .containsExactly(JacksonVersion.JACKSON_3) + } + + @Test + fun `resolve supports targeting both Jackson versions`() { + assertThat(JacksonVersionDetector.resolve(listOf("2", "3"), emptySet())) + .containsExactlyInAnyOrder(JacksonVersion.JACKSON_2, JacksonVersion.JACKSON_3) + } + + @Test + fun `resolve rejects an unsupported override value with a build-failing user error`() { + assertThatThrownBy { JacksonVersionDetector.resolve(listOf("4"), emptySet()) } + .isInstanceOf(InvalidUserDataException::class.java) + .hasMessageContaining("Invalid 'jacksonVersionOverride' value") + .hasMessageContaining("Invalid Jackson version '4'") + } +} diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_invalid_jackson_version.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_invalid_jackson_version.gradle new file mode 100644 index 000000000..8bd8186f7 --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_invalid_jackson_version.gradle @@ -0,0 +1,37 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +plugins { + id 'java' + id 'com.netflix.dgs.codegen' +} + +configurations { + // injected by Gradle Runner through test configuration, see CodegenGradlePluginTest + CodeGenConfiguration.exclude group: "com.netflix.graphql.dgs.codegen", module: "graphql-dgs-codegen-core" +} + +generateJava { + schemaPaths = ["${projectDir}/src/main/resources/schema"] + packageName = 'com.netflix.testproject.graphql' + generatedSourcesDir = "${projectDir}/build/graphql" + jacksonVersionOverride = ["4"] + generateKotlinNullableClasses = true +} + +codegen.clientCoreConventionsEnabled = false \ No newline at end of file diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_jackson3.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_jackson3.gradle new file mode 100644 index 000000000..95fce39f6 --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_jackson3.gradle @@ -0,0 +1,46 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +plugins { + id 'java' + id 'com.netflix.dgs.codegen' +} + +repositories { + mavenCentral() +} + +configurations { + // injected by Gradle Runner through test configuration, see CodegenGradlePluginTest + CodeGenConfiguration.exclude group: "com.netflix.graphql.dgs.codegen", module: "graphql-dgs-codegen-core" +} + +dependencies { + // Jackson 3 only on the compile classpath: detection must pick JACKSON_3 over the JACKSON_2 default. + implementation 'tools.jackson.core:jackson-databind:3.2.0' +} + +generateJava { + schemaPaths = ["${projectDir}/src/main/resources/schema"] + packageName = 'com.netflix.testproject.graphql' + generatedSourcesDir = "${projectDir}/build/graphql" + language = 'KOTLIN' + generateKotlinNullableClasses = true +} + +codegen.clientCoreConventionsEnabled = false diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_jackson_override.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_jackson_override.gradle new file mode 100644 index 000000000..6377b2ff5 --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_jackson_override.gradle @@ -0,0 +1,37 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +plugins { + id 'java' + id 'com.netflix.dgs.codegen' +} + +configurations { + // injected by Gradle Runner through test configuration, see CodegenGradlePluginTest + CodeGenConfiguration.exclude group: "com.netflix.graphql.dgs.codegen", module: "graphql-dgs-codegen-core" +} + +generateJava { + schemaPaths = ["${projectDir}/src/main/resources/schema"] + packageName = 'com.netflix.testproject.graphql' + generatedSourcesDir = "${projectDir}/build/graphql" + jacksonVersionOverride = ["2", "3"] + generateKotlinNullableClasses = true +} + +codegen.clientCoreConventionsEnabled = false diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_nullable_classes.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_nullable_classes.gradle new file mode 100644 index 000000000..284e52c07 --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_nullable_classes.gradle @@ -0,0 +1,38 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +plugins { + id 'java' + id 'com.netflix.dgs.codegen' +} + +configurations { + // injected by Gradle Runner through test configuration, see CodegenGradlePluginTest + CodeGenConfiguration.exclude group: "com.netflix.graphql.dgs.codegen", module: "graphql-dgs-codegen-core" +} + +// No jacksonVersionOverride: exercises the classpath-detection path. generateKotlinNullableClasses +// triggers detection (it is the only generation path that consumes the detected Jackson versions). +generateJava { + schemaPaths = ["${projectDir}/src/main/resources/schema"] + packageName = 'com.netflix.testproject.graphql' + generatedSourcesDir = "${projectDir}/build/graphql" + generateKotlinNullableClasses = true +} + +codegen.clientCoreConventionsEnabled = false diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_invalid_jackson.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_invalid_jackson.gradle new file mode 100644 index 000000000..aeffa386c --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_invalid_jackson.gradle @@ -0,0 +1,20 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +include 'smoke_test_settings.gradle' +rootProject.buildFileName = 'build_with_invalid_jackson_version.gradle' diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_jackson3.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_jackson3.gradle new file mode 100644 index 000000000..5ccd5a2a3 --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_jackson3.gradle @@ -0,0 +1,20 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +include 'smoke_test_settings.gradle' +rootProject.buildFileName = 'build_with_jackson3.gradle' diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_jackson_override.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_jackson_override.gradle new file mode 100644 index 000000000..226651f08 --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_jackson_override.gradle @@ -0,0 +1,20 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +include 'smoke_test_settings.gradle' +rootProject.buildFileName = 'build_with_jackson_override.gradle' diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_nullable.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_nullable.gradle new file mode 100644 index 000000000..eedc9f87d --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_nullable.gradle @@ -0,0 +1,20 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +include 'smoke_test_settings.gradle' +rootProject.buildFileName = 'build_with_nullable_classes.gradle'