diff --git a/graphql-dgs-codegen-core/build.gradle b/graphql-dgs-codegen-core/build.gradle index 2792da50..f9d38a5e 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:latest.release' } application { 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 9a28512b..17c73c05 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 @@ -20,6 +20,7 @@ package com.netflix.graphql.dgs.codegen import com.netflix.graphql.dgs.codegen.generators.java.* import com.netflix.graphql.dgs.codegen.generators.kotlin.* +import com.netflix.graphql.dgs.codegen.generators.kotlin.configureJacksonVersion import com.netflix.graphql.dgs.codegen.generators.kotlin2.* import com.netflix.graphql.dgs.codegen.generators.shared.DocFileSpec import com.netflix.graphql.dgs.codegen.generators.shared.DocGenerator @@ -75,6 +76,8 @@ class CodeGen( ) fun generate(): CodeGenResult { + configureJacksonVersion(config.jacksonVersions) + loadTypeMappingsFromDependencies() val codeGenResult = @@ -582,6 +585,7 @@ class CodeGenConfig( var addDeprecatedAnnotation: Boolean = false, var trackInputFieldSet: Boolean = false, var generateJSpecifyAnnotations: Boolean = false, + var jacksonVersions: Set = emptySet(), ) { val packageNameClient: String = "$packageName.$subPackageNameClient" @@ -618,6 +622,11 @@ enum class Language { KOTLIN, } +enum class JacksonVersion { + JACKSON_2, + JACKSON_3, +} + 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 0f285c4f..181e4fda 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.PackageParserUtil import com.netflix.graphql.dgs.codegen.generators.shared.ParserConstants @@ -46,6 +45,60 @@ import graphql.language.StringValue import graphql.language.Value import java.lang.IllegalArgumentException +private var configuredVersions: Set = emptySet() + +/** + * Configure the Jackson versions from project configuration. + * Called by the Gradle plugin after inspecting project dependencies. + */ +fun configureJacksonVersion(jacksonVersions: Set) { + configuredVersions = jacksonVersions +} + +/** + * Which Jackson versions to generate annotations for. + * + * 1. Use configured versions from Gradle plugin (inferred from project dependencies) if available + * 2. Default to Jackson 2 for backwards compatibility + */ +private fun getJacksonVersions(): Set = + // Use configured versions if available and non-empty + configuredVersions.takeIf { it.isNotEmpty() } ?: setOf(JacksonVersion.JACKSON_2) + +/** + * Get the ClassName(s) for JsonDeserialize annotation based on detected Jackson version(s). + * Jackson 2: com.fasterxml.jackson.databind.annotation.JsonDeserialize + * Jackson 3: tools.jackson.databind.annotation.JsonDeserialize + */ +private fun getJsonDeserializeClasses(): List { + val versions = getJacksonVersions() + return buildList { + if (JacksonVersion.JACKSON_2 in versions) { + add(ClassName("com.fasterxml.jackson.databind.annotation", "JsonDeserialize")) + } + if (JacksonVersion.JACKSON_3 in versions) { + add(ClassName("tools.jackson.databind.annotation", "JsonDeserialize")) + } + } +} + +/** + * Get the ClassName(s) for JsonPOJOBuilder annotation based on detected Jackson version(s). + * Jackson 2: com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder + * Jackson 3: tools.jackson.databind.annotation.JsonPOJOBuilder + */ +private fun getJsonPOJOBuilderClasses(): List { + val versions = getJacksonVersions() + return buildList { + if (JacksonVersion.JACKSON_2 in versions) { + add(ClassName("com.fasterxml.jackson.databind.annotation", "JsonPOJOBuilder")) + } + if (JacksonVersion.JACKSON_3 in versions) { + add(ClassName("tools.jackson.databind.annotation", "JsonPOJOBuilder")) + } + } +} + fun sanitizeKotlinIdentifier(name: String): String = if (name == "_") { "underscoreField_" @@ -134,25 +187,38 @@ fun jsonSubTypesAnnotation(subTypes: Collection): AnnotationSpec { * ``` * @JsonDeserialize(builder = Movie.Builder::class) * ``` + * ``` + * @FasterxmlJacksonDatabindAnnotationJsonDeserialize(builder = Movie.Builder::class) + * @ToolsJacksonDatabindAnnotationJsonDeserialize(builder = Movie.Builder::class) + * ``` */ -fun jsonDeserializeAnnotation(builderType: ClassName): AnnotationSpec = - AnnotationSpec - .builder(JsonDeserialize::class) - .addMember("builder = %T::class", builderType) - .build() +fun jsonDeserializeAnnotations(builderType: ClassName): List = + getJsonDeserializeClasses().map { jsonDeserializeClass -> + AnnotationSpec + .builder(jsonDeserializeClass) + .addMember("builder = %T::class", builderType) + .build() + } /** - * Generate a [JsonPOJOBuilder] annotation for the builder class. + * Generate [JsonPOJOBuilder] annotations for all available Jackson versions. + * Jackson 2: com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder + * Jackson 3: tools.jackson.databind.annotation.JsonPOJOBuilder + * When both versions are detected, generates annotations for both. * - * Example generated annotation: + * Example generated annotations: * ``` * @JsonPOJOBuilder * ``` + * ``` + * @FasterxmlJacksonDatabindAnnotationJsonPOJOBuilder + * @ToolsJacksonDatabindAnnotationJsonPOJOBuilder + * ``` */ -fun jsonBuilderAnnotation(): AnnotationSpec = - AnnotationSpec - .builder(JsonPOJOBuilder::class) - .build() +fun jsonBuilderAnnotations(): List = + getJsonPOJOBuilderClasses().map { jsonPOJOBuilderClass -> + AnnotationSpec.builder(jsonPOJOBuilderClass).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 c7adcf46..25af5c15 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,8 +134,10 @@ fun generateKotlin2DataTypes( TypeSpec .classBuilder("Builder") .addOptionalGeneratedAnnotation(config) - .addAnnotation(jsonBuilderAnnotation()) - .addAnnotation(jsonIgnorePropertiesAnnotation("__typename")) + .apply { + jsonBuilderAnnotations().forEach { addAnnotation(it) } + addAnnotation(jsonIgnorePropertiesAnnotation("__typename")) + } // add a backing property for each field .addProperties( fields.map { field -> @@ -193,11 +195,9 @@ fun generateKotlin2DataTypes( } // add jackson annotations .addAnnotation(disableJsonTypeInfoAnnotation()) - .addAnnotation( - jsonDeserializeAnnotation( - builderClassName, - ), - ) + .apply { + jsonDeserializeAnnotations(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 00000000..6727584f --- /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/GenerateJavaTask.kt b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt index 8a863814..372afb26 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 @@ -182,6 +182,8 @@ open class GenerateJavaTask logger.info("Processing $it") } + val jacksonVersions = JacksonVersionDetector.detectVersions(project) + val config = CodeGenConfig( schemas = emptySet(), @@ -224,6 +226,7 @@ open class GenerateJavaTask javaGenerateAllConstructor = javaGenerateAllConstructor, trackInputFieldSet = trackInputFieldSet, generateJSpecifyAnnotations = generateJSpecifyAnnotations, + jacksonVersions = jacksonVersions, ) 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 00000000..cc1e5e95 --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/JacksonVersionDetector.kt @@ -0,0 +1,67 @@ +/* + * + * 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.Project +import org.gradle.api.logging.Logging + +object JacksonVersionDetector { + private val logger = Logging.getLogger(JacksonVersionDetector::class.java) + + /** + * Detects which Jackson versions are present in the project's compile classpath. + */ + fun detectVersions(project: Project): Set { + val configurationsToCheck = + listOfNotNull( + project.configurations.findByName("compileClasspath"), + ).filter { it.isCanBeResolved } + + val result = mutableSetOf() + + for (configuration in configurationsToCheck) { + try { + val dependencies = configuration.resolvedConfiguration.resolvedArtifacts + for (artifact in dependencies) { + val id = artifact.moduleVersion.id + if (id.group == "com.fasterxml.jackson.core" && id.name == "jackson-databind") { + val version = id.version + logger.info("DGS Codegen: Found Jackson 2 ($version) in ${configuration.name}") + result.add(JacksonVersion.JACKSON_2) + } + if (id.group == "tools.jackson.core" && id.name == "jackson-databind") { + logger.info("DGS Codegen: Found Jackson 3 (${id.version}) in ${configuration.name}") + result.add(JacksonVersion.JACKSON_3) + } + } + } catch (e: Exception) { + logger.debug("Could not resolve configuration ${configuration.name}: ${e.message}") + } + } + + if (result.size == 2) { + logger.info("DGS Codegen: Both Jackson 2 and 3 detected in project dependencies. Will generate annotations for both.") + } else if (result.isEmpty()) { + logger.warn("DGS Codegen: Could not detect Jackson version from project dependencies. Defaulting to Jackson 2 annotations.") + } + + return result + } +}