diff --git a/README.md b/README.md index 96c72668..37d5e061 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,8 @@ Hoplite maps env vars as follows: * Underscores are separators for nested config. For example `TOPIC_NAME` would override a property `name` located in a `topic` parent. +* To bind env vars to arrays or lists, postfix with an index e.g. set env vars `TOPIC_NAME_0` and `TOPIC_NAME_1` to set two values for the `name` list property. Missing indices are ignored, which is useful for commenting out values without renumbering subsequent ones. + * To bind env vars to maps, the key is part of the nested config e.g. `TOPIC_NAME_FOO` and `TOPIC_NAME_BAR` would set the "foo" and "bar" keys for the `name` map property. Note that keys are one exception to the idiomatic uppercase rule -- the env var name determines the case of the map key. diff --git a/changelog.md b/changelog.md index b76f527c..644338e4 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ case is still accepted), letters, and digits. **Single underscores** now separat * Breaking: The `EnvironmentVariableOverridePropertySource` has been removed. The standard `EnvironmentVariablesPropertySource` is now loaded by default, and takes precedence over other default sources just like the `EnvironmentVariableOverridePropertySource` did. To maintain similar behavior, configure it with a filtering `prefix`. +* Add the ability to load a series of environment variables into arrays/lists via the `_n` syntax. ### 2.7.5 diff --git a/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/sources/EnvironmentVariablesPropertySource.kt b/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/sources/EnvironmentVariablesPropertySource.kt index ddc5bcff..5d87b0f5 100644 --- a/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/sources/EnvironmentVariablesPropertySource.kt +++ b/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/sources/EnvironmentVariablesPropertySource.kt @@ -1,11 +1,14 @@ package com.sksamuel.hoplite.sources +import com.sksamuel.hoplite.ArrayNode import com.sksamuel.hoplite.ConfigResult +import com.sksamuel.hoplite.MapNode import com.sksamuel.hoplite.Node import com.sksamuel.hoplite.PropertySource import com.sksamuel.hoplite.PropertySourceContext import com.sksamuel.hoplite.fp.valid import com.sksamuel.hoplite.parsers.toNode +import com.sksamuel.hoplite.transform class EnvironmentVariablesPropertySource( private val environmentVariableMap: () -> Map = { System.getenv() }, @@ -23,6 +26,13 @@ class EnvironmentVariablesPropertySource( .filterKeys { if (prefix == null) true else it.startsWith(prefix) } .mapKeys { if (prefix == null) it.key else it.key.removePrefix(prefix) } - return map.toNode("env", DELIMITER).valid() + return map.toNode("env", DELIMITER).transform { node -> + if (node is MapNode && node.map.keys.all { it.toIntOrNull() != null }) { + // all they map keys are ints, so lets transform the MapNode into an ArrayNode + ArrayNode(node.map.values.toList(), node.pos, node.path, node.meta, node.delimiter, node.sourceKey) + } else { + node + } + }.valid() } } diff --git a/hoplite-core/src/test/kotlin/com/sksamuel/hoplite/EnvironmentVariablesPropertySourceTest.kt b/hoplite-core/src/test/kotlin/com/sksamuel/hoplite/EnvironmentVariablesPropertySourceTest.kt index a4120947..c72dbf2c 100644 --- a/hoplite-core/src/test/kotlin/com/sksamuel/hoplite/EnvironmentVariablesPropertySourceTest.kt +++ b/hoplite-core/src/test/kotlin/com/sksamuel/hoplite/EnvironmentVariablesPropertySourceTest.kt @@ -97,6 +97,87 @@ class EnvironmentVariablesPropertySourceTest : FunSpec({ )) } + test("build env source can create lists") { + data class TestConfig(val listProp: List) + + val config = ConfigLoaderBuilder + .defaultWithoutPropertySources() + .addPropertySource( + EnvironmentVariablesPropertySource( + environmentVariableMap = { + mapOf( + "LISTPROP_0" to "value_a", + "LISTPROP_1" to "value_A", + "LISTPROP_2" to "value_abc", + ) + }, + ) + ) + .build() + .loadConfigOrThrow() + + config shouldBe TestConfig(listOf( + "value_a", + "value_A", + "value_abc", + )) + } + + test("build env source can create intermediate lists") { + data class ListEntry(val foo: String) + data class TestConfig(val listProp: List) + + val config = ConfigLoaderBuilder + .defaultWithoutPropertySources() + .addPropertySource( + EnvironmentVariablesPropertySource( + environmentVariableMap = { + mapOf( + "LISTPROP_0_FOO" to "value_a", + "LISTPROP_1_FOO" to "value_A", + "LISTPROP_2_FOO" to "value_abc", + ) + }, + ) + ) + .build() + .loadConfigOrThrow() + + config shouldBe TestConfig(listOf( + ListEntry("value_a"), + ListEntry("value_A"), + ListEntry("value_abc"), + )) + } + + + test("build env source can skip missing list indices") { + // this is handy to easily comment out env vars without breaking the functionality + + data class ListEntry(val foo: String) + data class TestConfig(val listProp: List) + + val config = ConfigLoaderBuilder + .defaultWithoutPropertySources() + .addPropertySource( + EnvironmentVariablesPropertySource( + environmentVariableMap = { + mapOf( + "LISTPROP_0_FOO" to "value_a", + "LISTPROP_2_FOO" to "value_abc", + ) + }, + ) + ) + .build() + .loadConfigOrThrow() + + config shouldBe TestConfig(listOf( + ListEntry("value_a"), + ListEntry("value_abc"), + )) + } + test("env var source should respect config aliases that need to be normalized to match") { data class TestConfig(@ConfigAlias("fooBar") val bazBar: String)