Skip to content

Commit

Permalink
feat: EnumDecoder: support case-insensitive decoding (#391)
Browse files Browse the repository at this point in the history
Co-authored-by: Sam <sam@sksamuel.com>
  • Loading branch information
monosoul and sksamuel committed Sep 18, 2023
1 parent ccea565 commit 42d1e75
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import com.sksamuel.hoplite.internal.DecodeMode
import com.sksamuel.hoplite.parsers.ParserRegistry
import com.sksamuel.hoplite.preprocessor.Preprocessor
import com.sksamuel.hoplite.report.Print
import com.sksamuel.hoplite.resolver.context.ContextResolverMode
import com.sksamuel.hoplite.resolver.Resolver
import com.sksamuel.hoplite.resolver.context.ContextResolverMode
import com.sksamuel.hoplite.secrets.Obfuscator
import com.sksamuel.hoplite.secrets.PrefixObfuscator
import com.sksamuel.hoplite.secrets.SecretsPolicy
Expand Down Expand Up @@ -44,6 +44,7 @@ class ConfigLoader(
val resolvers: List<Resolver> = emptyList(),
val sealedTypeDiscriminatorField: String? = null,
val allowNullOverride: Boolean = false,
val resolveTypesCaseInsensitive: Boolean = false,
val contextResolverMode: ContextResolverMode = ContextResolverMode.ErrorOnUnresolved,
) {

Expand Down Expand Up @@ -164,6 +165,7 @@ class ConfigLoader(
parserRegistry = parserRegistry,
allowEmptyTree = allowEmptyTree,
allowNullOverride = allowNullOverride,
resolveTypesCaseInsensitive = resolveTypesCaseInsensitive,
cascadeMode = cascadeMode,
preprocessors = preprocessors,
preprocessingIterations = preprocessingIterations,
Expand Down Expand Up @@ -218,6 +220,7 @@ class ConfigLoader(
parserRegistry = parserRegistry,
allowEmptyTree = allowEmptyTree,
allowNullOverride = allowNullOverride,
resolveTypesCaseInsensitive = resolveTypesCaseInsensitive,
cascadeMode = cascadeMode,
preprocessors = preprocessors,
preprocessingIterations = preprocessingIterations,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import com.sksamuel.hoplite.preprocessor.Preprocessor
import com.sksamuel.hoplite.preprocessor.RandomPreprocessor
import com.sksamuel.hoplite.report.Print
import com.sksamuel.hoplite.report.Reporter
import com.sksamuel.hoplite.resolver.Resolver
import com.sksamuel.hoplite.resolver.context.ContextResolverMode
import com.sksamuel.hoplite.resolver.context.EnvVarContextResolver
import com.sksamuel.hoplite.resolver.context.HopliteContextResolver
import com.sksamuel.hoplite.resolver.context.ManifestContextResolver
import com.sksamuel.hoplite.resolver.context.ReferenceContextResolver
import com.sksamuel.hoplite.resolver.Resolver
import com.sksamuel.hoplite.resolver.context.RandomContextResolver
import com.sksamuel.hoplite.resolver.context.ReferenceContextResolver
import com.sksamuel.hoplite.resolver.context.SystemContextResolver
import com.sksamuel.hoplite.resolver.context.SystemPropertyContextResolver
import com.sksamuel.hoplite.secrets.AllStringNodesSecretsPolicy
Expand All @@ -45,6 +45,7 @@ class ConfigLoaderBuilder private constructor() {
private var allowEmptyConfigFiles = false
private var allowNullOverride = false
private var allowUnresolvedSubstitutions = false
private var resolveTypesCaseInsensitive = false
private var sealedTypeDiscriminatorField: String? = null
private var contextResolverMode = ContextResolverMode.ErrorOnUnresolved

Expand Down Expand Up @@ -298,6 +299,13 @@ class ConfigLoaderBuilder private constructor() {
allowUnresolvedSubstitutions = true
}

/**
* When enabled, makes type/enum name resolution case-insensitive
*/
fun withResolveTypesCaseInsensitive(): ConfigLoaderBuilder = apply {
resolveTypesCaseInsensitive = true
}

fun withContextResolverMode(mode: ContextResolverMode) = apply {
contextResolverMode = mode
}
Expand Down Expand Up @@ -371,6 +379,7 @@ class ConfigLoaderBuilder private constructor() {
useReport = useReport,
allowEmptyTree = allowEmptyConfigFiles,
allowNullOverride = allowNullOverride,
resolveTypesCaseInsensitive = resolveTypesCaseInsensitive,
allowUnresolvedSubstitutions = allowUnresolvedSubstitutions,
preprocessingIterations = preprocessingIterations,
cascadeMode = cascadeMode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import com.sksamuel.hoplite.decoder.DecoderRegistry
import com.sksamuel.hoplite.decoder.DotPath
import com.sksamuel.hoplite.env.Environment
import com.sksamuel.hoplite.fp.Validated
import com.sksamuel.hoplite.resolver.context.ContextResolverMode
import com.sksamuel.hoplite.resolver.Resolving
import com.sksamuel.hoplite.resolver.context.ContextResolverMode
import kotlin.reflect.KParameter
import kotlin.reflect.KType

Expand All @@ -27,7 +27,7 @@ data class DecoderContext(
// this tracks the types that a node was marshalled into
val used: MutableMap<DotPath, NodeState> = mutableMapOf(),
val metadata: MutableMap<String, Any?> = mutableMapOf(),
val config: DecoderConfig = DecoderConfig(false),
val config: DecoderConfig = DecoderConfig(flattenArraysToString = false, resolveTypesCaseInsensitive = false),
val environment: Environment? = null,
val resolvers: Resolving = Resolving.empty,
// determines if we should error when a context resolver cannot find a substitution
Expand Down Expand Up @@ -72,5 +72,6 @@ data class NodeState(
)

data class DecoderConfig(
val flattenArraysToString: Boolean
val flattenArraysToString: Boolean,
val resolveTypesCaseInsensitive: Boolean,
)
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package com.sksamuel.hoplite.decoder

import com.sksamuel.hoplite.fp.invalid
import com.sksamuel.hoplite.fp.valid
import com.sksamuel.hoplite.BooleanNode
import com.sksamuel.hoplite.ConfigFailure
import com.sksamuel.hoplite.ConfigResult
import com.sksamuel.hoplite.DecoderContext
import com.sksamuel.hoplite.DoubleNode
import com.sksamuel.hoplite.LongNode
import com.sksamuel.hoplite.StringNode
import com.sksamuel.hoplite.Node
import com.sksamuel.hoplite.StringNode
import com.sksamuel.hoplite.fp.invalid
import com.sksamuel.hoplite.fp.valid
import kotlin.reflect.KClass
import kotlin.reflect.KType

Expand All @@ -26,7 +26,9 @@ class EnumDecoder<T : Any> : NullHandlingDecoder<T> {
val klass = type.classifier as KClass<*>

fun decode(value: String): ConfigResult<T> {
val t = klass.java.enumConstants.find { it.toString() == value }
val t = klass.java.enumConstants.find {
it.toString().contentEquals(other = value, ignoreCase = context.config.resolveTypesCaseInsensitive)
}
return if (t == null)
ConfigFailure.InvalidEnumConstant(node, type, value).invalid()
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import com.sksamuel.hoplite.parsers.ParserRegistry
import com.sksamuel.hoplite.preprocessor.Preprocessor
import com.sksamuel.hoplite.report.Print
import com.sksamuel.hoplite.report.Reporter
import com.sksamuel.hoplite.resolver.context.ContextResolverMode
import com.sksamuel.hoplite.resolver.Resolver
import com.sksamuel.hoplite.resolver.Resolving
import com.sksamuel.hoplite.resolver.context.ContextResolverMode
import com.sksamuel.hoplite.secrets.Obfuscator
import com.sksamuel.hoplite.secrets.SecretsPolicy
import kotlin.reflect.KClass
Expand All @@ -37,6 +37,7 @@ class ConfigParser(
private val decoderRegistry: DecoderRegistry,
private val paramMappers: List<ParameterMapper>,
private val flattenArraysToString: Boolean,
private val resolveTypesCaseInsensitive: Boolean,
private val allowUnresolvedSubstitutions: Boolean,
private val secretsPolicy: SecretsPolicy?,
private val decodeMode: DecodeMode,
Expand All @@ -57,7 +58,7 @@ class ConfigParser(
return DecoderContext(
decoders = decoderRegistry,
paramMappers = paramMappers,
config = DecoderConfig(flattenArraysToString),
config = DecoderConfig(flattenArraysToString, resolveTypesCaseInsensitive),
environment = environment,
resolvers = Resolving(resolvers, root),
sealedTypeDiscriminatorField = sealedTypeDiscriminatorField,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.sksamuel.hoplite.decoder

import com.sksamuel.hoplite.ConfigFailure
import com.sksamuel.hoplite.DecoderConfig
import com.sksamuel.hoplite.DecoderContext
import com.sksamuel.hoplite.Pos
import com.sksamuel.hoplite.PrimitiveNode
import com.sksamuel.hoplite.StringNode
import com.sksamuel.hoplite.defaultParamMappers
import com.sksamuel.hoplite.fp.Validated
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf
import kotlin.reflect.full.createType

class EnumDecoderTest : BehaviorSpec({

given("a value matching enum case") {
val node = StringNode("ONE", Pos.NoPos, DotPath.root)

`when`("using case sensitive decoding") {
val actual = EnumDecoder<TestEnum>().decode(node, ignoreCase = false)

then("should decode it") {
actual.shouldBeInstanceOf<Validated.Valid<TestEnum>>()
.value shouldBe TestEnum.ONE
}
}

`when`("using case insensitive decoding") {
val actual = EnumDecoder<TestEnum>().decode(node, ignoreCase = true)

then("should decode it") {
actual.shouldBeInstanceOf<Validated.Valid<TestEnum>>()
.value shouldBe TestEnum.ONE
}
}
}

given("a value not matching enum case") {
val node = StringNode("Two", Pos.NoPos, DotPath.root)

`when`("using case sensitive decoding") {
val actual = EnumDecoder<TestEnum>().decode(node, ignoreCase = false)

then("should return error") {
actual.shouldBeInstanceOf<Validated.Invalid<ConfigFailure>>()
}
}

`when`("using case insensitive decoding") {
val actual = EnumDecoder<TestEnum>().decode(node, ignoreCase = true)

then("should decode it") {
actual.shouldBeInstanceOf<Validated.Valid<TestEnum>>()
.value shouldBe TestEnum.TWO
}
}
}
}) {
private companion object {
fun <T : Any> EnumDecoder<T>.decode(node: PrimitiveNode, ignoreCase: Boolean) = decode(
node,
TestEnum::class.createType(),
DecoderContext(
decoders = defaultDecoderRegistry(),
paramMappers = defaultParamMappers(),
config = DecoderConfig(flattenArraysToString = false, resolveTypesCaseInsensitive = ignoreCase)
)
)

enum class TestEnum {
ONE, TWO
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.sksamuel.hoplite.decoder

import com.sksamuel.hoplite.ConfigFailure
import com.sksamuel.hoplite.ConfigLoaderBuilder
import com.sksamuel.hoplite.fp.Validated
import io.kotest.assertions.asClue
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf

class EnumDecoderTest : BehaviorSpec({

data class Test(val a: Wine, val b: Wine)

given("yml file with invalid enum name case") {
val yaml = "/enums.yml"

`when`("using case sensitive decoding") {
val config = ConfigLoaderBuilder.default()
.build()
.loadConfig<Test>(yaml)

then("should return an error") {
config.shouldBeInstanceOf<Validated.Invalid<ConfigFailure>>()
.error.shouldBeInstanceOf<ConfigFailure.DataClassFieldErrors>()
.errors.list.shouldHaveSize(1)
.first().shouldBeInstanceOf<ConfigFailure.ParamFailure>()
.error.shouldBeInstanceOf<ConfigFailure.InvalidEnumConstant>()
}
}

`when`("using case insensitive decoding") {
val config = ConfigLoaderBuilder.default()
.withResolveTypesCaseInsensitive()
.build()
.loadConfig<Test>(yaml)

then("should load the config") {
config.shouldBeInstanceOf<Validated.Valid<Test>>()
.value.asClue {
it.a shouldBe Wine.Malbec
it.b shouldBe Wine.Merlot
}
}
}
}
})
2 changes: 2 additions & 0 deletions hoplite-yaml/src/test/resources/enums.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
a: Malbec
b: MERLOT

0 comments on commit 42d1e75

Please sign in to comment.