From 6b6cb77a8bbfc712f5d4e2561bc70a7f6e3a2013 Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Fri, 5 Jan 2024 14:16:55 +0100 Subject: [PATCH] Add custom Kotlin serializers for ValidationReport and Violation --- .../KotlinSerializationProcessor.java | 17 +- .../runtime/pom.xml | 5 + .../KotlinSerializationMessageBodyWriter.kt | 4 +- .../ValidationJsonBuilderCustomizer.kt | 21 ++ .../runtime/ViolationReportSerializer.kt | 72 +++++++ .../ViolationReportViolationSerializer.kt | 51 +++++ integration-tests/pom.xml | 1 + .../pom.xml | 180 ++++++++++++++++++ .../io/quarkus/it/rest/ValidationResource.kt | 21 ++ .../src/main/resources/application.properties | 3 + .../io/quarkus/it/rest/client/BasicTest.kt | 25 +++ .../io/quarkus/it/rest/client/BasicTestIT.kt | 5 + 12 files changed, 398 insertions(+), 7 deletions(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/runtime/ValidationJsonBuilderCustomizer.kt create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/runtime/ViolationReportSerializer.kt create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/runtime/ViolationReportViolationSerializer.kt create mode 100644 integration-tests/rest-client-reactive-kotlin-serialization-with-validator/pom.xml create mode 100644 integration-tests/rest-client-reactive-kotlin-serialization-with-validator/src/main/kotlin/io/quarkus/it/rest/ValidationResource.kt create mode 100644 integration-tests/rest-client-reactive-kotlin-serialization-with-validator/src/main/resources/application.properties create mode 100644 integration-tests/rest-client-reactive-kotlin-serialization-with-validator/src/test/kotlin/io/quarkus/it/rest/client/BasicTest.kt create mode 100644 integration-tests/rest-client-reactive-kotlin-serialization-with-validator/src/test/kotlin/io/quarkus/it/rest/client/BasicTestIT.kt diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/deployment/src/main/java/io/quarkus/resteasy/reactive/kotlin/serialization/deployment/KotlinSerializationProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/deployment/src/main/java/io/quarkus/resteasy/reactive/kotlin/serialization/deployment/KotlinSerializationProcessor.java index b11780c04f24f..e57370512695a 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/deployment/src/main/java/io/quarkus/resteasy/reactive/kotlin/serialization/deployment/KotlinSerializationProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/deployment/src/main/java/io/quarkus/resteasy/reactive/kotlin/serialization/deployment/KotlinSerializationProcessor.java @@ -10,12 +10,15 @@ import jakarta.ws.rs.core.MediaType; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.resteasy.reactive.common.deployment.ServerDefaultProducesHandlerBuildItem; import io.quarkus.resteasy.reactive.kotlin.serialization.runtime.KotlinSerializationMessageBodyReader; import io.quarkus.resteasy.reactive.kotlin.serialization.runtime.KotlinSerializationMessageBodyWriter; +import io.quarkus.resteasy.reactive.kotlin.serialization.runtime.ValidationJsonBuilderCustomizer; import io.quarkus.resteasy.reactive.spi.MessageBodyReaderBuildItem; import io.quarkus.resteasy.reactive.spi.MessageBodyWriterBuildItem; @@ -25,11 +28,15 @@ public class KotlinSerializationProcessor { public void additionalProviders( BuildProducer additionalBean, BuildProducer additionalReaders, - BuildProducer additionalWriters) { - additionalBean.produce(AdditionalBeanBuildItem.builder() - .addBeanClass(KotlinSerializationMessageBodyReader.class.getName()) - .addBeanClass(KotlinSerializationMessageBodyWriter.class.getName()) - .setUnremovable().build()); + BuildProducer additionalWriters, + Capabilities capabilities) { + AdditionalBeanBuildItem.Builder builder = AdditionalBeanBuildItem.builder() + .addBeanClasses(KotlinSerializationMessageBodyReader.class.getName(), + KotlinSerializationMessageBodyWriter.class.getName()); + if (capabilities.isPresent(Capability.HIBERNATE_VALIDATOR)) { + builder.addBeanClass(ValidationJsonBuilderCustomizer.class.getName()); + } + additionalBean.produce(builder.setUnremovable().build()); additionalReaders.produce(new MessageBodyReaderBuildItem( KotlinSerializationMessageBodyReader.class.getName(), Object.class.getName(), List.of( MediaType.APPLICATION_JSON), diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/pom.xml index 8a26a26c73fe6..051f526603966 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/pom.xml +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/pom.xml @@ -23,6 +23,11 @@ io.quarkus quarkus-resteasy-reactive + + io.quarkus + quarkus-hibernate-validator + true + diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/runtime/KotlinSerializationMessageBodyWriter.kt b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/runtime/KotlinSerializationMessageBodyWriter.kt index 8c03b414962a6..4bd2d6b23b78c 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/runtime/KotlinSerializationMessageBodyWriter.kt +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/runtime/KotlinSerializationMessageBodyWriter.kt @@ -31,7 +31,7 @@ class KotlinSerializationMessageBodyWriter(private val json: Json) : if (o is String) { // YUK: done in order to avoid adding extra quotes... entityStream.write(o.toByteArray(StandardCharsets.UTF_8)) } else { - json.encodeToStream(serializer(genericType), o, entityStream) + json.encodeToStream(json.serializersModule.serializer(genericType), o, entityStream) } } @@ -42,7 +42,7 @@ class KotlinSerializationMessageBodyWriter(private val json: Json) : if (o is String) { // YUK: done in order to avoid adding extra quotes... stream.write(o.toByteArray(StandardCharsets.UTF_8)) } else { - json.encodeToStream(serializer(genericType), o, stream) + json.encodeToStream(json.serializersModule.serializer(genericType), o, stream) } // we don't use try-with-resources because that results in writing to the http output // without the exception mapping coming into play diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/runtime/ValidationJsonBuilderCustomizer.kt b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/runtime/ValidationJsonBuilderCustomizer.kt new file mode 100644 index 0000000000000..4d6e0da918600 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/runtime/ValidationJsonBuilderCustomizer.kt @@ -0,0 +1,21 @@ +package io.quarkus.resteasy.reactive.kotlin.serialization.runtime + +import io.quarkus.resteasy.reactive.kotlin.serialization.common.JsonBuilderCustomizer +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.JsonBuilder +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.contextual +import kotlinx.serialization.modules.plus + +class ValidationJsonBuilderCustomizer : JsonBuilderCustomizer { + @ExperimentalSerializationApi + override fun customize(jsonBuilder: JsonBuilder) { + jsonBuilder.serializersModule = + jsonBuilder.serializersModule.plus( + SerializersModule { + contextual(ViolationReportSerializer) + contextual(ViolationReportViolationSerializer) + } + ) + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/runtime/ViolationReportSerializer.kt b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/runtime/ViolationReportSerializer.kt new file mode 100644 index 0000000000000..bec3923ce94ef --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/runtime/ViolationReportSerializer.kt @@ -0,0 +1,72 @@ +package io.quarkus.resteasy.reactive.kotlin.serialization.runtime + +import io.quarkus.hibernate.validator.runtime.jaxrs.ViolationReport +import jakarta.ws.rs.core.Response +import kotlinx.serialization.* +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.listSerialDescriptor +import kotlinx.serialization.descriptors.serialDescriptor +import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure +import kotlinx.serialization.encoding.encodeStructure + +@OptIn(ExperimentalSerializationApi::class) +@Serializer(forClass = ViolationReport::class) +object ViolationReportSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("io.quarkus.hibernate.validator.runtime.jaxrs.ViolationReport") { + element("title", serialDescriptor()) + element("status", serialDescriptor()) + element( + "violations", + listSerialDescriptor(ListSerializer(ViolationReportViolationSerializer).descriptor) + ) + } + + override fun deserialize(decoder: Decoder): ViolationReport { + return decoder.decodeStructure(descriptor) { + var title: String? = null + var status: Int? = null + var violations: List = emptyList() + + loop@ while (true) { + when (val index = decodeElementIndex(descriptor)) { + DECODE_DONE -> break@loop + 0 -> title = decodeStringElement(descriptor, 0) + 1 -> status = decodeIntElement(descriptor, 1) + 2 -> + violations = + decodeSerializableElement( + descriptor, + 2, + ListSerializer(ViolationReportViolationSerializer) + ) + else -> throw SerializationException("Unexpected index $index") + } + } + + ViolationReport( + requireNotNull(title), + status?.let { Response.Status.fromStatusCode(it) }, + violations + ) + } + } + + override fun serialize(encoder: Encoder, value: ViolationReport) { + encoder.encodeStructure(descriptor) { + encodeStringElement(descriptor, 0, value.title) + encodeIntElement(descriptor, 1, value.status) + encodeSerializableElement( + descriptor, + 2, + ListSerializer(ViolationReportViolationSerializer), + value.violations + ) + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/runtime/ViolationReportViolationSerializer.kt b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/runtime/ViolationReportViolationSerializer.kt new file mode 100644 index 0000000000000..c8d1615893398 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/runtime/ViolationReportViolationSerializer.kt @@ -0,0 +1,51 @@ +package io.quarkus.resteasy.reactive.kotlin.serialization.runtime + +import io.quarkus.hibernate.validator.runtime.jaxrs.ViolationReport +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.Serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.serialDescriptor +import kotlinx.serialization.encoding.* + +@OptIn(ExperimentalSerializationApi::class) +@Serializer(forClass = ViolationReport.Violation::class) +object ViolationReportViolationSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor( + "io.quarkus.hibernate.validator.runtime.jaxrs.ViolationReport.Violation" + ) { + element("field", serialDescriptor()) + element("message", serialDescriptor()) + } + + override fun deserialize(decoder: Decoder): ViolationReport.Violation { + return decoder.decodeStructure(descriptor) { + var field: String? = null + var message: String? = null + + loop@ while (true) { + when (val index = decodeElementIndex(descriptor)) { + CompositeDecoder.DECODE_DONE -> break@loop + 0 -> field = decodeStringElement(descriptor, 0) + 1 -> message = decodeStringElement(descriptor, 1) + else -> throw SerializationException("Unexpected index $index") + } + } + + ViolationReport.Violation( + requireNotNull(field), + requireNotNull(message), + ) + } + } + + override fun serialize(encoder: Encoder, value: ViolationReport.Violation) { + encoder.encodeStructure(descriptor) { + encodeStringElement(descriptor, 0, value.field) + encodeStringElement(descriptor, 1, value.message) + } + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 7b9728847c268..ad79ab03ce3c6 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -359,6 +359,7 @@ rest-client-reactive rest-client-reactive-http2 rest-client-reactive-kotlin-serialization + rest-client-reactive-kotlin-serialization-with-validator rest-client-reactive-multipart rest-client-reactive-stork packaging diff --git a/integration-tests/rest-client-reactive-kotlin-serialization-with-validator/pom.xml b/integration-tests/rest-client-reactive-kotlin-serialization-with-validator/pom.xml new file mode 100644 index 0000000000000..dc2f7858ee4b8 --- /dev/null +++ b/integration-tests/rest-client-reactive-kotlin-serialization-with-validator/pom.xml @@ -0,0 +1,180 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + quarkus-integration-test-rest-client-reactive-kotlin-serialization-with-validator + Quarkus - Integration Tests - REST Client Reactive Kotlin Serialization With Validator + + + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization + + + io.quarkus + quarkus-rest-client-reactive-kotlin-serialization + + + io.quarkus + quarkus-hibernate-validator + + + + + io.quarkus + quarkus-junit5 + test + + + org.assertj + assertj-core + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-client-reactive-kotlin-serialization-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-hibernate-validator-deployment + ${project.version} + pom + test + + + * + * + + + + + + + src/main/kotlin + src/test/kotlin + + + src/main/resources + true + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-maven-serialization + ${kotlin.version} + + + + + all-open + kotlinx-serialization + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + native-image + + + native + + + + native + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + + + + diff --git a/integration-tests/rest-client-reactive-kotlin-serialization-with-validator/src/main/kotlin/io/quarkus/it/rest/ValidationResource.kt b/integration-tests/rest-client-reactive-kotlin-serialization-with-validator/src/main/kotlin/io/quarkus/it/rest/ValidationResource.kt new file mode 100644 index 0000000000000..16840cd138473 --- /dev/null +++ b/integration-tests/rest-client-reactive-kotlin-serialization-with-validator/src/main/kotlin/io/quarkus/it/rest/ValidationResource.kt @@ -0,0 +1,21 @@ +package io.quarkus.it.rest + +import jakarta.validation.constraints.Size +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.MediaType + +@Path("/") +class ValidationResource { + + @GET + @Path("/validate/{id}") + @Produces(MediaType.APPLICATION_JSON) + fun validate( + @Size(min = 5, message = "string is too short") @PathParam("id") id: String? + ): String? { + return id + } +} diff --git a/integration-tests/rest-client-reactive-kotlin-serialization-with-validator/src/main/resources/application.properties b/integration-tests/rest-client-reactive-kotlin-serialization-with-validator/src/main/resources/application.properties new file mode 100644 index 0000000000000..bea6812de9ec1 --- /dev/null +++ b/integration-tests/rest-client-reactive-kotlin-serialization-with-validator/src/main/resources/application.properties @@ -0,0 +1,3 @@ +quarkus.kotlin-serialization.json.encode-defaults=true +quarkus.kotlin-serialization.json.pretty-print=true +quarkus.kotlin-serialization.json.pretty-print-indent=\ \ diff --git a/integration-tests/rest-client-reactive-kotlin-serialization-with-validator/src/test/kotlin/io/quarkus/it/rest/client/BasicTest.kt b/integration-tests/rest-client-reactive-kotlin-serialization-with-validator/src/test/kotlin/io/quarkus/it/rest/client/BasicTest.kt new file mode 100644 index 0000000000000..7f06646b5174e --- /dev/null +++ b/integration-tests/rest-client-reactive-kotlin-serialization-with-validator/src/test/kotlin/io/quarkus/it/rest/client/BasicTest.kt @@ -0,0 +1,25 @@ +package io.quarkus.it.rest.client + +import io.quarkus.test.junit.QuarkusTest +import io.restassured.RestAssured +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test + +@QuarkusTest +open class BasicTest { + + @Test + fun valid() { + val response = RestAssured.with().get("/validate/{id}", "12345") + Assertions.assertThat(response.asString()).isEqualTo("12345") + } + + @Test + fun invalid() { + val response = RestAssured.with().get("/validate/{id}", "1234") + Assertions.assertThat(response.asString()) + .contains("Constraint Violation") + .contains("validate.id") + .contains("string is too short") + } +} diff --git a/integration-tests/rest-client-reactive-kotlin-serialization-with-validator/src/test/kotlin/io/quarkus/it/rest/client/BasicTestIT.kt b/integration-tests/rest-client-reactive-kotlin-serialization-with-validator/src/test/kotlin/io/quarkus/it/rest/client/BasicTestIT.kt new file mode 100644 index 0000000000000..2f30203d85b88 --- /dev/null +++ b/integration-tests/rest-client-reactive-kotlin-serialization-with-validator/src/test/kotlin/io/quarkus/it/rest/client/BasicTestIT.kt @@ -0,0 +1,5 @@ +package io.quarkus.it.rest.client + +import io.quarkus.test.junit.QuarkusIntegrationTest + +@QuarkusIntegrationTest class BasicTestIT : BasicTest()