diff options
author | Emil Kantis <e.kantis@gmail.com> | 2024-01-24 14:47:59 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-24 14:47:59 +0100 |
commit | 8a3ed23827c34920f6cd191c1e1806f2128ad50e (patch) | |
tree | fef644526404a16c53dfc858b9d0c5269aadfee4 | |
parent | b3f6e0f5405ab81c354ccaeeb2cc13ba278e4bfa (diff) | |
download | kotlinx.serialization-8a3ed23827c34920f6cd191c1e1806f2128ad50e.tar.gz |
Add kebab-case naming strategy (#2531)
3 files changed, 116 insertions, 25 deletions
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonNamingStrategyTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonNamingStrategyTest.kt index 28a4d121..9e2cf1d4 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonNamingStrategyTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonNamingStrategyTest.kt @@ -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) diff --git a/formats/json/api/kotlinx-serialization-json.api b/formats/json/api/kotlinx-serialization-json.api index 88dd29c2..6874d744 100644 --- a/formats/json/api/kotlinx-serialization-json.api +++ b/formats/json/api/kotlinx-serialization-json.api @@ -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; } diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt index 64b4e0b7..b737fd69 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt @@ -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" - } } } |