dgs-framework icon indicating copy to clipboard operation
dgs-framework copied to clipboard

bug: Kotlin inline value classes don't work as @InputArgument

Open krodyrobi opened this issue 4 years ago • 2 comments

Hello guys, New to graphql so may or may not be a configuration issue somewhere, or it might be a complicated multiparty problem.

Expected behavior

To be able to use kotlin inline value classes as custom scalar inputs and pass them as method inputs with @InputArgument.

Actual behavior

Various degrees of success, not able to use method definition as contract but dfe.getArgument works.

Steps to reproduce

// Fetcher

@DgsComponent
class SomethingFetcher {
    // Errors due to value class jvm mangling
    // Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'schema' defined in class path resource [com/netflix/graphql/dgs/autoconfig/DgsAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [graphql.schema.GraphQLSchema]: Factory method 'schema' threw exception; nested exception is graphql.AssertException: Name must be non-null, non-empty and match [_A-Za-z][_0-9A-Za-z]* - was 'something-ViFSl3M'
    @DgsQuery
    fun something(@InputArgument("color") color: Color?): Int = 1
    
    /**
     * {
     *   "errors": [
     *     {
     *       "message": "com.netflix.graphql.dgs.exceptions.DgsInvalidInputArgumentException: Specified type 'class java.lang.String' is invalid. Found com.example.valuescalar.backend.graphql.model.Color instead.",
     *       "locations": [],
     *       "path": [
     *         "something"
     *       ],
     *       "extensions": {
     *         "errorType": "INTERNAL"
     *       }
     *     }
     *   ],
     *   "data": {
     *     "teams": null
     *   }
     * }
     */
    @DgsQuery
    @Suppress("INAPPLICABLE_JVM_NAME") // Needed as @Component from spring stops @JvmName from being usable, not sure why this is happening in the first place
    @JvmName("something")
    fun something(@InputArgument("color") color: Color): Int = 1
    
    // WORKS but less nice
    @DgsQuery
    fun something(dataFetchingEnvironment: DataFetchingEnvironment): Int {
        val color: Color = dataFetchingEnvironment.getArgument("color")
        return 1
    }
}
// Color
@JvmInline
value class Color(val value: String) {
    init {
        require(value.matches("^#[0-9A-F]{6}$".toRegex()))
    }

    override fun toString(): String = value
}
// Scalar coercing

@DgsScalar(name = "Color")
internal class ColorScalar : Coercing<Color, String> {
    @Throws(CoercingParseValueException::class)
    override fun parseValue(input: Any): Color =
        runCatching { Color(serialize(input)) }
            .getOrElse { throw CoercingParseValueException("Invalid color.", it) }

    @Throws(CoercingParseLiteralException::class)
    override fun parseLiteral(input: Any): Color {
        val colorString = (input as? StringValue)?.value ?: throw CoercingParseLiteralException("Invalid color.")
        return runCatching { Color(colorString) }
            .getOrElse { throw CoercingParseLiteralException("Invalid color.", it) }
    }

    override fun serialize(dataFetcherResult: Any): String = dataFetcherResult.toString()
}
# Schema
type Query {
    something(color: Color): Int
}
scalar Color
// build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.4.5"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    kotlin("jvm") version "1.5.0"
    kotlin("plugin.spring") version "1.5.0"
    id("com.netflix.dgs.codegen") version "4.6.4"
}

group = "com.example.valuescalar"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")

    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")

    implementation(platform("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:3.12.1"))
    implementation("com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter")

    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks {
    withType<com.netflix.graphql.dgs.codegen.gradle.GenerateJavaTask> {
        schemaPaths = mutableListOf("${projectDir}/src/main/resources/schema")
        packageName = "com.example.valuescalar.backend.graphql.generated"
        typeMapping = mutableMapOf(
            "Color" to "com.example.valuescalar.backend.graphql.model.Color"
        )
    }

    withType<KotlinCompile> {
        kotlinOptions {
            freeCompilerArgs = listOf("-Xjsr305=strict")
            jvmTarget = "11"
        }
    }

    withType<Test> {
        useJUnitPlatform()
    }
}

krodyrobi avatar May 21 '21 09:05 krodyrobi

Thanks for reporting, will look into it.

srinivasankavitha avatar May 23 '21 21:05 srinivasankavitha

I looked into this a bit; the issue seems to be that we're using Java reflection (not Kotlin reflection) to work with input arguments.

If you define an argument like this:

@DgsData(parentType = "Query", field = "hello")
fun someFetcher(@InputArgument color: Color): String {
     return color.toString()
}

The Color argument is seen as String by reflection.

To get this to work we'll have to use Kotlin reflection instead. We might be able to switch to Kotlin reflection entirely, but we'll have to experiment with that further.

paulbakker avatar Aug 20 '21 19:08 paulbakker