Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix decoding of data classes in maps, and denormalizing map keys consistently #444

Merged
merged 4 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<Map<*, *>>() {
class MapDecoder : NullHandlingDecoder<Map<*, *>> {

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<Map<*, *>> {
override fun safeDecode(node: Node, type: KType, context: DecoderContext): ConfigResult<Map<*, *>> {
require(type.arguments.size == 2)

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

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)
Expand Down
13 changes: 13 additions & 0 deletions hoplite-core/src/main/kotlin/com/sksamuel/hoplite/nodes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 : Node> 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ private fun <T, K> Iterable<T>.toNode(
pos = pos,
path = path,
value = value?.transform(path, sourceKey) ?: Undefined,
sourceKey = this.sourceKey,
)
}
is Array<*> -> ArrayNode(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HikariDataSource>() {
class HikariDataSourceDecoder : Decoder<HikariDataSource> {

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

override fun safeDecodeUnnormalized(node: Node, type: KType, context: DecoderContext): ConfigResult<HikariDataSource> {
override fun decode(node: Node, type: KType, context: DecoderContext): ConfigResult<HikariDataSource> {

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 -> {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class MapDecoder : NullHandlingDecoder<Map<*, *>> {
kdecoder: Decoder<K>,
vdecoder: Decoder<V>,
context: DecoderContext): ConfigResult<Map<*, *>> =
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class MapDecoderTest : FunSpec({
data class Test(val map: Map<String, String>)

val config = ConfigLoader().loadConfigOrThrow<Test>("/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"))
}

})
2 changes: 2 additions & 0 deletions hoplite-vavr/src/test/resources/test_map.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
map:
key1: "test1"
key2: "test2"
key-3: "test3"
Key4: "test4"
Original file line number Diff line number Diff line change
@@ -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<String, Foo> = 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<MapContainer>()

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<MapContainer>()

config shouldBe MapContainer(
m = mapOf(
"DC1" to Foo("10"),
"DC2" to Foo("20"),
)
)
}
})
5 changes: 5 additions & 0 deletions hoplite-yaml/src/test/resources/test_data_class_in_map.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
m:
DC1:
x-val: 10
DC2:
x-val: 20
Loading