From 428c0a56b0f1ff3f69bd4ddfe77037fe6f55f244 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Mar 2026 10:17:34 +0400 Subject: [PATCH] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../EnvironmentConfigGenerator.kt | 60 ++++++++++--- .../EnvironmentConfigGeneratorTest.kt | 88 +++++++++++++++++++ 3 files changed, 137 insertions(+), 13 deletions(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index e4fac168bd..b693d601d9 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit e4fac168bd941fe90b0f081dfa70ea1b4148b49f +Subproject commit b693d601d9dc3de27f0536574bce59a6aa5e2e89 diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt index 5c6d9e6e79..93e83b4e87 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt @@ -4,6 +4,7 @@ import com.squareup.kotlinpoet.* import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import kotlinx.serialization.json.* import java.io.File +import java.util.Locale /** * 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 */ private fun addPropertyFromJsonValue(builder: TypeSpec.Builder, name: String, value: JsonElement) { + val propertyName = name.toValidIdentifier() when (value) { is JsonPrimitive -> { when { value.isString -> { val stringValue = value.content - val propertySpec = PropertySpec.builder(name, STRING) + val propertySpec = PropertySpec.builder(propertyName, STRING) .addModifiers(KModifier.CONST) .initializer("%S", stringValue) @@ -81,7 +83,7 @@ object EnvironmentConfigGenerator { } value.booleanOrNull != null -> { builder.addProperty( - PropertySpec.builder(name, BOOLEAN) + PropertySpec.builder(propertyName, BOOLEAN) .addModifiers(KModifier.CONST) .initializer("%L", value.boolean) .build() @@ -89,7 +91,7 @@ object EnvironmentConfigGenerator { } value.longOrNull != null -> { builder.addProperty( - PropertySpec.builder(name, LONG) + PropertySpec.builder(propertyName, LONG) .addModifiers(KModifier.CONST) .initializer("%L", value.long) .build() @@ -97,7 +99,7 @@ object EnvironmentConfigGenerator { } value.doubleOrNull != null -> { builder.addProperty( - PropertySpec.builder(name, DOUBLE) + PropertySpec.builder(propertyName, DOUBLE) .addModifiers(KModifier.CONST) .initializer("%L", value.double) .build() @@ -106,7 +108,7 @@ object EnvironmentConfigGenerator { else -> { // Null value builder.addProperty( - PropertySpec.builder(name, STRING.copy(nullable = true)) + PropertySpec.builder(propertyName, STRING.copy(nullable = true)) .initializer("null") .build() ) @@ -117,7 +119,7 @@ object EnvironmentConfigGenerator { val listType = LIST.parameterizedBy(STRING) val values = value.map { it.jsonPrimitive.content } builder.addProperty( - PropertySpec.builder(name, listType) + PropertySpec.builder(propertyName, listType) .initializer( CodeBlock.builder() .add("listOf(\n") @@ -147,14 +149,48 @@ object EnvironmentConfigGenerator { } /** - * Converts a string to PascalCase, handling dashes and underscores. - * Examples: "cosmos-hub" -> "CosmosHub", "polygon-zkevm" -> "PolygonZkevm" + * Converts a string to PascalCase for use as a class/object name. + * - 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 { - return this.split("-", "_") + val hasSeparators = contains('.') || contains('-') || contains('_') + return if (hasSeparators) { + this.split("-", "_", ".") + .filter { it.isNotEmpty() } + .joinToString("") { part -> + 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() } - .joinToString("") { part -> - part.replaceFirstChar { it.uppercase() } - } + return parts.mapIndexed { index, part -> + val normalized = part.lowercase(Locale.ROOT) + if (index == 0) normalized + else normalized.replaceFirstChar { it.uppercase(Locale.ROOT) } + }.joinToString("") } } diff --git a/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt b/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt index bfcccb71a8..dae3182b9d 100644 --- a/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt +++ b/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt @@ -210,6 +210,32 @@ class EnvironmentConfigGeneratorTest { 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 fun `generate converts dash-separated names to PascalCase`() { // Arrange @@ -451,6 +477,68 @@ class EnvironmentConfigGeneratorTest { 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 { val inputFile = File(tempDir, "config.json").apply { writeText(jsonContent)