Updated on 2026-08-14
This commit is contained in:
parent
4d1902af26
commit
bf4a7a9e4e
7 changed files with 891 additions and 5 deletions
|
|
@ -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