
software.amazon.smithy.kotlin.codegen.utils.CaseUtils.kt Maven / Gradle / Ivy
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.kotlin.codegen.utils
// These are whole words but cased differently, e.g. `IPv4`, `MiB`, `GiB`, `TtL`
private val completeWords = listOf("ipv4", "ipv6", "sigv4", "mib", "gib", "kib", "ttl", "iot", "s3")
/**
* Split a string on word boundaries
*/
fun String.splitOnWordBoundaries(): List {
// This is taken from Rust: https://github.com/awslabs/smithy-rs/pull/3037/files#diff-4175c66ee81a450fcf1cd3e256f36ae2c8e0b30b910be8ca505135fbe215144d
// with minor changes (s3 and iot as whole words). Previously we used the Java v2 implementation
// https://github.com/aws/aws-sdk-java-v2/blob/2.20.162/utils/src/main/java/software/amazon/awssdk/utils/internal/CodegenNamingUtils.java#L36
// but this has some edge cases it doesn't handle well
val out = mutableListOf()
var currentWord = ""
var computeWordInProgress = true
// emit the current word and update from the next character
val emit = { next: Char ->
computeWordInProgress = true
if (currentWord.isNotEmpty()) {
out += currentWord.lowercase()
}
currentWord = if (next.isLetterOrDigit()) {
next.toString()
} else {
""
}
}
val allLowerCase = lowercase() == this
forEachIndexed { index, nextChar ->
val peek = getOrNull(index + 1)
val doublePeek = getOrNull(index + 2)
val completeWordInProgress = {
val result = computeWordInProgress && currentWord.isNotEmpty() && completeWords.any {
(currentWord + substring(index)).startsWith(it, ignoreCase = true)
} && !completeWords.contains(currentWord.lowercase())
computeWordInProgress = result
result
}
when {
// [C] in these docs indicates the value of nextCharacter
// A[_]B
!nextChar.isLetterOrDigit() -> emit(nextChar)
// If we have no letters so far, push the next letter (we already know it's a letter or digit)
currentWord.isEmpty() -> currentWord += nextChar.toString()
// Abc[D]ef or Ab2[D]ef
!completeWordInProgress() && loweredFollowedByUpper(currentWord, nextChar) -> emit(nextChar)
// s3[k]ey
!completeWordInProgress() && allLowerCase && digitFollowedByLower(currentWord, nextChar) -> emit(nextChar)
// emit complete words
!completeWordInProgress() && completeWords.contains(currentWord.lowercase()) -> emit(nextChar)
// DB[P]roxy, or `IAM[U]ser` but not AC[L]s
!completeWordInProgress() && endOfAcronym(currentWord, nextChar, peek, doublePeek) -> emit(nextChar)
// If we haven't found a word boundary, push it and keep going
else -> currentWord += nextChar.toString()
}
}
if (currentWord.isNotEmpty()) {
out += currentWord
}
return out
}
/**
* Handle cases like `DB[P]roxy`, `ARN[S]upport`, `AC[L]s`
*/
private fun endOfAcronym(current: String, nextChar: Char, peek: Char?, doublePeek: Char?): Boolean {
if (!current.last().isUpperCase()) {
// Not an acronym in progress
return false
}
if (nextChar == 'v' && peek?.isDigit() == true) {
// Handle cases like `DynamoDB[v]2`
return true
}
if (!nextChar.isUpperCase()) {
// We aren't at the next word yet
return false
}
if (peek?.isLowerCase() != true) {
return false
}
// Skip cases like `AR[N]s`, `AC[L]s` but not `IAM[U]ser`
if (peek == 's' && doublePeek?.isLowerCase() != true) {
return false
}
// Skip cases like `DynamoD[B]v2`
return !(peek == 'v' && doublePeek?.isDigit() == true)
}
private fun loweredFollowedByUpper(current: String, nextChar: Char): Boolean {
if (!nextChar.isUpperCase()) {
return false
}
return current.last().isLowerCase() || current.last().isDigit()
}
private fun loweredFollowedByDigit(current: String, nextChar: Char): Boolean {
if (!nextChar.isDigit()) {
return false
}
return current.last().isLowerCase()
}
private fun digitFollowedByLower(current: String, nextChar: Char): Boolean =
(current.last().isDigit() && nextChar.isLowerCase())
/**
* Convert a string to `PascalCase` (uppercase start with upper case word boundaries)
*/
fun String.toPascalCase(): String = splitOnWordBoundaries().joinToString(separator = "") { it.lowercase().replaceFirstChar { c -> c.uppercaseChar() } }
/**
* Convert a string to `camelCase` (lowercase start with upper case word boundaries)
*/
fun String.toCamelCase(): String = toPascalCase().replaceFirstChar { c -> c.lowercaseChar() }
/**
* Inverts the case of a character. For example:
* * 'a' → 'A'
* * 'A' → 'a'
* * '!' → '!'
*/
fun Char.toggleCase(): Char = if (isUpperCase()) lowercaseChar() else uppercaseChar()
/**
* Toggles the case of the first character in the string. For example:
* * "apple" → "Apple"
* * "Apple" → "apple"
* * "!apple" → "!apple"
*/
fun String.toggleFirstCharacterCase(): String = when {
isEmpty() -> this
else -> first().toggleCase() + substring(1)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy