Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-20 16:25:40 +04:00
parent 4d1902af26
commit bf4a7a9e4e
7 changed files with 891 additions and 5 deletions

View file

@ -1,4 +1,6 @@
import com.tangem.plugin.configuration.configurations.EnvironmentConfigGenerator
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
import com.tangem.plugin.configuration.model.BuildType
plugins { plugins {
alias(deps.plugins.android.library) alias(deps.plugins.android.library)
@ -10,14 +12,53 @@ plugins {
id("configuration") id("configuration")
} }
abstract class GenerateEnvironmentConfigTask : DefaultTask() {
@get:InputFile
abstract val configFile: RegularFileProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun generate() {
val input = configFile.get().asFile
require(input.exists()) { "Config file not found: ${input.absolutePath}" }
logger.lifecycle("Generating EnvironmentConfig from ${input.name}")
EnvironmentConfigGenerator.generate(input, outputDir.get().asFile)
}
}
android { android {
namespace = "com.tangem.datasource" namespace = "com.tangem.datasource"
sourceSets["main"].java.srcDir(layout.buildDirectory.dir("generated/source/environment-config"))
room { room {
schemaDirectory("$projectDir/schemas") schemaDirectory("$projectDir/schemas")
} }
} }
androidComponents {
onVariants { variant ->
val buildType = BuildType.values().firstOrNull { it.id == variant.buildType } ?: BuildType.Debug
val configFile = rootProject.file(
"app/src/main/assets/tangem-app-config/config_${buildType.environment}.json",
)
tasks.register<GenerateEnvironmentConfigTask>(
"generateEnvironmentConfig${variant.name.replaceFirstChar { it.uppercaseChar() }}",
) {
this.configFile.set(configFile)
outputDir.set(layout.buildDirectory.dir("generated/source/environment-config"))
}
}
}
tasks.named("preBuild") {
dependsOn(tasks.matching { it.name.startsWith("generateEnvironmentConfig") })
}
tasks.withType<Test>().configureEach { tasks.withType<Test>().configureEach {
useJUnitPlatform() useJUnitPlatform()
} }

View file

@ -0,0 +1,181 @@
package com.tangem.datasource.local.config.environment.converter
import com.tangem.blockchain.common.*
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.AppsFlyer
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.DevExpress
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.Express
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.GetBlockAccessTokens
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.P2pApiKey
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.TonCenterApiKey
import com.tangem.datasource.local.config.environment.models.ExpressModel
import com.tangem.datasource.local.config.environment.models.P2PKeys
/**
* Converts [GeneratedEnvironmentConfig] to [EnvironmentConfig]
*
* This converter maps the auto-generated config (from JSON) to the domain model.
* The generated config has nested objects that mirror the JSON structure.
*/
internal object GeneratedEnvironmentConfigConverter {
fun convert(): EnvironmentConfig {
return EnvironmentConfig(
moonPayApiKey = GeneratedEnvironmentConfig.moonPayApiKey,
moonPayApiSecretKey = GeneratedEnvironmentConfig.moonPayApiSecretKey,
mercuryoWidgetId = GeneratedEnvironmentConfig.mercuryoWidgetId,
mercuryoSecret = GeneratedEnvironmentConfig.mercuryoSecret,
blockchainSdkConfig = createBlockchainSdkConfig(),
amplitudeApiKey = GeneratedEnvironmentConfig.amplitudeApiKey,
appsFlyerApiKey = AppsFlyer.appsFlyerDevKey,
appsAppId = AppsFlyer.appsFlyerAppID,
walletConnectProjectId = GeneratedEnvironmentConfig.walletConnectProjectId,
express = createExpressModel(
apiKey = Express.apiKey,
signVerifierPublicKey = Express.signVerifierPublicKey,
),
devExpress = createExpressModel(
apiKey = DevExpress.apiKey,
signVerifierPublicKey = DevExpress.signVerifierPublicKey,
),
stakeKitApiKey = GeneratedEnvironmentConfig.stakeKitApiKey,
p2pApiKey = createP2PKeys(),
blockAidApiKey = GeneratedEnvironmentConfig.blockaidApiKey,
tangemApiKey = GeneratedEnvironmentConfig.tangemApiKey,
tangemApiKeyDev = GeneratedEnvironmentConfig.tangemApiKeyDev,
tangemApiKeyStage = GeneratedEnvironmentConfig.tangemApiKeyStage,
yieldModuleApiKey = GeneratedEnvironmentConfig.yieldModuleApiKey,
yieldModuleApiKeyDev = GeneratedEnvironmentConfig.yieldModuleApiKeyDev,
bffStaticToken = GeneratedEnvironmentConfig.bffStaticToken,
bffStaticTokenDev = GeneratedEnvironmentConfig.bffStaticTokenDev,
gaslessTxApiKeyDev = GeneratedEnvironmentConfig.gaslessTxApiKeyDev,
gaslessTxApiKey = GeneratedEnvironmentConfig.gaslessTxApiKey,
)
}
private fun createExpressModel(apiKey: String?, signVerifierPublicKey: String?): ExpressModel? {
return if (!apiKey.isNullOrEmpty() && !signVerifierPublicKey.isNullOrEmpty()) {
ExpressModel(apiKey = apiKey, signVerifierPublicKey = signVerifierPublicKey)
} else {
null
}
}
private fun createP2PKeys(): P2PKeys? {
val mainnet = P2pApiKey.mainnet
val hoodi = P2pApiKey.hoodi
return if (mainnet.isNotEmpty() && hoodi.isNotEmpty()) {
P2PKeys(mainnet = mainnet, hoodi = hoodi)
} else {
null
}
}
private fun createBlockchainSdkConfig(): BlockchainSdkConfig {
return BlockchainSdkConfig(
blockchairCredentials = BlockchairCredentials(
apiKey = GeneratedEnvironmentConfig.blockchairApiKeys,
authToken = GeneratedEnvironmentConfig.blockchairAuthorizationToken,
),
blockcypherTokens = GeneratedEnvironmentConfig.blockcypherTokens.toSet(),
quickNodeSolanaCredentials = QuickNodeCredentials(
apiKey = GeneratedEnvironmentConfig.quiknodeApiKey,
subdomain = GeneratedEnvironmentConfig.quiknodeSubdomain,
),
quickNodeBscCredentials = QuickNodeCredentials(
apiKey = GeneratedEnvironmentConfig.bscQuiknodeApiKey,
subdomain = GeneratedEnvironmentConfig.bscQuiknodeSubdomain,
),
quickNodePlasmaCredentials = QuickNodeCredentials(
apiKey = GeneratedEnvironmentConfig.quiknodePlasmaApiKey,
subdomain = GeneratedEnvironmentConfig.quiknodePlasmaSubdomain,
),
quickNodeMonadCredentials = QuickNodeCredentials(
apiKey = GeneratedEnvironmentConfig.quiknodeMonadApiKey,
subdomain = GeneratedEnvironmentConfig.quiknodeMonadSubdomain,
),
infuraProjectId = GeneratedEnvironmentConfig.infuraProjectId,
tronGridApiKey = GeneratedEnvironmentConfig.tronGridApiKey,
nowNodeCredentials = NowNodeCredentials(apiKey = GeneratedEnvironmentConfig.nowNodesApiKey),
getBlockCredentials = createGetBlockCredentials(),
kaspaSecondaryApiUrl = GeneratedEnvironmentConfig.kaspaSecondaryApiUrl,
tonCenterCredentials = TonCenterCredentials(
mainnetApiKey = TonCenterApiKey.mainnet,
testnetApiKey = TonCenterApiKey.testnet,
),
chiaFireAcademyApiKey = GeneratedEnvironmentConfig.chiaFireAcademyApiKey,
chiaTangemApiKey = GeneratedEnvironmentConfig.chiaTangemApiKey,
hederaArkhiaApiKey = GeneratedEnvironmentConfig.hederaArkhiaKey,
polygonScanApiKey = GeneratedEnvironmentConfig.polygonScanApiKey,
bittensorDwellirApiKey = GeneratedEnvironmentConfig.bittensorDwellirKey,
bittensorOnfinalityApiKey = GeneratedEnvironmentConfig.bittensorOnfinalityKey,
dwellirApiKey = GeneratedEnvironmentConfig.dwellirApiKey,
koinosProApiKey = GeneratedEnvironmentConfig.koinosProApiKey,
alephiumApiKey = GeneratedEnvironmentConfig.alephiumTangemApiKey,
moralisApiKey = GeneratedEnvironmentConfig.moralisApiKey,
etherscanApiKey = GeneratedEnvironmentConfig.etherscanApiKey,
blinkApiKey = GeneratedEnvironmentConfig.blinkApiKey,
tatumApiKey = GeneratedEnvironmentConfig.tatumApiKey,
)
}
private fun createGetBlockCredentials(): GetBlockCredentials {
return GetBlockCredentials(
xrp = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Xrp.jsonRpc),
cardano = GetBlockAccessToken(rosetta = GetBlockAccessTokens.Cardano.rosetta),
avalanche = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Avalanche.jsonRpc),
eth = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Ethereum.jsonRpc),
etc = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.EthereumClassic.jsonRpc),
fantom = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Fantom.jsonRpc),
rsk = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Rsk.jsonRpc),
bsc = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Bsc.jsonRpc),
polygon = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Polygon.jsonRpc),
gnosis = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Xdai.jsonRpc),
cronos = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Cronos.jsonRpc),
solana = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Solana.jsonRpc),
ton = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Ton.jsonRpc),
tron = GetBlockAccessToken(rest = GetBlockAccessTokens.Tron.rest),
cosmos = GetBlockAccessToken(rest = GetBlockAccessTokens.CosmosHub.rest),
near = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Near.jsonRpc),
aptos = GetBlockAccessToken(rest = GetBlockAccessTokens.Aptos.rest),
dogecoin = GetBlockAccessToken(
jsonRpc = GetBlockAccessTokens.Dogecoin.jsonRpc,
blockBookRest = GetBlockAccessTokens.Dogecoin.blockBookRest,
),
litecoin = GetBlockAccessToken(
jsonRpc = GetBlockAccessTokens.Litecoin.jsonRpc,
blockBookRest = GetBlockAccessTokens.Litecoin.blockBookRest,
),
dash = GetBlockAccessToken(
jsonRpc = GetBlockAccessTokens.Dash.jsonRpc,
blockBookRest = GetBlockAccessTokens.Dash.blockBookRest,
),
bitcoin = GetBlockAccessToken(
jsonRpc = GetBlockAccessTokens.Bitcoin.jsonRpc,
blockBookRest = GetBlockAccessTokens.Bitcoin.blockBookRest,
),
algorand = GetBlockAccessToken(rest = GetBlockAccessTokens.Algorand.rest),
zkSyncEra = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Zksync.jsonRpc),
polygonZkEvm = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.PolygonZkevm.jsonRpc),
base = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Base.jsonRpc),
blast = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Blast.jsonRpc),
filecoin = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Filecoin.jsonRpc),
arbitrum = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.ArbitrumOne.jsonRpc),
bitcoinCash = GetBlockAccessToken(
jsonRpc = GetBlockAccessTokens.BitcoinCash.jsonRpc,
blockBookRest = GetBlockAccessTokens.BitcoinCash.blockBookRest,
),
kusama = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Kusama.jsonRpc),
moonbeam = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Moonbeam.jsonRpc),
optimism = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Optimism.jsonRpc),
polkadot = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Polkadot.jsonRpc),
shibarium = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Shibarium.jsonRpc),
sui = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Sui.jsonRpc),
telos = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Telos.jsonRpc),
tezos = GetBlockAccessToken(rest = GetBlockAccessTokens.Tezos.rest),
monad = GetBlockAccessToken(rest = GetBlockAccessTokens.Monad.rest),
stellar = GetBlockAccessToken(rest = GetBlockAccessTokens.Stellar.rest),
)
}
}

