Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-04 10:17:34 +04:00
parent 26a6da1b85
commit 428c0a56b0
3 changed files with 137 additions and 13 deletions

@ -1 +1 @@
Subproject commit e4fac168bd941fe90b0f081dfa70ea1b4148b49f Subproject commit b693d601d9dc3de27f0536574bce59a6aa5e2e89

View file

@ -4,6 +4,7 @@ import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import java.io.File import java.io.File
import java.util.Locale
/** /**
* Generator for environment configuration Kotlin object from JSON file. * Generator for environment configuration Kotlin object from JSON file.
@ -68,12 +69,13 @@ object EnvironmentConfigGenerator {
* Adds a property to the TypeSpec based on the JSON value type * Adds a property to the TypeSpec based on the JSON value type
*/ */
private fun addPropertyFromJsonValue(builder: TypeSpec.Builder, name: String, value: JsonElement) { private fun addPropertyFromJsonValue(builder: TypeSpec.Builder, name: String, value: JsonElement) {
val propertyName = name.toValidIdentifier()
when (value) { when (value) {
is JsonPrimitive -> { is JsonPrimitive -> {
when { when {
value.isString -> { value.isString -> {
val stringValue = value.content val stringValue = value.content
val propertySpec = PropertySpec.builder(name, STRING) val propertySpec = PropertySpec.builder(propertyName, STRING)
.addModifiers(KModifier.CONST) .addModifiers(KModifier.CONST)
.initializer("%S", stringValue) .initializer("%S", stringValue)
@ -81,7 +83,7 @@ object EnvironmentConfigGenerator {
} }
value.booleanOrNull != null -> { value.booleanOrNull != null -> {
builder.addProperty( builder.addProperty(
PropertySpec.builder(name, BOOLEAN) PropertySpec.builder(propertyName, BOOLEAN)
.addModifiers(KModifier.CONST) .addModifiers(KModifier.CONST)
.initializer("%L", value.boolean) .initializer("%L", value.boolean)
.build() .build()
@ -89,7 +91,7 @@ object EnvironmentConfigGenerator {
} }
value.longOrNull != null -> { value.longOrNull != null -> {
builder.addProperty( builder.addProperty(
PropertySpec.builder(name, LONG) PropertySpec.builder(propertyName, LONG)
.addModifiers(KModifier.CONST) .addModifiers(KModifier.CONST)
.initializer("%L", value.long) .initializer("%L", value.long)
.build() .build()
@ -97,7 +99,7 @@ object EnvironmentConfigGenerator {
} }
value.doubleOrNull != null -> { value.doubleOrNull != null -> {
builder.addProperty( builder.addProperty(
PropertySpec.builder(name, DOUBLE) PropertySpec.builder(propertyName, DOUBLE)
.addModifiers(KModifier.CONST) .addModifiers(KModifier.CONST)
.initializer("%L", value.double) .initializer("%L", value.double)
.build() .build()
@ -106,7 +108,7 @@ object EnvironmentConfigGenerator {
else -> { else -> {
// Null value // Null value
builder.addProperty( builder.addProperty(
PropertySpec.builder(name, STRING.copy(nullable = true)) PropertySpec.builder(propertyName, STRING.copy(nullable = true))
.initializer("null") .initializer("null")
.build() .build()
) )
@ -117,7 +119,7 @@ object EnvironmentConfigGenerator {
val listType = LIST.parameterizedBy(STRING) val listType = LIST.parameterizedBy(STRING)
val values = value.map { it.jsonPrimitive.content } val values = value.map { it.jsonPrimitive.content }
builder.addProperty( builder.addProperty(
PropertySpec.builder(name, listType) PropertySpec.builder(propertyName, listType)
.initializer( .initializer(
CodeBlock.builder() CodeBlock.builder()
.add("listOf(\n") .add("listOf(\n")
@ -147,14 +149,48 @@ object EnvironmentConfigGenerator {
} }
/** /**
* Converts a string to PascalCase, handling dashes and underscores. * Converts a string to PascalCase for use as a class/object name.
* Examples: "cosmos-hub" -> "CosmosHub", "polygon-zkevm" -> "PolygonZkevm" * - If the string contains separators (dots, dashes, underscores), splits and joins in PascalCase
* - If no separators, just capitalizes the first letter to preserve original casing (e.g., "AppsFlyer" stays "AppsFlyer")
*/ */
private fun String.toPascalCase(): String { private fun String.toPascalCase(): String {
return this.split("-", "_") val hasSeparators = contains('.') || contains('-') || contains('_')
return if (hasSeparators) {
this.split("-", "_", ".")
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.joinToString("") { part -> .joinToString("") { part ->
part.replaceFirstChar { it.uppercase() } part.lowercase(Locale.ROOT).replaceFirstChar { it.uppercase(Locale.ROOT) }
} }
} else {
this.replaceFirstChar { it.uppercase(Locale.ROOT) }
}
}
/**
* Converts a string to a valid Kotlin property identifier.
* - If the string contains dots, converts to camelCase (dots cannot be escaped by KotlinPoet)
* - Otherwise, ensures the first letter is lowercase (Kotlin property naming convention)
*/
private fun String.toValidIdentifier(): String {
return if (contains('.')) {
toCamelCase()
} else {
this.replaceFirstChar { it.lowercase(Locale.ROOT) }
}
}
/**
* Converts a string to camelCase, handling dashes, underscores, and dots.
* Normalizes each segment to lowercase first for consistent results.
* Examples: "cosmos-hub" -> "cosmosHub", "customer.io" -> "customerIo", "CUSTOMER.IO" -> "customerIo"
*/
private fun String.toCamelCase(): String {
val parts = this.split("-", "_", ".")
.filter { it.isNotEmpty() }
return parts.mapIndexed { index, part ->
val normalized = part.lowercase(Locale.ROOT)
if (index == 0) normalized
else normalized.replaceFirstChar { it.uppercase(Locale.ROOT) }
}.joinToString("")
} }
} }

View file

@ -210,6 +210,32 @@ class EnvironmentConfigGeneratorTest {
assertThat(generatedCode).contains("""const val deepValue: String = "deep"""") assertThat(generatedCode).contains("""const val deepValue: String = "deep"""")
} }
@Test
fun `generate preserves camelCase object names without separators`() {
// Arrange - object names like "AppsFlyer" should stay as "AppsFlyer", not become "Appsflyer"
val json = """
{
"AppsFlyer": {
"DevKey": "key123"
},
"GetBlockAccessTokens": {
"ethereum": {
"jsonRpc": "token"
}
}
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert - object names preserved, property names have first letter lowercased
assertThat(generatedCode).contains("object AppsFlyer {")
assertThat(generatedCode).contains("""const val devKey: String = "key123"""")
assertThat(generatedCode).contains("object GetBlockAccessTokens {")
assertThat(generatedCode).contains("object Ethereum {")
}
@Test @Test
fun `generate converts dash-separated names to PascalCase`() { fun `generate converts dash-separated names to PascalCase`() {
// Arrange // Arrange
@ -451,6 +477,68 @@ class EnvironmentConfigGeneratorTest {
assertThat(generatedCode).contains("object QuickNodeSolanaCredentials {") assertThat(generatedCode).contains("object QuickNodeSolanaCredentials {")
} }
@Test
fun `generate converts dot-separated object names to PascalCase`() {
// Arrange - testing the customer.io case that caused the original build failure
val json = """
{
"customer.io": {
"TrackSiteID": "site-id-123",
"TrackApiKey": "api-key-456"
}
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("object CustomerIo {")
// Property names have first letter lowercased (Kotlin convention)
assertThat(generatedCode).contains("""const val trackSiteID: String = "site-id-123"""")
assertThat(generatedCode).contains("""const val trackApiKey: String = "api-key-456"""")
}
@Test
fun `generate converts dot-separated property names to camelCase`() {
// Arrange - testing property names with dots (not nested objects)
val json = """
{
"api.key": "test-key",
"service.url": "https://example.com"
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert - dots in property names are converted to camelCase
assertThat(generatedCode).contains("""const val apiKey: String = "test-key"""")
assertThat(generatedCode).contains("""const val serviceUrl: String = "https://example.com"""")
}
@Test
fun `generate preserves valid property names without transformation`() {
// Arrange - valid Kotlin identifiers should not be transformed
val json = """
{
"apiKey": "key1",
"moonPayApiKey": "moon-pay-key",
"isEnabled": true,
"maxRetryCount": 5
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert - original names preserved exactly
assertThat(generatedCode).contains("""const val apiKey: String = "key1"""")
assertThat(generatedCode).contains("""const val moonPayApiKey: String = "moon-pay-key"""")
assertThat(generatedCode).contains("const val isEnabled: Boolean = true")
assertThat(generatedCode).contains("const val maxRetryCount: Long = 5")
}
private fun generateAndReadOutput(jsonContent: String): String { private fun generateAndReadOutput(jsonContent: String): String {
val inputFile = File(tempDir, "config.json").apply { val inputFile = File(tempDir, "config.json").apply {
writeText(jsonContent) writeText(jsonContent)