From c648ee35bd354a66332f7c29a112c83c28639688 Mon Sep 17 00:00:00 2001 From: Seongbin Lee Date: Mon, 30 Dec 2024 15:56:18 +0900 Subject: [PATCH 1/4] Add bsonNamingStrategy option to support snake_case naming strategy --- .../bson/codecs/kotlinx/BsonConfiguration.kt | 5 ++ .../org/bson/codecs/kotlinx/BsonDecoder.kt | 10 ++- .../org/bson/codecs/kotlinx/BsonEncoder.kt | 11 ++- .../bson/codecs/kotlinx/JsonBsonDecoder.kt | 2 + .../bson/codecs/kotlinx/JsonBsonEncoder.kt | 2 + .../codecs/kotlinx/utils/BsonCodecUtils.kt | 16 ++++ .../kotlinx/KotlinSerializerCodecTest.kt | 76 +++---------------- .../codecs/kotlinx/samples/DataClasses.kt | 6 ++ 8 files changed, 60 insertions(+), 68 deletions(-) diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonConfiguration.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonConfiguration.kt index 027fe8925da..9138bf12276 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonConfiguration.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonConfiguration.kt @@ -31,4 +31,9 @@ public data class BsonConfiguration( val encodeDefaults: Boolean = true, val explicitNulls: Boolean = false, val classDiscriminator: String = "_t", + val bsonNamingStrategy: BsonNamingStrategy? = null ) + +public enum class BsonNamingStrategy { + SNAKE_CASE, +} diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt index 99e5d2acb17..65be97f47fd 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt @@ -42,6 +42,7 @@ import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonDecoder import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonDocumentDecoder import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonMapDecoder import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonPolymorphicDecoder +import org.bson.codecs.kotlinx.utils.BsonCodecUtils.toCamelCase import org.bson.internal.NumberCodecHelper import org.bson.internal.StringCodecHelper import org.bson.types.ObjectId @@ -116,7 +117,14 @@ internal sealed class AbstractBsonDecoder( val elementMetadata = elementsMetadata ?: error("elementsMetadata may not be null.") val name: String? = when (reader.state ?: error("State of reader may not be null.")) { - AbstractBsonReader.State.NAME -> reader.readName() + AbstractBsonReader.State.NAME -> + reader.readName().let { + if (configuration.bsonNamingStrategy == BsonNamingStrategy.SNAKE_CASE) { + it.toCamelCase + } else { + it + } + } AbstractBsonReader.State.VALUE -> reader.currentName AbstractBsonReader.State.TYPE -> { reader.readBsonType() diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt index 1470bbb76a5..94957cf30c0 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt @@ -31,6 +31,7 @@ import org.bson.BsonValue import org.bson.BsonWriter import org.bson.codecs.BsonValueCodec import org.bson.codecs.EncoderContext +import org.bson.codecs.kotlinx.utils.BsonCodecUtils.toSnakeCase import org.bson.types.ObjectId /** @@ -203,7 +204,15 @@ internal open class BsonEncoderImpl( } internal fun encodeName(value: Any) { - writer.writeName(value.toString()) + val name = + value.toString().let { + if (configuration.bsonNamingStrategy == BsonNamingStrategy.SNAKE_CASE) { + it.toSnakeCase + } else { + it + } + } + writer.writeName(name) state = STATE.VALUE } diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonDecoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonDecoder.kt index 4b0eee8213a..bd8b6739958 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonDecoder.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonDecoder.kt @@ -31,6 +31,7 @@ import org.bson.AbstractBsonReader import org.bson.BsonBinarySubType import org.bson.BsonType import org.bson.UuidRepresentation +import org.bson.codecs.kotlinx.utils.BsonCodecUtils.toJsonNamingStrategy import org.bson.internal.UuidHelper @OptIn(ExperimentalSerializationApi::class) @@ -42,6 +43,7 @@ internal interface JsonBsonDecoder : BsonDecoder, JsonDecoder { explicitNulls = configuration.explicitNulls encodeDefaults = configuration.encodeDefaults classDiscriminator = configuration.classDiscriminator + namingStrategy = configuration.bsonNamingStrategy.toJsonNamingStrategy() serializersModule = this@JsonBsonDecoder.serializersModule } diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt index 6cff36a0909..4a754834e6d 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt @@ -30,6 +30,7 @@ import kotlinx.serialization.json.int import kotlinx.serialization.json.long import kotlinx.serialization.modules.SerializersModule import org.bson.BsonWriter +import org.bson.codecs.kotlinx.utils.BsonCodecUtils.toJsonNamingStrategy import org.bson.types.Decimal128 @OptIn(ExperimentalSerializationApi::class) @@ -52,6 +53,7 @@ internal class JsonBsonEncoder( explicitNulls = configuration.explicitNulls encodeDefaults = configuration.encodeDefaults classDiscriminator = configuration.classDiscriminator + namingStrategy = configuration.bsonNamingStrategy.toJsonNamingStrategy() serializersModule = this@JsonBsonEncoder.serializersModule } diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/utils/BsonCodecUtils.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/utils/BsonCodecUtils.kt index eabfebc5833..3829a46de81 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/utils/BsonCodecUtils.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/utils/BsonCodecUtils.kt @@ -15,8 +15,10 @@ */ package org.bson.codecs.kotlinx.utils +import java.util.* import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.json.JsonNamingStrategy import kotlinx.serialization.modules.SerializersModule import org.bson.AbstractBsonReader import org.bson.BsonWriter @@ -28,6 +30,7 @@ import org.bson.codecs.kotlinx.BsonDocumentDecoder import org.bson.codecs.kotlinx.BsonEncoder import org.bson.codecs.kotlinx.BsonEncoderImpl import org.bson.codecs.kotlinx.BsonMapDecoder +import org.bson.codecs.kotlinx.BsonNamingStrategy import org.bson.codecs.kotlinx.BsonPolymorphicDecoder import org.bson.codecs.kotlinx.JsonBsonArrayDecoder import org.bson.codecs.kotlinx.JsonBsonDecoderImpl @@ -116,4 +119,17 @@ internal object BsonCodecUtils { return if (hasJsonDecoder) JsonBsonMapDecoder(descriptor, reader, serializersModule, configuration) else BsonMapDecoder(descriptor, reader, serializersModule, configuration) } + + internal val String.toSnakeCase + get() = replace(Regex("([A-Z])"), "_$1").lowercase(Locale.getDefault()).replace(Regex("^_"), "") + + internal val String.toCamelCase + get() = replace(Regex("_([a-z])")) { it.value[1].uppercaseChar().toString() } + + internal fun BsonNamingStrategy?.toJsonNamingStrategy(): JsonNamingStrategy? { + return when (this) { + BsonNamingStrategy.SNAKE_CASE -> JsonNamingStrategy.SnakeCase + else -> null + } + } } diff --git a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt index aa749368e04..85fd79385f5 100644 --- a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt +++ b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt @@ -26,10 +26,7 @@ import kotlinx.datetime.LocalTime import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.MissingFieldException import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonArray -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put +import kotlinx.serialization.json.* import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.plus import kotlinx.serialization.modules.polymorphic @@ -49,68 +46,7 @@ import org.bson.BsonUndefined import org.bson.codecs.DecoderContext import org.bson.codecs.EncoderContext import org.bson.codecs.configuration.CodecConfigurationException -import org.bson.codecs.kotlinx.samples.Box -import org.bson.codecs.kotlinx.samples.DataClassBsonValues -import org.bson.codecs.kotlinx.samples.DataClassContainsOpen -import org.bson.codecs.kotlinx.samples.DataClassContainsValueClass -import org.bson.codecs.kotlinx.samples.DataClassEmbedded -import org.bson.codecs.kotlinx.samples.DataClassKey -import org.bson.codecs.kotlinx.samples.DataClassLastItemDefaultsToNull -import org.bson.codecs.kotlinx.samples.DataClassListOfDataClasses -import org.bson.codecs.kotlinx.samples.DataClassListOfListOfDataClasses -import org.bson.codecs.kotlinx.samples.DataClassListOfSealed -import org.bson.codecs.kotlinx.samples.DataClassMapOfDataClasses -import org.bson.codecs.kotlinx.samples.DataClassMapOfListOfDataClasses -import org.bson.codecs.kotlinx.samples.DataClassNestedParameterizedTypes -import org.bson.codecs.kotlinx.samples.DataClassOpen -import org.bson.codecs.kotlinx.samples.DataClassOpenA -import org.bson.codecs.kotlinx.samples.DataClassOpenB -import org.bson.codecs.kotlinx.samples.DataClassOptionalBsonValues -import org.bson.codecs.kotlinx.samples.DataClassParameterized -import org.bson.codecs.kotlinx.samples.DataClassSealed -import org.bson.codecs.kotlinx.samples.DataClassSealedA -import org.bson.codecs.kotlinx.samples.DataClassSealedB -import org.bson.codecs.kotlinx.samples.DataClassSealedC -import org.bson.codecs.kotlinx.samples.DataClassSelfReferential -import org.bson.codecs.kotlinx.samples.DataClassWithAnnotations -import org.bson.codecs.kotlinx.samples.DataClassWithBooleanMapKey -import org.bson.codecs.kotlinx.samples.DataClassWithBsonConstructor -import org.bson.codecs.kotlinx.samples.DataClassWithBsonDiscriminator -import org.bson.codecs.kotlinx.samples.DataClassWithBsonExtraElements -import org.bson.codecs.kotlinx.samples.DataClassWithBsonId -import org.bson.codecs.kotlinx.samples.DataClassWithBsonIgnore -import org.bson.codecs.kotlinx.samples.DataClassWithBsonProperty -import org.bson.codecs.kotlinx.samples.DataClassWithBsonRepresentation -import org.bson.codecs.kotlinx.samples.DataClassWithCollections -import org.bson.codecs.kotlinx.samples.DataClassWithContextualDateValues -import org.bson.codecs.kotlinx.samples.DataClassWithDataClassMapKey -import org.bson.codecs.kotlinx.samples.DataClassWithDateValues -import org.bson.codecs.kotlinx.samples.DataClassWithDefaults -import org.bson.codecs.kotlinx.samples.DataClassWithEmbedded -import org.bson.codecs.kotlinx.samples.DataClassWithEncodeDefault -import org.bson.codecs.kotlinx.samples.DataClassWithEnum -import org.bson.codecs.kotlinx.samples.DataClassWithEnumMapKey -import org.bson.codecs.kotlinx.samples.DataClassWithFailingInit -import org.bson.codecs.kotlinx.samples.DataClassWithJsonElement -import org.bson.codecs.kotlinx.samples.DataClassWithJsonElements -import org.bson.codecs.kotlinx.samples.DataClassWithJsonElementsNullable -import org.bson.codecs.kotlinx.samples.DataClassWithListThatLastItemDefaultsToNull -import org.bson.codecs.kotlinx.samples.DataClassWithMutableList -import org.bson.codecs.kotlinx.samples.DataClassWithMutableMap -import org.bson.codecs.kotlinx.samples.DataClassWithMutableSet -import org.bson.codecs.kotlinx.samples.DataClassWithNestedParameterized -import org.bson.codecs.kotlinx.samples.DataClassWithNestedParameterizedDataClass -import org.bson.codecs.kotlinx.samples.DataClassWithNullableGeneric -import org.bson.codecs.kotlinx.samples.DataClassWithNulls -import org.bson.codecs.kotlinx.samples.DataClassWithPair -import org.bson.codecs.kotlinx.samples.DataClassWithParameterizedDataClass -import org.bson.codecs.kotlinx.samples.DataClassWithRequired -import org.bson.codecs.kotlinx.samples.DataClassWithSequence -import org.bson.codecs.kotlinx.samples.DataClassWithSimpleValues -import org.bson.codecs.kotlinx.samples.DataClassWithTriple -import org.bson.codecs.kotlinx.samples.Key -import org.bson.codecs.kotlinx.samples.SealedInterface -import org.bson.codecs.kotlinx.samples.ValueClass +import org.bson.codecs.kotlinx.samples.* import org.bson.json.JsonMode import org.bson.json.JsonWriterSettings import org.junit.jupiter.api.Test @@ -1126,6 +1062,13 @@ class KotlinSerializerCodecTest { } } + @Test + fun testSnakeCaseNamingStrategy() { + val expected = """{"camel_case_key": "snake_case_value", "a_b_cd": "camelCaseValue"}""" + val dataClass = DataClassWithCamelCase("snake_case_value", "camelCaseValue") + assertRoundTrips(expected, dataClass, BsonConfiguration(bsonNamingStrategy = BsonNamingStrategy.SNAKE_CASE)) + } + private inline fun assertRoundTrips( expected: String, value: T, @@ -1184,6 +1127,7 @@ class KotlinSerializerCodecTest { serializersModule: SerializersModule = defaultSerializersModule, configuration: BsonConfiguration = BsonConfiguration() ): T { + println("Deserializing: ${value.toJson()}") val codec = KotlinSerializerCodec.create(T::class, serializersModule, configuration)!! return codec.decode(BsonDocumentReader(value), DecoderContext.builder().build()) } diff --git a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt index e7a06600d20..df09e9528ea 100644 --- a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt +++ b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt @@ -102,6 +102,12 @@ data class DataClassWithDefaults( val listSimple: List = listOf("a", "b", "c") ) +@Serializable +data class DataClassWithCamelCase( + val camelCaseKey: String, + val aBCd: String, +) + @Serializable data class DataClassWithNulls(val boolean: Boolean?, val string: String?, val listSimple: List?) @Serializable From 5224ef8acac20828d35bc05570fabbe22639350a Mon Sep 17 00:00:00 2001 From: Seongbin Lee Date: Mon, 13 Jan 2025 15:47:29 +0900 Subject: [PATCH 2/4] Fix case converting logic with caching --- .../org/bson/codecs/kotlinx/BsonDecoder.kt | 21 +++--- .../org/bson/codecs/kotlinx/BsonEncoder.kt | 4 +- .../codecs/kotlinx/utils/BsonCodecUtils.kt | 67 +++++++++++++++++-- .../kotlinx/KotlinSerializerCodecTest.kt | 29 +++++++- .../codecs/kotlinx/samples/DataClasses.kt | 21 +++++- 5 files changed, 121 insertions(+), 21 deletions(-) diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt index 65be97f47fd..c00d09345d0 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt @@ -37,12 +37,13 @@ import org.bson.BsonType import org.bson.BsonValue import org.bson.codecs.BsonValueCodec import org.bson.codecs.DecoderContext +import org.bson.codecs.kotlinx.utils.BsonCodecUtils.cacheElementNamesByDescriptor import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonArrayDecoder import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonDecoder import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonDocumentDecoder import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonMapDecoder import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonPolymorphicDecoder -import org.bson.codecs.kotlinx.utils.BsonCodecUtils.toCamelCase +import org.bson.codecs.kotlinx.utils.BsonCodecUtils.getCachedElementNamesByDescriptor import org.bson.internal.NumberCodecHelper import org.bson.internal.StringCodecHelper import org.bson.types.ObjectId @@ -103,6 +104,7 @@ internal sealed class AbstractBsonDecoder( elementDescriptor.serialName, elementDescriptor.isNullable && !descriptor.isElementOptional(it)) } this.elementsMetadata = elementsMetadata + cacheElementNamesByDescriptor(descriptor, configuration) } override fun decodeElementIndex(descriptor: SerialDescriptor): Int { @@ -117,14 +119,7 @@ internal sealed class AbstractBsonDecoder( val elementMetadata = elementsMetadata ?: error("elementsMetadata may not be null.") val name: String? = when (reader.state ?: error("State of reader may not be null.")) { - AbstractBsonReader.State.NAME -> - reader.readName().let { - if (configuration.bsonNamingStrategy == BsonNamingStrategy.SNAKE_CASE) { - it.toCamelCase - } else { - it - } - } + AbstractBsonReader.State.NAME -> reader.readName() AbstractBsonReader.State.VALUE -> reader.currentName AbstractBsonReader.State.TYPE -> { reader.readBsonType() @@ -137,7 +132,13 @@ internal sealed class AbstractBsonDecoder( } return name?.let { - val index = descriptor.getElementIndex(it) + val index = + if (configuration.bsonNamingStrategy == BsonNamingStrategy.SNAKE_CASE) { + getCachedElementNamesByDescriptor(descriptor)[it]?.let { name -> descriptor.getElementIndex(name) } + ?: UNKNOWN_NAME + } else { + descriptor.getElementIndex(it) + } return if (index == UNKNOWN_NAME) { reader.skipValue() decodeElementIndexImpl(descriptor) diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt index 94957cf30c0..8a34bccdb36 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt @@ -31,7 +31,7 @@ import org.bson.BsonValue import org.bson.BsonWriter import org.bson.codecs.BsonValueCodec import org.bson.codecs.EncoderContext -import org.bson.codecs.kotlinx.utils.BsonCodecUtils.toSnakeCase +import org.bson.codecs.kotlinx.utils.BsonCodecUtils.convertCamelCase import org.bson.types.ObjectId /** @@ -207,7 +207,7 @@ internal open class BsonEncoderImpl( val name = value.toString().let { if (configuration.bsonNamingStrategy == BsonNamingStrategy.SNAKE_CASE) { - it.toSnakeCase + convertCamelCase(it, '_') } else { it } diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/utils/BsonCodecUtils.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/utils/BsonCodecUtils.kt index 3829a46de81..21549de7d34 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/utils/BsonCodecUtils.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/utils/BsonCodecUtils.kt @@ -15,9 +15,10 @@ */ package org.bson.codecs.kotlinx.utils -import java.util.* import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.elementNames import kotlinx.serialization.json.JsonNamingStrategy import kotlinx.serialization.modules.SerializersModule import org.bson.AbstractBsonReader @@ -62,6 +63,8 @@ internal object BsonCodecUtils { } } + private val cachedElementNamesByDescriptor: MutableMap> = mutableMapOf() + internal fun createBsonEncoder( writer: BsonWriter, serializersModule: SerializersModule, @@ -120,11 +123,65 @@ internal object BsonCodecUtils { else BsonMapDecoder(descriptor, reader, serializersModule, configuration) } - internal val String.toSnakeCase - get() = replace(Regex("([A-Z])"), "_$1").lowercase(Locale.getDefault()).replace(Regex("^_"), "") + internal fun cacheElementNamesByDescriptor(descriptor: SerialDescriptor, configuration: BsonConfiguration) { + val convertedNameMap = + when (configuration.bsonNamingStrategy) { + BsonNamingStrategy.SNAKE_CASE -> { + val snakeCasedNames = descriptor.elementNames.associateWith { name -> convertCamelCase(name, '_') } + + snakeCasedNames.entries + .groupBy { entry -> entry.value } + .filter { group -> group.value.size > 1 } + .entries + .forEach { group -> + val keys = group.value.joinToString(", ") { entry -> entry.key } + throw SerializationException( + "$keys in ${descriptor.serialName} generate same name: ${group.key}.") + } + + snakeCasedNames.entries.associate { it.value to it.key } + } + else -> emptyMap() + } + + cachedElementNamesByDescriptor[descriptor.serialName] = convertedNameMap + } + + internal fun getCachedElementNamesByDescriptor(descriptor: SerialDescriptor): Map { + return cachedElementNamesByDescriptor[descriptor.serialName] ?: emptyMap() + } + + // https://github.com/Kotlin/kotlinx.serialization/blob/f9f160a680da9f92c3bb121ae3644c96e57ba42e/formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt#L142-L174 + internal fun convertCamelCase(value: String, delimiter: Char) = + buildString(value.length * 2) { + var bufferedChar: Char? = null + var previousUpperCharsCount = 0 + + value.forEach { c -> + if (c.isUpperCase()) { + if (previousUpperCharsCount == 0 && isNotEmpty() && last() != delimiter) append(delimiter) - internal val String.toCamelCase - get() = replace(Regex("_([a-z])")) { it.value[1].uppercaseChar().toString() } + bufferedChar?.let(::append) + + previousUpperCharsCount++ + bufferedChar = c.lowercaseChar() + } else { + if (bufferedChar != null) { + if (previousUpperCharsCount > 1 && c.isLetter()) { + append(delimiter) + } + append(bufferedChar) + previousUpperCharsCount = 0 + bufferedChar = null + } + append(c) + } + } + + if (bufferedChar != null) { + append(bufferedChar) + } + } internal fun BsonNamingStrategy?.toJsonNamingStrategy(): JsonNamingStrategy? { return when (this) { diff --git a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt index 85fd79385f5..05921c10e5d 100644 --- a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt +++ b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt @@ -1064,8 +1064,33 @@ class KotlinSerializerCodecTest { @Test fun testSnakeCaseNamingStrategy() { - val expected = """{"camel_case_key": "snake_case_value", "a_b_cd": "camelCaseValue"}""" - val dataClass = DataClassWithCamelCase("snake_case_value", "camelCaseValue") + val expected = + """{"two_words": "", "my_property": "", "camel_case_underscores": "", "url_mapping": "", + | "my_http_auth": "", "my_http2_api_key": "", "my_http2fast_api_key": ""}""" + .trimMargin() + val dataClass = DataClassWithCamelCase() + assertRoundTrips(expected, dataClass, BsonConfiguration(bsonNamingStrategy = BsonNamingStrategy.SNAKE_CASE)) + } + + @Test + fun testSameSnakeCaseName() { + val expected = """{"my_http_auth": ""}""" + val dataClass = DataClassWithSameSnakeCaseName() + val exception = + assertThrows { + assertRoundTrips( + expected, dataClass, BsonConfiguration(bsonNamingStrategy = BsonNamingStrategy.SNAKE_CASE)) + } + assertEquals( + "myHTTPAuth, myHttpAuth in org.bson.codecs.kotlinx.samples.DataClassWithSameSnakeCaseName " + + "generate same name: my_http_auth.", + exception.message) + } + + @Test + fun testKotlinAllowedName() { + val expected = """{"имя_переменной": "", "variable _name": ""}""" + val dataClass = DataClassWithKotlinAllowedName() assertRoundTrips(expected, dataClass, BsonConfiguration(bsonNamingStrategy = BsonNamingStrategy.SNAKE_CASE)) } diff --git a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt index df09e9528ea..91b8f5a218a 100644 --- a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt +++ b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt @@ -104,8 +104,25 @@ data class DataClassWithDefaults( @Serializable data class DataClassWithCamelCase( - val camelCaseKey: String, - val aBCd: String, + val twoWords: String = "", + @Suppress("ConstructorParameterNaming") val MyProperty: String = "", + @Suppress("ConstructorParameterNaming") val camel_Case_Underscores: String = "", + @Suppress("ConstructorParameterNaming") val URLMapping: String = "", + val myHTTPAuth: String = "", + val myHTTP2ApiKey: String = "", + val myHTTP2fastApiKey: String = "", +) + +@Serializable +data class DataClassWithSameSnakeCaseName( + val myHTTPAuth: String = "", + val myHttpAuth: String = "", +) + +@Serializable +data class DataClassWithKotlinAllowedName( + @Suppress("ConstructorParameterNaming") val имяПеременной: String = "", + @Suppress("ConstructorParameterNaming") val `variable Name`: String = "", ) @Serializable data class DataClassWithNulls(val boolean: Boolean?, val string: String?, val listSimple: List?) From 1660f2470faa48d9f3cc52c3ef59676a8debe71c Mon Sep 17 00:00:00 2001 From: Seongbin Lee Date: Tue, 14 Jan 2025 10:09:14 +0900 Subject: [PATCH 3/4] Refactor imports in KotlinSerializerCodecTest.kt --- .../kotlinx/KotlinSerializerCodecTest.kt | 73 ++++++++++++++++++- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt index 05921c10e5d..b37c2cfa787 100644 --- a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt +++ b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt @@ -16,7 +16,7 @@ package org.bson.codecs.kotlinx import java.math.BigDecimal -import java.util.Base64 +import java.util.* import java.util.stream.Stream import kotlin.test.assertEquals import kotlinx.datetime.Instant @@ -26,7 +26,10 @@ import kotlinx.datetime.LocalTime import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.MissingFieldException import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.* +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.plus import kotlinx.serialization.modules.polymorphic @@ -46,7 +49,71 @@ import org.bson.BsonUndefined import org.bson.codecs.DecoderContext import org.bson.codecs.EncoderContext import org.bson.codecs.configuration.CodecConfigurationException -import org.bson.codecs.kotlinx.samples.* +import org.bson.codecs.kotlinx.samples.Box +import org.bson.codecs.kotlinx.samples.DataClassBsonValues +import org.bson.codecs.kotlinx.samples.DataClassContainsOpen +import org.bson.codecs.kotlinx.samples.DataClassContainsValueClass +import org.bson.codecs.kotlinx.samples.DataClassEmbedded +import org.bson.codecs.kotlinx.samples.DataClassKey +import org.bson.codecs.kotlinx.samples.DataClassLastItemDefaultsToNull +import org.bson.codecs.kotlinx.samples.DataClassListOfDataClasses +import org.bson.codecs.kotlinx.samples.DataClassListOfListOfDataClasses +import org.bson.codecs.kotlinx.samples.DataClassListOfSealed +import org.bson.codecs.kotlinx.samples.DataClassMapOfDataClasses +import org.bson.codecs.kotlinx.samples.DataClassMapOfListOfDataClasses +import org.bson.codecs.kotlinx.samples.DataClassNestedParameterizedTypes +import org.bson.codecs.kotlinx.samples.DataClassOpen +import org.bson.codecs.kotlinx.samples.DataClassOpenA +import org.bson.codecs.kotlinx.samples.DataClassOpenB +import org.bson.codecs.kotlinx.samples.DataClassOptionalBsonValues +import org.bson.codecs.kotlinx.samples.DataClassParameterized +import org.bson.codecs.kotlinx.samples.DataClassSealed +import org.bson.codecs.kotlinx.samples.DataClassSealedA +import org.bson.codecs.kotlinx.samples.DataClassSealedB +import org.bson.codecs.kotlinx.samples.DataClassSealedC +import org.bson.codecs.kotlinx.samples.DataClassSelfReferential +import org.bson.codecs.kotlinx.samples.DataClassWithAnnotations +import org.bson.codecs.kotlinx.samples.DataClassWithBooleanMapKey +import org.bson.codecs.kotlinx.samples.DataClassWithBsonConstructor +import org.bson.codecs.kotlinx.samples.DataClassWithBsonDiscriminator +import org.bson.codecs.kotlinx.samples.DataClassWithBsonExtraElements +import org.bson.codecs.kotlinx.samples.DataClassWithBsonId +import org.bson.codecs.kotlinx.samples.DataClassWithBsonIgnore +import org.bson.codecs.kotlinx.samples.DataClassWithBsonProperty +import org.bson.codecs.kotlinx.samples.DataClassWithBsonRepresentation +import org.bson.codecs.kotlinx.samples.DataClassWithCamelCase +import org.bson.codecs.kotlinx.samples.DataClassWithCollections +import org.bson.codecs.kotlinx.samples.DataClassWithContextualDateValues +import org.bson.codecs.kotlinx.samples.DataClassWithDataClassMapKey +import org.bson.codecs.kotlinx.samples.DataClassWithDateValues +import org.bson.codecs.kotlinx.samples.DataClassWithDefaults +import org.bson.codecs.kotlinx.samples.DataClassWithEmbedded +import org.bson.codecs.kotlinx.samples.DataClassWithEncodeDefault +import org.bson.codecs.kotlinx.samples.DataClassWithEnum +import org.bson.codecs.kotlinx.samples.DataClassWithEnumMapKey +import org.bson.codecs.kotlinx.samples.DataClassWithFailingInit +import org.bson.codecs.kotlinx.samples.DataClassWithJsonElement +import org.bson.codecs.kotlinx.samples.DataClassWithJsonElements +import org.bson.codecs.kotlinx.samples.DataClassWithJsonElementsNullable +import org.bson.codecs.kotlinx.samples.DataClassWithKotlinAllowedName +import org.bson.codecs.kotlinx.samples.DataClassWithListThatLastItemDefaultsToNull +import org.bson.codecs.kotlinx.samples.DataClassWithMutableList +import org.bson.codecs.kotlinx.samples.DataClassWithMutableMap +import org.bson.codecs.kotlinx.samples.DataClassWithMutableSet +import org.bson.codecs.kotlinx.samples.DataClassWithNestedParameterized +import org.bson.codecs.kotlinx.samples.DataClassWithNestedParameterizedDataClass +import org.bson.codecs.kotlinx.samples.DataClassWithNullableGeneric +import org.bson.codecs.kotlinx.samples.DataClassWithNulls +import org.bson.codecs.kotlinx.samples.DataClassWithPair +import org.bson.codecs.kotlinx.samples.DataClassWithParameterizedDataClass +import org.bson.codecs.kotlinx.samples.DataClassWithRequired +import org.bson.codecs.kotlinx.samples.DataClassWithSameSnakeCaseName +import org.bson.codecs.kotlinx.samples.DataClassWithSequence +import org.bson.codecs.kotlinx.samples.DataClassWithSimpleValues +import org.bson.codecs.kotlinx.samples.DataClassWithTriple +import org.bson.codecs.kotlinx.samples.Key +import org.bson.codecs.kotlinx.samples.SealedInterface +import org.bson.codecs.kotlinx.samples.ValueClass import org.bson.json.JsonMode import org.bson.json.JsonWriterSettings import org.junit.jupiter.api.Test From ee1e3e2b40a5f376127fe937807e0b2295a471c8 Mon Sep 17 00:00:00 2001 From: Seongbin Lee Date: Tue, 14 Jan 2025 10:09:56 +0900 Subject: [PATCH 4/4] Reimport java.util.Base64 --- .../kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt index b37c2cfa787..85d922c3096 100644 --- a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt +++ b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt @@ -16,7 +16,7 @@ package org.bson.codecs.kotlinx import java.math.BigDecimal -import java.util.* +import java.util.Base64 import java.util.stream.Stream import kotlin.test.assertEquals import kotlinx.datetime.Instant