From 6b2bb60efb0418e308552b2ac63ee4dbba2537f8 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 2 Mar 2023 10:30:11 +0000 Subject: [PATCH 1/3] Bson-kotlin Library adds Bson support for Kotlin data classes Follows the spirit of the bson-record-codec library JAVA-4872 --- bson-kotlin/build.gradle.kts | 145 ++++++++ .../org/bson/codecs/kotlin/DataClassCodec.kt | 272 ++++++++++++++ .../codecs/kotlin/DataClassCodecProvider.kt | 25 ++ .../kotlin/DataClassCodecProviderTest.kt | 87 +++++ .../bson/codecs/kotlin/DataClassCodecTest.kt | 332 ++++++++++++++++++ .../bson/codecs/kotlin/samples/DataClasses.kt | 112 ++++++ .../pojo/annotations/BsonExtraElements.java | 2 +- .../codecs/pojo/annotations/BsonIgnore.java | 2 +- .../pojo/annotations/BsonRepresentation.java | 3 +- config/spotbugs/exclude.xml | 8 + driver-core/build.gradle | 1 + .../mongodb/KotlinDataClassCodecProvider.java | 56 +++ .../main/com/mongodb/MongoClientSettings.java | 3 +- settings.gradle | 1 + 14 files changed, 1045 insertions(+), 4 deletions(-) create mode 100644 bson-kotlin/build.gradle.kts create mode 100644 bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodec.kt create mode 100644 bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodecProvider.kt create mode 100644 bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecProviderTest.kt create mode 100644 bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecTest.kt create mode 100644 bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/samples/DataClasses.kt create mode 100644 driver-core/src/main/com/mongodb/KotlinDataClassCodecProvider.java diff --git a/bson-kotlin/build.gradle.kts b/bson-kotlin/build.gradle.kts new file mode 100644 index 00000000000..9b82584f4db --- /dev/null +++ b/bson-kotlin/build.gradle.kts @@ -0,0 +1,145 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import io.gitlab.arturbosch.detekt.Detekt +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("org.jetbrains.kotlin.jvm") version "1.8.10" + `java-library` + + // Test based plugins + id("com.diffplug.spotless") + id("org.jetbrains.dokka") version "1.7.20" + id("io.gitlab.arturbosch.detekt") version "1.21.0" +} + +repositories { + mavenCentral() + google() +} + +base.archivesName.set("bson-kotlin") + +description = "Bson Kotlin Codecs" + +ext.set("pomName", "Bson Kotlin") + +dependencies { + // Align versions of all Kotlin components + implementation(platform("org.jetbrains.kotlin:kotlin-bom")) + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + api(project(path = ":bson", configuration = "default")) + implementation("org.jetbrains.kotlin:kotlin-reflect") + + testImplementation("org.jetbrains.kotlin:kotlin-test-junit") + testImplementation(project(path = ":driver-core", configuration = "default")) +} + +kotlin { explicitApi() } + +tasks.withType { kotlinOptions.jvmTarget = "1.8" } + +// =========================== +// Code Quality checks +// =========================== +spotless { + kotlinGradle { + ktfmt("0.39").dropboxStyle().configure { it.setMaxWidth(120) } + trimTrailingWhitespace() + indentWithSpaces() + endWithNewline() + licenseHeaderFile(rootProject.file("config/mongodb.license"), "(group|plugins|import|buildscript|rootProject)") + } + + kotlin { + target("**/*.kt") + ktfmt().dropboxStyle().configure { it.setMaxWidth(120) } + trimTrailingWhitespace() + indentWithSpaces() + endWithNewline() + licenseHeaderFile(rootProject.file("config/mongodb.license")) + } + + format("extraneous") { + target("*.xml", "*.yml", "*.md") + trimTrailingWhitespace() + indentWithSpaces() + endWithNewline() + } +} + +tasks.named("check") { dependsOn("spotlessApply") } + +detekt { + allRules = true // fail build on any finding + buildUponDefaultConfig = true // preconfigure defaults + config = rootProject.files("config/detekt/detekt.yml") // point to your custom config defining rules to run, + // overwriting default behavior + baseline = rootProject.file("config/detekt/baseline.xml") // a way of suppressing issues before introducing detekt + source = + files( + file("src/main/kotlin"), + file("src/test/kotlin"), + file("src/integrationTest/kotlin"), + ) +} + +tasks.withType().configureEach { + reports { + html.required.set(true) // observe findings in your browser with structure and code snippets + xml.required.set(true) // checkstyle like format mainly for integrations like Jenkins + txt.required.set(false) // similar to the console output, contains issue signature to manually edit + } +} + +spotbugs { showProgress.set(true) } + +// =========================== +// Test Configuration +// =========================== +tasks.create("kCheck") { + description = "Runs all the kotlin checks" + group = "verification" + + dependsOn("clean", "check") + tasks.findByName("check")?.mustRunAfter("clean") +} + +tasks.test { useJUnitPlatform() } + +// =========================== +// Dokka Configuration +// =========================== +val dokkaOutputDir = "${rootProject.buildDir}/docs/${base.archivesName.get()}" + +tasks.dokkaHtml.configure { + outputDirectory.set(file(dokkaOutputDir)) + moduleName.set(base.archivesName.get()) +} + +val cleanDokka by tasks.register("cleanDokka") { delete(dokkaOutputDir) } + +project.parent?.tasks?.named("docs") { + dependsOn(tasks.dokkaHtml) + mustRunAfter(cleanDokka) +} + +tasks.javadocJar.configure { + dependsOn(cleanDokka, tasks.dokkaHtml) + archiveClassifier.set("javadoc") + from(dokkaOutputDir) +} diff --git a/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodec.kt b/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodec.kt new file mode 100644 index 00000000000..9e57541dd99 --- /dev/null +++ b/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodec.kt @@ -0,0 +1,272 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.bson.codecs.kotlin + +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.KProperty1 +import kotlin.reflect.KType +import kotlin.reflect.KTypeParameter +import kotlin.reflect.full.createType +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.findAnnotations +import kotlin.reflect.full.hasAnnotation +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.javaType +import org.bson.BsonReader +import org.bson.BsonType +import org.bson.BsonWriter +import org.bson.codecs.Codec +import org.bson.codecs.DecoderContext +import org.bson.codecs.EncoderContext +import org.bson.codecs.Parameterizable +import org.bson.codecs.RepresentationConfigurable +import org.bson.codecs.configuration.CodecConfigurationException +import org.bson.codecs.configuration.CodecRegistry +import org.bson.codecs.pojo.annotations.BsonCreator +import org.bson.codecs.pojo.annotations.BsonDiscriminator +import org.bson.codecs.pojo.annotations.BsonExtraElements +import org.bson.codecs.pojo.annotations.BsonId +import org.bson.codecs.pojo.annotations.BsonIgnore +import org.bson.codecs.pojo.annotations.BsonProperty +import org.bson.codecs.pojo.annotations.BsonRepresentation +import org.bson.diagnostics.Loggers + +internal data class DataClassCodec( + private val kClass: KClass, + private val primaryConstructor: KFunction, + private val propertyModels: List, +) : Codec { + + private val fieldNamePropertyModelMap = propertyModels.associateBy { it.fieldName } + private val propertyModelId: PropertyModel? = fieldNamePropertyModelMap[idFieldName] + + data class PropertyModel(val param: KParameter, val fieldName: String, val codec: Codec) + + override fun encode(writer: BsonWriter, value: T, encoderContext: EncoderContext) { + writer.writeStartDocument() + if (propertyModelId != null) { + encodeProperty(propertyModelId, value, writer, encoderContext) + } + propertyModels + .filter { it != propertyModelId } + .forEach { propertyModel -> encodeProperty(propertyModel, value, writer, encoderContext) } + writer.writeEndDocument() + } + + override fun getEncoderClass(): Class = kClass.java + + @Suppress("TooGenericExceptionCaught") + override fun decode(reader: BsonReader, decoderContext: DecoderContext): T { + val args: MutableMap = mutableMapOf() + fieldNamePropertyModelMap.values.forEach { args[it.param] = null } + + reader.readStartDocument() + while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) { + val fieldName = reader.readName() + val propertyModel = fieldNamePropertyModelMap[fieldName] + if (propertyModel == null) { + reader.skipValue() + if (logger.isTraceEnabled) { + logger.trace("Found property not present in the DataClass: $fieldName") + } + } else { + try { + args[propertyModel.param] = decoderContext.decodeWithChildContext(propertyModel.codec, reader) + } catch (e: Exception) { + throw CodecConfigurationException( + "Unable to decode $fieldName for ${kClass.simpleName} data class.", e) + } + } + } + reader.readEndDocument() + + try { + return primaryConstructor.callBy(args) + } catch (e: Exception) { + throw CodecConfigurationException( + "Unable to invoke primary constructor of ${kClass.simpleName} data class", e) + } + } + + @Suppress("UNCHECKED_CAST") + private fun encodeProperty( + propertyModel: PropertyModel, + value: T, + writer: BsonWriter, + encoderContext: EncoderContext + ) { + value::class + .members + .firstOrNull { it.name == propertyModel.param.name } + ?.let { + val propertyValue = (it as KProperty1).get(value) + propertyValue?.let { pValue -> + writer.writeName(propertyModel.fieldName) + encoderContext.encodeWithChildContext(propertyModel.codec, writer, pValue) + } + } + } + + companion object { + + internal val logger = Loggers.getLogger("DataClassCodec") + private const val idFieldName = "_id" + + fun create(kClass: KClass, codecRegistry: CodecRegistry): Codec? { + return if (!kClass.isData) null + else if (kClass.typeParameters.isEmpty()) createDataClassCodec(kClass, codecRegistry) + else RawDataClassCodec(kClass) + } + + internal fun createDataClassCodec( + kClass: KClass, + codecRegistry: CodecRegistry, + types: List = emptyList() + ): DataClassCodec { + validateAnnotations(kClass) + val primaryConstructor = kClass.primaryConstructor!! + val typeMap = types.mapIndexed { i, k -> primaryConstructor.typeParameters[i].createType() to k }.toMap() + + val propertyModels = + primaryConstructor.parameters.map { kParameter -> + PropertyModel( + kParameter, computeFieldName(kParameter), getCodec(kParameter, typeMap, codecRegistry)) + } + return DataClassCodec(kClass, primaryConstructor, propertyModels) + } + + private fun validateAnnotations(kClass: KClass) { + codecConfigurationRequires(kClass.findAnnotation() == null) { + """Annotation 'BsonDiscriminator' is not supported on kotlin data classes, + | but found on ${kClass.simpleName}.""" + .trimMargin() + } + + codecConfigurationRequires(kClass.constructors.all { it.findAnnotations().isEmpty() }) { + """Annotation 'BsonCreator' is not supported on kotlin data classes, + | but found in ${kClass.simpleName}.""" + .trimMargin() + } + + kClass.primaryConstructor?.parameters?.map { param -> + codecConfigurationRequires(param.findAnnotations().isEmpty()) { + """Annotation 'BsonIgnore' is not supported in kotlin data classes, + | found on the parameter for ${param.name}.""" + .trimMargin() + } + codecConfigurationRequires(param.findAnnotations().isEmpty()) { + """Annotation 'BsonExtraElements' is not supported in kotlin data classes, + | found on the parameter for ${param.name}.""" + .trimMargin() + } + } + } + + private fun computeFieldName(parameter: KParameter): String { + return if (parameter.hasAnnotation()) { + idFieldName + } else if (parameter.hasAnnotation()) { + parameter.findAnnotation()!!.value + } else { + requireNotNull(parameter.name) + } + } + + @Suppress("UNCHECKED_CAST") + private fun getCodec( + kParameter: KParameter, + typeMap: Map, + codecRegistry: CodecRegistry + ): Codec { + return when (kParameter.type.classifier) { + is KClass<*> -> { + codecRegistry.getCodec( + kParameter, + (kParameter.type.classifier as KClass).javaObjectType, + kParameter.type.arguments.mapNotNull { typeMap[it.type] ?: it.type?.javaType }.toList()) + } + is KTypeParameter -> { + when (val pType = typeMap[kParameter.type] ?: kParameter.type.javaType) { + is Class<*> -> + codecRegistry.getCodec(kParameter, (pType as Class).kotlin.javaObjectType, emptyList()) + is ParameterizedType -> + codecRegistry.getCodec( + kParameter, + (pType.rawType as Class).kotlin.javaObjectType, + pType.actualTypeArguments.toList()) + else -> null + } + } + else -> null + } + ?: throw CodecConfigurationException( + "Could not find codec for ${kParameter.name} with type ${kParameter.type}") + } + + @Suppress("UNCHECKED_CAST") + private fun CodecRegistry.getCodec(kParameter: KParameter, clazz: Class, types: List): Codec { + val codec = + if (types.isEmpty()) { + this.get(clazz) + } else { + this.get(clazz, types) + } + return kParameter.findAnnotation()?.let { + if (codec !is RepresentationConfigurable<*>) { + throw CodecConfigurationException( + "Codec for `${kParameter.name}` must implement RepresentationConfigurable" + + " to supportBsonRepresentation") + } + codec.withRepresentation(it.value) as Codec + } + ?: codec + } + + private fun codecConfigurationRequires(value: Boolean, lazyMessage: () -> String) { + if (!value) { + throw CodecConfigurationException(lazyMessage.invoke()) + } + } + + /** + * A Raw unparameterized data class + * + * It cannot encode or decode it just can create parameterized DataClassCodecs + */ + internal data class RawDataClassCodec(private val kClass: KClass) : Codec, Parameterizable { + + override fun getEncoderClass(): Class = kClass.java + + override fun parameterize(codecRegistry: CodecRegistry, types: List): Codec<*> { + return createDataClassCodec(kClass, codecRegistry, types) + } + + override fun decode(reader: BsonReader?, decoderContext: DecoderContext?): T { + throw CodecConfigurationException( + "Can not decode to ${kClass.simpleName} as it has type parameters and has not been parameterized.") + } + + override fun encode(writer: BsonWriter?, value: T, encoderContext: EncoderContext?) { + throw CodecConfigurationException( + "Can not encode to ${kClass.simpleName} as it has type parameters and has not been parameterized.") + } + } + } +} diff --git a/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodecProvider.kt b/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodecProvider.kt new file mode 100644 index 00000000000..7d55effe886 --- /dev/null +++ b/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodecProvider.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.bson.codecs.kotlin + +import org.bson.codecs.Codec +import org.bson.codecs.configuration.CodecProvider +import org.bson.codecs.configuration.CodecRegistry + +public class DataClassCodecProvider : CodecProvider { + override fun get(clazz: Class, registry: CodecRegistry): Codec? = + DataClassCodec.create(clazz.kotlin, registry) +} diff --git a/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecProviderTest.kt b/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecProviderTest.kt new file mode 100644 index 00000000000..a1596359271 --- /dev/null +++ b/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecProviderTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.bson.codecs.kotlin + +import com.mongodb.MongoClientSettings +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue +import org.bson.BsonDocument +import org.bson.BsonDocumentReader +import org.bson.BsonDocumentWriter +import org.bson.codecs.DecoderContext +import org.bson.codecs.EncoderContext +import org.bson.codecs.configuration.CodecConfigurationException +import org.bson.codecs.kotlin.samples.DataClass +import org.bson.codecs.kotlin.samples.DataClassEmbedded +import org.bson.codecs.kotlin.samples.DataClassParameterized +import org.bson.codecs.kotlin.samples.DataClassWithParameterizedDataClass +import org.bson.conversions.Bson +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class DataClassCodecProviderTest { + + @Test + fun shouldReturnNullForNonDataClass() { + assertNull(DataClassCodecProvider().get(String::class.java, Bson.DEFAULT_CODEC_REGISTRY)) + } + + @Test + fun shouldReturnDataClassCodecForDataClass() { + val provider = DataClassCodecProvider() + val codec = provider.get(DataClass::class.java, Bson.DEFAULT_CODEC_REGISTRY) + + assertNotNull(codec) + assertTrue { codec is DataClassCodec } + assertEquals(DataClass::class.java, codec.encoderClass) + } + + @Test + fun shouldReturnRawDataClassCodecForParameterizedDataClass() { + val provider = DataClassCodecProvider() + val codec = provider.get(DataClassParameterized::class.java, Bson.DEFAULT_CODEC_REGISTRY) + + assertNotNull(codec) + assertTrue { codec is DataClassCodec.Companion.RawDataClassCodec } + assertEquals(DataClassParameterized::class.java, codec.encoderClass) + + assertThrows { + val writer = BsonDocumentWriter(BsonDocument()) + val dataClass = + DataClassWithParameterizedDataClass( + "myId", DataClassParameterized(2.0, "myString", listOf(DataClassEmbedded("embedded1")))) + codec.encode(writer, dataClass.parameterizedDataClass, EncoderContext.builder().build()) + } + + assertThrows { + val value = + BsonDocument.parse( + """{"number": 2.0, "string": "myString", "parameterizedList": [{"name": "embedded1"}]}""") + codec.decode(BsonDocumentReader(value), DecoderContext.builder().build()) + } + } + + @Test + fun shouldReturnDataClassCodecUsingDefaultRegistry() { + val codec = MongoClientSettings.getDefaultCodecRegistry().get(DataClass::class.java) + + assertNotNull(codec) + assertTrue { codec is DataClassCodec } + assertEquals(DataClass::class.java, codec.encoderClass) + } +} diff --git a/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecTest.kt b/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecTest.kt new file mode 100644 index 00000000000..4d191c2ba6e --- /dev/null +++ b/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecTest.kt @@ -0,0 +1,332 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.bson.codecs.kotlin + +import kotlin.test.assertEquals +import org.bson.BsonDocument +import org.bson.BsonDocumentReader +import org.bson.BsonDocumentWriter +import org.bson.codecs.Codec +import org.bson.codecs.DecoderContext +import org.bson.codecs.EncoderContext +import org.bson.codecs.configuration.CodecConfigurationException +import org.bson.codecs.configuration.CodecRegistries.fromProviders +import org.bson.codecs.kotlin.DataClassCodec.Companion.createDataClassCodec +import org.bson.codecs.kotlin.samples.DataClass +import org.bson.codecs.kotlin.samples.DataClassEmbedded +import org.bson.codecs.kotlin.samples.DataClassListOfDataClasses +import org.bson.codecs.kotlin.samples.DataClassListOfListOfDataClasses +import org.bson.codecs.kotlin.samples.DataClassMapOfDataClasses +import org.bson.codecs.kotlin.samples.DataClassMapOfListOfDataClasses +import org.bson.codecs.kotlin.samples.DataClassNestedParameterizedTypes +import org.bson.codecs.kotlin.samples.DataClassParameterized +import org.bson.codecs.kotlin.samples.DataClassSelfReferential +import org.bson.codecs.kotlin.samples.DataClassWithAnnotations +import org.bson.codecs.kotlin.samples.DataClassWithBsonConstructor +import org.bson.codecs.kotlin.samples.DataClassWithBsonDiscriminator +import org.bson.codecs.kotlin.samples.DataClassWithBsonExtraElements +import org.bson.codecs.kotlin.samples.DataClassWithBsonIgnore +import org.bson.codecs.kotlin.samples.DataClassWithDefaults +import org.bson.codecs.kotlin.samples.DataClassWithEmbedded +import org.bson.codecs.kotlin.samples.DataClassWithFailingInit +import org.bson.codecs.kotlin.samples.DataClassWithInvalidRepresentation +import org.bson.codecs.kotlin.samples.DataClassWithNestedParameterized +import org.bson.codecs.kotlin.samples.DataClassWithNestedParameterizedDataClass +import org.bson.codecs.kotlin.samples.DataClassWithNulls +import org.bson.codecs.kotlin.samples.DataClassWithPair +import org.bson.codecs.kotlin.samples.DataClassWithParameterizedDataClass +import org.bson.codecs.kotlin.samples.DataClassWithTriple +import org.bson.conversions.Bson +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class DataClassCodecTest { + + @Test + fun testDataClass() { + val expected = """{"_id": "myId", "name": "Felix", "age": 14, "hobbies": ["rugby", "weights"]}""" + val dataClass = DataClass("myId", "Felix", 14, listOf("rugby", "weights")) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassWithEmbedded() { + val expected = """{"_id": "myId", "embedded": {"name": "embedded1"}}""" + val dataClass = DataClassWithEmbedded("myId", DataClassEmbedded("embedded1")) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassWithAnnotations() { + val oid = "\$oid" + val expected = + """{"_id": {"$oid": "111111111111111111111111"}, + |"nom": "Felix", "age": 14, "hobbies": ["rugby", "weights"]}""" + .trimMargin() + val dataClass = DataClassWithAnnotations("111111111111111111111111", "Felix", 14, listOf("rugby", "weights")) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassListOfDataClasses() { + val expected = """{"_id": "myId", "nested": [{"name": "embedded1"}, {"name": "embedded2"}]}""" + val dataClass = + DataClassListOfDataClasses("myId", listOf(DataClassEmbedded("embedded1"), DataClassEmbedded("embedded2"))) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassListOfListOfDataClasses() { + val expected = """{"_id": "myId", "nested": [[{"name": "embedded1"}], [{"name": "embedded2"}]]}""" + val dataClass = + DataClassListOfListOfDataClasses( + "myId", listOf(listOf(DataClassEmbedded("embedded1")), listOf(DataClassEmbedded("embedded2")))) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassMapOfDataClasses() { + val expected = + """{"_id": "myId", "nested": {"first": {"name": "embedded1"}, "second": {"name": "embedded2"}}}""" + val dataClass = + DataClassMapOfDataClasses( + "myId", mapOf("first" to DataClassEmbedded("embedded1"), "second" to DataClassEmbedded("embedded2"))) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassMapOfListOfDataClasses() { + val expected = + """{"_id": "myId", "nested": {"first": [{"name": "embedded1"}], "second": [{"name": "embedded2"}]}}""" + val dataClass = + DataClassMapOfListOfDataClasses( + "myId", + mapOf( + "first" to listOf(DataClassEmbedded("embedded1")), + "second" to listOf(DataClassEmbedded("embedded2")))) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassWithNulls() { + val expected = """{"name": "Felix", "hobbies": ["rugby", "weights"]}""" + val dataClass = DataClassWithNulls(null, "Felix", null, listOf("rugby", "weights")) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassWithDefaults() { + val expected = """{"_id": "myId", "name": "Arthur Dent", "age": 42, "hobbies": ["computers", "databases"]}""" + val dataClass = DataClassWithDefaults("myId", "Arthur Dent") + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testHandlesExtraData() { + val expected = + """{"_id": "myId", "extra1": "extraField", "name": "Felix", "extra2": "extraField", "age": 14, + | "extra3": "extraField", "hobbies": ["rugby", "weights"], "extra4": "extraField"}""" + .trimMargin() + val dataClass = DataClass("myId", "Felix", 14, listOf("rugby", "weights")) + + assertDecodesTo(dataClass, BsonDocument.parse(expected)) + } + + @Test + fun testDataClassSelfReferential() { + val expected = + """{"_id": "myId", "name": "tree", + | "left": {"name": "L", "left": {"name": "LL"}, "right": {"name": "LR"}}, + | "right": {"name": "R", + | "left": {"name": "RL", + | "left": {"name": "RLL"}, + | "right": {"name": "RLR"}}, + | "right": {"name": "RR"}} + |}""" + .trimMargin() + val dataClass = + DataClassSelfReferential( + "tree", + DataClassSelfReferential("L", DataClassSelfReferential("LL"), DataClassSelfReferential("LR")), + DataClassSelfReferential( + "R", + DataClassSelfReferential("RL", DataClassSelfReferential("RLL"), DataClassSelfReferential("RLR")), + DataClassSelfReferential("RR")), + id = "myId") + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassWithParameterizedDataClass() { + val expected = + """{"_id": "myId", + | "parameterizedDataClass": {"number": 2.0, "string": "myString", + | "parameterizedList": [{"name": "embedded1"}]} + |}""" + .trimMargin() + val dataClass = + DataClassWithParameterizedDataClass( + "myId", DataClassParameterized(2.0, "myString", listOf(DataClassEmbedded("embedded1")))) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassWithNestedParameterizedDataClass() { + val expected = + """{"_id": "myId", + |"nestedParameterized": { + | "parameterizedDataClass": + | {"number": 4.2, "string": "myString", "parameterizedList": [{"name": "embedded1"}]}, + | "other": "myOtherString" + | } + |}""" + .trimMargin() + val dataClass = + DataClassWithNestedParameterizedDataClass( + "myId", + DataClassWithNestedParameterized( + DataClassParameterized(4.2, "myString", listOf(DataClassEmbedded("embedded1"))), "myOtherString")) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassWithPair() { + val expected = """{"pair": {"first": "a", "second": 1}}""" + val dataClass = DataClassWithPair("a" to 1) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassWithTriple() { + val expected = """{"triple": {"first": "a", "second": 1, "third": 2.1}}""" + val dataClass = DataClassWithTriple(Triple("a", 1, 2.1)) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassNestedParameterizedTypes() { + + val expected = + """{ + |"triple": { + | "first": "0", + | "second": {"first": 1, "second": {"first": 1.2, "second": {"first": "1.3", "second": 1.3}}}, + | "third": {"first": 2, "second": {"first": 2.1, "second": "two dot two"}, + | "third": {"first": "3.1", "second": {"first": 3.2, "second": "three dot two" }, + | "third": 3.3 }} + | } + |}""" + .trimMargin() + val dataClass = + DataClassNestedParameterizedTypes( + Triple( + "0", + Pair(1, Pair(1.2, Pair("1.3", 1.3))), + Triple(2, Pair(2.1, "two dot two"), Triple("3.1", Pair(3.2, "three dot two"), 3.3)))) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataFailures() { + assertThrows("Missing data") { + val codec: Codec = createDataClassCodec(DataClass::class, registry()) + codec.decode(BsonDocumentReader(BsonDocument()), DecoderContext.builder().build()) + } + + assertThrows("Invalid types") { + val data = + BsonDocument.parse("""{"_id": "myId", "name": "Imogen", "age": "16", "hobbies": ["rugby", "gym"]}""") + val codec: Codec = createDataClassCodec(DataClass::class, registry()) + codec.decode(BsonDocumentReader(data), DecoderContext.builder().build()) + } + + assertThrows("Invalid complex types") { + val data = BsonDocument.parse("""{"_id": "myId", "embedded": 123}""") + val codec: Codec = createDataClassCodec(DataClassWithEmbedded::class, registry()) + codec.decode(BsonDocumentReader(data), DecoderContext.builder().build()) + } + + assertThrows("Failing init") { + val data = BsonDocument.parse("""{"_id": "myId"}""") + val codec: DataClassCodec = + createDataClassCodec(DataClassWithFailingInit::class, registry()) + codec.decode(BsonDocumentReader(data), DecoderContext.builder().build()) + } + } + + @Test + fun testInvalidAnnotations() { + assertThrows { + createDataClassCodec(DataClassWithBsonDiscriminator::class, Bson.DEFAULT_CODEC_REGISTRY) + } + assertThrows { + createDataClassCodec(DataClassWithBsonConstructor::class, Bson.DEFAULT_CODEC_REGISTRY) + } + assertThrows { + createDataClassCodec(DataClassWithBsonIgnore::class, Bson.DEFAULT_CODEC_REGISTRY) + } + assertThrows { + createDataClassCodec(DataClassWithBsonExtraElements::class, Bson.DEFAULT_CODEC_REGISTRY) + } + assertThrows { + createDataClassCodec(DataClassWithInvalidRepresentation::class, Bson.DEFAULT_CODEC_REGISTRY) + } + } + + private fun assertRoundTrips(expected: String, value: T) { + assertDecodesTo(value, assertEncodesTo(expected, value)) + } + + @Suppress("UNCHECKED_CAST") + private fun assertEncodesTo(json: String, value: T): BsonDocument { + val expected = BsonDocument.parse(json) + val codec: DataClassCodec = createDataClassCodec(value::class, registry()) as DataClassCodec + val document = BsonDocument() + val writer = BsonDocumentWriter(document) + + codec.encode(writer, value, EncoderContext.builder().build()) + assertEquals(expected, document) + if (expected.contains("_id")) { + assertEquals("_id", document.firstKey) + } + return document + } + + @Suppress("UNCHECKED_CAST") + private fun assertDecodesTo(expected: T, actual: BsonDocument) { + val codec: DataClassCodec = createDataClassCodec(expected::class, registry()) as DataClassCodec + val decoded: T = codec.decode(BsonDocumentReader(actual), DecoderContext.builder().build()) + + assertEquals(expected, decoded) + } + + private fun registry() = fromProviders(DataClassCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY) +} diff --git a/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/samples/DataClasses.kt b/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/samples/DataClasses.kt new file mode 100644 index 00000000000..db59891d472 --- /dev/null +++ b/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/samples/DataClasses.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.bson.codecs.kotlin.samples + +import org.bson.BsonMaxKey +import org.bson.BsonType +import org.bson.Document +import org.bson.codecs.pojo.annotations.BsonCreator +import org.bson.codecs.pojo.annotations.BsonDiscriminator +import org.bson.codecs.pojo.annotations.BsonExtraElements +import org.bson.codecs.pojo.annotations.BsonId +import org.bson.codecs.pojo.annotations.BsonIgnore +import org.bson.codecs.pojo.annotations.BsonProperty +import org.bson.codecs.pojo.annotations.BsonRepresentation + +@Suppress("PropertyName", "ConstructorParameterNaming") +data class DataClass(val _id: String, val name: String, val age: Int, val hobbies: List) + +data class DataClassWithAnnotations( + @BsonRepresentation(BsonType.OBJECT_ID) @BsonId val oid: String, + @BsonProperty("nom") val name: String, + val age: Int, + val hobbies: List +) + +data class DataClassEmbedded(val name: String) + +data class DataClassWithEmbedded(@BsonId val id: String, val embedded: DataClassEmbedded) + +data class DataClassListOfDataClasses(@BsonId val id: String, val nested: List) + +data class DataClassListOfListOfDataClasses(@BsonId val id: String, val nested: List>) + +data class DataClassMapOfDataClasses(@BsonId val id: String, val nested: Map) + +data class DataClassMapOfListOfDataClasses(@BsonId val id: String, val nested: Map>) + +data class DataClassWithNulls(@BsonId val id: String?, val name: String, val age: Int?, val hobbies: List) + +data class DataClassWithDefaults( + @BsonId val id: String, + val name: String, + val age: Int = 42, + val hobbies: List = listOf("computers", "databases") +) + +data class DataClassSelfReferential( + val name: String, + val left: DataClassSelfReferential? = null, + val right: DataClassSelfReferential? = null, + @BsonId val id: String? = null +) + +data class DataClassWithParameterizedDataClass( + @BsonId val id: String, + val parameterizedDataClass: DataClassParameterized +) + +data class DataClassParameterized(val number: N, val string: String, val parameterizedList: List) + +data class DataClassWithNestedParameterizedDataClass( + @BsonId val id: String, + val nestedParameterized: DataClassWithNestedParameterized +) + +data class DataClassWithNestedParameterized( + val parameterizedDataClass: DataClassParameterized, + val other: B +) + +data class DataClassWithPair(val pair: Pair) + +data class DataClassWithTriple(val triple: Triple) + +data class DataClassNestedParameterizedTypes( + val triple: + Triple< + String, + Pair>>, + Triple, Triple, Double>>> +) + +@BsonDiscriminator data class DataClassWithBsonDiscriminator(val id: String) + +data class DataClassWithBsonIgnore(val id: String, @BsonIgnore val ignored: String) + +data class DataClassWithBsonExtraElements(val id: String, @BsonExtraElements val extraElements: Document) + +data class DataClassWithBsonConstructor(val id: String, val count: Int) { + @BsonCreator constructor(id: String) : this(id, -1) +} + +data class DataClassWithInvalidRepresentation(@BsonRepresentation(BsonType.STRING) val id: BsonMaxKey) + +data class DataClassWithFailingInit(@BsonId val id: String) { + init { + require(false) + } +} diff --git a/bson/src/main/org/bson/codecs/pojo/annotations/BsonExtraElements.java b/bson/src/main/org/bson/codecs/pojo/annotations/BsonExtraElements.java index 7362ee66839..1ae25e5da3d 100644 --- a/bson/src/main/org/bson/codecs/pojo/annotations/BsonExtraElements.java +++ b/bson/src/main/org/bson/codecs/pojo/annotations/BsonExtraElements.java @@ -37,6 +37,6 @@ */ @Documented @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.FIELD}) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) public @interface BsonExtraElements { } diff --git a/bson/src/main/org/bson/codecs/pojo/annotations/BsonIgnore.java b/bson/src/main/org/bson/codecs/pojo/annotations/BsonIgnore.java index 98adcdb7145..96b91051995 100644 --- a/bson/src/main/org/bson/codecs/pojo/annotations/BsonIgnore.java +++ b/bson/src/main/org/bson/codecs/pojo/annotations/BsonIgnore.java @@ -32,7 +32,7 @@ * @see org.bson.codecs.pojo.Conventions#ANNOTATION_CONVENTION */ @Documented -@Target({ElementType.METHOD, ElementType.FIELD}) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface BsonIgnore { } diff --git a/bson/src/main/org/bson/codecs/pojo/annotations/BsonRepresentation.java b/bson/src/main/org/bson/codecs/pojo/annotations/BsonRepresentation.java index 3f3056f77ef..465e64d016f 100644 --- a/bson/src/main/org/bson/codecs/pojo/annotations/BsonRepresentation.java +++ b/bson/src/main/org/bson/codecs/pojo/annotations/BsonRepresentation.java @@ -29,13 +29,14 @@ * *

