Skip to content

Commit eab4d7c

Browse files
Adds command line property source. (#217)
* Adds command line property source. * Adds map property source and tests for map and command line. Co-authored-by: Frederic Kneier <[email protected]>
1 parent 68971ad commit eab4d7c

File tree

3 files changed

+162
-28
lines changed

3 files changed

+162
-28
lines changed

hoplite-core/src/main/kotlin/com/sksamuel/hoplite/PropertySource.kt

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,24 @@ interface PropertySource {
6565
fun stream(input: InputStream, ext: String) =
6666
InputStreamPropertySource(input, ext)
6767

68+
/**
69+
* Returns a [PropertySource] that read the specified map values.
70+
*
71+
* @param map map
72+
*/
73+
fun map(map: Map<String, Any>) =
74+
MapPropertySource(map)
75+
76+
/**
77+
* Returns a [PropertySource] that will read the specified command line arguments.
78+
*
79+
* @param arguments command line arguments as passed to main method
80+
* @param prefix argument prefix
81+
* @param delimiter key value delimiter
82+
*/
83+
fun commandLine(arguments: Array<String>, prefix: String = "--", delimiter: String = "=") =
84+
CommandLinePropertySource(arguments, prefix, delimiter)
85+
6886
/**
6987
* Returns a [PropertySource] that will read from the specified string.
7088
*
@@ -132,6 +150,53 @@ class EnvironmentVariablesPropertySource(
132150
}
133151
}
134152

153+
/**
154+
* An implementation of [PropertySource] that provides config based on command line arguments.
155+
*
156+
* Parameters will be processed if they start with a given prefix. Key and value are split by a given delimiter.
157+
*/
158+
class MapPropertySource(
159+
private val map: Map<String, Any?>,
160+
) : PropertySource {
161+
override fun node(context: PropertySourceContext): ConfigResult<Node> {
162+
return when {
163+
map.isEmpty() -> Undefined.valid()
164+
else -> map.toNode("map").valid()
165+
}
166+
}
167+
}
168+
169+
/**
170+
* An implementation of [PropertySource] that provides config based on command line arguments.
171+
*
172+
* Parameters will be processed if they start with a given prefix. Key and value are split by a given delimiter.
173+
*/
174+
class CommandLinePropertySource(
175+
private val arguments: Array<String>,
176+
private val prefix: String,
177+
private val delimiter: String,
178+
) : PropertySource {
179+
override fun node(context: PropertySourceContext): ConfigResult<Node> {
180+
val values = arguments.asSequence().filter {
181+
it.startsWith(prefix) && it.contains(delimiter)
182+
}.map {
183+
it.removePrefix(prefix).split(delimiter, limit = 2)
184+
}.groupingBy {
185+
it[0]
186+
}.aggregate { _, accumulator: Any?, element, _ ->
187+
when (accumulator) {
188+
null -> element[1]
189+
is List<*> -> accumulator + element[1]
190+
else -> listOf(accumulator, element[1])
191+
}
192+
}
193+
return when {
194+
values.isEmpty() -> Undefined.valid()
195+
else -> values.toNode("commandLine").valid()
196+
}
197+
}
198+
}
199+
135200
/**
136201
* An implementation of [PropertySource] that provides config through a config file
137202
* defined at ~/.userconfig.ext
Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,71 @@
11
package com.sksamuel.hoplite.parsers
22

3-
import com.sksamuel.hoplite.MapNode
4-
import com.sksamuel.hoplite.Pos
5-
import com.sksamuel.hoplite.StringNode
6-
import com.sksamuel.hoplite.Node
3+
import com.sksamuel.hoplite.*
74
import java.util.*
85

96
@Suppress("UNCHECKED_CAST")
10-
fun Properties.toNode(source: String): Node {
7+
fun Properties.toNode(source: String) = asIterable().toNode(
8+
source = source,
9+
keyExtractor = { it.key.toString() },
10+
valueExtractor = { it.value }
11+
)
1112

12-
val valueMarker = "____value"
13+
@Suppress("UNCHECKED_CAST")
14+
fun <T : Any> Map<String, T?>.toNode(source: String) = entries.toNode(
15+
source = source,
16+
keyExtractor = { it.key },
17+
valueExtractor = { it.value },
18+
)
19+
20+
data class Element(
21+
val values: MutableMap<String, Element> = hashMapOf(),
22+
var value: Any? = null,
23+
)
24+
25+
@Suppress("UNCHECKED_CAST")
26+
private fun <T> Iterable<T>.toNode(source: String, keyExtractor: (T) -> String, valueExtractor: (T) -> Any?): Node {
27+
val map = Element()
1328

14-
val root = mutableMapOf<String, Any>()
15-
stringPropertyNames().toList().map { key ->
29+
forEach { item ->
30+
val key = keyExtractor(item)
31+
val value = valueExtractor(item)
32+
val segments = key.split(".")
1633

17-
val components = key.split('.')
18-
val map = components.fold(root) { acc, k ->
19-
acc.getOrPut(k) { mutableMapOf<String, Any>() } as MutableMap<String, Any>
34+
segments.foldIndexed(map) { index, element, segment ->
35+
element.values.computeIfAbsent(segment) { Element() }.also {
36+
if (index == segments.size - 1) it.value = value
37+
}
2038
}
21-
map.put(valueMarker, getProperty(key))
2239
}
2340

2441
val pos = Pos.FilePos(source)
2542

26-
fun Map<String, Any>.toNode(): Node {
27-
val maps = filterValues { it is MutableMap<*, *> }.mapValues {
28-
when (val v = it.value) {
29-
is MutableMap<*, *> -> (v as MutableMap<String, Any>).toNode()
30-
else -> throw java.lang.RuntimeException("Bug: unsupported state $it")
31-
}
32-
}
33-
val value = this[valueMarker]
34-
return when {
35-
value == null && maps.isEmpty() -> MapNode(emptyMap(), pos)
36-
value == null && maps.isNotEmpty() -> MapNode(maps.toMap(), pos)
37-
maps.isEmpty() -> StringNode(value.toString(), pos)
38-
else -> MapNode(maps.toMap(), pos, StringNode(value.toString(), pos))
43+
fun Any.transform(): Node = when (this) {
44+
is Element -> when {
45+
value != null && values.isEmpty() -> value?.transform() ?: Undefined
46+
else -> MapNode(
47+
map = values.takeUnless { it.isEmpty() }?.mapValues { it.value.transform() } ?: emptyMap(),
48+
value = value?.transform() ?: Undefined,
49+
pos = pos,
50+
)
3951
}
52+
is Array<*> -> ArrayNode(
53+
elements = mapNotNull { it?.transform() },
54+
pos = pos,
55+
)
56+
is Collection<*> -> ArrayNode(
57+
elements = mapNotNull { it?.transform() },
58+
pos = pos,
59+
)
60+
is Map<*, *> -> MapNode(
61+
map = takeUnless { it.isEmpty() }?.mapNotNull { entry ->
62+
entry.value?.let { entry.key.toString() to it.transform() }
63+
}?.toMap() ?: emptyMap(),
64+
pos = pos,
65+
)
66+
else -> StringNode(this.toString(), pos)
4067
}
4168

42-
return root.toNode()
69+
val result = map.transform()
70+
return result
4371
}

hoplite-core/src/test/kotlin/com/sksamuel/hoplite/PropertySourceTest.kt

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ class PropertySourceTest : FunSpec() {
88

99
test("reads config from string") {
1010
data class TestConfig(val a: String, val b: Int)
11+
1112
val config = ConfigLoader.Builder()
12-
.addPropertySource(PropertySource.string("""
13+
.addPropertySource(
14+
PropertySource.string(
15+
"""
1316
a = A value
1417
b = 42
15-
""".trimIndent(), "props"))
18+
""".trimIndent(), "props"
19+
)
20+
)
1621
.build()
1722
.loadConfigOrThrow<TestConfig>()
1823

@@ -35,5 +40,41 @@ class PropertySourceTest : FunSpec() {
3540
config shouldBe TestConfig("A value", 42)
3641
}
3742

43+
test("reads config from map") {
44+
data class TestConfig(val a: String, val b: Int, val other: List<String>)
45+
46+
val arguments = mapOf(
47+
"a" to "A value",
48+
"b" to "42",
49+
"other" to listOf("Value1", "Value2"),
50+
)
51+
52+
val config = ConfigLoader.Builder()
53+
.addPropertySource(PropertySource.map(arguments))
54+
.build()
55+
.loadConfigOrThrow<TestConfig>()
56+
57+
config shouldBe TestConfig("A value", 42, listOf("Value1", "Value2"))
58+
}
59+
60+
test("reads config from command line") {
61+
data class TestConfig(val a: String, val b: Int, val other: List<String>)
62+
63+
val arguments = arrayOf(
64+
"--a=A value",
65+
"--b=42",
66+
"some other value",
67+
"--other=Value1",
68+
"--other=Value2",
69+
)
70+
71+
val config = ConfigLoader.Builder()
72+
.addPropertySource(PropertySource.commandLine(arguments))
73+
.build()
74+
.loadConfigOrThrow<TestConfig>()
75+
76+
config shouldBe TestConfig("A value", 42, listOf("Value1", "Value2"))
77+
}
78+
3879
}
3980
}

0 commit comments

Comments
 (0)