From 025def78efbe0091432dc1af1003480636877612 Mon Sep 17 00:00:00 2001 From: Aaur1s Date: Fri, 6 Jun 2025 13:57:34 +0300 Subject: [PATCH 1/2] Refactor BsonNamingStrategy into fun interface --- .../bson/codecs/kotlinx/BsonConfiguration.kt | 27 ++++++++--- .../org/bson/codecs/kotlinx/BsonDecoder.kt | 2 +- .../org/bson/codecs/kotlinx/BsonEncoder.kt | 10 +---- .../bson/codecs/kotlinx/JsonBsonDecoder.kt | 4 +- .../bson/codecs/kotlinx/JsonBsonEncoder.kt | 4 +- .../codecs/kotlinx/utils/BsonCodecUtils.kt | 45 +++++++++---------- 6 files changed, 48 insertions(+), 44 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 8a163f42f83..6adf85fb4c1 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 @@ -15,6 +15,9 @@ */ package org.bson.codecs.kotlinx +import kotlinx.serialization.ExperimentalSerializationApi +import org.bson.codecs.kotlinx.utils.BsonCodecUtils.convertCamelCase + /** * Bson Configuration for serialization * @@ -37,13 +40,23 @@ public data class BsonConfiguration( /** * Optional BSON naming strategy for a field. * - * @since 5.4 + * @since 5.6 */ -public enum class BsonNamingStrategy { +@OptIn(ExperimentalSerializationApi::class) +public fun interface BsonNamingStrategy { + + public fun transformName(serialName: String): String + + public companion object { + /** + * A strategy that transforms serial names from camel case to snake case — lowercase characters with words + * separated by underscores. + */ + public val SNAKE_CASE: BsonNamingStrategy = + object : BsonNamingStrategy { + override fun transformName(serialName: String): String = convertCamelCase(serialName, '_') + override fun toString() = "BsonNamingStrategySnakeCase" + } - /** - * A strategy that transforms serial names from camel case to snake case — lowercase characters with words separated - * by underscores. - */ - 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 c00d09345d0..e278e0d8f4a 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 @@ -133,7 +133,7 @@ internal sealed class AbstractBsonDecoder( return name?.let { val index = - if (configuration.bsonNamingStrategy == BsonNamingStrategy.SNAKE_CASE) { + if (configuration.bsonNamingStrategy != null) { getCachedElementNamesByDescriptor(descriptor)[it]?.let { name -> descriptor.getElementIndex(name) } ?: UNKNOWN_NAME } else { 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 8a34bccdb36..781d42487bf 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,6 @@ 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.convertCamelCase import org.bson.types.ObjectId /** @@ -204,14 +203,7 @@ internal open class BsonEncoderImpl( } internal fun encodeName(value: Any) { - val name = - value.toString().let { - if (configuration.bsonNamingStrategy == BsonNamingStrategy.SNAKE_CASE) { - convertCamelCase(it, '_') - } else { - it - } - } + val name = value.toString().let { configuration.bsonNamingStrategy?.transformName(it) ?: 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 bd8b6739958..ff0a06765c7 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,7 +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.codecs.kotlinx.utils.BsonCodecUtils.asJsonNamingStrategy import org.bson.internal.UuidHelper @OptIn(ExperimentalSerializationApi::class) @@ -43,7 +43,7 @@ internal interface JsonBsonDecoder : BsonDecoder, JsonDecoder { explicitNulls = configuration.explicitNulls encodeDefaults = configuration.encodeDefaults classDiscriminator = configuration.classDiscriminator - namingStrategy = configuration.bsonNamingStrategy.toJsonNamingStrategy() + namingStrategy = configuration.bsonNamingStrategy.asJsonNamingStrategy() 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 4a754834e6d..b61ead53d4c 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,7 +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.codecs.kotlinx.utils.BsonCodecUtils.asJsonNamingStrategy import org.bson.types.Decimal128 @OptIn(ExperimentalSerializationApi::class) @@ -53,7 +53,7 @@ internal class JsonBsonEncoder( explicitNulls = configuration.explicitNulls encodeDefaults = configuration.encodeDefaults classDiscriminator = configuration.classDiscriminator - namingStrategy = configuration.bsonNamingStrategy.toJsonNamingStrategy() + namingStrategy = configuration.bsonNamingStrategy.asJsonNamingStrategy() 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 daf6c7df6f9..945b1f02385 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 @@ -125,25 +125,25 @@ internal object BsonCodecUtils { 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 - .fold(StringBuilder("")) { acc, group -> - val keys = group.value.joinToString(", ") { entry -> entry.key } - acc.append("$keys in ${descriptor.serialName} generate same name: ${group.key}.\n") - } - .toString() - .takeIf { it.trim().isNotEmpty() } - ?.let { errorMessage: String -> throw SerializationException(errorMessage) } + if (configuration.bsonNamingStrategy != null) { + val transformedNames = + descriptor.elementNames.associateWith(configuration.bsonNamingStrategy::transformName) + + transformedNames.entries + .groupBy { entry -> entry.value } + .filter { group -> group.value.size > 1 } + .entries + .fold(StringBuilder("")) { acc, group -> + val keys = group.value.joinToString(", ") { entry -> entry.key } + acc.append("$keys in ${descriptor.serialName} generate same name: ${group.key}.\n") + } + .toString() + .takeIf { it.trim().isNotEmpty() } + ?.let { errorMessage: String -> throw SerializationException(errorMessage) } - snakeCasedNames.entries.associate { it.value to it.key } - } - else -> emptyMap() + transformedNames.entries.associate { it.value to it.key } + } else { + emptyMap() } cachedElementNamesByDescriptor[descriptor.serialName] = convertedNameMap @@ -185,10 +185,9 @@ internal object BsonCodecUtils { } } - internal fun BsonNamingStrategy?.toJsonNamingStrategy(): JsonNamingStrategy? { - return when (this) { - BsonNamingStrategy.SNAKE_CASE -> JsonNamingStrategy.SnakeCase - else -> null - } + internal fun BsonNamingStrategy?.asJsonNamingStrategy(): JsonNamingStrategy? { + this ?: return null + + return JsonNamingStrategy { descriptor, index, serialName -> this.transformName(serialName) } } } From 28f3853ce731a21efa31992ffb85a885d420ccac Mon Sep 17 00:00:00 2001 From: Aaur1s Date: Fri, 6 Jun 2025 13:58:36 +0300 Subject: [PATCH 2/2] Add Kotlinx Kebab case --- .../org/bson/codecs/kotlinx/BsonConfiguration.kt | 9 +++++++++ .../bson/codecs/kotlinx/KotlinSerializerCodecTest.kt | 10 ++++++++++ 2 files changed, 19 insertions(+) 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 6adf85fb4c1..563f74ac3df 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 @@ -58,5 +58,14 @@ public fun interface BsonNamingStrategy { override fun toString() = "BsonNamingStrategySnakeCase" } + /** + * A strategy that transforms serial names from camel case to kebab case — lowercase characters with words + * separated by hyphens. + */ + public val KEBAB_CASE: BsonNamingStrategy = + object : BsonNamingStrategy { + override fun transformName(serialName: String): String = convertCamelCase(serialName, '-') + override fun toString() = "BsonNamingStrategyKebabCase" + } } } 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 f9b3eb753c5..d5ad478fd90 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 @@ -1139,6 +1139,16 @@ class KotlinSerializerCodecTest { assertRoundTrips(expected, dataClass, BsonConfiguration(bsonNamingStrategy = BsonNamingStrategy.SNAKE_CASE)) } + @Test + fun testKebabCaseNamingStrategy() { + 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.KEBAB_CASE)) + } + @Test fun testSameSnakeCaseName() { val expected = """{"my_http_auth": "", "my_http_auth1": ""}"""