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 8a163f42f8..563f74ac3d 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,32 @@ 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, + /** + * 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/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt index c00d09345d..e278e0d8f4 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 8a34bccdb3..781d42487b 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 bd8b673995..ff0a06765c 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 4a754834e6..b61ead53d4 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 daf6c7df6f..945b1f0238 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) } } } 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 f9b3eb753c..d5ad478fd9 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": ""}"""