Skip to content

Commit

Permalink
Allow two-step parsing and decoding/binding
Browse files Browse the repository at this point in the history
With this approach, if there are multiple prefixes and config classes to
be bound, we parse the configuration only a single time, and decode
multiple times.
  • Loading branch information
rocketraman committed Apr 4, 2024
1 parent 7bcd7ab commit f7c7456
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 72 deletions.
89 changes: 50 additions & 39 deletions hoplite-core/src/main/kotlin/com/sksamuel/hoplite/ConfigLoader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ 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
import com.sksamuel.hoplite.secrets.StrictObfuscator
import kotlin.reflect.KClass

class ConfigException(msg: String, val t: Throwable? = null) : java.lang.RuntimeException(msg, t)
Expand Down Expand Up @@ -89,6 +88,8 @@ class ConfigLoader(
}
}

private lateinit var configParser: ConfigParser

/**
* Attempts to load config from the specified resources either on the class path or as files on the
* file system, and returns the successfully created instance A, or throws an error.
Expand Down Expand Up @@ -178,18 +179,12 @@ class ConfigLoader(
*/
inline fun <reified A : Any> loadConfig(): ConfigResult<A> = loadConfig(A::class, emptyList(), emptyList(), null)

// This is where the actual processing takes place for marshalled config.
// All other loadConfig or loadConfigOrThrow methods ultimately end up in this method.
@PublishedApi
internal fun <A : Any> loadConfig(
kclass: KClass<A>,
configSources: List<ConfigSource>,
resourceOrFiles: List<String>,
prefix: String?,
classpathResourceLoader: ClasspathResourceLoader = Companion::class.java.toClasspathResourceLoader()
): ConfigResult<A> {
require(kclass.isData) { "Can only decode into data classes [was ${kclass}]" }
return ConfigParser(
fun withSources(
resourceOrFiles: List<String> = emptyList(),
configSources: List<ConfigSource> = emptyList(),
classpathResourceLoader: ClasspathResourceLoader = ConfigLoader::class.java.toClasspathResourceLoader()
): ConfigLoader {
configParser = ConfigParser(
classpathResourceLoader = classpathResourceLoader,
parserRegistry = parserRegistry,
allowEmptyTree = allowEmptyTree,
Expand All @@ -198,7 +193,6 @@ class ConfigLoader(
preprocessors = preprocessors,
preprocessingIterations = preprocessingIterations,
nodeTransformers = nodeTransformers,
prefix = prefix,
resolvers = resolvers,
decoderRegistry = decoderRegistry,
paramMappers = paramMappers,
Expand All @@ -213,7 +207,43 @@ class ConfigLoader(
environment = environment,
sealedTypeDiscriminatorField = sealedTypeDiscriminatorField,
contextResolverMode = contextResolverMode,
).decode(kclass, environment, resourceOrFiles, propertySources, configSources)
resourceOrFiles = resourceOrFiles,
propertySources = propertySources,
configSources = configSources,
)
return this
}

// This is where the actual processing takes place for marshalled config.
// All other loadConfig or loadConfigOrThrow methods ultimately end up in this method.
fun <A : Any> bindConfig(
parser: ConfigParser,
kclass: KClass<A>,
prefix: String?,
): ConfigResult<A> {
require(kclass.isData) { "Can only decode into data classes [was ${kclass}]" }
return parser.decode(kclass, environment, prefix)
}

// This is where the actual processing takes place for marshalled config.
// All other loadConfig or loadConfigOrThrow methods ultimately end up in this method.
@PublishedApi
internal fun <A : Any> loadConfig(
kclass: KClass<A>,
configSources: List<ConfigSource>,
resourceOrFiles: List<String>,
prefix: String?,
classpathResourceLoader: ClasspathResourceLoader = Companion::class.java.toClasspathResourceLoader()
): ConfigResult<A> {
require(kclass.isData) { "Can only decode into data classes [was ${kclass}]" }
if (!::configParser.isInitialized) {
withSources(
resourceOrFiles = resourceOrFiles,
configSources = configSources,
classpathResourceLoader = classpathResourceLoader,
)
}
return configParser.decode(kclass, environment, prefix)
}

/**
Expand Down Expand Up @@ -246,31 +276,12 @@ class ConfigLoader(
configSources: List<ConfigSource> = emptyList(),
classpathResourceLoader: ClasspathResourceLoader = ConfigLoader::class.java.toClasspathResourceLoader()
): ConfigResult<Node> {
return ConfigParser(
withSources(
resourceOrFiles = resourceOrFiles,
configSources = configSources,
classpathResourceLoader = classpathResourceLoader,
parserRegistry = parserRegistry,
allowEmptyTree = allowEmptyTree,
allowNullOverride = allowNullOverride,
cascadeMode = cascadeMode,
preprocessors = preprocessors,
preprocessingIterations = preprocessingIterations,
nodeTransformers = nodeTransformers,
prefix = null,
resolvers = resolvers,
decoderRegistry = decoderRegistry,
paramMappers = paramMappers, // not needed to load nodes
flattenArraysToString = false,
resolveTypesCaseInsensitive = resolveTypesCaseInsensitive, // not used when loading nodes
allowUnresolvedSubstitutions = allowUnresolvedSubstitutions, // not used when loading nodes
secretsPolicy = null, // not used when loading nodes
decodeMode = DecodeMode.Lenient, // not used when loading nodes
useReport = false, // not used when loading nodes
obfuscator = StrictObfuscator("*"),
reportPrintFn = reportPrintFn ?: { },
environment = environment,
sealedTypeDiscriminatorField = sealedTypeDiscriminatorField,
contextResolverMode = contextResolverMode,
).load(resourceOrFiles, propertySources, configSources)
)
return configParser.load()
}

@PublishedApi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ class ConfigParser(
preprocessors: List<Preprocessor>,
preprocessingIterations: Int,
nodeTransformers: List<NodeTransformer>,
private val prefix: String?,
private val resolvers: List<Resolver>,
private val decoderRegistry: DecoderRegistry,
private val paramMappers: List<ParameterMapper>,
Expand All @@ -51,12 +50,21 @@ class ConfigParser(
private val environment: Environment?,
private val sealedTypeDiscriminatorField: String?,
private val contextResolverMode: ContextResolverMode,
private val resourceOrFiles: List<String>,
private val propertySources: List<PropertySource>,
private val configSources: List<ConfigSource>,
) {

private val loader = PropertySourceLoader(nodeTransformers, classpathResourceLoader, parserRegistry, allowEmptyTree)
private val cascader = Cascader(cascadeMode, allowEmptyTree, allowNullOverride)
private val preprocessing = Preprocessing(preprocessors, preprocessingIterations)
private val decoding = Decoding(decoderRegistry, secretsPolicy)
private lateinit var configResult: ConfigResult<Node>
private lateinit var context: DecoderContext

init {
loadResultAndContext()
}

private fun context(root: Node): DecoderContext {
return DecoderContext(
Expand All @@ -73,48 +81,53 @@ class ConfigParser(
fun <A : Any> decode(
kclass: KClass<A>,
environment: Environment?,
resourceOrFiles: List<String>,
propertySources: List<PropertySource>,
configSources: List<ConfigSource>,
prefix: String? = null,
): ConfigResult<A> {

if (decoderRegistry.size == 0)
return ConfigFailure.EmptyDecoderRegistry.invalid()

return loader.loadNodes(propertySources, configSources, resourceOrFiles).flatMap { nodes ->
cascader.cascade(nodes).flatMap { node ->
val context = context(node)
preprocessing.preprocess(node, context).flatMap { preprocessed ->
val normalizedPrefix = if (prefix == null) null else PathNormalizer.normalizePathElement(prefix)
check(preprocessed.let { if (normalizedPrefix == null) it else it.atPath(normalizedPrefix) }).flatMap {
val decoded = decoding.decode(kclass, it, decodeMode, context)
val state = createDecodingState(it, context, secretsPolicy)
if (configResult.isInvalid()) {
return configResult.getInvalidUnsafe().invalid()
}

// always do report regardless of decoder result
if (useReport) {
Reporter(reportPrintFn, obfuscator, environment)
.printReport(propertySources, state, context.reporter.getReport())
}
return configResult
.map {
if (prefix == null) it else it.atPath(PathNormalizer.normalizePathElement(prefix))
}
.flatMap {
val decoded = decoding.decode(kclass, it, decodeMode, context)
val state = createDecodingState(it, context, secretsPolicy)

decoded
}
// always do report regardless of decoder result
if (useReport) {
Reporter(reportPrintFn, obfuscator, environment, prefix)
.printReport(propertySources, state, context.reporter.getReport())
}

decoded
}
}
}

fun load(
resourceOrFiles: List<String>,
propertySources: List<PropertySource>,
configSources: List<ConfigSource>,
): ConfigResult<Node> {
return loader.loadNodes(propertySources, configSources, resourceOrFiles).flatMap { nodes ->
fun load(): ConfigResult<Node> = configResult

private fun loadResultAndContext() {
if (decoderRegistry.size == 0) {
configResult = ConfigFailure.EmptyDecoderRegistry.invalid()
return
}

loader.loadNodes(propertySources, configSources, resourceOrFiles).flatMap { nodes ->
cascader.cascade(nodes).flatMap { node ->
val context = context(node)
preprocessing.preprocess(node, context).flatMap { preprocessed ->
if (allowUnresolvedSubstitutions) preprocessed.valid() else UnresolvedSubstitutionChecker.process(preprocessed)
check(preprocessed)
}.map {
it to context
}
}
}.onSuccess { result ->
configResult = result.first.valid()
context = result.second
}.onFailure { result ->
configResult = result.invalid()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@ class ReporterBuilder {
@Deprecated("Specify secretsPolicy through ConfigBuilderLoader", level = DeprecationLevel.ERROR)
fun withSecretsPolicy(secretsPolicy: SecretsPolicy): ReporterBuilder = TODO("Unsupported")

fun build(): Reporter = Reporter(print, obfuscator, null)
fun build(): Reporter = Reporter(print, obfuscator, null, null)
}

typealias Print = (String) -> Unit

class Reporter(
private val print: Print,
private val obfuscator: Obfuscator,
private val environment: Environment?
private val environment: Environment?,
private val prefix: String?
) {

object Titles {
Expand All @@ -60,7 +61,7 @@ class Reporter(

val r = buildString {
appendLine()
appendLine("--Start Hoplite Config Report---")
appendLine("--Start Hoplite Config Report${if (prefix != null) " @ Prefix $prefix" else ""}---")
appendLine()
environment?.let { appendLine("Environment: ${it.name}") }
appendLine()
Expand Down

0 comments on commit f7c7456

Please sign in to comment.