Skip to content

Commit 8a3ed23

Browse files
authored
Add kebab-case naming strategy (#2531)
1 parent b3f6e0f commit 8a3ed23

File tree

3 files changed

+116
-25
lines changed

3 files changed

+116
-25
lines changed

formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonNamingStrategyTest.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,52 @@ class JsonNamingStrategyTest : JsonTestBase() {
103103
}
104104
}
105105

106+
@Test
107+
fun testKebabCaseStrategy() {
108+
fun apply(name: String) =
109+
JsonNamingStrategy.KebabCase.serialNameForJson(String.serializer().descriptor, 0, name)
110+
111+
val cases = mapOf<String, String>(
112+
"" to "",
113+
"_" to "_",
114+
"-" to "-",
115+
"___" to "___",
116+
"---" to "---",
117+
"a" to "a",
118+
"A" to "a",
119+
"-1" to "-1",
120+
"-a" to "-a",
121+
"-A" to "-a",
122+
"property" to "property",
123+
"twoWords" to "two-words",
124+
"threeDistinctWords" to "three-distinct-words",
125+
"ThreeDistinctWords" to "three-distinct-words",
126+
"Oneword" to "oneword",
127+
"camel-Case-WithDashes" to "camel-case-with-dashes",
128+
"_many----dashes--" to "_many----dashes--",
129+
"URLmapping" to "ur-lmapping",
130+
"URLMapping" to "url-mapping",
131+
"IOStream" to "io-stream",
132+
"IOstream" to "i-ostream",
133+
"myIo2Stream" to "my-io2-stream",
134+
"myIO2Stream" to "my-io2-stream",
135+
"myIO2stream" to "my-io2stream",
136+
"myIO2streamMax" to "my-io2stream-max",
137+
"InURLBetween" to "in-url-between",
138+
"myHTTP2APIKey" to "my-http2-api-key",
139+
"myHTTP2fastApiKey" to "my-http2fast-api-key",
140+
"myHTTP23APIKey" to "my-http23-api-key",
141+
"myHttp23ApiKey" to "my-http23-api-key",
142+
"theWWW" to "the-www",
143+
"theWWW-URL-xxx" to "the-www-url-xxx",
144+
"hasDigit123AndPostfix" to "has-digit123-and-postfix"
145+
)
146+
147+
cases.forEach { (input, expected) ->
148+
assertEquals(expected, apply(input))
149+
}
150+
}
151+
106152
@Serializable
107153
data class DontUseOriginal(val testCase: String)
108154

formats/json/api/kotlinx-serialization-json.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ public abstract interface class kotlinx/serialization/json/JsonNamingStrategy {
279279
}
280280

281281
public final class kotlinx/serialization/json/JsonNamingStrategy$Builtins {
282+
public final fun getKebabCase ()Lkotlinx/serialization/json/JsonNamingStrategy;
282283
public final fun getSnakeCase ()Lkotlinx/serialization/json/JsonNamingStrategy;
283284
}
284285

formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt

