Skip to content

Commit bfca6e0

Browse files
committed
Fix case converting logic with caching
1 parent 9e52922 commit bfca6e0

File tree

5 files changed

+121
-21
lines changed

5 files changed

+121
-21
lines changed

bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,13 @@ import org.bson.BsonType
3737
import org.bson.BsonValue
3838
import org.bson.codecs.BsonValueCodec
3939
import org.bson.codecs.DecoderContext
40+
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.cacheElementNamesByDescriptor
4041
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonArrayDecoder
4142
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonDecoder
4243
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonDocumentDecoder
4344
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonMapDecoder
4445
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonPolymorphicDecoder
45-
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.toCamelCase
46+
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.getCachedElementNamesByDescriptor
4647
import org.bson.internal.NumberCodecHelper
4748
import org.bson.internal.StringCodecHelper
4849
import org.bson.types.ObjectId
@@ -103,6 +104,7 @@ internal sealed class AbstractBsonDecoder(
103104
elementDescriptor.serialName, elementDescriptor.isNullable && !descriptor.isElementOptional(it))
104105
}
105106
this.elementsMetadata = elementsMetadata
107+
cacheElementNamesByDescriptor(descriptor, configuration)
106108
}
107109

108110
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
@@ -117,14 +119,7 @@ internal sealed class AbstractBsonDecoder(
117119
val elementMetadata = elementsMetadata ?: error("elementsMetadata may not be null.")
118120
val name: String? =
119121
when (reader.state ?: error("State of reader may not be null.")) {
120-
AbstractBsonReader.State.NAME ->
121-
reader.readName().let {
122-
if (configuration.bsonNamingStrategy == BsonNamingStrategy.SNAKE_CASE) {
123-
it.toCamelCase
124-
} else {
125-
it
126-
}
127-
}
122+
AbstractBsonReader.State.NAME -> reader.readName()
128123
AbstractBsonReader.State.VALUE -> reader.currentName
129124
AbstractBsonReader.State.TYPE -> {
130125
reader.readBsonType()
@@ -137,7 +132,13 @@ internal sealed class AbstractBsonDecoder(
137132
}
138133

139134
return name?.let {
140-
val index = descriptor.getElementIndex(it)
135+
val index =
136+
if (configuration.bsonNamingStrategy == BsonNamingStrategy.SNAKE_CASE) {
137+
getCachedElementNamesByDescriptor(descriptor)[it]?.let { name -> descriptor.getElementIndex(name) }
138+
?: UNKNOWN_NAME
139+
} else {
140+
descriptor.getElementIndex(it)
141+
}
141142
return if (index == UNKNOWN_NAME) {
142143
reader.skipValue()
143144
decodeElementIndexImpl(descriptor)

bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import org.bson.BsonValue
3131
import org.bson.BsonWriter
3232
import org.bson.codecs.BsonValueCodec
3333
import org.bson.codecs.EncoderContext
34-
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.toSnakeCase
34+
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.convertCamelCase
3535
import org.bson.types.ObjectId
3636

3737
/**
@@ -207,7 +207,7 @@ internal open class BsonEncoderImpl(
207207
val name =
208208
value.toString().let {
209209
if (configuration.bsonNamingStrategy == BsonNamingStrategy.SNAKE_CASE) {
210-
it.toSnakeCase
210+
convertCamelCase(it, '_')
211211
} else {
212212
it
213213
}

bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/utils/BsonCodecUtils.kt

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
*/
1616
package org.bson.codecs.kotlinx.utils
1717

18-
import java.util.*
1918
import kotlinx.serialization.ExperimentalSerializationApi
19+
import kotlinx.serialization.SerializationException
2020
import kotlinx.serialization.descriptors.SerialDescriptor
21+
import kotlinx.serialization.descriptors.elementNames
2122
import kotlinx.serialization.json.JsonNamingStrategy
2223
import kotlinx.serialization.modules.SerializersModule
2324
import org.bson.AbstractBsonReader
@@ -62,6 +63,8 @@ internal object BsonCodecUtils {
6263
}
6364
}
6465

66+
private val cachedElementNamesByDescriptor: MutableMap<String, Map<String, String>> = mutableMapOf()
67+
6568
internal fun createBsonEncoder(
6669
writer: BsonWriter,
6770
serializersModule: SerializersModule,
@@ -120,11 +123,65 @@ internal object BsonCodecUtils {
120123
else BsonMapDecoder(descriptor, reader, serializersModule, configuration)
121124
}
122125

123-
internal val String.toSnakeCase
124-
get() = replace(Regex("([A-Z])"), "_$1").lowercase(Locale.getDefault()).replace(Regex("^_"), "")
126+
internal fun cacheElementNamesByDescriptor(descriptor: SerialDescriptor, configuration: BsonConfiguration) {
127+
val convertedNameMap =
128+
when (configuration.bsonNamingStrategy) {
129+
BsonNamingStrategy.SNAKE_CASE -> {
130+
val snakeCasedNames = descriptor.elementNames.associateWith { name -> convertCamelCase(name, '_') }
131+
132+
snakeCasedNames.entries
133+
.groupBy { entry -> entry.value }
134+
.filter { group -> group.value.size > 1 }
135+
.entries
136+
.forEach { group ->
137+
val keys = group.value.joinToString(", ") { entry -> entry.key }
138+
throw SerializationException(
139+
"$keys in ${descriptor.serialName} generate same name: ${group.key}.")
140+
}
141+
142+
snakeCasedNames.entries.associate { it.value to it.key }
143+
}
144+
else -> emptyMap()
145+
}
146+
147+
cachedElementNamesByDescriptor[descriptor.serialName] = convertedNameMap
148+
}
149+
150+
internal fun getCachedElementNamesByDescriptor(descriptor: SerialDescriptor): Map<String, String> {
151+
return cachedElementNamesByDescriptor[descriptor.serialName] ?: emptyMap()
152+
}
153+
154+
// https://github.com/Kotlin/kotlinx.serialization/blob/f9f160a680da9f92c3bb121ae3644c96e57ba42e/formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt#L142-L174
155+
internal fun convertCamelCase(value: String, delimiter: Char) =
156+
buildString(value.length * 2) {
157+
var bufferedChar: Char? = null
158+
var previousUpperCharsCount = 0
159+
160+
value.forEach { c ->
161+
if (c.isUpperCase()) {
162+
if (previousUpperCharsCount == 0 && isNotEmpty() && last() != delimiter) append(delimiter)
125163

126-
internal val String.toCamelCase
127-
get() = replace(Regex("_([a-z])")) { it.value[1].uppercaseChar().toString() }
164+
bufferedChar?.let(::append)
165+
166+
previousUpperCharsCount++
167+
bufferedChar = c.lowercaseChar()
168+
} else {
169+
if (bufferedChar != null) {
170+
if (previousUpperCharsCount > 1 && c.isLetter()) {
171+
append(delimiter)
172+
}
173+
append(bufferedChar)
174+
previousUpperCharsCount = 0
175+
bufferedChar = null
176+
}
177+
append(c)
178+
}
179+
}
180+
181+
if (bufferedChar != null) {
182+
append(bufferedChar)
183+
}
184+
}
128185

129186
internal fun BsonNamingStrategy?.toJsonNamingStrategy(): JsonNamingStrategy? {
130187
return when (this) {

bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,8 +1064,33 @@ class KotlinSerializerCodecTest {
10641064

10651065
@Test
10661066
fun testSnakeCaseNamingStrategy() {
1067-
val expected = """{"camel_case_key": "snake_case_value", "a_b_cd": "camelCaseValue"}"""
1068-
val dataClass = DataClassWithCamelCase("snake_case_value", "camelCaseValue")
1067+
val expected =
1068+
"""{"two_words": "", "my_property": "", "camel_case_underscores": "", "url_mapping": "",
1069+
| "my_http_auth": "", "my_http2_api_key": "", "my_http2fast_api_key": ""}"""
1070+
.trimMargin()
1071+
val dataClass = DataClassWithCamelCase()
1072+
assertRoundTrips(expected, dataClass, BsonConfiguration(bsonNamingStrategy = BsonNamingStrategy.SNAKE_CASE))
1073+
}
1074+
1075+
@Test
1076+
fun testSameSnakeCaseName() {
1077+
val expected = """{"my_http_auth": ""}"""
1078+
val dataClass = DataClassWithSameSnakeCaseName()
1079+
val exception =
1080+
assertThrows<SerializationException> {
1081+
assertRoundTrips(
1082+
expected, dataClass, BsonConfiguration(bsonNamingStrategy = BsonNamingStrategy.SNAKE_CASE))
1083+
}
1084+
assertEquals(
1085+
"myHTTPAuth, myHttpAuth in org.bson.codecs.kotlinx.samples.DataClassWithSameSnakeCaseName " +
1086+
"generate same name: my_http_auth.",
1087+
exception.message)
1088+
}
1089+
1090+
@Test
1091+
fun testKotlinAllowedName() {
1092+
val expected = """{"имя_переменной": "", "variable _name": ""}"""
1093+
val dataClass = DataClassWithKotlinAllowedName()
10691094
assertRoundTrips(expected, dataClass, BsonConfiguration(bsonNamingStrategy = BsonNamingStrategy.SNAKE_CASE))
10701095
}
10711096

bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,25 @@ data class DataClassWithDefaults(
104104

105105
@Serializable
106106
data class DataClassWithCamelCase(
107-
val camelCaseKey: String,
108-
val aBCd: String,
107+
val twoWords: String = "",
108+
@Suppress("ConstructorParameterNaming") val MyProperty: String = "",
109+
@Suppress("ConstructorParameterNaming") val camel_Case_Underscores: String = "",
110+
@Suppress("ConstructorParameterNaming") val URLMapping: String = "",
111+
val myHTTPAuth: String = "",
112+
val myHTTP2ApiKey: String = "",
113+
val myHTTP2fastApiKey: String = "",
114+
)
115+
116+
@Serializable
117+
data class DataClassWithSameSnakeCaseName(
118+
val myHTTPAuth: String = "",
119+
val myHttpAuth: String = "",
120+
)
121+
122+
@Serializable
123+
data class DataClassWithKotlinAllowedName(
124+
@Suppress("ConstructorParameterNaming") val имяПеременной: String = "",
125+
@Suppress("ConstructorParameterNaming") val `variable Name`: String = "",
109126
)
110127

111128
@Serializable data class DataClassWithNulls(val boolean: Boolean?, val string: String?, val listSimple: List<String?>?)

0 commit comments

Comments
 (0)