Skip to content

Commit 5967b2f

Browse files
rocketramanosoykan
andauthored
Fix decoding of data classes in maps, and denormalizing map keys consistently (#444)
Co-authored-by: Oguzhan Soykan <oguzhansoykan@gmail.com>
1 parent 6715caf commit 5967b2f

File tree

12 files changed

+88
-48
lines changed

12 files changed

+88
-48
lines changed

hoplite-core/src/main/kotlin/com/sksamuel/hoplite/decoder/AbstractUnnormalizedKeysDecoder.kt

-33
This file was deleted.

hoplite-core/src/main/kotlin/com/sksamuel/hoplite/decoder/MapDecoder.kt

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
package com.sksamuel.hoplite.decoder
22

33
import com.sksamuel.hoplite.ArrayNode
4-
import com.sksamuel.hoplite.fp.invalid
54
import com.sksamuel.hoplite.ConfigFailure
65
import com.sksamuel.hoplite.ConfigResult
76
import com.sksamuel.hoplite.DecoderContext
87
import com.sksamuel.hoplite.MapNode
9-
import com.sksamuel.hoplite.StringNode
108
import com.sksamuel.hoplite.Node
9+
import com.sksamuel.hoplite.StringNode
10+
import com.sksamuel.hoplite.denormalize
1111
import com.sksamuel.hoplite.fp.flatMap
12+
import com.sksamuel.hoplite.fp.invalid
1213
import com.sksamuel.hoplite.fp.sequence
1314
import kotlin.reflect.KType
1415
import kotlin.reflect.full.isSubtypeOf
1516
import kotlin.reflect.full.starProjectedType
1617
import kotlin.reflect.full.withNullability
1718

18-
class MapDecoder : AbstractUnnormalizedKeysDecoder<Map<*, *>>() {
19+
class MapDecoder : NullHandlingDecoder<Map<*, *>> {
1920

2021
override fun supports(type: KType): Boolean =
2122
type.isSubtypeOf(Map::class.starProjectedType) ||
2223
type.isSubtypeOf(Map::class.starProjectedType.withNullability(true))
2324

24-
override fun safeDecodeUnnormalized(node: Node, type: KType, context: DecoderContext): ConfigResult<Map<*, *>> {
25+
override fun safeDecode(node: Node, type: KType, context: DecoderContext): ConfigResult<Map<*, *>> {
2526
require(type.arguments.size == 2)
2627

2728
val kType = type.arguments[0].type!!
@@ -32,7 +33,7 @@ class MapDecoder : AbstractUnnormalizedKeysDecoder<Map<*, *>>() {
3233
vdecoder: Decoder<V>,
3334
context: DecoderContext): ConfigResult<Map<*, *>> {
3435

35-
return node.map.entries.map { (k, v) ->
36+
return node.denormalize().map.entries.map { (k, v) ->
3637
kdecoder.decode(StringNode(k, node.pos, node.path, emptyMap()), kType, context).flatMap { kk ->
3738
vdecoder.decode(v, vType, context).map { vv ->
3839
context.usedPaths.add(v.path)

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

+13
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,19 @@ fun Node.transform(transformer: (Node) -> Node): Node = when (val transformed =
134134
else -> transformed
135135
}
136136

137+
/**
138+
* Denormalizes a node, restoring its original key from the source. This is not recursive -- it only transforms
139+
* the given node, not its children.
140+
*/
141+
fun <T : Node> T.denormalize(): T {
142+
return when (this) {
143+
is MapNode -> copy(map = map.mapKeys { (k, v) ->
144+
(v.sourceKey ?: k).removePrefix("$sourceKey.")
145+
})
146+
else -> this
147+
} as T
148+
}
149+
137150
sealed class ContainerNode : Node
138151

139152
data class MapNode(

hoplite-core/src/main/kotlin/com/sksamuel/hoplite/parsers/loadProps.kt

-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ private fun <T, K> Iterable<T>.toNode(
7474
pos = pos,
7575
path = path,
7676
value = value?.transform(path, sourceKey) ?: Undefined,
77-
sourceKey = this.sourceKey,
7877
)
7978
}
8079
is Array<*> -> ArrayNode(

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class LoadPropsTest : FunSpec({
2727
pos = Pos.SourcePos(source = "source"),
2828
DotPath("a", "b"),
2929
value = Undefined,
30-
sourceKey = null
30+
sourceKey = "a.b"
3131
),
3232
"d" to StringNode("true", pos = Pos.SourcePos(source = "source"), DotPath("a", "d"), sourceKey = "a.d")
3333
),

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class PropsParserTest : StringSpec() {
3232
),
3333
pos = Pos.SourcePos(source = "a.props"),
3434
DotPath("a"),
35-
sourceKey = null
35+
sourceKey = "a"
3636
),
3737
"e" to StringNode(value = "5.5", pos = Pos.SourcePos(source = "a.props"), DotPath("e"), sourceKey = "e")
3838
),

hoplite-hikaricp/src/main/kotlin/com/sksamuel/hoplite/hikari/HikariDataSourceDecoder.kt

+6-5
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,26 @@ import com.sksamuel.hoplite.DecoderContext
66
import com.sksamuel.hoplite.MapNode
77
import com.sksamuel.hoplite.Node
88
import com.sksamuel.hoplite.PrimitiveNode
9-
import com.sksamuel.hoplite.decoder.AbstractUnnormalizedKeysDecoder
9+
import com.sksamuel.hoplite.decoder.Decoder
10+
import com.sksamuel.hoplite.denormalize
1011
import com.sksamuel.hoplite.fp.invalid
1112
import com.sksamuel.hoplite.fp.valid
1213
import com.zaxxer.hikari.HikariConfig
1314
import com.zaxxer.hikari.HikariDataSource
14-
import java.util.Properties
15+
import java.util.*
1516
import kotlin.reflect.KType
1617

17-
class HikariDataSourceDecoder : AbstractUnnormalizedKeysDecoder<HikariDataSource>() {
18+
class HikariDataSourceDecoder : Decoder<HikariDataSource> {
1819

1920
override fun supports(type: KType): Boolean = type.classifier == HikariDataSource::class
2021

21-
override fun safeDecodeUnnormalized(node: Node, type: KType, context: DecoderContext): ConfigResult<HikariDataSource> {
22+
override fun decode(node: Node, type: KType, context: DecoderContext): ConfigResult<HikariDataSource> {
2223

2324
val props = Properties()
2425

2526
fun populate(node: Node, prefix: String) {
2627
when (node) {
27-
is MapNode -> node.map.forEach { (k, v) -> populate(v, if (prefix == "") k else "$prefix.$k") }
28+
is MapNode -> node.denormalize().map.forEach { (k, v) -> populate(v, if (prefix == "") k else "$prefix.$k") }
2829
is PrimitiveNode -> props[prefix] = node.value
2930
else -> {
3031
}

hoplite-vavr/src/main/kotlin/com/sksamuel/hoplite/decoder/vavr/MapDecoder.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class MapDecoder : NullHandlingDecoder<Map<*, *>> {
3232
kdecoder: Decoder<K>,
3333
vdecoder: Decoder<V>,
3434
context: DecoderContext): ConfigResult<Map<*, *>> =
35-
node.map.entries.map { (k, v) ->
35+
node.denormalize().map.entries.map { (k, v) ->
3636
kdecoder.decode(StringNode(k, node.pos, node.path, emptyMap()), kType, context).flatMap { kk ->
3737
vdecoder.decode(v, vType, context).map { vv ->
3838
kk to vv

hoplite-vavr/src/test/kotlin/com/sksamuel/hoplite/decoder/vavr/MapDecoderTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class MapDecoderTest : FunSpec({
1212
data class Test(val map: Map<String, String>)
1313

1414
val config = ConfigLoader().loadConfigOrThrow<Test>("/test_map.yml")
15-
config shouldBe Test(linkedHashMap("key1" to "test1", "key2" to "test2"))
15+
config shouldBe Test(linkedHashMap("key1" to "test1", "key2" to "test2", "key-3" to "test3", "Key4" to "test4"))
1616
}
1717

1818
})
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
map:
22
key1: "test1"
33
key2: "test2"
4+
key-3: "test3"
5+
Key4: "test4"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.sksamuel.hoplite.yaml
2+
3+
import com.sksamuel.hoplite.ConfigLoaderBuilder
4+
import com.sksamuel.hoplite.addCommandLineSource
5+
import com.sksamuel.hoplite.addResourceOrFileSource
6+
import io.kotest.core.spec.style.FunSpec
7+
import io.kotest.matchers.shouldBe
8+
9+
class DenormalizedMapKeysTest : FunSpec({
10+
data class Foo(
11+
val xVal: String = "x"
12+
)
13+
14+
data class MapContainer(
15+
val m: Map<String, Foo> = emptyMap()
16+
)
17+
18+
test("should set denormalized map keys and decode a data class inside a map") {
19+
val config = ConfigLoaderBuilder.default()
20+
.addResourceOrFileSource("/test_data_class_in_map.yaml")
21+
.build()
22+
.loadConfigOrThrow<MapContainer>()
23+
24+
config shouldBe MapContainer(
25+
m = mapOf(
26+
"DC1" to Foo("10"),
27+
"DC2" to Foo("20"),
28+
)
29+
)
30+
}
31+
32+
test("should set denormalized map keys for CLI arguments") {
33+
val config = ConfigLoaderBuilder.default()
34+
.addCommandLineSource(
35+
arrayOf(
36+
"--m.DC1.x-val=10",
37+
"--m.DC2.x-val=20",
38+
),
39+
prefix = "--",
40+
delimiter = "="
41+
)
42+
.build()
43+
.loadConfigOrThrow<MapContainer>()
44+
45+
config shouldBe MapContainer(
46+
m = mapOf(
47+
"DC1" to Foo("10"),
48+
"DC2" to Foo("20"),
49+
)
50+
)
51+
}
52+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
m:
2+
DC1:
3+
x-val: 10
4+
DC2:
5+
x-val: 20

0 commit comments

Comments
 (0)