Skip to content

Commit

Permalink
fix: add support for object default values to kotlin codegen
Browse files Browse the repository at this point in the history
  • Loading branch information
dwilkolek committed Apr 21, 2024
1 parent 95f27dc commit c521a26
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.netflix.graphql.dgs.codegen.cases.inputWithDefaultValueForObject.expected

public object DgsClient
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.netflix.graphql.dgs.codegen.cases.inputWithDefaultValueForObject.expected

import kotlin.String

public object DgsConstants {
public object PERSON {
public const val TYPE_NAME: String = "Person"

public const val Name: String = "name"

public const val Age: String = "age"

public const val Car: String = "car"
}

public object CAR {
public const val TYPE_NAME: String = "Car"

public const val Brand: String = "brand"
}

public object MOVIEFILTER {
public const val TYPE_NAME: String = "MovieFilter"

public const val Director: String = "director"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.netflix.graphql.dgs.codegen.cases.inputWithDefaultValueForObject.expected.types

import com.netflix.graphql.dgs.codegen.GraphQLInput
import kotlin.Any
import kotlin.Pair
import kotlin.String
import kotlin.collections.List

public class Car(
public val brand: String,
) : GraphQLInput() {
override fun fields(): List<Pair<String, Any?>> = listOf("brand" to brand)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.netflix.graphql.dgs.codegen.cases.inputWithDefaultValueForObject.expected.types

import com.netflix.graphql.dgs.codegen.GraphQLInput
import kotlin.Any
import kotlin.Pair
import kotlin.String
import kotlin.collections.List

public class MovieFilter(
public val director: Person? = default<MovieFilter, Person?>("director"),
) : GraphQLInput() {
override fun fields(): List<Pair<String, Any?>> = listOf("director" to director)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.netflix.graphql.dgs.codegen.cases.inputWithDefaultValueForObject.expected.types

import com.netflix.graphql.dgs.codegen.GraphQLInput
import kotlin.Any
import kotlin.Int
import kotlin.Pair
import kotlin.String
import kotlin.collections.List

public class Person(
public val name: String? = default<Person, String?>("name"),
public val age: Int? = default<Person, Int?>("age"),
public val car: Car? = default<Person, Car?>("car"),
) : GraphQLInput() {
override fun fields(): List<Pair<String, Any?>> = listOf("name" to name, "age" to age, "car" to
car)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
input Person {
name: String = ""
age: Int = 0
car: Car = { brand: "Ford" }
}

input Car {
brand: String! = "BMW"
}

input MovieFilter {
director: Person = { name: "Damian", car: { brand: "Tesla" } }
}
Original file line number Diff line number Diff line change
Expand Up @@ -411,12 +411,13 @@ class CodeGen(private val config: CodeGenConfig) {
}

private fun generateKotlinInputTypes(definitions: Collection<Definition<*>>): CodeGenResult {
return definitions.asSequence()
val inputTypeDefinitions = definitions
.filterIsInstance<InputObjectTypeDefinition>()
return inputTypeDefinitions.asSequence()
.excludeSchemaTypeExtension()
.filter { config.generateDataTypes || it.name in requiredTypeCollector.requiredTypes }
.map {
KotlinInputTypeGenerator(config, document).generate(it, findInputExtensions(it.name, definitions))
KotlinInputTypeGenerator(config, document).generate(it, findInputExtensions(it.name, definitions), inputTypeDefinitions)
}
.fold(CodeGenResult()) { t: CodeGenResult, u: CodeGenResult -> t.merge(u) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ class KotlinInputTypeGenerator(config: CodeGenConfig, document: Document) :
AbstractKotlinDataTypeGenerator(packageName = config.packageNameTypes, config = config, document = document) {
private val logger: Logger = LoggerFactory.getLogger(InputTypeGenerator::class.java)

fun generate(definition: InputObjectTypeDefinition, extensions: List<InputObjectTypeExtensionDefinition>): CodeGenResult {
fun generate(
definition: InputObjectTypeDefinition,
extensions: List<InputObjectTypeExtensionDefinition>,
inputTypeDefinitions: Collection<InputObjectTypeDefinition>
): CodeGenResult {
if (definition.shouldSkip(config)) {
return CodeGenResult()
}
Expand All @@ -86,7 +90,7 @@ class KotlinInputTypeGenerator(config: CodeGenConfig, document: Document) :
.filter(ReservedKeywordFilter.filterInvalidNames)
.map {
val type = typeUtils.findReturnType(it.type)
val defaultValue = it.defaultValue?.let { value -> generateCode(value, type) }
val defaultValue = it.defaultValue?.let { value -> generateCode(value, type, inputTypeDefinitions) }
Field(name = it.name, type = type, nullable = it.type !is NonNullType, default = defaultValue, description = it.description, directives = it.directives)
}.plus(
extensions.flatMap { it.inputValueDefinitions }.map {
Expand All @@ -97,7 +101,7 @@ class KotlinInputTypeGenerator(config: CodeGenConfig, document: Document) :
return generate(definition.name, fields, interfaces, document, definition.description, definition.directives)
}

private fun generateCode(value: Value<Value<*>>, type: KtTypeName): CodeBlock =
private fun generateCode(value: Value<Value<*>>, type: KtTypeName, inputTypeDefinitions: Collection<InputObjectTypeDefinition>): CodeBlock =
when (value) {
is BooleanValue -> CodeBlock.of("%L", value.isValue)
is IntValue -> CodeBlock.of("%L", value.value)
Expand All @@ -110,7 +114,25 @@ class KotlinInputTypeGenerator(config: CodeGenConfig, document: Document) :
is EnumValue -> CodeBlock.of("%M", MemberName(type.className, value.name))
is ArrayValue ->
if (value.values.isEmpty()) CodeBlock.of("emptyList()")
else CodeBlock.of("listOf(%L)", value.values.joinToString { v -> generateCode(v, type).toString() })
else CodeBlock.of(
"listOf(%L)",
value.values.joinToString { v -> generateCode(v, type, inputTypeDefinitions).toString() }
)

is ObjectValue -> {
val inputObjectDefinition = inputTypeDefinitions
.first { it.name == type.className.simpleName }
CodeBlock.of(
type.className.canonicalName + "(%L)",
value.objectFields.joinToString { objectProperty ->
val argumentType = checkNotNull(inputObjectDefinition.inputValueDefinitions.find { it.name == objectProperty.name }) {
"Property \"${objectProperty.name}\" does not exist in \"${inputObjectDefinition.name}\""
}.type
"${objectProperty.name} = ${generateCode(objectProperty.value, typeUtils.findReturnType(argumentType), inputTypeDefinitions)}"
}
)
}

else -> CodeBlock.of("%L", value)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3763,4 +3763,186 @@ It takes a title and such.
assertThat(typeSpec.primaryConstructor!!.parameters[0].defaultValue.toString()).isEqualTo("Locale.forLanguageTag(\"en-US\")")
assertCompilesKotlin(dataTypes)
}

@Test
fun `The default empty object value should result in constructor call`() {
val schema = """
input Movie {
director: Person = {}
}
input Person {
name: String = "Damian"
age: Int = 33
}
""".trimIndent()

val dataTypes = CodeGen(
CodeGenConfig(
schemas = setOf(schema),
packageName = basePackageName,
language = Language.KOTLIN
)
).generate().kotlinDataTypes

assertThat(dataTypes).hasSize(2)

val data = dataTypes[0]
assertThat(data.packageName).isEqualTo(typesPackageName)

val members = data.members
assertThat(members).hasSize(1)

val type = members[0] as TypeSpec
assertThat(type.name).isEqualTo("Movie")

val ctorSpec = type.primaryConstructor
assertThat(ctorSpec).isNotNull
assertThat(ctorSpec!!.parameters).hasSize(1)

val colorParam = ctorSpec.parameters[0]
assertThat(colorParam.name).isEqualTo("director")
assertThat(colorParam.type.toString()).isEqualTo("$typesPackageName.Person?")
assertThat(colorParam.defaultValue).isNotNull
assertThat(colorParam.defaultValue.toString()).isEqualTo("$typesPackageName.Person()")

assertCompilesKotlin(dataTypes)
}

@Test
fun `The default object with properties should result in constructor call with args`() {
val schema = """
input Movie {
director: Person = { name: "Harrison", car: { brand: "Ford" } }
}
input Person {
name: String = "Damian"
car: Car = { brand: "Tesla" }
}
input Car {
brand: String = "VW"
}
""".trimIndent()

val dataTypes = CodeGen(
CodeGenConfig(
schemas = setOf(schema),
packageName = basePackageName,
language = Language.KOTLIN
)
).generate().kotlinDataTypes

assertThat(dataTypes).hasSize(3)

val data = dataTypes[0]
assertThat(data.packageName).isEqualTo(typesPackageName)

val members = data.members
assertThat(members).hasSize(1)

val type = members[0] as TypeSpec
assertThat(type.name).isEqualTo("Movie")

val ctorSpec = type.primaryConstructor
assertThat(ctorSpec).isNotNull
assertThat(ctorSpec!!.parameters).hasSize(1)

val colorParam = ctorSpec.parameters[0]
assertThat(colorParam.name).isEqualTo("director")
assertThat(colorParam.type.toString()).isEqualTo("$typesPackageName.Person?")
assertThat(colorParam.defaultValue).isNotNull
assertThat(colorParam.defaultValue.toString()).isEqualTo("$typesPackageName.Person(name = \"Harrison\", car = $typesPackageName.Car(brand = \"Ford\"))")

assertCompilesKotlin(dataTypes)
}

@Test
fun `The default list value should support objects`() {
val schema = """
input Director {
movies: [Movie!]! = [{ name: "Braveheart" }, { name: "Matrix", year: 1999 }]
}
input Movie {
name: String = "Toy Story"
year: Int = 1995
}
""".trimIndent()

val dataTypes = CodeGen(
CodeGenConfig(
schemas = setOf(schema),
packageName = basePackageName,
language = Language.KOTLIN
)
).generate().kotlinDataTypes
assertThat(dataTypes).hasSize(2)

val data = dataTypes[0]
assertThat(data.packageName).isEqualTo(typesPackageName)

val members = data.members
assertThat(members).hasSize(1)

val type = members[0] as TypeSpec
assertThat(type.name).isEqualTo("Director")

val ctorSpec = type.primaryConstructor
assertThat(ctorSpec).isNotNull
assertThat(ctorSpec!!.parameters).hasSize(1)

val colorParam = ctorSpec.parameters[0]
assertThat(colorParam.name).isEqualTo("movies")
assertThat(colorParam.type.toString()).isEqualTo("kotlin.collections.List<$typesPackageName.Movie>")
assertThat(colorParam.defaultValue).isNotNull
assertThat(colorParam.defaultValue.toString()).isEqualTo("""listOf($typesPackageName.Movie(name = "Braveheart"), $typesPackageName.Movie(name = "Matrix", year = 1_999))""")

assertCompilesKotlin(dataTypes)
}

@Test
fun `The default object value should call constructor from typeMapping`() {
val schema = """
input Movie {
director: Person = { name: "Harrison" }
}
input Person {
name: String = "Damian"
age: Int = 33
}
""".trimIndent()

val dataTypes = CodeGen(
CodeGenConfig(
schemas = setOf(schema),
packageName = basePackageName,
language = Language.KOTLIN,
typeMapping = mapOf("Person" to "mypackage.Person")
)
).generate().kotlinDataTypes

assertThat(dataTypes).hasSize(1)

val data = dataTypes[0]
assertThat(data.packageName).isEqualTo(typesPackageName)

val members = data.members
assertThat(members).hasSize(1)

val type = members[0] as TypeSpec
assertThat(type.name).isEqualTo("Movie")

val ctorSpec = type.primaryConstructor
assertThat(ctorSpec).isNotNull
assertThat(ctorSpec!!.parameters).hasSize(1)

val colorParam = ctorSpec.parameters[0]
assertThat(colorParam.name).isEqualTo("director")
assertThat(colorParam.type.toString()).isEqualTo("mypackage.Person?")
assertThat(colorParam.defaultValue).isNotNull
assertThat(colorParam.defaultValue.toString()).isEqualTo("mypackage.Person(name = \"Harrison\")")
}
}

0 comments on commit c521a26

Please sign in to comment.