Skip to content

Commit 47fbef4

Browse files
authored
Implemented configuration file support. (#12)
1 parent ffe33dd commit 47fbef4

File tree

5 files changed

+240
-0
lines changed

5 files changed

+240
-0
lines changed

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,44 @@ cag [--new-architecture [--no-compose] [--ktlint] [--detekt]]... [--new-feature
111111
```
112112
113113
When run without arguments, the command prints a short usage and suggests using `--help` or `-h` for more options.
114+
115+
### CLI configuration (.cagrc)
116+
117+
You can configure library and plugin versions used by the CLI via a simple INI-style config file named `.cagrc`.
118+
119+
- Locations:
120+
- Project root: `./.cagrc`
121+
- User home: `~/.cagrc`
122+
123+
- Precedence:
124+
- Values in the project `.cagrc` override values in `~/.cagrc`.
125+
126+
- Sections:
127+
- `[new.versions]` — applied when generating new projects (e.g., `--new-project`).
128+
- `[existing.versions]` — applied when generating into an existing project (e.g., new architecture, feature, data source, use case, or view model).
129+
130+
- Keys correspond to version keys used by the generator, for example: `kotlin`, `androidGradlePlugin`, `composeBom`, `composeNavigation`, `retrofit`, `ktor`, `okhttp3`, etc.
131+
132+
Example `~/.cagrc`:
133+
134+
```
135+
[new.versions]
136+
kotlin=2.2.10
137+
composeBom=2025.08.01
138+
139+
[existing.versions]
140+
retrofit=2.11.0
141+
ktor=3.0.3
142+
```
143+
144+
Example `./.cagrc` (project overrides):
145+
146+
```
147+
[new.versions]
148+
composeBom=2025.09.01
149+
150+
[existing.versions]
151+
okhttp3=4.12.0
152+
```
153+
154+
With the above, new projects will use `composeBom=2025.09.01` (from project), `kotlin=2.2.10` (from home). For operations on existing projects, `retrofit=2.11.0` (home) and `okhttp3=4.12.0` (project) will be applied.

cli/src/main/kotlin/com/mitteloupe/cag/cli/Main.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.mitteloupe.cag.cli
22

3+
import com.mitteloupe.cag.cli.configuration.ClientConfigurationLoader
34
import com.mitteloupe.cag.cli.filesystem.CliFileSystemBridge
45
import com.mitteloupe.cag.core.DirectoryFinder
56
import com.mitteloupe.cag.core.GenerationException
@@ -24,6 +25,7 @@ import com.mitteloupe.cag.core.generation.UiLayerContentGenerator
2425
import com.mitteloupe.cag.core.generation.architecture.ArchitectureModulesContentGenerator
2526
import com.mitteloupe.cag.core.generation.architecture.CoroutineModuleContentGenerator
2627
import com.mitteloupe.cag.core.generation.filesystem.FileCreator
28+
import com.mitteloupe.cag.core.generation.versioncatalog.VersionCatalogSettingsAccessor
2729
import com.mitteloupe.cag.core.generation.versioncatalog.VersionCatalogUpdater
2830
import com.mitteloupe.cag.core.request.GenerateArchitectureRequest
2931
import com.mitteloupe.cag.core.request.GenerateFeatureRequestBuilder
@@ -47,6 +49,7 @@ fun main(arguments: Array<String>) {
4749
val projectRoot = findGradleProjectRoot(Paths.get("").toAbsolutePath().toFile()) ?: Paths.get("").toAbsolutePath().toFile()
4850
val projectModel = FilesystemProjectModel(projectRoot)
4951
val basePackage = NamespaceResolver().determineBasePackage(projectModel)
52+
val configuration = ClientConfigurationLoader().load(projectRoot)
5053

5154
if (argumentProcessor.isHelpRequested(arguments)) {
5255
printHelpMessage()
@@ -102,6 +105,7 @@ fun main(arguments: Array<String>) {
102105
enableRetrofit = request.enableRetrofit
103106
)
104107
executeAndReport {
108+
setVersionProvider(configuration.newProjectVersions)
105109
generator.generateProjectTemplate(projectTemplateRequest)
106110
}
107111
}
@@ -117,6 +121,7 @@ fun main(arguments: Array<String>) {
117121
enableDetekt = request.enableDetekt
118122
)
119123
executeAndReport {
124+
setVersionProvider(configuration.existingProjectVersions)
120125
generator.generateArchitecture(architectureRequest)
121126
}
122127
}
@@ -136,12 +141,14 @@ fun main(arguments: Array<String>) {
136141
.enableDetekt(requestFeature.enableDetekt)
137142
.build()
138143
executeAndReport {
144+
setVersionProvider(configuration.existingProjectVersions)
139145
generator.generateFeature(request)
140146
}
141147
}
142148

143149
dataSourceRequests.forEach { request ->
144150
executeAndReport {
151+
setVersionProvider(configuration.existingProjectVersions)
145152
generator.generateDataSource(
146153
destinationRootDirectory = destinationRootDirectory,
147154
dataSourceName = request.dataSourceName,
@@ -165,6 +172,7 @@ fun main(arguments: Array<String>) {
165172
.build()
166173

167174
executeAndReport {
175+
setVersionProvider(configuration.existingProjectVersions)
168176
generator.generateUseCase(useCaseRequest)
169177
}
170178
}
@@ -184,6 +192,7 @@ fun main(arguments: Array<String>) {
184192
).build()
185193

186194
executeAndReport {
195+
setVersionProvider(configuration.existingProjectVersions)
187196
generator.generateViewModel(viewModelRequest)
188197
}
189198
}
@@ -312,3 +321,9 @@ private fun executeAndReport(operation: () -> Unit) =
312321
println("Error: ${exception.message}")
313322
exitProcess(1)
314323
}
324+
325+
private fun setVersionProvider(overrides: Map<String, String>) {
326+
VersionCatalogSettingsAccessor.setProvider { key, default ->
327+
overrides[key] ?: default
328+
}
329+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.mitteloupe.cag.cli.configuration
2+
3+
data class ClientConfiguration(
4+
val newProjectVersions: Map<String, String> = emptyMap(),
5+
val existingProjectVersions: Map<String, String> = emptyMap()
6+
) {
7+
companion object {
8+
val EMPTY = ClientConfiguration()
9+
}
10+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.mitteloupe.cag.cli.configuration
2+
3+
import java.io.File
4+
5+
private const val FILE_NAME = ".cagrc"
6+
7+
class ClientConfigurationLoader {
8+
fun load(projectRoot: File): ClientConfiguration {
9+
val projectFile = File(projectRoot, FILE_NAME)
10+
val homeFile = File(System.getProperty("user.home"), FILE_NAME)
11+
return loadFromFiles(projectFile = projectFile, homeFile = homeFile)
12+
}
13+
14+
internal fun loadFromFiles(
15+
projectFile: File?,
16+
homeFile: File?
17+
): ClientConfiguration {
18+
val homeConfiguration = if (homeFile?.exists() == true && homeFile.isFile) parse(homeFile.readText()) else ClientConfiguration.EMPTY
19+
val projectConfiguration =
20+
if (projectFile?.exists() == true && projectFile.isFile) {
21+
parse(projectFile.readText())
22+
} else {
23+
ClientConfiguration.EMPTY
24+
}
25+
return mergeConfigurations(baseConfiguration = homeConfiguration, override = projectConfiguration)
26+
}
27+
28+
private fun mergeConfigurations(
29+
baseConfiguration: ClientConfiguration,
30+
override: ClientConfiguration
31+
): ClientConfiguration =
32+
ClientConfiguration(
33+
newProjectVersions = baseConfiguration.newProjectVersions + override.newProjectVersions,
34+
existingProjectVersions = baseConfiguration.existingProjectVersions + override.existingProjectVersions
35+
)
36+
37+
internal fun parse(text: String): ClientConfiguration {
38+
val newProjectVersions = mutableMapOf<String, String>()
39+
val existingProjectVersions = mutableMapOf<String, String>()
40+
41+
var currentVersionsMap: MutableMap<String, String>? = null
42+
43+
text.lineSequence().forEach { rawLine ->
44+
val line = rawLine.trim()
45+
if (line.isEmpty() || line.startsWith("#") || line.startsWith(";")) return@forEach
46+
47+
if (line.startsWith("[") && line.endsWith("]")) {
48+
currentVersionsMap =
49+
when (line.substring(1, line.length - 1).lowercase()) {
50+
"new.versions" -> newProjectVersions
51+
"existing.versions" -> existingProjectVersions
52+
else -> null
53+
}
54+
return@forEach
55+
}
56+
57+
val index = line.indexOf('=')
58+
if (index > 0 && currentVersionsMap != null) {
59+
val key = line.take(index).trim()
60+
val value = line.substring(index + 1).trim()
61+
if (key.isNotEmpty() && value.isNotEmpty()) {
62+
currentVersionsMap[key] = value
63+
}
64+
}
65+
}
66+
67+
return ClientConfiguration(newProjectVersions = newProjectVersions, existingProjectVersions = existingProjectVersions)
68+
}
69+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package com.mitteloupe.cag.cli.configuration
2+
3+
import org.junit.Assert.assertEquals
4+
import org.junit.Before
5+
import org.junit.Test
6+
import java.io.File
7+
import kotlin.io.path.createTempDirectory
8+
9+
class VersionConfigLoaderTest {
10+
private lateinit var classUnderTest: ClientConfigurationLoader
11+
12+
@Before
13+
fun setUp() {
14+
classUnderTest = ClientConfigurationLoader()
15+
}
16+
17+
@Test
18+
fun `Given INI text with new and existing sections when parse then parses values`() {
19+
// Given
20+
val text =
21+
"""
22+
# Comment
23+
[new.versions]
24+
kotlin=2.2.99
25+
composeBom=2099.01.01
26+
27+
[existing.versions]
28+
retrofit=2.99.0
29+
ktor=9.9.9
30+
""".trimIndent()
31+
val expectedClientConfiguration =
32+
ClientConfiguration(
33+
newProjectVersions =
34+
mapOf(
35+
"kotlin" to "2.2.99",
36+
"composeBom" to "2099.01.01"
37+
),
38+
existingProjectVersions =
39+
mapOf(
40+
"retrofit" to "2.99.0",
41+
"ktor" to "9.9.9"
42+
)
43+
)
44+
45+
// When
46+
val actualConfiguration = classUnderTest.parse(text)
47+
48+
// Then
49+
assertEquals(expectedClientConfiguration, actualConfiguration)
50+
}
51+
52+
@Test
53+
fun `Given home, project configurations when loadFromFiles then project overrides home`() {
54+
// Given
55+
val temporaryDirectory = createTempDirectory(prefix = "cag-test-").toFile()
56+
try {
57+
val homeFile =
58+
File(temporaryDirectory, "home.cagrc").apply {
59+
writeText(
60+
"""
61+
[new.versions]
62+
composeBom=2000.01.01
63+
ktor=1.0.0
64+
[existing.versions]
65+
retrofit=2.0.0
66+
""".trimIndent()
67+
)
68+
}
69+
70+
val projectFile =
71+
File(temporaryDirectory, ".cagrc").apply {
72+
writeText(
73+
"""
74+
[new.versions]
75+
composeBom=2025.08.01
76+
[existing.versions]
77+
retrofit=2.11.0
78+
okhttp3=4.12.0
79+
""".trimIndent()
80+
)
81+
}
82+
val expectedConfiguration =
83+
ClientConfiguration(
84+
newProjectVersions =
85+
mapOf(
86+
"composeBom" to "2025.08.01",
87+
"ktor" to "1.0.0"
88+
),
89+
existingProjectVersions =
90+
mapOf(
91+
"retrofit" to "2.11.0",
92+
"okhttp3" to "4.12.0"
93+
)
94+
)
95+
96+
// When
97+
val actualConfiguration = classUnderTest.loadFromFiles(projectFile = projectFile, homeFile = homeFile)
98+
99+
// Then
100+
assertEquals(expectedConfiguration, actualConfiguration)
101+
} finally {
102+
temporaryDirectory.deleteRecursively()
103+
}
104+
}
105+
}

0 commit comments

Comments
 (0)