View file

@ -102,6 +102,7 @@ markdownComposeView = "0.5.4"
usedesk = "4.4.0" usedesk = "4.4.0"
sumsub = "1.38.0" sumsub = "1.38.0"
haze = "1.7.1" haze = "1.7.1"
kotlinpoet = "1.18.1"
# endregion Other libraries # endregion Other libraries
# region Tools # region Tools
@ -149,6 +150,7 @@ agconnect = { id = "com.huawei.agconnect", version.ref = "agconnect" }
gradle-android = { module = "com.android.tools.build:gradle", version.ref = "androidGradlePlugin" } gradle-android = { module = "com.android.tools.build:gradle", version.ref = "androidGradlePlugin" }
gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
gradle-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } gradle-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
gradle-kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" }
# end region Classpath # end region Classpath
# region AndroidX # region AndroidX

View file

@ -17,6 +17,15 @@ dependencies {
implementation(deps.gradle.kotlin) implementation(deps.gradle.kotlin)
implementation(deps.gradle.android) implementation(deps.gradle.android)
implementation(deps.gradle.detekt) implementation(deps.gradle.detekt)
implementation(deps.gradle.kotlinpoet)
implementation(deps.kotlin.serialization)
testImplementation(deps.test.junit5)
testImplementation(deps.test.truth)
}
tasks.withType<Test> {
useJUnitPlatform()
} }
gradlePlugin { gradlePlugin {

View file

@ -0,0 +1,165 @@
package com.tangem.plugin.configuration.configurations
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import kotlinx.serialization.json.*
import java.io.File
/**
* Generator for environment configuration Kotlin object from JSON file.
* Automatically parses JSON structure and generates corresponding Kotlin code.
*
[REDACTED_AUTHOR]
*/
object EnvironmentConfigGenerator {
private const val PACKAGE_NAME = "com.tangem.datasource.local.config.environment.generated"
private const val CLASS_NAME = "GeneratedEnvironmentConfig"
/**
* Generates GeneratedEnvironmentConfig object from JSON file.
*
* @param inputFile JSON configuration file
* @param outputDir Output directory for generated Kotlin file
*/
fun generate(inputFile: File, outputDir: File) {
val jsonText = inputFile.readText()
val json = Json.parseToJsonElement(jsonText).jsonObject
val objectBuilder = TypeSpec.objectBuilder(CLASS_NAME)
.addKdoc("Generated from ${inputFile.name}\nAuto-generated - do not edit manually.")
// Iterate over all JSON keys and generate properties
json.entries.forEach { (key, value) ->
addPropertyFromJsonValue(objectBuilder, key, value)
}
val fileSpec = FileSpec.builder(PACKAGE_NAME, CLASS_NAME)
.indent(" ") // Use 4 spaces for indentation
.addType(objectBuilder.build())
.build()
outputDir.mkdirs()
fileSpec.writeTo(outputDir)
// Post-process generated file
val generatedFile = File(outputDir, PACKAGE_NAME.replace('.', '/') + "/$CLASS_NAME.kt")
if (generatedFile.exists()) {
val content = generatedFile.readText()
val fixedContent = content
// Add suppress annotation at file level
.replaceFirst(
"package $PACKAGE_NAME",
"@file:Suppress(\n" +
" \"MaximumLineLength\",\n" +
" \"MaxLineLength\",\n" +
" \"Indentation\",\n" +
")\n\npackage $PACKAGE_NAME"
)
// Remove redundant public modifiers
.replace("public object ", "object ")
.replace("public val ", "val ")
.replace("public const val ", "const val ")
generatedFile.writeText(fixedContent)
}
}
/**
* Adds a property to the TypeSpec based on the JSON value type
*/
private fun addPropertyFromJsonValue(builder: TypeSpec.Builder, name: String, value: JsonElement) {
when (value) {
is JsonPrimitive -> {
when {
value.isString -> {
val stringValue = value.content
val isNullable = stringValue.isEmpty()
val propertySpec = PropertySpec.builder(name, STRING.copy(nullable = isNullable))
.initializer(if (isNullable) "null" else "%S", stringValue)
// Add const modifier for non-nullable strings
if (!isNullable) {
propertySpec.addModifiers(KModifier.CONST)
}
builder.addProperty(propertySpec.build())
}
value.booleanOrNull != null -> {
builder.addProperty(
PropertySpec.builder(name, BOOLEAN)
.addModifiers(KModifier.CONST)
.initializer("%L", value.boolean)
.build()
)
}
value.longOrNull != null -> {
builder.addProperty(
PropertySpec.builder(name, LONG)
.addModifiers(KModifier.CONST)
.initializer("%L", value.long)
.build()
)
}
value.doubleOrNull != null -> {
builder.addProperty(
PropertySpec.builder(name, DOUBLE)
.addModifiers(KModifier.CONST)
.initializer("%L", value.double)
.build()
)
}
else -> {
// Null value
builder.addProperty(
PropertySpec.builder(name, STRING.copy(nullable = true))
.initializer("null")
.build()
)
}
}
}
is JsonArray -> {
val listType = LIST.parameterizedBy(STRING)
val values = value.map { it.jsonPrimitive.content }
builder.addProperty(
PropertySpec.builder(name, listType)
.initializer(
CodeBlock.builder()
.add("listOf(\n")
.apply {
values.forEach { v ->
add(" %S,\n", v)
}
}
.add(")")
.build()
)
.build()
)
}
is JsonObject -> {
// Generate nested object with proper naming (convert dashes to camelCase)
val nestedClassName = name.toPascalCase()
val nestedObjectBuilder = TypeSpec.objectBuilder(nestedClassName)
value.entries.forEach { (nestedKey, nestedValue) ->
addPropertyFromJsonValue(nestedObjectBuilder, nestedKey, nestedValue)
}
builder.addType(nestedObjectBuilder.build())
}
}
}
/**
* Converts a string to PascalCase, handling dashes and underscores.
* Examples: "cosmos-hub" -> "CosmosHub", "polygon-zkevm" -> "PolygonZkevm"
*/
private fun String.toPascalCase(): String {
return this.split("-", "_")
.filter { it.isNotEmpty() }
.joinToString("") { part ->
part.replaceFirstChar { it.uppercase() }
}
}
}

View file

@ -1,11 +1,11 @@
package com.tangem.plugin.configuration.model package com.tangem.plugin.configuration.model
internal enum class BuildType( enum class BuildType(
val id: String, val id: String,
val appIdSuffix: String? = null, internal val appIdSuffix: String? = null,
val versionSuffix: String? = null, internal val versionSuffix: String? = null,
val obfuscating: Boolean = false, internal val obfuscating: Boolean = false,
val configFields: List<BuildConfigField>, internal val configFields: List<BuildConfigField>,
) { ) {
/** /**
@ -117,4 +117,19 @@ internal enum class BuildType(
BuildConfigField.ABTestsEnabled(isEnabled = false), BuildConfigField.ABTestsEnabled(isEnabled = false),
), ),
), ),
;
/** Returns the environment value (dev/prod) for this build type */
val environment: String
get() {
val environmentField = configFields
.filterIsInstance<BuildConfigField.Environment>()
.firstOrNull()
requireNotNull(environmentField) {
"BuildType '$id' must have a BuildConfigField.Environment in configFields"
}
return environmentField.value.removeSurrounding("\"")
}
} }

View file

@ -0,0 +1,473 @@
package com.tangem.plugin.configuration.configurations
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.io.File
/**
* Tests for [EnvironmentConfigGenerator] covering JSON parsing edge cases.
*/
class EnvironmentConfigGeneratorTest {
@TempDir
lateinit var tempDir: File
private lateinit var outputDir: File
@BeforeEach
fun setup() {
outputDir = File(tempDir, "output")
}
@Test
fun `generate handles string values correctly`() {
// Arrange
val json = """
{
"apiKey": "test-api-key",
"baseUrl": "https://example.com"
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("""const val apiKey: String = "test-api-key"""")
assertThat(generatedCode).contains("""const val baseUrl: String = "https://example.com"""")
}
@Test
fun `generate handles empty string as nullable`() {
// Arrange
val json = """
{
"emptyValue": ""
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("val emptyValue: String? = null")
}
@Test
fun `generate handles null values`() {
// Arrange
val json = """
{
"nullValue": null
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("val nullValue: String? = null")
}
@Test
fun `generate handles boolean values`() {
// Arrange
val json = """
{
"isEnabled": true,
"isDisabled": false
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("const val isEnabled: Boolean = true")
assertThat(generatedCode).contains("const val isDisabled: Boolean = false")
}
@Test
fun `generate handles integer values as Long`() {
// Arrange
val json = """
{
"count": 42,
"negativeNumber": -100,
"largeNumber": 9223372036854775807
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("const val count: Long = 42")
assertThat(generatedCode).contains("const val negativeNumber: Long = -100")
// KotlinPoet formats large numbers with underscores
assertThat(generatedCode).contains("const val largeNumber: Long = 9_223_372_036_854_775_807")
}
@Test
fun `generate handles double values`() {
// Arrange
val json = """
{
"ratio": 3.14,
"negativeDouble": -2.5
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("const val ratio: Double = 3.14")
assertThat(generatedCode).contains("const val negativeDouble: Double = -2.5")
}
@Test
fun `generate handles string arrays`() {
// Arrange
val json = """
{
"items": ["one", "two", "three"]
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("val items: List<String> = listOf(")
assertThat(generatedCode).contains(""""one",""")
assertThat(generatedCode).contains(""""two",""")
assertThat(generatedCode).contains(""""three",""")
}
@Test
fun `generate handles empty arrays`() {
// Arrange
val json = """
{
"emptyList": []
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("val emptyList: List<String> = listOf(")
}
@Test
fun `generate handles nested objects`() {
// Arrange
val json = """
{
"database": {
"host": "localhost",
"port": 5432
}
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("object Database {")
assertThat(generatedCode).contains("""const val host: String = "localhost"""")
// KotlinPoet formats numbers >= 1000 with underscores
assertThat(generatedCode).contains("const val port: Long = 5_432")
}
@Test
fun `generate handles deeply nested objects`() {
// Arrange
val json = """
{
"level1": {
"level2": {
"level3": {
"deepValue": "deep"
}
}
}
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("object Level1 {")
assertThat(generatedCode).contains("object Level2 {")
assertThat(generatedCode).contains("object Level3 {")
assertThat(generatedCode).contains("""const val deepValue: String = "deep"""")
}
@Test
fun `generate converts dash-separated names to PascalCase`() {
// Arrange
val json = """
{
"cosmos-hub": {
"chainId": "cosmoshub-4"
}
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("object CosmosHub {")
}
@Test
fun `generate converts underscore-separated names to PascalCase`() {
// Arrange
val json = """
{
"api_config": {
"timeout": 30
}
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("object ApiConfig {")
}
@Test
fun `generate handles consecutive dashes in names`() {
// Arrange
val json = """
{
"cosmos--hub": {
"testValue": "test"
}
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("object CosmosHub {")
}
@Test
fun `generate handles trailing dash in names`() {
// Arrange
val json = """
{
"config-": {
"testValue": "test"
}
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("object Config {")
}
@Test
fun `generate handles special characters in string values`() {
// Arrange
val json = """
{
"query": "SELECT * FROM users WHERE name = 'John'",
"path": "C:\\Users\\test",
"newline": "line1\nline2",
"unicode": "Hello 世界"
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("const val query: String")
assertThat(generatedCode).contains("const val path: String")
assertThat(generatedCode).contains("const val newline: String")
assertThat(generatedCode).contains("const val unicode: String")
}
@Test
fun `generate handles arrays with special characters`() {
// Arrange
val json = """
{
"urls": [
"https://api.example.com/v1",
"https://api.example.com/v2?key=value&other=1"
]
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("val urls: List<String> = listOf(")
assertThat(generatedCode).contains(""""https://api.example.com/v1",""")
}
@Test
fun `generate adds file suppress annotations`() {
// Arrange
val json = """
{
"key": "value"
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("@file:Suppress(")
assertThat(generatedCode).contains(""""MaximumLineLength"""")
assertThat(generatedCode).contains(""""MaxLineLength"""")
assertThat(generatedCode).contains(""""Indentation"""")
}
@Test
fun `generate creates proper package declaration`() {
// Arrange
val json = """
{
"key": "value"
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("package com.tangem.datasource.local.config.environment.generated")
}
@Test
fun `generate creates object with correct name`() {
// Arrange
val json = """
{
"key": "value"
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("object GeneratedEnvironmentConfig {")
}
@Test
fun `generate adds kdoc with source file reference`() {
// Arrange
val json = """
{
"key": "value"
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).contains("Generated from")
assertThat(generatedCode).contains("Auto-generated - do not edit manually")
}
@Test
fun `generate removes public modifiers`() {
// Arrange
val json = """
{
"key": "value"
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
assertThat(generatedCode).doesNotContain("public object")
assertThat(generatedCode).doesNotContain("public val")
assertThat(generatedCode).doesNotContain("public const val")
}
@Test
fun `generate handles complex real-world config`() {
// Arrange
val json = """
{
"tangemComApiKey": "api-key-123",
"moonPayApiKey": "moon-pay-key",
"moonPayApiSecretKey": "secret-key",
"mercuryoWidgetId": "",
"blockchainSdkConfig": {
"blockchairApiKey": "blockchair-key",
"blockcypherTokens": ["token1", "token2"],
"quickNodeSolanaCredentials": {
"apiKey": "solana-key",
"subdomain": "solana-node"
}
},
"isFeatureEnabled": true,
"maxRetryCount": 3
}
""".trimIndent()
// Act
val generatedCode = generateAndReadOutput(json)
// Assert
// Top-level properties
assertThat(generatedCode).contains("""const val tangemComApiKey: String = "api-key-123"""")
assertThat(generatedCode).contains("val mercuryoWidgetId: String? = null")
assertThat(generatedCode).contains("const val isFeatureEnabled: Boolean = true")
assertThat(generatedCode).contains("const val maxRetryCount: Long = 3")
// Nested object
assertThat(generatedCode).contains("object BlockchainSdkConfig {")
assertThat(generatedCode).contains("""const val blockchairApiKey: String = "blockchair-key"""")
assertThat(generatedCode).contains("val blockcypherTokens: List<String>")
// Deeply nested object
assertThat(generatedCode).contains("object QuickNodeSolanaCredentials {")
}
private fun generateAndReadOutput(jsonContent: String): String {
val inputFile = File(tempDir, "config.json").apply {
writeText(jsonContent)
}
EnvironmentConfigGenerator.generate(inputFile, outputDir)
val generatedFile = File(
outputDir,
"com/tangem/datasource/local/config/environment/generated/GeneratedEnvironmentConfig.kt"
)
assertThat(generatedFile.exists()).isTrue()
return generatedFile.readText()
}
}