Skip to content

Commit

Permalink
Fix decoding of data classes in maps, and denormalizing map keys cons…
Browse files Browse the repository at this point in the history
…istently (#444)

Co-authored-by: Oguzhan Soykan <oguzhansoykan@gmail.com>
  • Loading branch information
rocketraman and osoykan authored Sep 16, 2024
1 parent 6715caf commit 5967b2f
Show file tree
Hide file tree
Showing 12 changed files with 88 additions and 48 deletions.

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

0 comments on commit 5967b2f

Please sign in to comment.