Updated on 2026-08-14
This commit is contained in:
parent
4d1902af26
commit
bf4a7a9e4e
7 changed files with 891 additions and 5 deletions
|
|
@ -1,4 +1,6 @@
|
|||
import com.tangem.plugin.configuration.configurations.EnvironmentConfigGenerator
|
||||
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
|
||||
import com.tangem.plugin.configuration.model.BuildType
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
|
|
@ -10,14 +12,53 @@ plugins {
|
|||
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 {
|
||||
namespace = "com.tangem.datasource"
|
||||
|
||||
sourceSets["main"].java.srcDir(layout.buildDirectory.dir("generated/source/environment-config"))
|
||||
|
||||
room {
|
||||
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 {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -102,6 +102,7 @@ markdownComposeView = "0.5.4"
|
|||
usedesk = "4.4.0"
|
||||
sumsub = "1.38.0"
|
||||
haze = "1.7.1"
|
||||
kotlinpoet = "1.18.1"
|
||||
# endregion Other libraries
|
||||
|
||||
# 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-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-kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" }
|
||||
# end region Classpath
|
||||
|
||||
# region AndroidX
|
||||
|
|
|
|||
|
|
@ -17,6 +17,15 @@ dependencies {
|
|||
implementation(deps.gradle.kotlin)
|
||||
implementation(deps.gradle.android)
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
package com.tangem.plugin.configuration.model
|
||||
|
||||
internal enum class BuildType(
|
||||
enum class BuildType(
|
||||
val id: String,
|
||||
val appIdSuffix: String? = null,
|
||||
val versionSuffix: String? = null,
|
||||
val obfuscating: Boolean = false,
|
||||
val configFields: List<BuildConfigField>,
|
||||
internal val appIdSuffix: String? = null,
|
||||
internal val versionSuffix: String? = null,
|
||||
internal val obfuscating: Boolean = false,
|
||||
internal val configFields: List<BuildConfigField>,
|
||||
) {
|
||||
|
||||
/**
|
||||
|
|
@ -117,4 +117,19 @@ internal enum class BuildType(
|
|||
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("\"")
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue