Skip to content

JAVA-5894 Refactor BsonNamingStrategy into fun interface #1730

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 = [email protected]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 = [email protected]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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": ""}"""
Expand Down