From 01a9e0d2d2f740240c6fc60f69fe4c47fadb0456 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Mon, 18 Mar 2024 09:51:15 +0000 Subject: [PATCH] Bug: empty string serde and Kotlin in 2.8.2 There is an issue in 2.8.2 when serializing an empty string to a non-null field in Kotlin. This works in 2.8.1, and was exposed by the eclipsestore guide https://github.com/micronaut-projects/micronaut-guides/pull/1443\#commits-pushed-0276ab1 --- doc-examples/example-java/build.gradle.kts | 3 ++ .../src/main/java/example/FruitCommand.java | 10 ++++ .../src/main/resources/logback.xml | 2 +- .../test/java/example/FruitCommandTest.java | 49 +++++++++++++++++++ .../example-kotlin-ksp/build.gradle.kts | 3 ++ .../src/main/kotlin/example/FruitCommand.kt | 10 ++++ .../test/kotlin/example/FruitCommandTest.kt | 47 ++++++++++++++++++ doc-examples/example-kotlin/build.gradle.kts | 3 ++ .../src/main/kotlin/example/FruitCommand.kt | 10 ++++ .../test/kotlin/example/FruitCommandTest.kt | 47 ++++++++++++++++++ 10 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 doc-examples/example-java/src/main/java/example/FruitCommand.java create mode 100644 doc-examples/example-java/src/test/java/example/FruitCommandTest.java create mode 100644 doc-examples/example-kotlin-ksp/src/main/kotlin/example/FruitCommand.kt create mode 100644 doc-examples/example-kotlin-ksp/src/test/kotlin/example/FruitCommandTest.kt create mode 100644 doc-examples/example-kotlin/src/main/kotlin/example/FruitCommand.kt create mode 100644 doc-examples/example-kotlin/src/test/kotlin/example/FruitCommandTest.kt diff --git a/doc-examples/example-java/build.gradle.kts b/doc-examples/example-java/build.gradle.kts index 832dcd9e3..0187f447c 100644 --- a/doc-examples/example-java/build.gradle.kts +++ b/doc-examples/example-java/build.gradle.kts @@ -10,10 +10,13 @@ micronaut { dependencies { annotationProcessor(projects.micronautSerdeProcessor) + annotationProcessor(mnValidation.micronaut.validation.processor) implementation(projects.micronautSerdeJackson) implementation(mn.micronaut.http.client) implementation(libs.oci.aidocument) + implementation(mnValidation.micronaut.validation) + implementation("jakarta.validation:jakarta.validation-api") runtimeOnly(mnLogging.logback.classic) diff --git a/doc-examples/example-java/src/main/java/example/FruitCommand.java b/doc-examples/example-java/src/main/java/example/FruitCommand.java new file mode 100644 index 000000000..0158010b3 --- /dev/null +++ b/doc-examples/example-java/src/main/java/example/FruitCommand.java @@ -0,0 +1,10 @@ +package example; + +import io.micronaut.core.annotation.Nullable; +import io.micronaut.serde.annotation.Serdeable; +import jakarta.validation.constraints.NotEmpty; + +@Serdeable +record FruitCommand( + @NotEmpty String name, @Nullable String description +) {} diff --git a/doc-examples/example-java/src/main/resources/logback.xml b/doc-examples/example-java/src/main/resources/logback.xml index afaebf8e1..44b79c40d 100644 --- a/doc-examples/example-java/src/main/resources/logback.xml +++ b/doc-examples/example-java/src/main/resources/logback.xml @@ -11,4 +11,4 @@ - \ No newline at end of file + diff --git a/doc-examples/example-java/src/test/java/example/FruitCommandTest.java b/doc-examples/example-java/src/test/java/example/FruitCommandTest.java new file mode 100644 index 000000000..4f9f9c12d --- /dev/null +++ b/doc-examples/example-java/src/test/java/example/FruitCommandTest.java @@ -0,0 +1,49 @@ +package example; + +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.runtime.server.EmbeddedServer; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.validation.Valid; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@MicronautTest +@Property(name = "spec.name", value = "FruitCommandTest") +class FruitCommandTest { + + @Test + void testCommandPost(EmbeddedServer embeddedServer) { + try(var client = embeddedServer.getApplicationContext().createBean(HttpClient.class, embeddedServer.getURL())) { + var ex = assertThrows( + HttpClientResponseException.class, + () -> client.toBlocking().exchange(HttpRequest.POST("/fruits", new FruitCommand("", "")), String.class) + ); + Map embedded = (Map) ex.getResponse().getBody(Map.class).get().get("_embedded"); + Object message = ((Map) ((List) embedded.get("errors")).get(0)).get("message"); + + assertEquals("fruitCommand.name: must not be empty", message); + } + } + + @Controller("/fruits") + @Requires(property = "spec.name", value = "FruitCommandTest") + static class FruitCommandController { + + @Post + FruitCommand save(@Valid @Body FruitCommand fruitCommand) { + return fruitCommand; + } + } +} diff --git a/doc-examples/example-kotlin-ksp/build.gradle.kts b/doc-examples/example-kotlin-ksp/build.gradle.kts index fe7232dfc..b9d7fc59d 100644 --- a/doc-examples/example-kotlin-ksp/build.gradle.kts +++ b/doc-examples/example-kotlin-ksp/build.gradle.kts @@ -13,9 +13,12 @@ micronaut { dependencies { ksp(projects.micronautSerdeProcessor) + ksp(mnValidation.micronaut.validation.processor) implementation(projects.micronautSerdeJackson) implementation(mn.micronaut.http.client) + implementation(mnValidation.micronaut.validation) + implementation("jakarta.validation:jakarta.validation-api") runtimeOnly(mnLogging.logback.classic) diff --git a/doc-examples/example-kotlin-ksp/src/main/kotlin/example/FruitCommand.kt b/doc-examples/example-kotlin-ksp/src/main/kotlin/example/FruitCommand.kt new file mode 100644 index 000000000..1f7a8c797 --- /dev/null +++ b/doc-examples/example-kotlin-ksp/src/main/kotlin/example/FruitCommand.kt @@ -0,0 +1,10 @@ +package example + +import io.micronaut.serde.annotation.Serdeable +import jakarta.validation.constraints.NotEmpty + +@Serdeable // <1> +data class FruitCommand( + @field:NotEmpty val name: String, // <2> + val description: String? = null // <3> +) diff --git a/doc-examples/example-kotlin-ksp/src/test/kotlin/example/FruitCommandTest.kt b/doc-examples/example-kotlin-ksp/src/test/kotlin/example/FruitCommandTest.kt new file mode 100644 index 000000000..d87c26a86 --- /dev/null +++ b/doc-examples/example-kotlin-ksp/src/test/kotlin/example/FruitCommandTest.kt @@ -0,0 +1,47 @@ +package example + +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import jakarta.inject.Singleton +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +@MicronautTest +@Property(name = "spec.name", value = "FruitCommandTest") +class FruitCommandTest { + + @Test + fun testCommandPost(embeddedServer: EmbeddedServer) { + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url).use { client -> + val ex = assertThrows { + client.toBlocking() + .exchange( + HttpRequest.POST("/fruits", FruitCommand("", "")) + ) + } + val embedded = ex.response.getBody(MutableMap::class.java).get().get("_embedded") as Map + val message = ((embedded["errors"] as List<*>)[0] as Map)["message"] + Assertions.assertEquals("fruitCommand.name: must not be empty", message) + } + } + + @Singleton + @Controller("/fruits") + @Requires(property = "spec.name", value = "FruitCommandTest") + class FruitCommandController { + + @Post + fun save(@Body fruitCommand: FruitCommand): FruitCommand { + return fruitCommand + } + } +} diff --git a/doc-examples/example-kotlin/build.gradle.kts b/doc-examples/example-kotlin/build.gradle.kts index 975a93c02..1a797c05f 100644 --- a/doc-examples/example-kotlin/build.gradle.kts +++ b/doc-examples/example-kotlin/build.gradle.kts @@ -12,9 +12,12 @@ micronaut { dependencies { kapt(projects.micronautSerdeProcessor) + kapt(mnValidation.micronaut.validation.processor) implementation(projects.micronautSerdeJackson) implementation(mn.micronaut.http.client) + implementation(mnValidation.micronaut.validation) + implementation("jakarta.validation:jakarta.validation-api") runtimeOnly(mnLogging.logback.classic) diff --git a/doc-examples/example-kotlin/src/main/kotlin/example/FruitCommand.kt b/doc-examples/example-kotlin/src/main/kotlin/example/FruitCommand.kt new file mode 100644 index 000000000..276079810 --- /dev/null +++ b/doc-examples/example-kotlin/src/main/kotlin/example/FruitCommand.kt @@ -0,0 +1,10 @@ +package example + +import io.micronaut.serde.annotation.Serdeable +import jakarta.validation.constraints.NotEmpty + +@Serdeable +data class FruitCommand( + @field:NotEmpty val name: String, + val description: String? = null +) diff --git a/doc-examples/example-kotlin/src/test/kotlin/example/FruitCommandTest.kt b/doc-examples/example-kotlin/src/test/kotlin/example/FruitCommandTest.kt new file mode 100644 index 000000000..def515c57 --- /dev/null +++ b/doc-examples/example-kotlin/src/test/kotlin/example/FruitCommandTest.kt @@ -0,0 +1,47 @@ +package example + +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import jakarta.inject.Singleton +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +@MicronautTest +@Property(name = "spec.name", value = "FruitCommandTest") +class FruitCommandTest { + + @Test + fun testCommandPost(embeddedServer: EmbeddedServer) { + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url).use { client -> + val ex = assertThrows { + client.toBlocking() + .exchange( + HttpRequest.POST("/fruits", FruitCommand("", "")) + ) + } + val embedded = ex.response.getBody(MutableMap::class.java).get().get("_embedded") as Map + val message = ((embedded["errors"] as List<*>)[0] as Map)["message"] + assertEquals("fruitCommand.name: must not be empty", message) + } + } + + @Singleton + @Controller("/fruits") + @Requires(property = "spec.name", value = "FruitCommandTest") + class FruitCommandController { + + @Post + fun save(@Body fruitCommand: FruitCommand): FruitCommand { + return fruitCommand + } + } +}