diff --git a/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/decoder/AbstractUnnormalizedKeysDecoder.kt b/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/decoder/AbstractUnnormalizedKeysDecoder.kt deleted file mode 100644 index 79f8e02e..00000000 --- a/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/decoder/AbstractUnnormalizedKeysDecoder.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.sksamuel.hoplite.decoder - -import com.sksamuel.hoplite.ConfigResult -import com.sksamuel.hoplite.DecoderContext -import com.sksamuel.hoplite.MapNode -import com.sksamuel.hoplite.Node -import com.sksamuel.hoplite.transform -import kotlin.reflect.KType - -/** - * A decoder which decodes based on unnormalized keys. - * - * This is useful for decoders that need to know the original key names. - * - * It restores the original key names from the node source key. - */ -abstract class AbstractUnnormalizedKeysDecoder : NullHandlingDecoder { - override fun safeDecode(node: Node, type: KType, context: DecoderContext): ConfigResult { - val unnormalizedNode = node.transform { - val sourceKey = it.sourceKey - when (it) { - is MapNode -> it.copy(map = it.map.mapKeys { (k, v) -> - (v.sourceKey ?: k).removePrefix("$sourceKey.") - }) - else -> it - } - } - - return safeDecodeUnnormalized(unnormalizedNode, type, context) - } - - abstract fun safeDecodeUnnormalized(node: Node, type: KType, context: DecoderContext): ConfigResult -} diff --git a/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/decoder/MapDecoder.kt b/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/decoder/MapDecoder.kt index d53264fb..3c0f538f 100644 --- a/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/decoder/MapDecoder.kt +++ b/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/decoder/MapDecoder.kt @@ -1,27 +1,28 @@ package com.sksamuel.hoplite.decoder import com.sksamuel.hoplite.ArrayNode -import com.sksamuel.hoplite.fp.invalid import com.sksamuel.hoplite.ConfigFailure import com.sksamuel.hoplite.ConfigResult import com.sksamuel.hoplite.DecoderContext import com.sksamuel.hoplite.MapNode -import com.sksamuel.hoplite.StringNode import com.sksamuel.hoplite.Node +import com.sksamuel.hoplite.StringNode +import com.sksamuel.hoplite.denormalize import com.sksamuel.hoplite.fp.flatMap +import com.sksamuel.hoplite.fp.invalid import com.sksamuel.hoplite.fp.sequence import kotlin.reflect.KType import kotlin.reflect.full.isSubtypeOf import kotlin.reflect.full.starProjectedType import kotlin.reflect.full.withNullability -class MapDecoder : AbstractUnnormalizedKeysDecoder>() { +class MapDecoder : NullHandlingDecoder> { override fun supports(type: KType): Boolean = type.isSubtypeOf(Map::class.starProjectedType) || type.isSubtypeOf(Map::class.starProjectedType.withNullability(true)) - override fun safeDecodeUnnormalized(node: Node, type: KType, context: DecoderContext): ConfigResult> { + override fun safeDecode(node: Node, type: KType, context: DecoderContext): ConfigResult> { require(type.arguments.size == 2) val kType = type.arguments[0].type!! @@ -32,7 +33,7 @@ class MapDecoder : AbstractUnnormalizedKeysDecoder>() { vdecoder: Decoder, context: DecoderContext): ConfigResult> { - return node.map.entries.map { (k, v) -> + return node.denormalize().map.entries.map { (k, v) -> kdecoder.decode(StringNode(k, node.pos, node.path, emptyMap()), kType, context).flatMap { kk -> vdecoder.decode(v, vType, context).map { vv -> context.usedPaths.add(v.path) diff --git a/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/nodes.kt b/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/nodes.kt index b0a24676..1ef86ce2 100644 --- a/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/nodes.kt +++ b/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/nodes.kt @@ -134,6 +134,19 @@ fun Node.transform(transformer: (Node) -> Node): Node = when (val transformed = else -> transformed } +/** + * Denormalizes a node, restoring its original key from the source. This is not recursive -- it only transforms + * the given node, not its children. + */ +fun T.denormalize(): T { + return when (this) { + is MapNode -> copy(map = map.mapKeys { (k, v) -> + (v.sourceKey ?: k).removePrefix("$sourceKey.") + }) + else -> this + } as T +} + sealed class ContainerNode : Node data class MapNode( diff --git a/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/parsers/loadProps.kt b/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/parsers/loadProps.kt index f12b8ad3..46701daa 100644 --- a/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/parsers/loadProps.kt +++ b/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/parsers/loadProps.kt @@ -74,7 +74,6 @@ private fun Iterable.toNode( pos = pos, path = path, value = value?.transform(path, sourceKey) ?: Undefined, - sourceKey = this.sourceKey, ) } is Array<*> -> ArrayNode( diff --git a/hoplite-core/src/test/kotlin/com/sksamuel/hoplite/LoadPropsTest.kt b/hoplite-core/src/test/kotlin/com/sksamuel/hoplite/LoadPropsTest.kt index 6e81f3c5..8fca0c98 100644 --- a/hoplite-core/src/test/kotlin/com/sksamuel/hoplite/LoadPropsTest.kt +++ b/hoplite-core/src/test/kotlin/com/sksamuel/hoplite/LoadPropsTest.kt @@ -27,7 +27,7 @@ class LoadPropsTest : FunSpec({ pos = Pos.SourcePos(source = "source"), DotPath("a", "b"), value = Undefined, - sourceKey = null + sourceKey = "a.b" ), "d" to StringNode("true", pos = Pos.SourcePos(source = "source"), DotPath("a", "d"), sourceKey = "a.d") ), diff --git a/hoplite-core/src/test/kotlin/com/sksamuel/hoplite/PropsParserTest.kt b/hoplite-core/src/test/kotlin/com/sksamuel/hoplite/PropsParserTest.kt index f402ea65..5275ba0a 100644 --- a/hoplite-core/src/test/kotlin/com/sksamuel/hoplite/PropsParserTest.kt +++ b/hoplite-core/src/test/kotlin/com/sksamuel/hoplite/PropsParserTest.kt @@ -32,7 +32,7 @@ class PropsParserTest : StringSpec() { ), pos = Pos.SourcePos(source = "a.props"), DotPath("a"), - sourceKey = null + sourceKey = "a" ), "e" to StringNode(value = "5.5", pos = Pos.SourcePos(source = "a.props"), DotPath("e"), sourceKey = "e") ), diff --git a/hoplite-hikaricp/src/main/kotlin/com/sksamuel/hoplite/hikari/HikariDataSourceDecoder.kt b/hoplite-hikaricp/src/main/kotlin/com/sksamuel/hoplite/hikari/HikariDataSourceDecoder.kt index c65a24ad..4fca95c0 100644 --- a/hoplite-hikaricp/src/main/kotlin/com/sksamuel/hoplite/hikari/HikariDataSourceDecoder.kt +++ b/hoplite-hikaricp/src/main/kotlin/com/sksamuel/hoplite/hikari/HikariDataSourceDecoder.kt @@ -6,25 +6,26 @@ import com.sksamuel.hoplite.DecoderContext import com.sksamuel.hoplite.MapNode import com.sksamuel.hoplite.Node import com.sksamuel.hoplite.PrimitiveNode -import com.sksamuel.hoplite.decoder.AbstractUnnormalizedKeysDecoder +import com.sksamuel.hoplite.decoder.Decoder +import com.sksamuel.hoplite.denormalize import com.sksamuel.hoplite.fp.invalid import com.sksamuel.hoplite.fp.valid import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource -import java.util.Properties +import java.util.* import kotlin.reflect.KType -class HikariDataSourceDecoder : AbstractUnnormalizedKeysDecoder() { +class HikariDataSourceDecoder : Decoder { override fun supports(type: KType): Boolean = type.classifier == HikariDataSource::class - override fun safeDecodeUnnormalized(node: Node, type: KType, context: DecoderContext): ConfigResult { + override fun decode(node: Node, type: KType, context: DecoderContext): ConfigResult { val props = Properties() fun populate(node: Node, prefix: String) { when (node) { - is MapNode -> node.map.forEach { (k, v) -> populate(v, if (prefix == "") k else "$prefix.$k") } + is MapNode -> node.denormalize().map.forEach { (k, v) -> populate(v, if (prefix == "") k else "$prefix.$k") } is PrimitiveNode -> props[prefix] = node.value else -> { } diff --git a/hoplite-vavr/src/main/kotlin/com/sksamuel/hoplite/decoder/vavr/MapDecoder.kt b/hoplite-vavr/src/main/kotlin/com/sksamuel/hoplite/decoder/vavr/MapDecoder.kt index a04b65d1..6bd9e3a5 100644 --- a/hoplite-vavr/src/main/kotlin/com/sksamuel/hoplite/decoder/vavr/MapDecoder.kt +++ b/hoplite-vavr/src/main/kotlin/com/sksamuel/hoplite/decoder/vavr/MapDecoder.kt @@ -32,7 +32,7 @@ class MapDecoder : NullHandlingDecoder> { kdecoder: Decoder, vdecoder: Decoder, context: DecoderContext): ConfigResult> = - node.map.entries.map { (k, v) -> + node.denormalize().map.entries.map { (k, v) -> kdecoder.decode(StringNode(k, node.pos, node.path, emptyMap()), kType, context).flatMap { kk -> vdecoder.decode(v, vType, context).map { vv -> kk to vv diff --git a/hoplite-vavr/src/test/kotlin/com/sksamuel/hoplite/decoder/vavr/MapDecoderTest.kt b/hoplite-vavr/src/test/kotlin/com/sksamuel/hoplite/decoder/vavr/MapDecoderTest.kt index 9a1490d2..dff66619 100644 --- a/hoplite-vavr/src/test/kotlin/com/sksamuel/hoplite/decoder/vavr/MapDecoderTest.kt +++ b/hoplite-vavr/src/test/kotlin/com/sksamuel/hoplite/decoder/vavr/MapDecoderTest.kt @@ -12,7 +12,7 @@ class MapDecoderTest : FunSpec({ data class Test(val map: Map) val config = ConfigLoader().loadConfigOrThrow("/test_map.yml") - config shouldBe Test(linkedHashMap("key1" to "test1", "key2" to "test2")) + config shouldBe Test(linkedHashMap("key1" to "test1", "key2" to "test2", "key-3" to "test3", "Key4" to "test4")) } }) diff --git a/hoplite-vavr/src/test/resources/test_map.yml b/hoplite-vavr/src/test/resources/test_map.yml index 9c56991c..928fd136 100644 --- a/hoplite-vavr/src/test/resources/test_map.yml +++ b/hoplite-vavr/src/test/resources/test_map.yml @@ -1,3 +1,5 @@ map: key1: "test1" key2: "test2" + key-3: "test3" + Key4: "test4" diff --git a/hoplite-yaml/src/test/kotlin/com/sksamuel/hoplite/yaml/DenormalizedMapKeysTest.kt b/hoplite-yaml/src/test/kotlin/com/sksamuel/hoplite/yaml/DenormalizedMapKeysTest.kt new file mode 100644 index 00000000..077fa410 --- /dev/null +++ b/hoplite-yaml/src/test/kotlin/com/sksamuel/hoplite/yaml/DenormalizedMapKeysTest.kt @@ -0,0 +1,52 @@ +package com.sksamuel.hoplite.yaml + +import com.sksamuel.hoplite.ConfigLoaderBuilder +import com.sksamuel.hoplite.addCommandLineSource +import com.sksamuel.hoplite.addResourceOrFileSource +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class DenormalizedMapKeysTest : FunSpec({ + data class Foo( + val xVal: String = "x" + ) + + data class MapContainer( + val m: Map = emptyMap() + ) + + test("should set denormalized map keys and decode a data class inside a map") { + val config = ConfigLoaderBuilder.default() + .addResourceOrFileSource("/test_data_class_in_map.yaml") + .build() + .loadConfigOrThrow() + + config shouldBe MapContainer( + m = mapOf( + "DC1" to Foo("10"), + "DC2" to Foo("20"), + ) + ) + } + + test("should set denormalized map keys for CLI arguments") { + val config = ConfigLoaderBuilder.default() + .addCommandLineSource( + arrayOf( + "--m.DC1.x-val=10", + "--m.DC2.x-val=20", + ), + prefix = "--", + delimiter = "=" + ) + .build() + .loadConfigOrThrow() + + config shouldBe MapContainer( + m = mapOf( + "DC1" to Foo("10"), + "DC2" to Foo("20"), + ) + ) + } +}) diff --git a/hoplite-yaml/src/test/resources/test_data_class_in_map.yaml b/hoplite-yaml/src/test/resources/test_data_class_in_map.yaml new file mode 100644 index 00000000..052fd12c --- /dev/null +++ b/hoplite-yaml/src/test/resources/test_data_class_in_map.yaml @@ -0,0 +1,5 @@ +m: + DC1: + x-val: 10 + DC2: + x-val: 20