Lines changed: 69 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -95,39 +95,83 @@ public fun interface JsonNamingStrategy {
9595
*/
9696
@ExperimentalSerializationApi
9797
public val SnakeCase: JsonNamingStrategy = object : JsonNamingStrategy {
98-
override fun serialNameForJson(descriptor: SerialDescriptor, elementIndex: Int, serialName: String): String =
99-
buildString(serialName.length * 2) {
100-
var bufferedChar: Char? = null
101-
var previousUpperCharsCount = 0
98+
override fun serialNameForJson(
99+
descriptor: SerialDescriptor,
100+
elementIndex: Int,
101+
serialName: String
102+
): String = convertCamelCase(serialName, '_')
102103

103-
serialName.forEach { c ->
104-
if (c.isUpperCase()) {
105-
if (previousUpperCharsCount == 0 && isNotEmpty() && last() != '_')
106-
append('_')
104+
override fun toString(): String = "kotlinx.serialization.json.JsonNamingStrategy.SnakeCase"
105+
}
106+
107+
/**
108+
* A strategy that transforms serial names from camel case to kebab case — lowercase characters with words separated by dashes.
109+
* The descriptor parameter is not used.
110+
*
111+
* **Transformation rules**
112+
*
113+
* Words' bounds are defined by uppercase characters. If there is a single uppercase char, it is transformed into lowercase one with a dash in front:
114+
* `twoWords` -> `two-words`. No dash is added if it was a beginning of the name: `MyProperty` -> `my-property`. Also, no dash is added if it was already there:
115+
* `camel-Case-WithDashes` -> `camel-case-with-dashes`.
116+
*
117+
* **Acronyms**
118+
*
119+
* Since acronym rules are quite complex, it is recommended to lowercase all acronyms in source code.
120+
* If there is an uppercase acronym — a sequence of uppercase chars — they are considered as a whole word from the start to second-to-last character of the sequence:
121+
* `URLMapping` -> `url-mapping`, `myHTTPAuth` -> `my-http-auth`. Non-letter characters allow the word to continue:
122+
* `myHTTP2APIKey` -> `my-http2-api-key`, `myHTTP2fastApiKey` -> `my-http2fast-api-key`.
123+
*
124+
* **Note on cases**
125+
*
126+
* Whether a character is in upper case is determined by the result of [Char.isUpperCase] function.
127+
* Lowercase transformation is performed by [Char.lowercaseChar], not by [Char.lowercase],
128+
* and therefore does not support one-to-many and many-to-one character mappings.
129+
* See the documentation of these functions for details.
130+
*/
131+
@ExperimentalSerializationApi
132+
public val KebabCase: JsonNamingStrategy = object : JsonNamingStrategy {
133+
override fun serialNameForJson(
134+
descriptor: SerialDescriptor,
135+
elementIndex: Int,
136+
serialName: String
137+
): String = convertCamelCase(serialName, '-')
107138

108-
bufferedChar?.let(::append)
139+
override fun toString(): String = "kotlinx.serialization.json.JsonNamingStrategy.KebabCase"
140+
}
109141

110-
previousUpperCharsCount++
111-
bufferedChar = c.lowercaseChar()
112-
} else {
113-
if (bufferedChar != null) {
114-
if (previousUpperCharsCount > 1 && c.isLetter()) {
115-
append('_')
116-
}
117-
append(bufferedChar)
118-
previousUpperCharsCount = 0
119-
bufferedChar = null
142+
private fun convertCamelCase(
143+
serialName: String,
144+
delimiter: Char
145+
) = buildString(serialName.length * 2) {
146+
var bufferedChar: Char? = null
147+
var previousUpperCharsCount = 0
148+
149+
serialName.forEach { c ->
150+
if (c.isUpperCase()) {
151+
if (previousUpperCharsCount == 0 && isNotEmpty() && last() != delimiter)
152+
append(delimiter)
153+
154+
bufferedChar?.let(::append)
155+
156+
previousUpperCharsCount++
157+
bufferedChar = c.lowercaseChar()
158+
} else {
159+
if (bufferedChar != null) {
160+
if (previousUpperCharsCount > 1 && c.isLetter()) {
161+
append(delimiter)
120162
}
121-
append(c)
163+
append(bufferedChar)
164+
previousUpperCharsCount = 0
165+
bufferedChar = null
122166
}
167+
append(c)
123168
}
169+
}
124170

125-
if(bufferedChar != null) {
126-
append(bufferedChar)
127-
}
171+
if (bufferedChar != null) {
172+
append(bufferedChar)
128173
}
174+
}
129175

130-
override fun toString(): String = "kotlinx.serialization.json.JsonNamingStrategy.SnakeCase"
131-
}
132176
}
133177
}

0 commit comments

Comments
 (0)