For POJOs, requires the {@link org.bson.codecs.pojo.Conventions#ANNOTATION_CONVENTION}

*

For Java records, the annotation is only supported on the record component.

+ *

For Kotlin data classes, the annotation is only supported on the constructor parameter.

* * @since 4.2 * @see org.bson.codecs.pojo.Conventions#ANNOTATION_CONVENTION */ @Documented @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.FIELD, ElementType.METHOD}) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) public @interface BsonRepresentation { /** * The type that the property is stored as in the database. diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml index 5e095d213b2..17a49807775 100644 --- a/config/spotbugs/exclude.xml +++ b/config/spotbugs/exclude.xml @@ -259,4 +259,12 @@ + + + + + + + + diff --git a/driver-core/build.gradle b/driver-core/build.gradle index f62014ee38b..067af77087e 100644 --- a/driver-core/build.gradle +++ b/driver-core/build.gradle @@ -37,6 +37,7 @@ def classifiers = ["linux-x86_64", "linux-aarch_64", "osx-x86_64", "osx-aarch_64 dependencies { api project(path: ':bson', configuration: 'default') implementation project(path: ':bson-record-codec', configuration: 'default') + implementation project(path: ':bson-kotlin', configuration: 'default') implementation "com.github.jnr:jnr-unixsocket:$jnrUnixsocketVersion", optional api platform("io.netty:netty-bom:$nettyVersion") diff --git a/driver-core/src/main/com/mongodb/KotlinDataClassCodecProvider.java b/driver-core/src/main/com/mongodb/KotlinDataClassCodecProvider.java new file mode 100644 index 00000000000..9a6a7955153 --- /dev/null +++ b/driver-core/src/main/com/mongodb/KotlinDataClassCodecProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mongodb; + +import com.mongodb.lang.Nullable; +import org.bson.codecs.Codec; +import org.bson.codecs.configuration.CodecProvider; +import org.bson.codecs.configuration.CodecRegistry; +import org.bson.codecs.kotlin.DataClassCodecProvider; + + +/** + * A CodecProvider for Kotlin data classes. + * + *

Requires the bson-kotlin package.

+ * + * @since 4.10 + */ +public class KotlinDataClassCodecProvider implements CodecProvider { + + @Nullable + private static final CodecProvider DATA_CLASS_CODEC_PROVIDER; + static { + + CodecProvider possibleCodecProvider; + try { + Class.forName("org.bson.codecs.kotlin.DataClassCodecProvider"); // Kotlin bson canary test + possibleCodecProvider = new DataClassCodecProvider(); + } catch (ClassNotFoundException e) { + // No kotlin data class support + possibleCodecProvider = null; + } + DATA_CLASS_CODEC_PROVIDER = possibleCodecProvider; + } + + @Override + @Nullable + public Codec get(final Class clazz, final CodecRegistry registry) { + return DATA_CLASS_CODEC_PROVIDER != null ? DATA_CLASS_CODEC_PROVIDER.get(clazz, registry) : null; + } + +} + diff --git a/driver-core/src/main/com/mongodb/MongoClientSettings.java b/driver-core/src/main/com/mongodb/MongoClientSettings.java index 7fd7e116043..78eca3dca36 100644 --- a/driver-core/src/main/com/mongodb/MongoClientSettings.java +++ b/driver-core/src/main/com/mongodb/MongoClientSettings.java @@ -78,7 +78,8 @@ public final class MongoClientSettings { new BsonCodecProvider(), new EnumCodecProvider(), new ExpressionCodecProvider(), - new Jep395RecordCodecProvider())); + new Jep395RecordCodecProvider(), + new KotlinDataClassCodecProvider())); private final ReadPreference readPreference; private final WriteConcern writeConcern; diff --git a/settings.gradle b/settings.gradle index 1e1e661fd5c..7ef5498b954 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,6 +22,7 @@ include ':driver-core' include ':driver-legacy' include ':driver-sync' include ':driver-reactive-streams' +include ':bson-kotlin' include ':driver-kotlin-sync' include ':bson-scala' include ':driver-scala' From 1074f629cdca2d2ce5d3a99ece63db4d47c6f60e Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 3 Mar 2023 16:42:54 +0000 Subject: [PATCH 2/3] Added extra test cases --- .../bson/codecs/kotlin/DataClassCodecTest.kt | 32 +++++++++++++++++++ .../bson/codecs/kotlin/samples/DataClasses.kt | 8 +++++ 2 files changed, 40 insertions(+) diff --git a/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecTest.kt b/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecTest.kt index 4d191c2ba6e..cbc65e12e9a 100644 --- a/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecTest.kt +++ b/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecTest.kt @@ -43,11 +43,15 @@ import org.bson.codecs.kotlin.samples.DataClassWithDefaults import org.bson.codecs.kotlin.samples.DataClassWithEmbedded import org.bson.codecs.kotlin.samples.DataClassWithFailingInit import org.bson.codecs.kotlin.samples.DataClassWithInvalidRepresentation +import org.bson.codecs.kotlin.samples.DataClassWithMutableList +import org.bson.codecs.kotlin.samples.DataClassWithMutableMap +import org.bson.codecs.kotlin.samples.DataClassWithMutableSet import org.bson.codecs.kotlin.samples.DataClassWithNestedParameterized import org.bson.codecs.kotlin.samples.DataClassWithNestedParameterizedDataClass import org.bson.codecs.kotlin.samples.DataClassWithNulls import org.bson.codecs.kotlin.samples.DataClassWithPair import org.bson.codecs.kotlin.samples.DataClassWithParameterizedDataClass +import org.bson.codecs.kotlin.samples.DataClassWithSequence import org.bson.codecs.kotlin.samples.DataClassWithTriple import org.bson.conversions.Bson import org.junit.jupiter.api.Test @@ -254,6 +258,30 @@ class DataClassCodecTest { assertRoundTrips(expected, dataClass) } + @Test + fun testDataClassWithMutableList() { + val expected = """{"value": ["A", "B", "C"]}""" + val dataClass = DataClassWithMutableList(mutableListOf("A", "B", "C")) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassWithMutableSet() { + val expected = """{"value": ["A", "B", "C"]}""" + val dataClass = DataClassWithMutableSet(mutableSetOf("A", "B", "C")) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassWithMutableMap() { + val expected = """{"value": {"a": "A", "b": "B", "c": "C"}}""" + val dataClass = DataClassWithMutableMap(mutableMapOf("a" to "A", "b" to "B", "c" to "C")) + + assertRoundTrips(expected, dataClass) + } + @Test fun testDataFailures() { assertThrows("Missing data") { @@ -261,6 +289,10 @@ class DataClassCodecTest { codec.decode(BsonDocumentReader(BsonDocument()), DecoderContext.builder().build()) } + assertThrows("Unsupported type Sequence") { + createDataClassCodec(DataClassWithSequence::class, registry()) + } + assertThrows("Invalid types") { val data = BsonDocument.parse("""{"_id": "myId", "name": "Imogen", "age": "16", "hobbies": ["rugby", "gym"]}""") diff --git a/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/samples/DataClasses.kt b/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/samples/DataClasses.kt index db59891d472..a11f069be59 100644 --- a/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/samples/DataClasses.kt +++ b/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/samples/DataClasses.kt @@ -93,6 +93,12 @@ data class DataClassNestedParameterizedTypes( Triple, Triple, Double>>> ) +data class DataClassWithMutableList(val value: MutableList) + +data class DataClassWithMutableSet(val value: MutableSet) + +data class DataClassWithMutableMap(val value: MutableMap) + @BsonDiscriminator data class DataClassWithBsonDiscriminator(val id: String) data class DataClassWithBsonIgnore(val id: String, @BsonIgnore val ignored: String) @@ -110,3 +116,5 @@ data class DataClassWithFailingInit(@BsonId val id: String) { require(false) } } + +data class DataClassWithSequence(val value: Sequence) From cfcbaabc4291e7825fa8c9fa3c2191e20f5378f5 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 8 Mar 2023 14:40:24 +0000 Subject: [PATCH 3/3] PR updates --- .../main/kotlin/org/bson/codecs/kotlin/DataClassCodec.kt | 7 +++---- driver-core/src/main/com/mongodb/MongoClientSettings.java | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodec.kt b/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodec.kt index 9e57541dd99..922e1a51e87 100644 --- a/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodec.kt +++ b/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodec.kt @@ -141,7 +141,8 @@ internal data class DataClassCodec( types: List = emptyList() ): DataClassCodec { validateAnnotations(kClass) - val primaryConstructor = kClass.primaryConstructor!! + val primaryConstructor = + kClass.primaryConstructor ?: throw CodecConfigurationException("No primary constructor for $kClass") val typeMap = types.mapIndexed { i, k -> primaryConstructor.typeParameters[i].createType() to k }.toMap() val propertyModels = @@ -182,10 +183,8 @@ internal data class DataClassCodec( private fun computeFieldName(parameter: KParameter): String { return if (parameter.hasAnnotation()) { idFieldName - } else if (parameter.hasAnnotation()) { - parameter.findAnnotation()!!.value } else { - requireNotNull(parameter.name) + parameter.findAnnotation()?.value ?: requireNotNull(parameter.name) } } diff --git a/driver-core/src/main/com/mongodb/MongoClientSettings.java b/driver-core/src/main/com/mongodb/MongoClientSettings.java index 78eca3dca36..14dc46f1711 100644 --- a/driver-core/src/main/com/mongodb/MongoClientSettings.java +++ b/driver-core/src/main/com/mongodb/MongoClientSettings.java @@ -128,6 +128,7 @@ public final class MongoClientSettings { *
  • {@link org.bson.codecs.EnumCodecProvider}
  • *
  • {@link ExpressionCodecProvider}
  • *
  • {@link com.mongodb.Jep395RecordCodecProvider}
  • + *
  • {@link com.mongodb.KotlinDataClassCodecProvider}
  • * * *