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 193c89b8..96cc8573 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 @@ -145,6 +145,7 @@ class CodeGen private constructor( ) } + private val dependencyTypeMappings = mutableMapOf() private val document = buildDocument() private val schemaIndex = SchemaIndex(document) private val requiredTypes: Set by lazy { @@ -164,6 +165,13 @@ class CodeGen private constructor( typeName in requiredTypes fun generate(): CodeGenResult { + // Apply dependency mappings at generation time so callers can change their mappings after + // constructing CodeGen. User-provided mappings take precedence over dependency mappings. + // Without dependency mappings the caller's map is left untouched, as in 8.7.0. + if (dependencyTypeMappings.isNotEmpty()) { + config.typeMapping = dependencyTypeMappings + config.typeMapping + } + val generated = when (config.language) { Language.JAVA -> generateJava() @@ -267,10 +275,10 @@ class CodeGen private constructor( val props = Properties() props.load(typeMappingInput) - // Add the new type mappings from dependencies to existing type mappings. - // The user provided config overrides mappings from the dependencies. - @Suppress("UNCHECKED_CAST") - config.typeMapping = (props as Map).plus(config.typeMapping) + // Earlier dependencies take precedence over later ones, matching the + // previous merge order. User mappings are applied in generate(). + val mappings = props.stringPropertyNames().associateWith(props::getProperty) + dependencyTypeMappings.putAll(mappings + dependencyTypeMappings) } } diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodeGenTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodeGenTest.kt index ce2f24dd..a001390c 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodeGenTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodeGenTest.kt @@ -6883,6 +6883,143 @@ It takes a title and such. ).isEqualTo("java.lang.String") } + @Test + fun `Codegen should apply dependency type mappings after caller changes config`() { + val typeMappingDependency = + createSchemaJar( + """ + type Query { invoice: Invoice } + type Invoice { money: Money } + type Money { amount: Int } + """.trimIndent(), + "Money=com.acme.Money", + ) + val config = + CodeGenConfig( + schemaJarFilesFromDependencies = listOf(typeMappingDependency), + packageName = BASE_PACKAGE_NAME, + ) + val codeGen = CodeGen(config) + + assertThat(config.typeMapping).isEmpty() + + config.typeMapping = mapOf("Other" to "x.Other") + + val result = codeGen.generate() + + assertThat(config.typeMapping) + .containsEntry("Money", "com.acme.Money") + .containsEntry("Other", "x.Other") + assertThat(result.javaDataTypes).noneMatch { it.typeSpec().name() == "Money" } + assertThat( + result.javaDataTypes + .single { it.typeSpec().name() == "Invoice" } + .typeSpec() + .fieldSpecs() + .single() + .type() + .toString(), + ).isEqualTo("com.acme.Money") + + val repeatedResult = codeGen.generate() + + assertThat(config.typeMapping) + .containsEntry("Money", "com.acme.Money") + .containsEntry("Other", "x.Other") + assertThat(repeatedResult.javaDataTypes.map { it.typeSpec().toString() }) + .isEqualTo(result.javaDataTypes.map { it.typeSpec().toString() }) + + config.typeMapping = mapOf("Money" to "x.Money", "Other" to "x.Other") + + assertThat(invoiceMoneyType(codeGen.generate())).isEqualTo("x.Money") + assertThat(config.typeMapping).containsEntry("Money", "x.Money") + } + + @Test + fun `Codegen should honor caller map mutations between generate calls without dependency mappings`() { + val schema = + """ + type Query { invoice: Invoice } + type Invoice { money: Money } + type Money { amount: Int } + """.trimIndent() + val callerMap = mutableMapOf() + val config = CodeGenConfig(schemas = setOf(schema), packageName = BASE_PACKAGE_NAME, typeMapping = callerMap) + val codeGen = CodeGen(config) + + assertThat(invoiceMoneyType(codeGen.generate())).isEqualTo("$BASE_PACKAGE_NAME.types.Money") + + callerMap["Money"] = "com.acme.Money" + + assertThat(invoiceMoneyType(codeGen.generate())).isEqualTo("com.acme.Money") + assertThat(config.typeMapping).isSameAs(callerMap) + } + + @Test + fun `Codegen should honor caller map mutations when dependency jar has no type mappings`() { + val schemaJar = + createSchemaJar( + """ + type Query { invoice: Invoice } + type Invoice { money: Money } + type Money { amount: Int } + """.trimIndent(), + ) + val callerMap = mutableMapOf() + val config = + CodeGenConfig( + schemaJarFilesFromDependencies = listOf(schemaJar), + packageName = BASE_PACKAGE_NAME, + typeMapping = callerMap, + ) + val codeGen = CodeGen(config) + + assertThat(invoiceMoneyType(codeGen.generate())).isEqualTo("$BASE_PACKAGE_NAME.types.Money") + + callerMap["Money"] = "com.acme.Money" + + assertThat(invoiceMoneyType(codeGen.generate())).isEqualTo("com.acme.Money") + assertThat(config.typeMapping).isSameAs(callerMap) + } + + private fun invoiceMoneyType(result: CodeGenResult): String = + result.javaDataTypes + .single { it.typeSpec().name() == "Invoice" } + .typeSpec() + .fieldSpecs() + .single() + .type() + .toString() + + @Test + fun `Codegen should preserve earlier dependency type mapping precedence`() { + val firstDependency = + createSchemaJar( + """ + type Query { item: Item } + type Item { amount: Int } + """.trimIndent(), + "Item=com.acme.FirstItem", + ) + val secondDependency = + createSchemaJar( + """ + extend type Query { other: Other } + type Other { amount: Int } + """.trimIndent(), + "Item=com.acme.SecondItem", + ) + val config = + CodeGenConfig( + schemaJarFilesFromDependencies = listOf(firstDependency, secondDependency), + packageName = BASE_PACKAGE_NAME, + ) + + CodeGen(config).generate() + + assertThat(config.typeMapping).containsEntry("Item", "com.acme.FirstItem") + } + @Test fun `Codegen should generate schema with unsigned int`() { val schema =