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 4c5b85d09..1768fdaa8 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 @@ -75,6 +75,10 @@ class Kotlin2CodeGenTest { mapOf( "Currency" to "java.util.Currency", ) + "inputWithDefaultMap" -> + mapOf( + "JSON" to "kotlin.collections.Map", + ) else -> emptyMap() }, ), diff --git a/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/DgsClient.kt b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/DgsClient.kt new file mode 100644 index 000000000..353ea147b --- /dev/null +++ b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/DgsClient.kt @@ -0,0 +1,15 @@ +package com.netflix.graphql.dgs.codegen.cases.inputWithDefaultMap.expected + +import com.netflix.graphql.dgs.client.codegen.InputValueSerializerInterface +import com.netflix.graphql.dgs.codegen.GraphQLProjection +import com.netflix.graphql.dgs.codegen.cases.inputWithDefaultMap.expected.client.QueryProjection +import graphql.language.OperationDefinition +import kotlin.String + +@Generated +public object DgsClient { + public fun buildQuery(inputValueSerializer: InputValueSerializerInterface? = null, + _projection: QueryProjection.() -> QueryProjection): String = + GraphQLProjection.asQuery(OperationDefinition.Operation.QUERY, + QueryProjection(inputValueSerializer), _projection) +} diff --git a/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/DgsConstants.kt b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/DgsConstants.kt new file mode 100644 index 000000000..06f865c4b --- /dev/null +++ b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/DgsConstants.kt @@ -0,0 +1,31 @@ +package com.netflix.graphql.dgs.codegen.cases.inputWithDefaultMap.expected + +import kotlin.String + +@Generated +public object DgsConstants { + public const val QUERY_TYPE: String = "Query" + + @Generated + public object QUERY { + public const val TYPE_NAME: String = "Query" + + public const val Movies: String = "movies" + + @Generated + public object MOVIES_INPUT_ARGUMENT { + public const val Filter: String = "filter" + } + } + + @Generated + public object MOVIEFILTER { + public const val TYPE_NAME: String = "MovieFilter" + + public const val Metadata: String = "metadata" + + public const val Details: String = "details" + + public const val Variants: String = "variants" + } +} diff --git a/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/Generated.kt b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/Generated.kt new file mode 100644 index 000000000..d0110e7c7 --- /dev/null +++ b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/Generated.kt @@ -0,0 +1,7 @@ +package com.netflix.graphql.dgs.codegen.cases.inputWithDefaultMap.expected + +import kotlin.`annotation`.AnnotationRetention +import kotlin.`annotation`.Retention + +@Retention(value = AnnotationRetention.BINARY) +public annotation class Generated diff --git a/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/client/QueryProjection.kt b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/client/QueryProjection.kt new file mode 100644 index 000000000..1ec98f128 --- /dev/null +++ b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/client/QueryProjection.kt @@ -0,0 +1,17 @@ +package com.netflix.graphql.dgs.codegen.cases.inputWithDefaultMap.expected.client + +import com.netflix.graphql.dgs.client.codegen.InputValueSerializerInterface +import com.netflix.graphql.dgs.codegen.GraphQLProjection +import com.netflix.graphql.dgs.codegen.cases.inputWithDefaultMap.expected.Generated +import com.netflix.graphql.dgs.codegen.cases.inputWithDefaultMap.expected.types.MovieFilter + +@Generated +public class QueryProjection( + inputValueSerializer: InputValueSerializerInterface? = null, +) : GraphQLProjection(inputValueSerializer) { + public fun movies(filter: MovieFilter? = default("filter")): + QueryProjection { + field("movies", "filter" to filter) + return this + } +} diff --git a/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/types/MovieFilter.kt b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/types/MovieFilter.kt new file mode 100644 index 000000000..d855c6126 --- /dev/null +++ b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/types/MovieFilter.kt @@ -0,0 +1,27 @@ +package com.netflix.graphql.dgs.codegen.cases.inputWithDefaultMap.expected.types + +import com.fasterxml.jackson.`annotation`.JsonProperty +import com.netflix.graphql.dgs.codegen.GraphQLInput +import com.netflix.graphql.dgs.codegen.cases.inputWithDefaultMap.expected.Generated +import kotlin.Any +import kotlin.Pair +import kotlin.String +import kotlin.collections.List +import kotlin.collections.Map + +@Generated +public data class MovieFilter( + @JsonProperty("metadata") + public val metadata: Map = default>("metadata", + emptyMap()), + @JsonProperty("details") + public val details: Map? = default?>("details", + mapOf("title" to "Alien", "year" to 1_979, "tags" to listOf("sci-fi"), "director" to + mapOf("name" to "Ridley Scott"))), + @JsonProperty("variants") + public val variants: List?>? = default?>?>("variants", listOf(emptyMap(), mapOf("title" to "Aliens"))), +) : GraphQLInput() { + override fun fields(): List> = listOf("metadata" to metadata, "details" to + details, "variants" to variants) +} diff --git a/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/types/Query.kt b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/types/Query.kt new file mode 100644 index 000000000..abedcd0bb --- /dev/null +++ b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/expected/types/Query.kt @@ -0,0 +1,46 @@ +package com.netflix.graphql.dgs.codegen.cases.inputWithDefaultMap.expected.types + +import com.fasterxml.jackson.`annotation`.JsonIgnoreProperties +import com.fasterxml.jackson.`annotation`.JsonProperty +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.cases.inputWithDefaultMap.expected.Generated +import java.lang.IllegalStateException +import kotlin.String +import kotlin.jvm.JvmName + +@Generated +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +@JsonDeserialize(builder = Query.Builder::class) +public class Query( + movies: () -> String? = moviesDefault, +) { + private val __movies: () -> String? = movies + + @get:JvmName("getMovies") + public val movies: String? + get() = __movies.invoke() + + @Generated + public companion object { + private val moviesDefault: () -> String? = + { throw IllegalStateException("Field `movies` was not requested") } + } + + @Generated + @JsonPOJOBuilder + @JsonIgnoreProperties("__typename") + public class Builder { + private var movies: () -> String? = moviesDefault + + @JsonProperty("movies") + public fun withMovies(movies: String?): Builder = this.apply { + this.movies = { movies } + } + + public fun build(): Query = Query( + movies = movies, + ) + } +} diff --git a/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/schema.graphql b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/schema.graphql new file mode 100644 index 000000000..f8740e40b --- /dev/null +++ b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/cases/inputWithDefaultMap/schema.graphql @@ -0,0 +1,11 @@ +scalar JSON + +type Query { + movies(filter: MovieFilter): String +} + +input MovieFilter { + metadata: JSON! = {} + details: JSON = {title: "Alien", year: 1979, tags: ["sci-fi"], director: {name: "Ridley Scott"}} + variants: [JSON] = [{}, {title: "Aliens"}] +} diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataTypeGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataTypeGenerator.kt index b3cb70784..4b2afb1be 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataTypeGenerator.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataTypeGenerator.kt @@ -247,7 +247,12 @@ class InputTypeGenerator internal constructor( LOCALE -> localeCodeBlock(value, type) JAVA_URI -> uriCodeBlock(value, type) ClassName.LONG.box() -> longCodeBlock(value, type) - else -> defaultCodeBlock(value, type, inputTypeDefinitions) + else -> + if (value is ObjectValue && type.isMapLike) { + mapCodeBlock(value, type, inputTypeDefinitions) + } else { + defaultCodeBlock(value, type, inputTypeDefinitions) + } } private fun defaultCodeBlock( @@ -259,8 +264,8 @@ class InputTypeGenerator internal constructor( is BooleanValue -> CodeBlock.of("\$L", value.isValue) is IntValue -> CodeBlock.of("\$L", value.value) is StringValue -> { - // Only generate string literal default values for string types to prevent invalid Java initialization - if (type is ClassName && type == ClassName.get(String::class.java)) { + // Only generate string literal default values for String and Object types to prevent invalid Java initialization + if (type is ClassName && (type == ClassName.get(String::class.java) || type == ClassName.OBJECT)) { CodeBlock.of("\$S", value.value) } else { null @@ -277,7 +282,7 @@ class InputTypeGenerator internal constructor( "\$T.asList(\$L)", Arrays::class.java, CodeBlock.join( - value.values.map { generateCode(it, type.className, inputTypeDefinitions) }, + value.values.map { generateCode(it, type.listElementType, inputTypeDefinitions) }, ", ", ), ) @@ -285,11 +290,11 @@ class InputTypeGenerator internal constructor( is ObjectValue -> { val inputObjectDefinition = - inputTypeDefinitions.first { + inputTypeDefinitions.firstOrNull { val expectedCanonicalClassName = config.typeMapping[it.name] ?: "${config.packageNameTypes}.${it.name}" expectedCanonicalClassName == type.className.canonicalName() - } + } ?: error("$type cannot be created from $value, expected an input type or a Map") if (value.objectFields.isEmpty()) { CodeBlock.of("new \$T()", type) } else { @@ -325,6 +330,31 @@ class InputTypeGenerator internal constructor( else -> CodeBlock.of("\$L", value) } + private fun mapCodeBlock( + value: ObjectValue, + type: JavaTypeName, + inputTypeDefinitions: List, + ): CodeBlock { + if (value.objectFields.isEmpty()) { + return CodeBlock.of("\$T.emptyMap()", Collections::class.java) + } + val typeArguments = (type as? ParameterizedTypeName)?.typeArguments() + val keyType = typeArguments?.getOrNull(0) ?: ClassName.get(String::class.java) + val valueType = typeArguments?.getOrNull(1) ?: ClassName.OBJECT + return CodeBlock.of( + "new \$T<\$T, \$T>(){{\$L}}", + LinkedHashMap::class.java, + keyType, + valueType, + CodeBlock.join( + value.objectFields.map { + CodeBlock.of("put(\$S, \$L);", it.name, generateCode(it.value, valueType, inputTypeDefinitions)) + }, + "", + ), + ) + } + private fun bigDecimalCodeBlock( value: Value>, type: JavaTypeName, @@ -375,6 +405,17 @@ class InputTypeGenerator internal constructor( return CodeBlock.of("\$LL", value.value) } + private val JavaTypeName.isMapLike: Boolean + get() = this == ClassName.OBJECT || (if (this is ParameterizedTypeName) rawType() else this) == ClassName.get(Map::class.java) + + private val JavaTypeName.listElementType: JavaTypeName + get() = + if (this is ParameterizedTypeName && rawType() == ClassName.get(List::class.java)) { + typeArguments().first() + } else { + className + } + private val JavaTypeName.className: ClassName get() = when (this) { diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/shared/GenerateKotlinCode.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/shared/GenerateKotlinCode.kt index 7811d3666..c53caee0a 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/shared/GenerateKotlinCode.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/shared/GenerateKotlinCode.kt @@ -35,6 +35,7 @@ fun generateKotlinCode( ?: checkAndGetLongCodeBlock(value, type) ?: checkAndGetBigDecimalCodeBlock(value, type) ?: checkAndGetCurrencyCodeBlock(value, type) + ?: checkAndGetMapCodeBlock(value, type, inputTypeDefinitions, config, typeUtils) ?: when (value) { is BooleanValue -> CodeBlock.of("%L", value.isValue) is IntValue -> CodeBlock.of("%L", value.value) @@ -50,7 +51,7 @@ fun generateKotlinCode( value.values.joinToString { v -> generateKotlinCode( v, - type, + type.listElementType, inputTypeDefinitions, config, typeUtils, @@ -61,11 +62,11 @@ fun generateKotlinCode( is ObjectValue -> { val inputObjectDefinition = - inputTypeDefinitions.first { + inputTypeDefinitions.firstOrNull { val expectedCanonicalClassName = config.typeMapping[it.name] ?: "${config.packageNameTypes}.${it.name}" expectedCanonicalClassName == type.className.canonicalName - } + } ?: error("$type cannot be created from $value, expected an input type or a Map") CodeBlock.of( type.className.canonicalName + "(%L)", @@ -143,6 +144,54 @@ private fun checkAndGetCurrencyCodeBlock( null } +private fun checkAndGetMapCodeBlock( + value: Value>, + type: TypeName, + inputTypeDefinitions: Collection, + config: CodeGenConfig, + typeUtils: KotlinTypeUtils, +): CodeBlock? { + if (value !is ObjectValue || !type.isMapLike) { + return null + } + val isMutable = type.rawClassName?.canonicalName == MUTABLE_MAP.canonicalName + if (value.objectFields.isEmpty()) { + return CodeBlock.of(if (isMutable) "mutableMapOf()" else "emptyMap()") + } + val valueType = (type as? ParameterizedTypeName)?.typeArguments?.getOrNull(1) ?: ANY.copy(nullable = true) + return CodeBlock.of( + if (isMutable) "mutableMapOf(%L)" else "mapOf(%L)", + value.objectFields + .map { + CodeBlock.of( + "%S to %L", + it.name, + generateKotlinCode( + it.value, + valueType, + inputTypeDefinitions, + config, + typeUtils, + ), + ) + }.joinToCode(), + ) +} + +private val TypeName.rawClassName: ClassName? + get() = + when (this) { + is ClassName -> this + is ParameterizedTypeName -> rawType + else -> null + } + +private val TypeName.isMapLike: Boolean + get() = rawClassName?.canonicalName in setOf(MAP.canonicalName, MUTABLE_MAP.canonicalName, ANY.canonicalName) + +private val TypeName.listElementType: TypeName + get() = if (this is ParameterizedTypeName && rawType.canonicalName == LIST.canonicalName) typeArguments[0] else this + private val TypeName.className: ClassName get() = when (this) { 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 a001390ce..400b06cd0 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 @@ -6775,6 +6775,143 @@ It takes a title and such. }.hasMessage("java.math.BigDecimal cannot be created from BooleanValue{value=true}, expected String, Int or Float value") } + @ParameterizedTest + @ValueSource(booleans = [true, false]) + fun `The default empty object value for a scalar mapped to Map should result in an empty map`(trackInputFieldSet: Boolean) { + val schema = + """ + scalar JSON + + input Movie { + metadata: JSON = {} + } + """.trimIndent() + + val codeGenResult = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = BASE_PACKAGE_NAME, + typeMapping = mapOf("JSON" to "java.util.Map"), + trackInputFieldSet = trackInputFieldSet, + ), + ).generate() + val dataTypes = codeGenResult.javaDataTypes + + assertThat(dataTypes).hasSize(1) + + val fields = dataTypes[0].typeSpec().fieldSpecs() + assertThat(fields).hasSize(1) + + val metadataField = fields[0] + assertThat(metadataField.name()).isEqualTo("metadata") + if (trackInputFieldSet) { + assertThat(metadataField.initializer().toString()).isEqualTo("Optional.of(java.util.Collections.emptyMap())") + } else { + assertThat(metadataField.initializer().toString()).isEqualTo("java.util.Collections.emptyMap()") + } + + assertCompilesJava(codeGenResult) + } + + @Test + fun `The default object value for a scalar mapped to Map should result in a populated map`() { + val schema = + """ + scalar JSON + + input Movie { + metadata: JSON = {title: "Alien", year: 1979, rating: null, tags: ["sci-fi"], director: {name: "Ridley Scott"}} + } + """.trimIndent() + + val codeGenResult = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = BASE_PACKAGE_NAME, + typeMapping = mapOf("JSON" to "java.util.Map"), + ), + ).generate() + val dataTypes = codeGenResult.javaDataTypes + + assertThat(dataTypes).hasSize(1) + + val fields = dataTypes[0].typeSpec().fieldSpecs() + assertThat(fields).hasSize(1) + + val metadataField = fields[0] + assertThat(metadataField.name()).isEqualTo("metadata") + assertThat(metadataField.initializer().toString()).isEqualTo( + "new java.util.LinkedHashMap()" + + """{{put("title", "Alien");put("year", 1979);put("rating", null);put("tags", java.util.Arrays.asList("sci-fi"));""" + + """put("director", new java.util.LinkedHashMap(){{put("name", "Ridley Scott");}});}}""", + ) + + assertCompilesJava(codeGenResult) + } + + @Test + fun `The default list value should support objects for a scalar mapped to Map`() { + val schema = + """ + scalar JSON + + input Movie { + metadata: [JSON] = [{}, {title: "Alien"}] + } + """.trimIndent() + + val codeGenResult = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = BASE_PACKAGE_NAME, + typeMapping = mapOf("JSON" to "java.util.Map"), + ), + ).generate() + val dataTypes = codeGenResult.javaDataTypes + + assertThat(dataTypes).hasSize(1) + + val fields = dataTypes[0].typeSpec().fieldSpecs() + assertThat(fields).hasSize(1) + + val metadataField = fields[0] + assertThat(metadataField.name()).isEqualTo("metadata") + assertThat(metadataField.initializer().toString()).isEqualTo( + "java.util.Arrays.asList(java.util.Collections.emptyMap(), " + + """new java.util.LinkedHashMap(){{put("title", "Alien");}})""", + ) + + assertCompilesJava(codeGenResult) + } + + @Test + fun `Codegen should fail with nice message given object default value for a scalar not mapped to Map`() { + val schema = + """ + scalar JSON + + input Movie { + metadata: JSON = {} + } + """.trimIndent() + + assertThatThrownBy { + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = BASE_PACKAGE_NAME, + typeMapping = mapOf("JSON" to "com.fasterxml.jackson.databind.JsonNode"), + ), + ).generate() + }.hasMessage( + "com.fasterxml.jackson.databind.JsonNode cannot be created from ObjectValue{objectFields=[]}, " + + "expected an input type or a Map", + ) + } + @Test fun `Codegen should generate class implementing interface provided in extended type`() { val schema = diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt index 408c7e73c..143c4c57c 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt @@ -5067,6 +5067,179 @@ It takes a title and such. }.hasMessage("java.math.BigDecimal cannot be created from BooleanValue{value=true}, expected String, Int or Float value") } + @Test + fun `The default empty object value for a scalar mapped to Map should result in an empty map`() { + val schema = + """ + scalar JSON + + input Movie { + metadata: JSON = {} + } + """.trimIndent() + + val generatedResult = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = BASE_PACKAGE_NAME, + language = Language.KOTLIN, + typeMapping = mapOf("JSON" to "Map"), + ), + ).generate() + val dataTypes = generatedResult.kotlinDataTypes + + assertThat(dataTypes).hasSize(1) + + val type = dataTypes[0].members[0] as TypeSpec + val ctorSpec = type.primaryConstructor + assertThat(ctorSpec).isNotNull + assertThat(ctorSpec!!.parameters).hasSize(1) + + val metadataParam = ctorSpec.parameters[0] + assertThat(metadataParam.name).isEqualTo("metadata") + assertThat(metadataParam.type.toString()).isEqualTo("kotlin.collections.Map?") + assertThat(metadataParam.defaultValue.toString()).isEqualTo("emptyMap()") + + assertCompilesKotlin(generatedResult) + } + + @Test + fun `The default object value for a scalar mapped to Map should result in a populated map`() { + val schema = + """ + scalar JSON + + input Movie { + metadata: JSON = {title: "Alien", year: 1979, rating: null, tags: ["sci-fi"], director: {name: "Ridley Scott"}} + } + """.trimIndent() + + val generatedResult = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = BASE_PACKAGE_NAME, + language = Language.KOTLIN, + typeMapping = mapOf("JSON" to "Map"), + ), + ).generate() + val dataTypes = generatedResult.kotlinDataTypes + + assertThat(dataTypes).hasSize(1) + + val type = dataTypes[0].members[0] as TypeSpec + val ctorSpec = type.primaryConstructor + assertThat(ctorSpec).isNotNull + assertThat(ctorSpec!!.parameters).hasSize(1) + + val metadataParam = ctorSpec.parameters[0] + assertThat(metadataParam.name).isEqualTo("metadata") + assertThat(metadataParam.defaultValue.toString()).isEqualTo( + """mapOf("title" to "Alien", "year" to 1_979, "rating" to null, "tags" to listOf("sci-fi"), """ + + """"director" to mapOf("name" to "Ridley Scott"))""", + ) + + assertCompilesKotlin(generatedResult) + } + + @Test + fun `The default object value for a scalar mapped to MutableMap should result in a mutable map`() { + val schema = + """ + scalar JSON + + input Movie { + metadata: JSON = {title: "Alien"} + tags: JSON = {} + } + """.trimIndent() + + val generatedResult = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = BASE_PACKAGE_NAME, + language = Language.KOTLIN, + typeMapping = mapOf("JSON" to "MutableMap"), + ), + ).generate() + val dataTypes = generatedResult.kotlinDataTypes + + assertThat(dataTypes).hasSize(1) + + val type = dataTypes[0].members[0] as TypeSpec + val ctorSpec = type.primaryConstructor + assertThat(ctorSpec).isNotNull + assertThat(ctorSpec!!.parameters).hasSize(2) + assertThat(ctorSpec.parameters[0].defaultValue.toString()).isEqualTo("""mutableMapOf("title" to "Alien")""") + assertThat(ctorSpec.parameters[1].defaultValue.toString()).isEqualTo("mutableMapOf()") + + assertCompilesKotlin(generatedResult) + } + + @Test + fun `The default list value should support objects for a scalar mapped to Map`() { + val schema = + """ + scalar JSON + + input Movie { + metadata: [JSON] = [{}, {title: "Alien"}] + } + """.trimIndent() + + val generatedResult = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = BASE_PACKAGE_NAME, + language = Language.KOTLIN, + typeMapping = mapOf("JSON" to "Map"), + ), + ).generate() + val dataTypes = generatedResult.kotlinDataTypes + + assertThat(dataTypes).hasSize(1) + + val type = dataTypes[0].members[0] as TypeSpec + val ctorSpec = type.primaryConstructor + assertThat(ctorSpec).isNotNull + assertThat(ctorSpec!!.parameters).hasSize(1) + + val metadataParam = ctorSpec.parameters[0] + assertThat(metadataParam.name).isEqualTo("metadata") + assertThat(metadataParam.defaultValue.toString()).isEqualTo("""listOf(emptyMap(), mapOf("title" to "Alien"))""") + + assertCompilesKotlin(generatedResult) + } + + @Test + fun `Codegen should fail with nice message given object default value for a scalar not mapped to Map`() { + val schema = + """ + scalar JSON + + input Movie { + metadata: JSON = {} + } + """.trimIndent() + + assertThatThrownBy { + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = BASE_PACKAGE_NAME, + language = Language.KOTLIN, + typeMapping = mapOf("JSON" to "com.fasterxml.jackson.databind.JsonNode"), + ), + ).generate() + }.hasMessage( + "com.fasterxml.jackson.databind.JsonNode? cannot be created from ObjectValue{objectFields=[]}, " + + "expected an input type or a Map", + ) + } + @Test fun `Codegen should generate class implementing interface provided in extended type`() { val schema =