From cad34b4dbd5af0e1fac32b7d5e617cdb5aed633e Mon Sep 17 00:00:00 2001 From: Frederic Kneier Date: Wed, 29 Sep 2021 19:58:53 +0200 Subject: [PATCH] Adds to define if defaults are applied to config loader. (#223) * Adds command line property source. * Adds map property source and tests for map and command line. * Adds to define if defaults are applied to config loader. Co-authored-by: Frederic Kneier --- .../com/sksamuel/hoplite/ConfigLoader.kt | 84 ++++++++++------- .../hoplite/ConfigLoaderBuilderExtensions.kt | 90 +++++++++++++++++++ .../com/sksamuel/hoplite/PropertySource.kt | 11 +++ .../hoplite/WithoutDefaultsRegistryTest.kt | 54 +++++++++++ 4 files changed, 208 insertions(+), 31 deletions(-) create mode 100644 hoplite-core/src/main/kotlin/com/sksamuel/hoplite/ConfigLoaderBuilderExtensions.kt create mode 100644 hoplite-core/src/test/kotlin/com/sksamuel/hoplite/WithoutDefaultsRegistryTest.kt diff --git a/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/ConfigLoader.kt b/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/ConfigLoader.kt index 350e7b32..218a5a84 100644 --- a/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/ConfigLoader.kt +++ b/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/ConfigLoader.kt @@ -2,12 +2,16 @@ package com.sksamuel.hoplite -import com.sksamuel.hoplite.decoder.* -import com.sksamuel.hoplite.fp.invalid +import com.sksamuel.hoplite.decoder.Decoder +import com.sksamuel.hoplite.decoder.DecoderRegistry +import com.sksamuel.hoplite.decoder.defaultDecoderRegistry import com.sksamuel.hoplite.fp.flatMap import com.sksamuel.hoplite.fp.getOrElse +import com.sksamuel.hoplite.fp.invalid import com.sksamuel.hoplite.fp.sequence -import com.sksamuel.hoplite.parsers.* +import com.sksamuel.hoplite.parsers.Parser +import com.sksamuel.hoplite.parsers.ParserRegistry +import com.sksamuel.hoplite.parsers.defaultParserRegistry import com.sksamuel.hoplite.preprocessor.Preprocessor import com.sksamuel.hoplite.preprocessor.defaultPreprocessors import java.io.File @@ -31,6 +35,10 @@ class ConfigLoader constructor( operator fun invoke(): ConfigLoader { return Builder().build() } + + inline operator fun invoke(block: Builder.() -> Unit): ConfigLoader { + return Builder().apply(block).build() + } } @Deprecated( @@ -106,74 +114,77 @@ class ConfigLoader constructor( private val paramMapperStaging = mutableListOf() private val failureCallbacks = mutableListOf<(Throwable) -> Unit>() private var mode: DecodeMode = DecodeMode.Lenient + private var defaultSources = true + private var defaultPreprocessors = true + private var defaultParamMappers = true + + fun withDefaultSources(defaultSources: Boolean): Builder = apply { + this.defaultSources = defaultSources + } + + fun withDefaultPreprocessors(defaultPreprocessors: Boolean): Builder = apply { + this.defaultPreprocessors = defaultPreprocessors + } + + fun withDefaultParamMappers(defaultParamMappers: Boolean): Builder = apply { + this.defaultParamMappers = defaultParamMappers + } - fun withClassLoader(classLoader: ClassLoader): Builder { + fun withClassLoader(classLoader: ClassLoader): Builder = apply { if (this.classLoader !== classLoader) { this.classLoader = classLoader } - return this } - fun addDecoder(decoder: Decoder<*>): Builder { + fun addDecoder(decoder: Decoder<*>): Builder = apply { this.decoderStaging.add(decoder) - return this } - fun addDecoders(decoders: Iterable>): Builder { + fun addDecoders(decoders: Iterable>): Builder = apply { this.decoderStaging.addAll(decoders) - return this } - fun addFileExtensionMapping(ext: String, parser: Parser): Builder { + fun addFileExtensionMapping(ext: String, parser: Parser): Builder = apply { this.parserStaging[ext] = parser - return this } - fun addFileExtensionMappins(map: Map): Builder { + fun addFileExtensionMappins(map: Map): Builder = apply { map.forEach { val (ext, parser) = it this.parserStaging[ext] = parser } - return this } - fun strict(): Builder { + fun strict(): Builder = apply { this.mode = DecodeMode.Strict - return this } fun addSource(source: PropertySource) = addPropertySource(source) - fun addPropertySource(propertySource: PropertySource): Builder { + fun addPropertySource(propertySource: PropertySource): Builder = apply { this.propertySourceStaging.add(propertySource) - return this } fun addSources(sources: Iterable) = addPropertySources(sources) - fun addPropertySources(propertySources: Iterable): Builder { + fun addPropertySources(propertySources: Iterable): Builder = apply { this.propertySourceStaging.addAll(propertySources) - return this } - fun addPreprocessor(preprocessor: Preprocessor): Builder { + fun addPreprocessor(preprocessor: Preprocessor): Builder = apply { this.preprocessorStaging.add(preprocessor) - return this } - fun addPreprocessors(preprocessors: Iterable): Builder { + fun addPreprocessors(preprocessors: Iterable): Builder = apply { this.preprocessorStaging.addAll(preprocessors) - return this } - fun addParameterMapper(paramMapper: ParameterMapper): Builder { + fun addParameterMapper(paramMapper: ParameterMapper): Builder = apply { this.paramMapperStaging.add(paramMapper) - return this } - fun addParameterMappers(paramMappers: Iterable): Builder { + fun addParameterMappers(paramMappers: Iterable): Builder = apply { this.paramMapperStaging.addAll(paramMappers) - return this } /** @@ -201,9 +212,20 @@ class ConfigLoader constructor( } // other defaults - val propertySources = defaultPropertySources() + this.propertySourceStaging - val preprocessors = defaultPreprocessors() + this.preprocessorStaging - val paramMappers = defaultParamMappers() + this.paramMapperStaging + val propertySources = when { + defaultSources -> defaultPropertySources() + this.propertySourceStaging + else -> this.propertySourceStaging + } + + val preprocessors = when { + defaultPreprocessors -> defaultPreprocessors() + this.preprocessorStaging + else -> this.preprocessorStaging + } + + val paramMappers = when { + defaultParamMappers -> defaultParamMappers() + this.paramMapperStaging + else -> this.paramMapperStaging + } return ConfigLoader( decoderRegistry = decoderRegistry, @@ -352,7 +374,7 @@ class ConfigLoader constructor( private fun loadNode(configs: List): ConfigResult { val srcs = propertySources + configs.map { ConfigFilePropertySource(it) } return srcs.map { it.node(PropertySourceContext(parserRegistry)) }.sequence() - .map { it.reduce { acc, b -> acc.merge(b) } } + .map { it.takeUnless { it.isEmpty() }?.reduce { acc, b -> acc.merge(b) } ?: NullNode(Pos.NoPos)} .mapInvalid { val multipleFailures = ConfigFailure.MultipleFailures(it) multipleFailures diff --git a/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/ConfigLoaderBuilderExtensions.kt b/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/ConfigLoaderBuilderExtensions.kt new file mode 100644 index 00000000..93abe77c --- /dev/null +++ b/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/ConfigLoaderBuilderExtensions.kt @@ -0,0 +1,90 @@ +package com.sksamuel.hoplite + +import java.io.File +import java.io.InputStream +import java.nio.file.Path + +/** + * Returns a [PropertySource] that will read the specified resource from the classpath. + * + * @param optional if true then the resource can not exist and the config loader will ignore this source + */ +fun ConfigLoader.Builder.addResourceSource(resource: String, optional: Boolean = false) = addSource( + ConfigFilePropertySource(ConfigSource.ClasspathSource(resource), optional = optional) +) + +/** + * Returns a [PropertySource] that will read the specified file from the filesystem. + * + * @param optional if true then the resource can not exist and the config loader will ignore this source + */ +fun ConfigLoader.Builder.addFileSource(file: File, optional: Boolean = false) = addSource( + ConfigFilePropertySource(ConfigSource.FileSource(file), optional = optional) +) + +/** + * Returns a [PropertySource] that will read the specified resource from the classpath. + * + * @param optional if true then the resource can not exist and the config loader will ignore this source + */ +fun ConfigLoader.Builder.addPathSource(path: Path, optional: Boolean = false) = addSource( + ConfigFilePropertySource(ConfigSource.PathSource(path), optional = optional) +) + +/** + * Returns a [PropertySource] that will read the specified input stream. + * + * @param input the input stream to read from + * @param ext the file extension of the input format + */ +fun ConfigLoader.Builder.addStreamSource(input: InputStream, ext: String) = addSource( + InputStreamPropertySource(input, ext) +) + +/** + * Returns a [PropertySource] that read the specified map values. + * + * @param map map + */ +fun ConfigLoader.Builder.addMapSource(map: Map) = addSource( + MapPropertySource(map) +) + +/** + * Returns a [PropertySource] that will read the specified command line arguments. + * + * @param arguments command line arguments as passed to main method + * @param prefix argument prefix + * @param delimiter key value delimiter + */ +fun ConfigLoader.Builder.addCommandLineSource( + arguments: Array, + prefix: String = "--", + delimiter: String = "=", +) = addSource( + CommandLinePropertySource(arguments, prefix, delimiter) +) + +/** + * Returns a [PropertySource] that will read the environment settings. + * + * @param arguments command line arguments as passed to main method + * @param prefix argument prefix + * @param delimiter key value delimiter + */ +fun ConfigLoader.Builder.addEnvironmentSource( + useUnderscoresAsSeparator: Boolean = true, + allowUppercaseNames: Boolean = true, +) = addSource( + EnvironmentVariablesPropertySource(useUnderscoresAsSeparator, allowUppercaseNames) +) + +/** + * Returns a [PropertySource] that will read from the specified string. + * + * @param str the string to read from + * @param ext the file extension of the input format + */ +fun ConfigLoader.Builder.string( + str: String, ext: String +) = addStreamSource(str.byteInputStream(), ext) diff --git a/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/PropertySource.kt b/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/PropertySource.kt index 370cb2c8..c563e4bc 100644 --- a/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/PropertySource.kt +++ b/hoplite-core/src/main/kotlin/com/sksamuel/hoplite/PropertySource.kt @@ -83,6 +83,17 @@ interface PropertySource { fun commandLine(arguments: Array, prefix: String = "--", delimiter: String = "=") = CommandLinePropertySource(arguments, prefix, delimiter) + /** + * Returns a [PropertySource] that will read the environment settings. + * + * @param arguments command line arguments as passed to main method + * @param prefix argument prefix + * @param delimiter key value delimiter + */ + fun environment(useUnderscoresAsSeparator: Boolean = true, allowUppercaseNames: Boolean = true) = + EnvironmentVariablesPropertySource(useUnderscoresAsSeparator, allowUppercaseNames) + + /** * Returns a [PropertySource] that will read from the specified string. * diff --git a/hoplite-core/src/test/kotlin/com/sksamuel/hoplite/WithoutDefaultsRegistryTest.kt b/hoplite-core/src/test/kotlin/com/sksamuel/hoplite/WithoutDefaultsRegistryTest.kt new file mode 100644 index 00000000..ad44d89b --- /dev/null +++ b/hoplite-core/src/test/kotlin/com/sksamuel/hoplite/WithoutDefaultsRegistryTest.kt @@ -0,0 +1,54 @@ +package com.sksamuel.hoplite + +import com.sksamuel.hoplite.fp.Validated +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.types.instanceOf + +class WithoutDefaultsRegistryTest : FunSpec() { + init { + test("default registry throws no error") { + val loader = ConfigLoader { + addMapSource(mapOf("custom_value" to "\${PATH}")) + } + val e = loader.loadConfig() + e as Validated.Valid + e.value.customValue shouldNotBe "\${path}" + } + + test("empty sources registry throws error") { + val loader = ConfigLoader { + withDefaultSources(false) + addMapSource(mapOf("custom_value" to "\${PATH}")) + } + val e = loader.loadConfig() + e as Validated.Invalid + e.error shouldBe instanceOf(ConfigFailure.DataClassFieldErrors::class) + } + + test("empty param mappers registry throws error") { + val loader = ConfigLoader { + withDefaultParamMappers(false) + addMapSource(mapOf("custom_value" to "\${PATH}")) + } + + val e = loader.loadConfig() + e as Validated.Invalid + e.error shouldBe instanceOf(ConfigFailure.DataClassFieldErrors::class) + } + + test("empty preprocessors registry throws error") { + val loader = ConfigLoader { + withDefaultPreprocessors(false) + addMapSource(mapOf("custom_value" to "\${PATH}")) + } + val e = loader.loadConfig() + e as Validated.Valid + e.value.customValue shouldBe "\${PATH}" + } + + } + + data class Config(val PATH: String, val customValue: String) +}