Updated on 2026-08-14
This commit is contained in:
parent
26a6da1b85
commit
428c0a56b0
3 changed files with 137 additions and 13 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit e4fac168bd941fe90b0f081dfa70ea1b4148b49f
|
Subproject commit b693d601d9dc3de27f0536574bce59a6aa5e2e89
|
||||||
|
|
@ -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() }
|
||||||
|
.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() }
|
.filter { it.isNotEmpty() }
|
||||||
.joinToString("") { part ->
|
return parts.mapIndexed { index, part ->
|
||||||
part.replaceFirstChar { it.uppercase() }
|
val normalized = part.lowercase(Locale.ROOT)
|
||||||
}
|
if (index == 0) normalized
|
||||||
|
else normalized.replaceFirstChar { it.uppercase(Locale.ROOT) }
|
||||||
|
}.joinToString("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue