Skip to content

Commit

Permalink
Adds command line property source. (#217)
Browse files Browse the repository at this point in the history
* Adds command line property source.

* Adds map property source and tests for map and command line.

Co-authored-by: Frederic Kneier <[email protected]>
  • Loading branch information
frederic-kneier and frederic-kneier authored Jul 17, 2021
1 parent 68971ad commit eab4d7c
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@ interface PropertySource {
fun stream(input: InputStream, ext: String) =
InputStreamPropertySource(input, ext)

/**
* Returns a [PropertySource] that read the specified map values.
*
* @param map map
*/
fun map(map: Map<String, Any>) =
MapPropertySource(map)

/**
* Returns a [PropertySource] that will read the specified command line arguments.
*
* @param arguments command line arguments as passed to main method
* @param prefix argument prefix
* @param delimiter key value delimiter
*/
fun commandLine(arguments: Array<String>, prefix: String = "--", delimiter: String = "=") =
CommandLinePropertySource(arguments, prefix, delimiter)

/**
* Returns a [PropertySource] that will read from the specified string.
*
Expand Down Expand Up @@ -132,6 +150,53 @@ class EnvironmentVariablesPropertySource(
}
}

/**
* An implementation of [PropertySource] that provides config based on command line arguments.
*
* Parameters will be processed if they start with a given prefix. Key and value are split by a given delimiter.
*/
class MapPropertySource(
private val map: Map<String, Any?>,
) : PropertySource {
override fun node(context: PropertySourceContext): ConfigResult<Node> {
return when {
map.isEmpty() -> Undefined.valid()
else -> map.toNode("map").valid()
}
}
}

/**
* An implementation of [PropertySource] that provides config based on command line arguments.
*
* Parameters will be processed if they start with a given prefix. Key and value are split by a given delimiter.
*/
class CommandLinePropertySource(
private val arguments: Array<String>,
private val prefix: String,
private val delimiter: String,
) : PropertySource {
override fun node(context: PropertySourceContext): ConfigResult<Node> {
val values = arguments.asSequence().filter {
it.startsWith(prefix) && it.contains(delimiter)
}.map {
it.removePrefix(prefix).split(delimiter, limit = 2)
}.groupingBy {
it[0]
}.aggregate { _, accumulator: Any?, element, _ ->
when (accumulator) {
null -> element[1]
is List<*> -> accumulator + element[1]
else -> listOf(accumulator, element[1])
}
}
return when {
values.isEmpty() -> Undefined.valid()
else -> values.toNode("commandLine").valid()
}
}
}

/**
* An implementation of [PropertySource] that provides config through a config file
* defined at ~/.userconfig.ext
Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,71 @@
package com.sksamuel.hoplite.parsers

import com.sksamuel.hoplite.MapNode
import com.sksamuel.hoplite.Pos
import com.sksamuel.hoplite.StringNode
import com.sksamuel.hoplite.Node
import com.sksamuel.hoplite.*
import java.util.*

@Suppress("UNCHECKED_CAST")
fun Properties.toNode(source: String): Node {
fun Properties.toNode(source: String) = asIterable().toNode(
source = source,
keyExtractor = { it.key.toString() },
valueExtractor = { it.value }
)

val valueMarker = "____value"
@Suppress("UNCHECKED_CAST")
fun <T : Any> Map<String, T?>.toNode(source: String) = entries.toNode(
source = source,
keyExtractor = { it.key },
valueExtractor = { it.value },
)

data class Element(
val values: MutableMap<String, Element> = hashMapOf(),
var value: Any? = null,
)

@Suppress("UNCHECKED_CAST")
private fun <T> Iterable<T>.toNode(source: String, keyExtractor: (T) -> String, valueExtractor: (T) -> Any?): Node {
val map = Element()

val root = mutableMapOf<String, Any>()
stringPropertyNames().toList().map { key ->
forEach { item ->
val key = keyExtractor(item)
val value = valueExtractor(item)
val segments = key.split(".")

val components = key.split('.')
val map = components.fold(root) { acc, k ->
acc.getOrPut(k) { mutableMapOf<String, Any>() } as MutableMap<String, Any>
segments.foldIndexed(map) { index, element, segment ->
element.values.computeIfAbsent(segment) { Element() }.also {
if (index == segments.size - 1) it.value = value
}
}
map.put(valueMarker, getProperty(key))
}

val pos = Pos.FilePos(source)

fun Map<String, Any>.toNode(): Node {
val maps = filterValues { it is MutableMap<*, *> }.mapValues {
when (val v = it.value) {
is MutableMap<*, *> -> (v as MutableMap<String, Any>).toNode()
else -> throw java.lang.RuntimeException("Bug: unsupported state $it")
}
}
val value = this[valueMarker]
return when {
value == null && maps.isEmpty() -> MapNode(emptyMap(), pos)
value == null && maps.isNotEmpty() -> MapNode(maps.toMap(), pos)
maps.isEmpty() -> StringNode(value.toString(), pos)
else -> MapNode(maps.toMap(), pos, StringNode(value.toString(), pos))
fun Any.transform(): Node = when (this) {
is Element -> when {
value != null && values.isEmpty() -> value?.transform() ?: Undefined
else -> MapNode(
map = values.takeUnless { it.isEmpty() }?.mapValues { it.value.transform() } ?: emptyMap(),
value = value?.transform() ?: Undefined,
pos = pos,
)
}
is Array<*> -> ArrayNode(
elements = mapNotNull { it?.transform() },
pos = pos,
)
is Collection<*> -> ArrayNode(
elements = mapNotNull { it?.transform() },
pos = pos,
)
is Map<*, *> -> MapNode(
map = takeUnless { it.isEmpty() }?.mapNotNull { entry ->
entry.value?.let { entry.key.toString() to it.transform() }
}?.toMap() ?: emptyMap(),
pos = pos,
)
else -> StringNode(this.toString(), pos)
}

return root.toNode()
val result = map.transform()
return result
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ class PropertySourceTest : FunSpec() {

test("reads config from string") {
data class TestConfig(val a: String, val b: Int)

val config = ConfigLoader.Builder()
.addPropertySource(PropertySource.string("""
.addPropertySource(
PropertySource.string(
"""
a = A value
b = 42
""".trimIndent(), "props"))
""".trimIndent(), "props"
)
)
.build()
.loadConfigOrThrow<TestConfig>()

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

test("reads config from map") {
data class TestConfig(val a: String, val b: Int, val other: List<String>)

val arguments = mapOf(
"a" to "A value",
"b" to "42",
"other" to listOf("Value1", "Value2"),
)

val config = ConfigLoader.Builder()
.addPropertySource(PropertySource.map(arguments))
.build()
.loadConfigOrThrow<TestConfig>()

config shouldBe TestConfig("A value", 42, listOf("Value1", "Value2"))
}

test("reads config from command line") {
data class TestConfig(val a: String, val b: Int, val other: List<String>)

val arguments = arrayOf(
"--a=A value",
"--b=42",
"some other value",
"--other=Value1",
"--other=Value2",
)

val config = ConfigLoader.Builder()
.addPropertySource(PropertySource.commandLine(arguments))
.build()
.loadConfigOrThrow<TestConfig>()

config shouldBe TestConfig("A value", 42, listOf("Value1", "Value2"))
}

}
}

0 comments on commit eab4d7c

Please sign in to comment.