summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEmil Kantis <e.kantis@gmail.com>2024-01-24 14:47:59 +0100
committerGitHub <noreply@github.com>2024-01-24 14:47:59 +0100
commit8a3ed23827c34920f6cd191c1e1806f2128ad50e (patch)
treefef644526404a16c53dfc858b9d0c5269aadfee4
parentb3f6e0f5405ab81c354ccaeeb2cc13ba278e4bfa (diff)
downloadkotlinx.serialization-8a3ed23827c34920f6cd191c1e1806f2128ad50e.tar.gz
Add kebab-case naming strategy (#2531)
-rw-r--r--formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonNamingStrategyTest.kt46
-rw-r--r--formats/json/api/kotlinx-serialization-json.api1
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt94
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"
- }
}
}