Skip to content

Commit

Permalink
Merge branch 'master' into kotlin-aws-sdk
Browse files Browse the repository at this point in the history
  • Loading branch information
sksamuel authored Nov 27, 2024
2 parents e32c990 + 4a760fa commit 404775d
Show file tree
Hide file tree
Showing 13 changed files with 116 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.sksamuel.hoplite.env.Environment
import com.sksamuel.hoplite.fp.Validated
import com.sksamuel.hoplite.resolver.Resolving
import com.sksamuel.hoplite.resolver.context.ContextResolverMode
import com.sksamuel.hoplite.transformer.NodeTransformer
import kotlin.reflect.KParameter
import kotlin.reflect.KType

Expand All @@ -21,6 +22,7 @@ import kotlin.reflect.KType
data class DecoderContext(
val decoders: DecoderRegistry,
val paramMappers: List<ParameterMapper>,
val nodeTransformers: List<NodeTransformer>,
val reporter: Reporter = Reporter(),
// these are the dot paths for every config value - overrided or not, that was used
val usedPaths: MutableSet<DotPath> = mutableSetOf(),
Expand Down Expand Up @@ -59,7 +61,7 @@ data class DecoderContext(
fun report(section: String, row: Map<String, Any?>) = reporter.report(section, row)

companion object {
val zero = DecoderContext(DecoderRegistry.zero, emptyList(), resolvers = Resolving(emptyList(), Undefined))
val zero = DecoderContext(DecoderRegistry.zero, emptyList(), emptyList(), resolvers = Resolving(emptyList(), Undefined))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,27 @@ class DataClassDecoder : NullHandlingDecoder<Any> {
// use parameter mappers to retrieve alternative names, then try each one in turn
// until we find one that is defined
val names = context.paramMappers.flatMap { it.map(param, constructor, kclass) }

fun nameLookup(name: String): Node {
var atKey = node.atKey(name)
// check if the node has a key matching a transformed path element
if (atKey is Undefined) atKey = node.atKey(
context.nodeTransformers.fold(name) { n, transformer -> transformer.transformPathElement(n) }
)
// also check the source key, as parameter mappers may be referring to the source name
if (atKey is Undefined) atKey = node.atSourceKey(name)
return atKey
}

// every alternative name is marked as used so strict can detect that overrides were 'used' too.
names.forEach {
context.usedPaths.add(node.atKey(it).path)
context.usedPaths.add(nameLookup(it).path)
}

val n = names.fold<String, Node>(Undefined) { n, name ->
if (n.isDefined) n else {
usedName = name
node.atKey(name)
nameLookup(name)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class ConfigParser(
return DecoderContext(
decoders = decoderRegistry,
paramMappers = paramMappers,
nodeTransformers = nodeTransformers,
config = DecoderConfig(flattenArraysToString, resolveTypesCaseInsensitive),
environment = environment,
resolvers = Resolving(resolvers, root),
Expand Down Expand Up @@ -139,7 +140,7 @@ class ConfigParser(

private fun Node.prefixedNode(prefix: String?) = when {
prefix == null -> this
nodeTransformers.contains(PathNormalizer) -> atPath(PathNormalizer.normalizePathElement(prefix))
nodeTransformers.contains(PathNormalizer) -> atPath(PathNormalizer.transformPathElement(prefix))
else -> atPath(prefix)
}
}
12 changes: 12 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 @@ -28,6 +28,14 @@ sealed interface Node {
*/
fun atKey(key: String): Node

/**
* Returns the [PrimitiveNode] at the given source key.
* If this node is not a [MapNode] or the node contained at the
* given key is not a primitive, or the node does not contain
* the key at all, then this will return [Undefined].
*/
fun atSourceKey(key: String): Node

operator fun get(key: String): Node = atKey(key)

/**
Expand Down Expand Up @@ -159,6 +167,7 @@ data class MapNode(
) : ContainerNode() {
override val simpleName: String = "Map"
override fun atKey(key: String): Node = map[key] ?: Undefined
override fun atSourceKey(key: String): Node = map.values.firstOrNull { it.sourceKey == key } ?: Undefined
override fun atIndex(index: Int): Node = Undefined
override val size: Int = map.size
}
Expand All @@ -172,12 +181,14 @@ data class ArrayNode(
) : ContainerNode() {
override val simpleName: String = "List"
override fun atKey(key: String): Node = Undefined
override fun atSourceKey(key: String): Node = Undefined
override fun atIndex(index: Int): Node = elements.getOrElse(index) { Undefined }
override val size: Int = elements.size
}

sealed class PrimitiveNode : Node {
override fun atKey(key: String): Node = Undefined
override fun atSourceKey(key: String): Node = Undefined
override fun atIndex(index: Int): Node = Undefined
override val size: Int = 0
abstract val value: Any?
Expand Down Expand Up @@ -241,6 +252,7 @@ object Undefined : Node {
override val path = DotPath.root
override val sourceKey: String? = null
override fun atKey(key: String): Node = this
override fun atSourceKey(key: String): Node = this
override fun atIndex(index: Int): Node = this
override val size: Int = 0
override val meta: Map<String, Any?> = emptyMap()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import com.sksamuel.hoplite.PropertySource
import com.sksamuel.hoplite.PropertySourceContext
import com.sksamuel.hoplite.fp.valid
import java.io.InputStream
import java.io.InputStreamReader
import java.nio.charset.Charset
import java.util.Properties

class PropsParser : Parser {

override fun load(input: InputStream, source: String): Node {
val props = Properties()
props.load(input)
props.load(InputStreamReader(input, Charset.forName("UTF-8")))
return props.toNode(source)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ import com.sksamuel.hoplite.*
* be applied at configuration loading time.
*/
interface NodeTransformer {
/** Used for one of path element transformations equivalent to the node transformation. */
fun transformPathElement(element: String): String

fun transform(node: Node, sealedTypeDiscriminatorField: String?): Node
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import com.sksamuel.hoplite.*
* It does NOT normalize the sealed type discriminator field for map nodes.
*/
object PathNormalizer : NodeTransformer {
fun normalizePathElement(element: String): String = element
override fun transformPathElement(element: String): String = element
.replace("-", "")
.replace("_", "")
.lowercase()
Expand All @@ -31,7 +31,7 @@ object PathNormalizer : NodeTransformer {
val normalizedPathNode = it.withPath(
it.path.copy(keys = it.path.keys.map { key ->
if (it is MapNode) normalizePathElementExceptDiscriminator(key, sealedTypeDiscriminatorField)
else normalizePathElement(key)
else transformPathElement(key)
})
)
when (normalizedPathNode){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,22 @@ class EnvironmentVariablesPropertySourceTest : FunSpec({
)
}

test("env var source should respect config aliases that need to be normalized to match") {
data class TestConfig(@ConfigAlias("fooBar") val bazBar: String)

val config = ConfigLoader {
addPropertySource(
EnvironmentVariablesPropertySource(
useUnderscoresAsSeparator = false,
allowUppercaseNames = true,
environmentVariableMap = {
mapOf("FOO_BAR" to "fooValue")
},
)
)
}.loadConfigOrThrow<TestConfig>()

config shouldBe TestConfig("fooValue")
}

})
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.sksamuel.hoplite.MapNode
import com.sksamuel.hoplite.NullNode
import com.sksamuel.hoplite.Pos
import com.sksamuel.hoplite.StringNode
import com.sksamuel.hoplite.defaultNodeTransformers
import com.sksamuel.hoplite.defaultParamMappers
import com.sksamuel.hoplite.fp.valid
import io.kotest.core.spec.style.StringSpec
Expand Down Expand Up @@ -35,7 +36,7 @@ class DataClassDecoderTest : StringSpec() {
DataClassDecoder().decode(
node,
Foo::class.createType(),
DecoderContext(defaultDecoderRegistry(), defaultParamMappers())
DecoderContext(defaultDecoderRegistry(), defaultParamMappers(), defaultNodeTransformers())
) shouldBe Foo("hello", 123, true).valid()
}

Expand All @@ -54,7 +55,7 @@ class DataClassDecoderTest : StringSpec() {
DataClassDecoder().decode(
node,
Foo::class.createType(),
DecoderContext(defaultDecoderRegistry(), defaultParamMappers())
DecoderContext(defaultDecoderRegistry(), defaultParamMappers(), defaultNodeTransformers())
) shouldBe Foo(null, null, null).valid()
}

Expand All @@ -73,7 +74,7 @@ class DataClassDecoderTest : StringSpec() {
DataClassDecoder().decode(
node,
Foo::class.createType(),
DecoderContext(defaultDecoderRegistry(), defaultParamMappers())
DecoderContext(defaultDecoderRegistry(), defaultParamMappers(), defaultNodeTransformers())
) shouldBe Foo("hello", 123, true).valid()
}

Expand All @@ -98,7 +99,7 @@ class DataClassDecoderTest : StringSpec() {
)
DataClassDecoder().decode(node,
Foo::class.createType(),
DecoderContext(defaultDecoderRegistry(), defaultParamMappers())
DecoderContext(defaultDecoderRegistry(), defaultParamMappers(), defaultNodeTransformers())
) shouldBe Foo(Year.of(1991), expectedDate, YearMonth.parse("2007-12"), expectedSqlTimestamp).valid()
}

Expand All @@ -117,7 +118,7 @@ class DataClassDecoderTest : StringSpec() {
DataClassDecoder().decode(
node,
Foo::class.createType(),
DecoderContext(defaultDecoderRegistry(), defaultParamMappers())
DecoderContext(defaultDecoderRegistry(), defaultParamMappers(), defaultNodeTransformers())
) shouldBe Foo(IntRange(1, 4), LongRange(50, 60), CharRange('d', 'g')).valid()
}

Expand All @@ -135,7 +136,7 @@ class DataClassDecoderTest : StringSpec() {
DataClassDecoder().decode(
node,
Foo::class.createType(),
DecoderContext(defaultDecoderRegistry(), defaultParamMappers())
DecoderContext(defaultDecoderRegistry(), defaultParamMappers(), defaultNodeTransformers())
) shouldBe Foo("value", "default b", false).valid()
}

Expand All @@ -147,7 +148,7 @@ class DataClassDecoderTest : StringSpec() {
DataClassDecoder().decode(
node,
Foo::class.createType(),
DecoderContext(defaultDecoderRegistry(), defaultParamMappers())
DecoderContext(defaultDecoderRegistry(), defaultParamMappers(), defaultNodeTransformers())
) shouldBe Foo(FooEnum.SECOND).valid()
}

Expand All @@ -161,7 +162,7 @@ class DataClassDecoderTest : StringSpec() {
DataClassDecoder().decode(
node,
Foo::class.createType(),
DecoderContext(defaultDecoderRegistry(), defaultParamMappers())
DecoderContext(defaultDecoderRegistry(), defaultParamMappers(), defaultNodeTransformers())
) shouldBe Foo(FooEnum.THIRD).valid()
}

Expand All @@ -183,7 +184,7 @@ class DataClassDecoderTest : StringSpec() {
DataClassDecoder().decode(
node,
Foo::class.createType(),
DecoderContext(defaultDecoderRegistry(), defaultParamMappers())
DecoderContext(defaultDecoderRegistry(), defaultParamMappers(), defaultNodeTransformers())
) shouldBe Foo(FooEnum.FIRST, "MultiParamCallExpected", false).valid()
}

Expand All @@ -205,7 +206,7 @@ class DataClassDecoderTest : StringSpec() {
DataClassDecoder().decode(
node,
Foo::class.createType(),
DecoderContext(defaultDecoderRegistry(), defaultParamMappers())
DecoderContext(defaultDecoderRegistry(), defaultParamMappers(), defaultNodeTransformers())
) shouldBe Foo(FooEnum.THIRD, true).valid()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.sksamuel.hoplite.DecoderContext
import com.sksamuel.hoplite.Pos
import com.sksamuel.hoplite.PrimitiveNode
import com.sksamuel.hoplite.StringNode
import com.sksamuel.hoplite.defaultNodeTransformers
import com.sksamuel.hoplite.defaultParamMappers
import com.sksamuel.hoplite.fp.Validated
import io.kotest.core.spec.style.BehaviorSpec
Expand Down Expand Up @@ -65,6 +66,7 @@ class EnumDecoderTest : BehaviorSpec({
DecoderContext(
decoders = defaultDecoderRegistry(),
paramMappers = defaultParamMappers(),
nodeTransformers = defaultNodeTransformers(),
config = DecoderConfig(flattenArraysToString = false, resolveTypesCaseInsensitive = ignoreCase)
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.sksamuel.hoplite.toml

import com.sksamuel.hoplite.ConfigAlias
import com.sksamuel.hoplite.ConfigLoader
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

class ConfigAliasNormalizationTest : FunSpec({
data class AliasConfig(
@ConfigAlias("fooBarAlias") val fooBar: String,
)

test("parsers should support @ConfigAlias with field normalization") {
val config = ConfigLoader().loadConfigOrThrow<AliasConfig>("/alias_normalization.toml")
config.fooBar shouldBe "Tom Preston-Werner"
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,35 @@ import com.sksamuel.hoplite.ConfigLoader
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

data class A(
@ConfigAlias("b") @ConfigAlias("zah") val bb: String,
val c: Int
)
class ConfigAliasTest : FunSpec({
data class A(
@ConfigAlias("b") @ConfigAlias("zah") val bb: String,
val c: Int
)

data class D(
val e: String,
@ConfigAlias("f") val ff: Boolean
)
data class D(
val e: String,
@ConfigAlias("f") val ff: Boolean
)

data class AliasConfig(
val a: A,
@ConfigAlias("d") val dd: D
)
data class AliasConfig(
val a: A,
@ConfigAlias("d") val dd: D
)

class ConfigAliasTest : FunSpec() {
init {

test("parsers should support @ConfigAlias") {
val config = ConfigLoader().loadConfigOrThrow<AliasConfig>("/alias.toml")
config.a.bb shouldBe "Tom Preston-Werner"
config.a.c shouldBe 5000
config.dd.e shouldBe "192.168.1.1"
config.dd.ff shouldBe true
}
test("parsers should support @ConfigAlias") {
val config = ConfigLoader().loadConfigOrThrow<AliasConfig>("/alias.toml")
config.a.bb shouldBe "Tom Preston-Werner"
config.a.c shouldBe 5000
config.dd.e shouldBe "192.168.1.1"
config.dd.ff shouldBe true
}

test("parsers should support multiple @ConfigAlias") {
val config = ConfigLoader().loadConfigOrThrow<AliasConfig>("/repeated_alias.toml")
config.a.bb shouldBe "Tom Preston-Werner"
config.a.c shouldBe 5000
config.dd.e shouldBe "192.168.1.1"
config.dd.ff shouldBe true
}
test("parsers should support multiple @ConfigAlias") {
val config = ConfigLoader().loadConfigOrThrow<AliasConfig>("/repeated_alias.toml")
config.a.bb shouldBe "Tom Preston-Werner"
config.a.c shouldBe 5000
config.dd.e shouldBe "192.168.1.1"
config.dd.ff shouldBe true
}
}
})
2 changes: 2 additions & 0 deletions hoplite-toml/src/test/resources/alias_normalization.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This is a TOML document.
fooBarAlias = "Tom Preston-Werner"

0 comments on commit 404775d

Please sign in to comment.