diff --git a/.github/native-tests.json b/.github/native-tests.json index b56ff2596a4f0..b48732a864771 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -99,7 +99,7 @@ { "category": "Misc1", "timeout": 65, - "test-modules": "maven, jackson, jsonb, quartz, qute, logging-min-level-unset, logging-min-level-set, simple with space", + "test-modules": "maven, jackson, jsonb, kotlin-serialization, quartz, qute, logging-min-level-unset, logging-min-level-set, simple with space", "os-name": "ubuntu-latest" }, { diff --git a/bom/application/pom.xml b/bom/application/pom.xml index c59633640a057..90a15b3a468f0 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -147,6 +147,7 @@ 1.4.2 1.6.0 1.5.2 + 1.3.0 2.6.0 3.0.1 4.1.1 @@ -498,6 +499,16 @@ quarkus-jsonp-deployment ${project.version} + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization + ${project.version} + + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization-deployment + ${project.version} + io.quarkus quarkus-netty @@ -1980,6 +1991,16 @@ quarkus-resteasy-reactive ${project.version} + + io.quarkus + quarkus-resteasy-reactive-kotlin-common + ${project.version} + + + io.quarkus + quarkus-resteasy-reactive-kotlin-common-deployment + ${project.version} + io.quarkus quarkus-resteasy-reactive-kotlin @@ -2052,6 +2073,26 @@ quarkus-rest-client-reactive-jackson-deployment ${project.version} + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization-common + ${project.version} + + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization-common-deployment + ${project.version} + + + io.quarkus + quarkus-rest-client-reactive-kotlin-serialization + ${project.version} + + + io.quarkus + quarkus-rest-client-reactive-kotlin-serialization-deployment + ${project.version} + io.quarkus quarkus-resteasy-reactive-qute @@ -4761,6 +4802,11 @@ kotlinx-coroutines-reactive ${kotlin.coroutine.version} + + org.jetbrains.kotlinx + kotlinx-serialization-json + ${kotlin-serialization.version} + com.amazonaws diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java index c5fe2fbf4c72c..242c6e4703279 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java @@ -93,6 +93,7 @@ public enum Feature { RESTEASY_REACTIVE_JSONB, RESTEASY_REACTIVE_JAXB, RESTEASY_REACTIVE_JACKSON, + RESTEASY_REACTIVE_KOTLIN_SERIALIZATION, RESTEASY_REACTIVE_LINKS, REST_CLIENT, REST_CLIENT_JACKSON, @@ -101,6 +102,7 @@ public enum Feature { REST_CLIENT_MUTINY, REST_CLIENT_REACTIVE, REST_CLIENT_REACTIVE_JACKSON, + REST_CLIENT_REACTIVE_KOTLIN_SERIALIZATION, SCALA, SCHEDULER, SECURITY, diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index c3ea8dce645d6..866a117c103c0 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -1748,6 +1748,19 @@ + + io.quarkus + quarkus-rest-client-reactive-kotlin-serialization + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-rest-data-panache @@ -1982,6 +1995,19 @@ + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-resteasy-reactive-links diff --git a/docs/pom.xml b/docs/pom.xml index 00a5870f0763e..1e7f548e01d39 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -1708,6 +1708,19 @@ + + io.quarkus + quarkus-rest-client-reactive-kotlin-serialization-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-rest-data-panache-deployment @@ -1942,6 +1955,19 @@ + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-resteasy-reactive-links-deployment diff --git a/extensions/resteasy-reactive/pom.xml b/extensions/resteasy-reactive/pom.xml index 12bf32ee7ecab..36e776edae515 100644 --- a/extensions/resteasy-reactive/pom.xml +++ b/extensions/resteasy-reactive/pom.xml @@ -25,8 +25,11 @@ jaxrs-client-reactive rest-client-reactive rest-client-reactive-jackson + rest-client-reactive-kotlin-serialization quarkus-resteasy-reactive-links quarkus-resteasy-reactive-kotlin + quarkus-resteasy-reactive-kotlin-serialization-common + quarkus-resteasy-reactive-kotlin-serialization diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/deployment/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/deployment/pom.xml new file mode 100644 index 0000000000000..163d021e52c7f --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/deployment/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization-common-parent + 999-SNAPSHOT + + + quarkus-resteasy-reactive-kotlin-serialization-common-deployment + Quarkus - RESTEasy Reactive - Kotlin Serialization Common Deployment + + + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization-common + + + io.quarkus + quarkus-kotlin-deployment + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + \ No newline at end of file diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/pom.xml new file mode 100644 index 0000000000000..7f595183dd590 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/pom.xml @@ -0,0 +1,20 @@ + + + + quarkus-resteasy-reactive-parent-aggregator + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-resteasy-reactive-kotlin-serialization-common-parent + Quarkus - RESTEasy Reactive - Kotlin Serialization Common Parent + pom + + deployment + runtime + + diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/pom.xml new file mode 100644 index 0000000000000..c0ebec3f5cf6f --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/pom.xml @@ -0,0 +1,111 @@ + + + 4.0.0 + + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization-common-parent + 999-SNAPSHOT + + + quarkus-resteasy-reactive-kotlin-serialization-common + Quarkus - RESTEasy Reactive - Kotlin Serialization Common + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + + + src/main/kotlin + src/main/java + + + + + test-compile + + test-compile + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + + compile + + + + java-test-compile + test-compile + + testCompile + + + + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + javadoc + none + + + + + + + + + io.quarkus + quarkus-kotlin + + + org.jetbrains.kotlinx + kotlinx-serialization-json + + + io.quarkus + quarkus-junit5-internal + test + + + + \ No newline at end of file diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/java/io/quarkus/kotlin/serialization/JsonConfig.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/java/io/quarkus/kotlin/serialization/JsonConfig.java new file mode 100644 index 0000000000000..5895e0e6eae9a --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/java/io/quarkus/kotlin/serialization/JsonConfig.java @@ -0,0 +1,119 @@ +package io.quarkus.kotlin.serialization; + +import java.util.StringJoiner; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class JsonConfig { + /** + * Removes JSON specification restriction on + * special floating-point values such as `NaN` and `Infinity` and enables their serialization and deserialization. + * When enabling it, please ensure that the receiving party will be able to encode and decode these special values. + */ + @ConfigItem(defaultValue = "false") + public boolean allowSpecialFloatingPointValues = false; + + /** + * Enables structured objects to be serialized as map keys by + * changing serialized form of the map from JSON object (key-value pairs) to flat array like `[k1, v1, k2, v2]`. + */ + @ConfigItem(defaultValue = "false") + public boolean allowStructuredMapKeys = false; + + /** + * Name of the class descriptor property for polymorphic serialization. + */ + @ConfigItem(defaultValue = "type") + public String classDiscriminator = "type"; + + /** + * Enables coercing incorrect JSON values to the default property value in the following cases: + * 1. JSON value is `null` but property type is non-nullable. + * 2. Property type is an enum type, but JSON value contains unknown enum member. + */ + @ConfigItem(defaultValue = "false") + public boolean coerceInputValues = false; + + /** + * Specifies whether default values of Kotlin properties should be encoded. + */ + @ConfigItem(defaultValue = "true") + public boolean encodeDefaults = true; + + /** + * Specifies whether `null` values should be encoded for nullable properties and must be present in JSON object + * during decoding. + *

+ * When this flag is disabled properties with `null` values without default are not encoded; + * during decoding, the absence of a field value is treated as `null` for nullable properties without a default value. + *

+ * {@code true} by default. + */ + @ConfigItem(defaultValue = "true") + public boolean explicitNulls = true; + + /** + * Specifies whether encounters of unknown properties in the input JSON + * should be ignored instead of throwing [SerializationException]. + */ + @ConfigItem(defaultValue = "false") + public boolean ignoreUnknownKeys = false; + + /** + * Removes JSON specification restriction (RFC-4627) and makes parser + * more liberal to the malformed input. In lenient mode quoted boolean literals, + * and unquoted string literals are allowed. + *

+ * Its relaxations can be expanded in the future, so that lenient parser becomes even more + * permissive to invalid value in the input, replacing them with defaults. + */ + @ConfigItem(defaultValue = "false") + public boolean isLenient = false; + + /** + * Specifies whether resulting JSON should be pretty-printed. + */ + @ConfigItem(defaultValue = "false") + public boolean prettyPrint = false; + + /** + * Specifies indent string to use with [prettyPrint] mode + */ + @ConfigItem(defaultValue = " ") + public String prettyPrintIndent = " "; + + /** + * Specifies whether Json instance makes use of [JsonNames] annotation. + *

+ * Disabling this flag when one does not use [JsonNames] at all may sometimes result in better performance, + * particularly when a large count of fields is skipped with [ignoreUnknownKeys]. + */ + @ConfigItem(defaultValue = "true") + public boolean useAlternativeNames = true; + + /** + * Switches polymorphic serialization to the default array format. + * This is an option for legacy JSON format and should not be generally used. + */ + @ConfigItem(defaultValue = "false") + public boolean useArrayPolymorphism = false; + + @Override + public String toString() { + return new StringJoiner(", ", JsonConfig.class.getSimpleName() + "[", "]") + .add("encodeDefaults=" + encodeDefaults) + .add("ignoreUnknownKeys=" + ignoreUnknownKeys) + .add("isLenient=" + isLenient) + .add("allowStructuredMapKeys=" + allowStructuredMapKeys) + .add("prettyPrint=" + prettyPrint) + .add("prettyPrintIndent='" + prettyPrintIndent + "'") + .add("coerceInputValues=" + coerceInputValues) + .add("useArrayPolymorphism=" + useArrayPolymorphism) + .add("classDiscriminator='" + classDiscriminator + "'") + .add("allowSpecialFloatingPointValues=" + allowSpecialFloatingPointValues) + .add("useAlternativeNames=" + useAlternativeNames) + .toString(); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/java/io/quarkus/kotlin/serialization/KotlinSerializationConfig.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/java/io/quarkus/kotlin/serialization/KotlinSerializationConfig.java new file mode 100644 index 0000000000000..72dae1eb4e310 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/java/io/quarkus/kotlin/serialization/KotlinSerializationConfig.java @@ -0,0 +1,25 @@ +package io.quarkus.kotlin.serialization; + +import java.util.StringJoiner; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = KotlinSerializationConfig.CONFIG_NAME, phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public class KotlinSerializationConfig { + public static final String CONFIG_NAME = "kotlin-serialization"; + + /** + * Configuration element for serializing to json + */ + @ConfigItem(name = "json") + public JsonConfig json = new JsonConfig(); + + @Override + public String toString() { + return new StringJoiner(", ", KotlinSerializationConfig.class.getSimpleName() + "[", "]") + .add("json=" + json) + .toString(); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/kotlin/io/quarkus/kotlin/serialization/KotlinSerializerRecorder.kt b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/kotlin/io/quarkus/kotlin/serialization/KotlinSerializerRecorder.kt new file mode 100644 index 0000000000000..26038b0002f4c --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/kotlin/io/quarkus/kotlin/serialization/KotlinSerializerRecorder.kt @@ -0,0 +1,36 @@ +package io.quarkus.kotlin.serialization + +import io.quarkus.runtime.annotations.Recorder +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.EmptySerializersModule +import java.util.function.Supplier + +@Recorder +@ExperimentalSerializationApi +open class KotlinSerializerRecorder { + open fun configFactory(configuration: KotlinSerializationConfig) = JsonSupplier(configuration) +} + +@ExperimentalSerializationApi +open class JsonSupplier(open var configuration: KotlinSerializationConfig) : Supplier { + @Suppress("unused") + constructor() : this(KotlinSerializationConfig()) + + override fun get(): Json { + return Json { + allowSpecialFloatingPointValues = configuration.json.allowSpecialFloatingPointValues + allowStructuredMapKeys = configuration.json.allowStructuredMapKeys + classDiscriminator = configuration.json.classDiscriminator + coerceInputValues = configuration.json.coerceInputValues + encodeDefaults = configuration.json.encodeDefaults + explicitNulls = configuration.json.explicitNulls + ignoreUnknownKeys = configuration.json.ignoreUnknownKeys + isLenient = configuration.json.isLenient + prettyPrint = configuration.json.prettyPrint + prettyPrintIndent = configuration.json.prettyPrintIndent + useAlternativeNames = configuration.json.useAlternativeNames + useArrayPolymorphism = configuration.json.useArrayPolymorphism + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/test/java/io/quarkus/kotlin/serialization/JsonConfigTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/test/java/io/quarkus/kotlin/serialization/JsonConfigTest.java new file mode 100644 index 0000000000000..0fa11adb53cd9 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/test/java/io/quarkus/kotlin/serialization/JsonConfigTest.java @@ -0,0 +1,26 @@ +package io.quarkus.kotlin.serialization; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import kotlinx.serialization.json.JsonConfiguration; + +public class JsonConfigTest { + @Test + public void ensureJsonCoverage() { + Set kotlinFields = new TreeSet<>(Arrays.stream(JsonConfiguration.class.getDeclaredFields()) + .map(f -> f.getName()) + .collect(Collectors.toSet())); + kotlinFields.removeAll(Arrays.stream(JsonConfig.class.getDeclaredFields()) + .map(f -> f.getName()) + .collect(Collectors.toSet())); + assertTrue(kotlinFields.isEmpty(), "Should find all the Kotlin fields on the quarkus config object. " + + "missing elements: " + kotlinFields); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/deployment/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/deployment/pom.xml new file mode 100644 index 0000000000000..39ffdaafac328 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/deployment/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + quarkus-resteasy-reactive-kotlin-serialization-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + + quarkus-resteasy-reactive-kotlin-serialization-deployment + Quarkus - RestEasy Reactive Kotlin Serialization - Deployment + + + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization-common-deployment + + + io.quarkus + quarkus-resteasy-reactive-deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-kotlin-deployment + + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/deployment/src/main/java/io/quarkus/kotlin/serialization/deployment/KotlinSerializationProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/deployment/src/main/java/io/quarkus/kotlin/serialization/deployment/KotlinSerializationProcessor.java new file mode 100644 index 0000000000000..24b8d37e71bb3 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/deployment/src/main/java/io/quarkus/kotlin/serialization/deployment/KotlinSerializationProcessor.java @@ -0,0 +1,64 @@ +package io.quarkus.kotlin.serialization.deployment; + +import static io.quarkus.deployment.Feature.RESTEASY_REACTIVE_KOTLIN_SERIALIZATION; +import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; +import static io.quarkus.resteasy.reactive.common.deployment.ServerDefaultProducesHandlerBuildItem.json; + +import java.util.List; + +import javax.inject.Singleton; +import javax.ws.rs.core.MediaType; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.kotlin.serialization.KotlinSerializationConfig; +import io.quarkus.kotlin.serialization.KotlinSerializationMessageBodyReader; +import io.quarkus.kotlin.serialization.KotlinSerializationMessageBodyWriter; +import io.quarkus.kotlin.serialization.KotlinSerializerRecorder; +import io.quarkus.resteasy.reactive.common.deployment.ServerDefaultProducesHandlerBuildItem; +import io.quarkus.resteasy.reactive.spi.MessageBodyReaderBuildItem; +import io.quarkus.resteasy.reactive.spi.MessageBodyWriterBuildItem; +import kotlinx.serialization.json.Json; + +public class KotlinSerializationProcessor { + @BuildStep + public void additionalProviders( + BuildProducer additionalBean, + BuildProducer additionalReaders, + BuildProducer additionalWriters) { + additionalBean.produce(AdditionalBeanBuildItem.builder() + .addBeanClass(KotlinSerializationMessageBodyReader.class.getName()) + .addBeanClass(KotlinSerializationMessageBodyWriter.class.getName()) + .setUnremovable().build()); + additionalReaders.produce(new MessageBodyReaderBuildItem( + KotlinSerializationMessageBodyReader.class.getName(), Object.class.getName(), List.of( + MediaType.APPLICATION_JSON))); + additionalWriters.produce(new MessageBodyWriterBuildItem( + KotlinSerializationMessageBodyWriter.class.getName(), Object.class.getName(), List.of( + MediaType.APPLICATION_JSON))); + } + + @BuildStep + @Record(STATIC_INIT) + public SyntheticBeanBuildItem createJson(KotlinSerializerRecorder recorder, KotlinSerializationConfig config) { + return SyntheticBeanBuildItem + .configure(Json.class) + .scope(Singleton.class) + .supplier(recorder.configFactory(config)) + .unremovable().done(); + } + + @BuildStep + public void feature(BuildProducer feature) { + feature.produce(new FeatureBuildItem(RESTEASY_REACTIVE_KOTLIN_SERIALIZATION)); + } + + @BuildStep + public ServerDefaultProducesHandlerBuildItem jsonDefault() { + return json(); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/pom.xml new file mode 100644 index 0000000000000..e34358d5e6b60 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/pom.xml @@ -0,0 +1,21 @@ + + + + quarkus-resteasy-reactive-parent-aggregator + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-resteasy-reactive-kotlin-serialization-parent + Quarkus - RestEasy Reactive Kotlin Serialization + pom + + + deployment + runtime + + \ No newline at end of file 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 new file mode 100644 index 0000000000000..c74b2e9c39a10 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/pom.xml @@ -0,0 +1,99 @@ + + + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization-parent + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-resteasy-reactive-kotlin-serialization + Quarkus - RestEasy Reactive Kotlin Serialization - Runtime + + + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization-common + + + io.quarkus + quarkus-resteasy-reactive + + + + + src/main/kotlin + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + io.quarkus.kotlin-serialization + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + compile + + compile + + + + test-compile + + test-compile + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + + compile + + + + java-test-compile + test-compile + + testCompile + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + javadoc + none + + + + + + \ No newline at end of file diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/kotlin/serialization/KotlinSerializationMessageBodyReader.kt b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/kotlin/serialization/KotlinSerializationMessageBodyReader.kt new file mode 100644 index 0000000000000..9aed0e44bfb14 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/kotlin/serialization/KotlinSerializationMessageBodyReader.kt @@ -0,0 +1,45 @@ +package io.quarkus.kotlin.serialization + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.serializer +import org.jboss.resteasy.reactive.common.providers.serialisers.AbstractJsonMessageBodyReader +import org.jboss.resteasy.reactive.common.util.StreamUtil +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo +import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyReader +import org.jboss.resteasy.reactive.server.spi.ServerRequestContext +import java.io.InputStream +import java.lang.reflect.Type +import javax.inject.Inject +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.MultivaluedMap + +class KotlinSerializationMessageBodyReader(@Inject var json: Json) : AbstractJsonMessageBodyReader(), ServerMessageBodyReader { + override fun isReadable(type: Class<*>, genericType: Type, annotations: Array, mediaType: MediaType) = + isReadable(mediaType, type) + + override fun isReadable(type: Class<*>, genericType: Type, lazyMethod: ResteasyReactiveResourceInfo, mediaType: MediaType) = + isReadable(mediaType, type) + + override fun readFrom( + type: Class, + genericType: Type, + annotations: Array, + mediaType: MediaType, + httpHeaders: MultivaluedMap, + entityStream: InputStream + ): Any? { + return doReadFrom(type, entityStream) + } + + override fun readFrom(type: Class, genericType: Type, mediaType: MediaType, context: ServerRequestContext): Any? { + return doReadFrom(type, context.inputStream) + } + + @ExperimentalSerializationApi + private fun doReadFrom(type: Class, entityStream: InputStream): Any? { + return if (StreamUtil.isEmpty(entityStream)) null else + json.decodeFromStream(serializer(type), entityStream) + } +} \ No newline at end of file diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/kotlin/serialization/KotlinSerializationMessageBodyWriter.kt b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/kotlin/serialization/KotlinSerializationMessageBodyWriter.kt new file mode 100644 index 0000000000000..50b0f994f967e --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/kotlin/serialization/KotlinSerializationMessageBodyWriter.kt @@ -0,0 +1,64 @@ +package io.quarkus.kotlin.serialization + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToStream +import kotlinx.serialization.serializer +import org.jboss.resteasy.reactive.common.providers.serialisers.JsonMessageBodyWriterUtil +import org.jboss.resteasy.reactive.server.providers.serialisers.json.JsonMessageServerBodyWriterUtil +import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter.AllWriteableMessageBodyWriter +import org.jboss.resteasy.reactive.server.spi.ServerRequestContext +import java.io.OutputStream +import java.lang.reflect.Type +import javax.inject.Inject +import javax.ws.rs.Produces +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.MultivaluedMap + +@Produces("application/json", "application/*+json", "text/json") +@OptIn(ExperimentalSerializationApi::class) +class KotlinSerializationMessageBodyWriter(@Inject var json: Json) : AllWriteableMessageBodyWriter() { + override fun writeTo( + o: Any, type: Class<*>, genericType: Type, annotations: Array, mediaType: MediaType, + httpHeaders: MultivaluedMap, entityStream: OutputStream + ) { + JsonMessageBodyWriterUtil.setContentTypeIfNecessary(httpHeaders) + if (o is String) { // YUK: done in order to avoid adding extra quotes... + entityStream.write(o.toByteArray()) + } else { + json.encodeToStream(o, entityStream) + } + } + + override fun writeResponse(o: Any, genericType: Type, context: ServerRequestContext) { + JsonMessageServerBodyWriterUtil.setContentTypeIfNecessary(context) + val originalStream = context.orCreateOutputStream + val stream: OutputStream = NoopCloseAndFlushOutputStream(originalStream) + + if (o is String) { // YUK: done in order to avoid adding extra quotes... + stream.write(o.toByteArray()) + } else { + val encodeToString = json.encodeToString(json.serializersModule.serializer(genericType), o) + stream.write(encodeToString.toByteArray()) + } + // we don't use try-with-resources because that results in writing to the http output without the exception mapping coming into play + originalStream.close() + } + + private class NoopCloseAndFlushOutputStream(private val delegate: OutputStream) : OutputStream() { + override fun flush() {} + override fun close() {} + override fun write(b: Int) { + delegate.write(b) + } + + override fun write(b: ByteArray) { + delegate.write(b) + } + + override fun write(b: ByteArray, off: Int, len: Int) { + delegate.write(b, off, len) + } + } +} \ No newline at end of file diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..12471282bc2fe --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,22 @@ +name: "RESTEasy Reactive Kotlin Serialization" +artifact: ${project.groupId}:${project.artifactId}:${project.version} +metadata: + short-name: "resteasy-reactive-kotlin-serialization" + keywords: + - "resteasy-reactive-json" + - "resteasy-reactive-kotlin-serialization" + - "kotlin-serialization" + - "kotlin" + - "jaxrs-json" + - "rest" + - "jaxrs" + - "json" + categories: + - "web" + - "reactive" + status: "experimental" + codestart: + name: "resteasy-reactive" + languages: + - "kotlin" + artifact: "io.quarkus:quarkus-project-core-extension-codestarts" diff --git a/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/deployment/pom.xml b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/deployment/pom.xml new file mode 100644 index 0000000000000..c1f9da96b07ff --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/deployment/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + quarkus-rest-client-reactive-kotlin-serialization-parent + io.quarkus + 999-SNAPSHOT + + quarkus-rest-client-reactive-kotlin-serialization-deployment + Quarkus - REST Client Reactive Kotlin Serialization - Deployment + + + io.quarkus + quarkus-rest-client-reactive-kotlin-serialization + + + io.quarkus + quarkus-resteasy-reactive-deployment + + + io.quarkus + quarkus-kotlin-deployment + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-rest-client-reactive-deployment + + + + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + + + org.assertj + assertj-core + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/deployment/src/main/java/io/quarkus/rest/client/reactive/kotlin/deployment/RestClientReactiveKotlinSerializationProcessor.java b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/deployment/src/main/java/io/quarkus/rest/client/reactive/kotlin/deployment/RestClientReactiveKotlinSerializationProcessor.java new file mode 100644 index 0000000000000..877027d12d513 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/deployment/src/main/java/io/quarkus/rest/client/reactive/kotlin/deployment/RestClientReactiveKotlinSerializationProcessor.java @@ -0,0 +1,59 @@ +package io.quarkus.rest.client.reactive.kotlin.deployment; + +import static io.quarkus.deployment.Feature.REST_CLIENT_REACTIVE_KOTLIN_SERIALIZATION; +import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; + +import java.util.Collections; + +import javax.inject.Singleton; +import javax.ws.rs.core.MediaType; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.kotlin.serialization.KotlinSerializationConfig; +import io.quarkus.kotlin.serialization.KotlinSerializerRecorder; +import io.quarkus.rest.client.reactive.kotlin.runtime.serializers.ClientKotlinMessageBodyReader; +import io.quarkus.rest.client.reactive.kotlin.runtime.serializers.ClientKotlinMessageBodyWriter; +import io.quarkus.resteasy.reactive.spi.MessageBodyReaderBuildItem; +import io.quarkus.resteasy.reactive.spi.MessageBodyWriterBuildItem; +import kotlinx.serialization.json.Json; + +public class RestClientReactiveKotlinSerializationProcessor { + + @BuildStep + void feature(BuildProducer features) { + features.produce(new FeatureBuildItem(REST_CLIENT_REACTIVE_KOTLIN_SERIALIZATION)); + } + + @BuildStep + void additionalProviders( + BuildProducer additionalBean, + BuildProducer additionalReaders, + BuildProducer additionalWriters) { + additionalBean.produce(AdditionalBeanBuildItem.builder() + .addBeanClass(ClientKotlinMessageBodyReader.class.getName()) + .addBeanClass(ClientKotlinMessageBodyWriter.class.getName()) + .setUnremovable().build()); + + additionalReaders + .produce(new MessageBodyReaderBuildItem(ClientKotlinMessageBodyReader.class.getName(), Object.class.getName(), + Collections.singletonList(MediaType.APPLICATION_JSON))); + additionalWriters + .produce(new MessageBodyWriterBuildItem(ClientKotlinMessageBodyWriter.class.getName(), Object.class.getName(), + Collections.singletonList(MediaType.APPLICATION_JSON))); + } + + @BuildStep + @Record(STATIC_INIT) + public SyntheticBeanBuildItem createJson(KotlinSerializerRecorder recorder, KotlinSerializationConfig config) { + return SyntheticBeanBuildItem + .configure(Json.class) + .scope(Singleton.class) + .supplier(recorder.configFactory(config)) + .unremovable().done(); + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/pom.xml b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/pom.xml new file mode 100644 index 0000000000000..da46d6db5352e --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/pom.xml @@ -0,0 +1,21 @@ + + + + quarkus-resteasy-reactive-parent-aggregator + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-rest-client-reactive-kotlin-serialization-parent + Quarkus - REST Client Reactive Kotlin Serialization Parent + pom + + runtime + deployment + tests + + diff --git a/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/runtime/pom.xml b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/runtime/pom.xml new file mode 100644 index 0000000000000..8e240163d7402 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/runtime/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + + quarkus-rest-client-reactive-kotlin-serialization-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + quarkus-rest-client-reactive-kotlin-serialization + Quarkus - REST Client Reactive Kotlin Serialization + Kotlin serialization support for REST Client Reactive + + + + io.quarkus + quarkus-rest-client-reactive + + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization-common + + + io.quarkus + quarkus-resteasy-reactive + + + + + src/main/kotlin + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + io.quarkus.kotlin-serialization.rest-client + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + compile + + compile + + + + test-compile + + test-compile + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + + compile + + + + java-test-compile + test-compile + + testCompile + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + javadoc + none + + + + + + \ No newline at end of file diff --git a/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/rest/client/reactive/kotlin/runtime/serializers/ClientKotlinMessageBodyReader.kt b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/rest/client/reactive/kotlin/runtime/serializers/ClientKotlinMessageBodyReader.kt new file mode 100644 index 0000000000000..82fd3efc8618d --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/rest/client/reactive/kotlin/runtime/serializers/ClientKotlinMessageBodyReader.kt @@ -0,0 +1,26 @@ +package io.quarkus.rest.client.reactive.kotlin.runtime.serializers + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.serializer +import org.jboss.resteasy.reactive.common.util.StreamUtil +import java.io.InputStream +import java.lang.reflect.Type +import javax.inject.Inject +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.MultivaluedMap +import javax.ws.rs.ext.MessageBodyReader + +@OptIn(ExperimentalSerializationApi::class) +class ClientKotlinMessageBodyReader(@Inject val json: Json) : MessageBodyReader { + override fun isReadable(type: Class<*>?, generic: Type?, annotations: Array?, mediaType: MediaType?) = true + + override fun readFrom( + type: Class, generic: Type, annotations: Array?, mediaType: MediaType?, + httpHeaders: MultivaluedMap?, entityStream: InputStream + ): Any? { + return if (StreamUtil.isEmpty(entityStream)) null else + json.decodeFromStream(serializer(generic), entityStream) + } +} \ No newline at end of file diff --git a/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/rest/client/reactive/kotlin/runtime/serializers/ClientKotlinMessageBodyWriter.kt b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/rest/client/reactive/kotlin/runtime/serializers/ClientKotlinMessageBodyWriter.kt new file mode 100644 index 0000000000000..8996fdfebf25c --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/rest/client/reactive/kotlin/runtime/serializers/ClientKotlinMessageBodyWriter.kt @@ -0,0 +1,24 @@ +package io.quarkus.rest.client.reactive.kotlin.runtime.serializers + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToStream +import kotlinx.serialization.serializer +import java.io.OutputStream +import java.lang.reflect.Type +import javax.inject.Inject +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.MultivaluedMap +import javax.ws.rs.ext.MessageBodyWriter + +@OptIn(ExperimentalSerializationApi::class) +class ClientKotlinMessageBodyWriter(@Inject val json: Json) : MessageBodyWriter { + override fun isWriteable(type: Class<*>, genericType: Type, annotations: Array?, mediaType: MediaType?) = true + + override fun writeTo( + t: Any, type: Class<*>, genericType: Type, annotations: Array?, mediaType: MediaType, + httpHeaders: MultivaluedMap, entityStream: OutputStream + ) { + json.encodeToStream(serializer(genericType), t, entityStream) + } +} \ No newline at end of file diff --git a/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..3adf07ac8b4b9 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,25 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "REST Client Reactive Kotlin Serialization" +metadata: + keywords: + - "rest-client-kotlin-serialization" + - "rest-client" + - "web-client" + - "microprofile-rest-client" + - "json" + - "kotlin-serialization" + - "resteasy-reactive" + categories: + - "web" + - "serialization" + status: "stable" + codestart: + name: "resteasy-reactive" + kind: "core" + languages: + - "java" + - "kotlin" + artifact: "io.quarkus:quarkus-project-core-extension-codestarts" + config: + - "quarkus.rest-client-reactive." diff --git a/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/pom.xml b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/pom.xml new file mode 100644 index 0000000000000..80ea03b251ed3 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/pom.xml @@ -0,0 +1,109 @@ + + + 4.0.0 + + quarkus-rest-client-reactive-kotlin-serialization-parent + io.quarkus + 999-SNAPSHOT + + quarkus-rest-client-reactive-kotlin-serialization-tests + Quarkus - REST Client Reactive Kotlin Serialization - Tests + + + io.quarkus + quarkus-rest-client-reactive-kotlin-serialization + + + io.quarkus + quarkus-resteasy-reactive-deployment + + + io.quarkus + quarkus-kotlin-deployment + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-rest-client-reactive-deployment + + + + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + + + org.assertj + assertj-core + test + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + + + src/main/kotlin + + + + + test-compile + + test-compile + + + + src/test/kotlin + + + + + + + org.jetbrains.kotlin + kotlin-maven-serialization + ${kotlin.version} + + + + + kotlinx-serialization + + + + + + diff --git a/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/kotlin/io/quarkus/rest/client/reactive/kotlin/test/EncodeDefaultValuesTest.kt b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/kotlin/io/quarkus/rest/client/reactive/kotlin/test/EncodeDefaultValuesTest.kt new file mode 100644 index 0000000000000..e1bb4a76674e1 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/kotlin/io/quarkus/rest/client/reactive/kotlin/test/EncodeDefaultValuesTest.kt @@ -0,0 +1,35 @@ +package io.quarkus.rest.client.reactive.kotlin.test + +import io.quarkus.test.QuarkusUnitTest +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import javax.inject.Inject + +class EncodeDefaultValuesTest { + companion object { + @RegisterExtension + val config = QuarkusUnitTest() + .withConfigurationResource("encode-default-values.properties") + } + + @Inject + lateinit var json: Json + + @Test + fun testNoDefaults() { + assertThat(json.encodeToString(TestObject())) + .isEqualTo("{}") + } + @Test + fun testExplicitNulls() { + assertThat(json.encodeToString(TestObject(blank = null))) + .isEqualTo("{\"blank\":null}") + } + + @Serializable + private class TestObject(var name: String = "Default Value", var blank: String? = "") +} diff --git a/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/kotlin/io/quarkus/rest/client/reactive/kotlin/test/ExplicitNullsTest.kt b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/kotlin/io/quarkus/rest/client/reactive/kotlin/test/ExplicitNullsTest.kt new file mode 100644 index 0000000000000..fc5d2facf4466 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/kotlin/io/quarkus/rest/client/reactive/kotlin/test/ExplicitNullsTest.kt @@ -0,0 +1,30 @@ +package io.quarkus.rest.client.reactive.kotlin.test + +import io.quarkus.test.QuarkusUnitTest +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import javax.inject.Inject + +class ExplicitNullsTest { + companion object { + @RegisterExtension + val config = QuarkusUnitTest() + .withConfigurationResource("explicit-nulls.properties") + } + + @Inject + lateinit var json: Json + + @Test + fun testExplicitNulls() { + assertThat(json.encodeToString(TestObject(blank = null))) + .isEqualTo("{}") + } + + @Serializable + private class TestObject(var blank: String? = "") +} diff --git a/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/kotlin/io/quarkus/rest/client/reactive/kotlin/test/LenientTest.kt b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/kotlin/io/quarkus/rest/client/reactive/kotlin/test/LenientTest.kt new file mode 100644 index 0000000000000..668c1245b35f3 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/kotlin/io/quarkus/rest/client/reactive/kotlin/test/LenientTest.kt @@ -0,0 +1,31 @@ +package io.quarkus.rest.client.reactive.kotlin.test + +import io.quarkus.test.QuarkusUnitTest +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import javax.inject.Inject + +class LenientTest { + companion object { + @RegisterExtension + val config = QuarkusUnitTest() + .withConfigurationResource("lenient.properties") + } + + @Inject + lateinit var json: Json + + @Test + fun testLenient() { + assertThat(json.decodeFromString("{\"name\":json}")) + .isEqualTo(TestObject("json")) + } + + @Serializable + private data class TestObject(var name: String) +} diff --git a/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/kotlin/io/quarkus/rest/client/reactive/kotlin/test/PrettyPrintTest.kt b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/kotlin/io/quarkus/rest/client/reactive/kotlin/test/PrettyPrintTest.kt new file mode 100644 index 0000000000000..ba2ee556436f7 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/kotlin/io/quarkus/rest/client/reactive/kotlin/test/PrettyPrintTest.kt @@ -0,0 +1,37 @@ + +package io.quarkus.rest.client.reactive.kotlin.test + +import io.quarkus.test.QuarkusUnitTest +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNames +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import javax.inject.Inject + +@OptIn(ExperimentalSerializationApi::class) +class PrettyPrintTest { + companion object { + @RegisterExtension + val config = QuarkusUnitTest() + .withConfigurationResource("pretty-print.properties") + } + + @Inject + lateinit var json: Json + + @Test + fun testAlternateNames() { + assertThat(json.encodeToString(TestObject("John Doe"))) + .isEqualTo(""" + { + "name": "John Doe" + }""".trimIndent()) + } + + @Serializable + private data class TestObject(@JsonNames("label") var name: String) +} diff --git a/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/resources/encode-default-values.properties b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/resources/encode-default-values.properties new file mode 100644 index 0000000000000..0092b8adef075 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/resources/encode-default-values.properties @@ -0,0 +1 @@ +quarkus.kotlin-serialization.json.encode-defaults=false diff --git a/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/resources/explicit-nulls.properties b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/resources/explicit-nulls.properties new file mode 100644 index 0000000000000..92e18a17a312c --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/resources/explicit-nulls.properties @@ -0,0 +1 @@ +quarkus.kotlin-serialization.json.explicit-nulls=false diff --git a/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/resources/lenient.properties b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/resources/lenient.properties new file mode 100644 index 0000000000000..b68606c396b7d --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/resources/lenient.properties @@ -0,0 +1,2 @@ +quarkus.kotlin-serialization.json.is-lenient=true +quarkus.kotlin-serialization.json.encode-defaults=false diff --git a/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/resources/pretty-print.properties b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/resources/pretty-print.properties new file mode 100644 index 0000000000000..fcad5f07927bb --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-kotlin-serialization/tests/src/test/resources/pretty-print.properties @@ -0,0 +1 @@ +quarkus.kotlin-serialization.json.pretty-print=true diff --git a/integration-tests/kotlin-serialization/pom.xml b/integration-tests/kotlin-serialization/pom.xml new file mode 100644 index 0000000000000..e72059383f116 --- /dev/null +++ b/integration-tests/kotlin-serialization/pom.xml @@ -0,0 +1,142 @@ + + + 4.0.0 + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-kotlin-serialization + Quarkus - Integration Tests - Kotlin Serialization + Kotlin Serialization integration tests module + + + 1.3.1 + + + + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization + 999-SNAPSHOT + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.assertj + assertj-core + test + + + + + io.quarkus + quarkus-resteasy-reactive-kotlin-serialization-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-arc-deployment + ${project.version} + pom + test + + + * + * + + + + + + + src/main/kotlin + src/test/kotlin + + + 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 + + + + + + + + maven-failsafe-plugin + + true + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + diff --git a/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingResource.kt b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingResource.kt new file mode 100644 index 0000000000000..a2cd5fc08712c --- /dev/null +++ b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingResource.kt @@ -0,0 +1,27 @@ +package io.quarkus.it.kotser + +import io.quarkus.it.kotser.model.Person +import javax.ws.rs.Consumes +import javax.ws.rs.GET +import javax.ws.rs.POST +import javax.ws.rs.Path +import javax.ws.rs.Produces +import javax.ws.rs.core.MediaType + +@Path("/") +class GreetingResource { + + @GET + @Produces(MediaType.APPLICATION_JSON) + fun hello(): Person { + return Person("Jim Halpert") + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + fun marry(person: Person): Person { + return Person(person.name.substringBefore(" ") + " Halpert") + } +} + diff --git a/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/model/Person.kt b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/model/Person.kt new file mode 100644 index 0000000000000..12191e7cafd10 --- /dev/null +++ b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/model/Person.kt @@ -0,0 +1,10 @@ +package io.quarkus.it.kotser.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Person(var name: String, var defaulted: String = "hi there!") { + override fun toString(): String { + TODO("this shouldn't get called. a proper serialization should be invoked.") + } +} \ No newline at end of file diff --git a/integration-tests/kotlin-serialization/src/main/resources/application.properties b/integration-tests/kotlin-serialization/src/main/resources/application.properties new file mode 100644 index 0000000000000..86df10fe75526 --- /dev/null +++ b/integration-tests/kotlin-serialization/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=\ \ \ No newline at end of file diff --git a/integration-tests/kotlin-serialization/src/test/kotlin/io/quarkus/it/kotser/ResourceTest.kt b/integration-tests/kotlin-serialization/src/test/kotlin/io/quarkus/it/kotser/ResourceTest.kt new file mode 100644 index 0000000000000..f7b11b299cf25 --- /dev/null +++ b/integration-tests/kotlin-serialization/src/test/kotlin/io/quarkus/it/kotser/ResourceTest.kt @@ -0,0 +1,40 @@ +package io.quarkus.it.kotser + +import io.quarkus.test.junit.QuarkusTest +import io.restassured.RestAssured.given +import io.restassured.http.ContentType.JSON +import org.hamcrest.CoreMatchers.`is` +import org.junit.jupiter.api.Test + +@QuarkusTest +class ResourceTest { + @Test + fun testGet() { + given() + .`when`().get("/") + .then() + .statusCode(200) + .body(`is`( + """ + { + "name": "Jim Halpert", + "defaulted": "hi there!" + }""".trimIndent() + )) + } + + @Test + fun testPost() { + given() + .body("{\"name\":\"Pam Beasley\"}") + .contentType(JSON) + .`when`().post("/") + .then() + .statusCode(200) + .body(`is`(""" + { + "name": "Pam Halpert", + "defaulted": "hi there!" + }""".trimIndent())) + } +} \ No newline at end of file diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 083b11198c578..476895aaa3f1c 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -236,6 +236,7 @@ maven scala kotlin + kotlin-serialization mongodb-panache mongodb-panache-kotlin mongodb-rest-data-panache @@ -266,6 +267,7 @@ rest-client resteasy-reactive-kotlin rest-client-reactive + rest-client-reactive-kotlin-serialization rest-client-reactive-multipart rest-client-reactive-stork packaging diff --git a/integration-tests/rest-client-reactive-kotlin-serialization/pom.xml b/integration-tests/rest-client-reactive-kotlin-serialization/pom.xml new file mode 100644 index 0000000000000..e836d40221a5f --- /dev/null +++ b/integration-tests/rest-client-reactive-kotlin-serialization/pom.xml @@ -0,0 +1,146 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + quarkus-integration-test-rest-client-reactive-kotlin-serialization + Quarkus - Integration Tests - REST Client Reactive Kotlin Serialization + + + + io.quarkus + quarkus-rest-client-reactive-kotlin-serialization + + + + + io.quarkus + quarkus-junit5 + test + + + org.assertj + assertj-core + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + + + io.quarkus + quarkus-rest-client-reactive-kotlin-serialization-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/src/main/kotlin/io/quarkus/it/rest/CountriesClient.kt b/integration-tests/rest-client-reactive-kotlin-serialization/src/main/kotlin/io/quarkus/it/rest/CountriesClient.kt new file mode 100644 index 0000000000000..205b9d2405023 --- /dev/null +++ b/integration-tests/rest-client-reactive-kotlin-serialization/src/main/kotlin/io/quarkus/it/rest/CountriesClient.kt @@ -0,0 +1,22 @@ +package io.quarkus.it.rest + +import javax.ws.rs.Consumes +import javax.ws.rs.GET +import javax.ws.rs.POST +import javax.ws.rs.Path +import javax.ws.rs.Produces +import javax.ws.rs.core.MediaType + +@Path("") +interface CountriesClient { + @POST + @Path("/country") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + fun country(country: Country): Country + + @GET + @Path("/countries") + @Produces(MediaType.APPLICATION_JSON) + fun countries(): List +} \ No newline at end of file diff --git a/integration-tests/rest-client-reactive-kotlin-serialization/src/main/kotlin/io/quarkus/it/rest/CountriesResource.kt b/integration-tests/rest-client-reactive-kotlin-serialization/src/main/kotlin/io/quarkus/it/rest/CountriesResource.kt new file mode 100644 index 0000000000000..cc1f5559d7fb9 --- /dev/null +++ b/integration-tests/rest-client-reactive-kotlin-serialization/src/main/kotlin/io/quarkus/it/rest/CountriesResource.kt @@ -0,0 +1,72 @@ +package io.quarkus.it.rest + +import io.vertx.ext.web.Router +import io.vertx.ext.web.RoutingContext +import io.vertx.ext.web.handler.BodyHandler +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.eclipse.microprofile.rest.client.RestClientBuilder +import java.net.URI +import javax.enterprise.context.ApplicationScoped +import javax.enterprise.event.Observes +import javax.inject.Inject +import javax.ws.rs.Path + +@Path("/") +@ApplicationScoped +class CountriesResource { + @Inject + lateinit var json: Json + + fun init(@Observes router: Router) { + router.post().handler(BodyHandler.create()) + + router.route("/call-country").blockingHandler { rc: RoutingContext -> + val client = RestClientBuilder.newBuilder() + .baseUri(URI.create(rc.body.toString())) + .build(CountriesClient::class.java) + val result = client.country( + Country( + "Sweden", "SE", "Stockholm", + listOf(Currency("SEK", "Swedish Crowns", "kr")) + ) + ) + rc.response() + .setStatusCode(200) + .end(result.capital) + } + + router.route("/country").blockingHandler { rc: RoutingContext -> + val body = json.decodeFromString(rc.bodyAsString) + body.capital = "Sthlm" + + rc.response() + .putHeader("content-type", "application/json") + .end(json.encodeToString(body)) + } + + router.route("/call-countries").blockingHandler { rc: RoutingContext -> + val client = RestClientBuilder.newBuilder() + .baseUri(URI.create(rc.body.toString())) + .build(CountriesClient::class.java) + val result = client.countries() + rc.response() + .setStatusCode(200) + .end("OK") + } + + router.route("/countries").blockingHandler { rc: RoutingContext -> + val body = listOf( + Country( + "Sweden", "SE", "Stockholm", + listOf(Currency("SEK", "Swedish Crowns", "kr")) + ) + ) + + rc.response() + .putHeader("content-type", "application/json") + .end(json.encodeToString(body)) + } + } +} \ No newline at end of file diff --git a/integration-tests/rest-client-reactive-kotlin-serialization/src/main/kotlin/io/quarkus/it/rest/Country.kt b/integration-tests/rest-client-reactive-kotlin-serialization/src/main/kotlin/io/quarkus/it/rest/Country.kt new file mode 100644 index 0000000000000..feaa1a335f8b3 --- /dev/null +++ b/integration-tests/rest-client-reactive-kotlin-serialization/src/main/kotlin/io/quarkus/it/rest/Country.kt @@ -0,0 +1,17 @@ +package io.quarkus.it.rest + +import kotlinx.serialization.Serializable + +@Serializable +data class Country(var name: String, var alpha2Code: String, var capital: String, var currencies: List) { + override fun toString(): String { + TODO("serialization should never call this method") + } +} + +@Serializable +data class Currency(var code: String, var name: String, var symbol: String) { + override fun toString(): String { + TODO("serialization should never call this method") + } +} diff --git a/integration-tests/rest-client-reactive-kotlin-serialization/src/main/resources/application.properties b/integration-tests/rest-client-reactive-kotlin-serialization/src/main/resources/application.properties new file mode 100644 index 0000000000000..bea6812de9ec1 --- /dev/null +++ b/integration-tests/rest-client-reactive-kotlin-serialization/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/src/test/kotlin/io/quarkus/it/rest/client/BasicTest.kt b/integration-tests/rest-client-reactive-kotlin-serialization/src/test/kotlin/io/quarkus/it/rest/client/BasicTest.kt new file mode 100644 index 0000000000000..afd417f3f04e2 --- /dev/null +++ b/integration-tests/rest-client-reactive-kotlin-serialization/src/test/kotlin/io/quarkus/it/rest/client/BasicTest.kt @@ -0,0 +1,31 @@ +package io.quarkus.it.rest.client + +import io.quarkus.test.common.http.TestHTTPResource +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 { + + @TestHTTPResource("/") + lateinit var country: String + + @Test + fun callCountry() { + val response = RestAssured.with() + .body(country) + .post("/call-country") + Assertions.assertThat(response.asString()).isEqualTo("Sthlm") + } + + @Test + fun callCountries() { + val response = RestAssured.with() + .body(country) + .post("/call-countries") + Assertions.assertThat(response.asString()).isEqualTo("OK") + } + +} \ No newline at end of file diff --git a/integration-tests/rest-client-reactive-kotlin-serialization/src/test/kotlin/io/quarkus/it/rest/client/BasicTestIT.kt b/integration-tests/rest-client-reactive-kotlin-serialization/src/test/kotlin/io/quarkus/it/rest/client/BasicTestIT.kt new file mode 100644 index 0000000000000..d30a10ebc9768 --- /dev/null +++ b/integration-tests/rest-client-reactive-kotlin-serialization/src/test/kotlin/io/quarkus/it/rest/client/BasicTestIT.kt @@ -0,0 +1,6 @@ +package io.quarkus.it.rest.client + +import io.quarkus.test.junit.NativeImageTest + +@NativeImageTest +class BasicTestIT : BasicTest() \ No newline at end of file