Skip to content

Commit

Permalink
Add kebab-case naming strategy (#2531)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kantis authored Jan 24, 2024
1 parent b3f6e0f commit 8a3ed23
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,52 @@ class JsonNamingStrategyTest : JsonTestBase() {
}
}

@Test
fun testKebabCaseStrategy() {
fun apply(name: String) =
JsonNamingStrategy.KebabCase.serialNameForJson(String.serializer().descriptor, 0, name)

val cases = mapOf<String, String>(
"" to "",
"_" to "_",
"-" to "-",
"___" to "___",
"---" to "---",
"a" to "a",
"A" to "a",
"-1" to "-1",
"-a" to "-a",
"-A" to "-a",
"property" to "property",
"twoWords" to "two-words",
"threeDistinctWords" to "three-distinct-words",
"ThreeDistinctWords" to "three-distinct-words",
"Oneword" to "oneword",
"camel-Case-WithDashes" to "camel-case-with-dashes",
"_many----dashes--" to "_many----dashes--",
"URLmapping" to "ur-lmapping",
"URLMapping" to "url-mapping",
"IOStream" to "io-stream",
"IOstream" to "i-ostream",
"myIo2Stream" to "my-io2-stream",
"myIO2Stream" to "my-io2-stream",
"myIO2stream" to "my-io2stream",
"myIO2streamMax" to "my-io2stream-max",
"InURLBetween" to "in-url-between",
"myHTTP2APIKey" to "my-http2-api-key",
"myHTTP2fastApiKey" to "my-http2fast-api-key",
"myHTTP23APIKey" to "my-http23-api-key",
"myHttp23ApiKey" to "my-http23-api-key",
"theWWW" to "the-www",
"theWWW-URL-xxx" to "the-www-url-xxx",
"hasDigit123AndPostfix" to "has-digit123-and-postfix"
)

cases.forEach { (input, expected) ->
assertEquals(expected, apply(input))
}
}

@Serializable
data class DontUseOriginal(val testCase: String)

Expand Down
1 change: 1 addition & 0 deletions formats/json/api/kotlinx-serialization-json.api
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ public abstract interface class kotlinx/serialization/json/JsonNamingStrategy {
}

public final class kotlinx/serialization/json/JsonNamingStrategy$Builtins {
public final fun getKebabCase ()Lkotlinx/serialization/json/JsonNamingStrategy;
public final fun getSnakeCase ()Lkotlinx/serialization/json/JsonNamingStrategy;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,39 +95,83 @@ public fun interface JsonNamingStrategy {
*/
@ExperimentalSerializationApi
public val SnakeCase: JsonNamingStrategy = object : JsonNamingStrategy {
override fun serialNameForJson(descriptor: SerialDescriptor, elementIndex: Int, serialName: String): String =
buildString(serialName.length * 2) {
var bufferedChar: Char? = null
var previousUpperCharsCount = 0
override fun serialNameForJson(
descriptor: SerialDescriptor,
elementIndex: Int,
serialName: String
): String = convertCamelCase(serialName, '_')

serialName.forEach { c ->
if (c.isUpperCase()) {
if (previousUpperCharsCount == 0 && isNotEmpty() && last() != '_')
append('_')
override fun toString(): String = "kotlinx.serialization.json.JsonNamingStrategy.SnakeCase"
}

/**
* A strategy that transforms serial names from camel case to kebab case — lowercase characters with words separated by dashes.
* The descriptor parameter is not used.
*
* **Transformation rules**
*
* 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:
* `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:
* `camel-Case-WithDashes` -> `camel-case-with-dashes`.
*
* **Acronyms**
*
* Since acronym rules are quite complex, it is recommended to lowercase all acronyms in source code.
* 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:
* `URLMapping` -> `url-mapping`, `myHTTPAuth` -> `my-http-auth`. Non-letter characters allow the word to continue:
* `myHTTP2APIKey` -> `my-http2-api-key`, `myHTTP2fastApiKey` -> `my-http2fast-api-key`.
*
* **Note on cases**
*
* Whether a character is in upper case is determined by the result of [Char.isUpperCase] function.
* Lowercase transformation is performed by [Char.lowercaseChar], not by [Char.lowercase],
* and therefore does not support one-to-many and many-to-one character mappings.
* See the documentation of these functions for details.
*/
@ExperimentalSerializationApi
public val KebabCase: JsonNamingStrategy = object : JsonNamingStrategy {
override fun serialNameForJson(
descriptor: SerialDescriptor,
elementIndex: Int,
serialName: String
): String = convertCamelCase(serialName, '-')

bufferedChar?.let(::append)
override fun toString(): String = "kotlinx.serialization.json.JsonNamingStrategy.KebabCase"
}

previousUpperCharsCount++
bufferedChar = c.lowercaseChar()
} else {
if (bufferedChar != null) {
if (previousUpperCharsCount > 1 && c.isLetter()) {
append('_')
}
append(bufferedChar)
previousUpperCharsCount = 0
bufferedChar = null
private fun convertCamelCase(
serialName: String,
delimiter: Char
) = buildString(serialName.length * 2) {
var bufferedChar: Char? = null
var previousUpperCharsCount = 0

serialName.forEach { c ->
if (c.isUpperCase()) {
if (previousUpperCharsCount == 0 && isNotEmpty() && last() != delimiter)
append(delimiter)

bufferedChar?.let(::append)

previousUpperCharsCount++
bufferedChar = c.lowercaseChar()
} else {
if (bufferedChar != null) {
if (previousUpperCharsCount > 1 && c.isLetter()) {
append(delimiter)
}
append(c)
append(bufferedChar)
previousUpperCharsCount = 0
bufferedChar = null
}
append(c)
}
}

if(bufferedChar != null) {
append(bufferedChar)
}
if (bufferedChar != null) {
append(bufferedChar)
}
}

override fun toString(): String = "kotlinx.serialization.json.JsonNamingStrategy.SnakeCase"
}
}
}

0 comments on commit 8a3ed23

Please sign in to comment.