diff --git a/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt b/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt index 654efc85..6ebf9e79 100644 --- a/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt +++ b/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt @@ -21,6 +21,9 @@ import com.moandjiezana.toml.BooleanValueReaderWriter.BOOLEAN_VALUE_READER_WRITE import com.moandjiezana.toml.DateValueReaderWriter.DATE_VALUE_READER_WRITER import com.moandjiezana.toml.NumberValueReaderWriter.NUMBER_VALUE_READER_WRITER import com.moandjiezana.toml.StringValueReaderWriter.STRING_VALUE_READER_WRITER +import com.nhubbard.konfig.ListNode +import com.nhubbard.konfig.TreeNode +import com.nhubbard.konfig.ValueNode import java.io.IOException import java.io.StringWriter import java.io.Writer @@ -93,7 +96,7 @@ internal object Toml4jValueWriters { return valueWriter } } - return NewMapValueWriter + error("Can't find writer for ${value::class.qualifiedName}") } private val VALUE_WRITERS = arrayOf( @@ -107,23 +110,35 @@ internal object Toml4jValueWriters { } internal object NewArrayValueWriter : ArrayValueWriter() { - override fun canWrite(value: Any?): Boolean = isArrayish(value) + override fun canWrite(value: Any?): Boolean = isArrayish(value) || value is ListNode override fun write(o: Any, context: WriterContext) { - val values = normalize(o) + val node = o as? ListNode + val values = normalize(node?.list ?: o) + context.writeComments(node) context.write('[') context.writeArrayDelimiterPadding() var first = true var firstWriter: ValueWriter? = null + val hasAnyComments = values.any { it is TreeNode && it.comments.isNotEmpty() } + if (hasAnyComments) context.write('\n') + for (value in values) { + if (value == null) continue + + val fromNode = value as? TreeNode + val fromValue = fromNode?.value ?: value + + if (hasAnyComments) context.indent() + if (first) { - firstWriter = Toml4jValueWriters.findWriterFor(value!!) + firstWriter = Toml4jValueWriters.findWriterFor(fromValue) first = false } else { - val writer = Toml4jValueWriters.findWriterFor(value!!) + val writer = Toml4jValueWriters.findWriterFor(fromValue) if (writer !== firstWriter) { throw IllegalStateException( context.contextPath + @@ -131,35 +146,61 @@ internal object NewArrayValueWriter : ArrayValueWriter() { " but found " + writer ) } + if (hasAnyComments) + context.write('\n') context.write(", ") } - val writer = Toml4jValueWriters.findWriterFor(value) + val writer = Toml4jValueWriters.findWriterFor(fromValue) val isNestedOldValue = NewMapValueWriter.isNested if (writer == NewMapValueWriter) { NewMapValueWriter.isNested = true } - writer.write(value, context) + context.writeComments(fromNode) + writer.write(fromValue, context) if (writer == NewMapValueWriter) { NewMapValueWriter.isNested = isNestedOldValue } } context.writeArrayDelimiterPadding() + if (hasAnyComments) + context.write('\n') context.write(']') } } +private val TreeNode.value: Any + get() = when (this) { + is ValueNode -> this.value + is ListNode -> this.list + else -> this.children + } + +private fun WriterContext.writeComments(node: TreeNode?, newLineAfter: Boolean = true) { + if (node == null || node.comments.isEmpty()) return + val comments = node.comments.split("\n") + comments.forEach { comment -> + write('\n') + indent() + write("# $comment") + } + if (newLineAfter) write('\n') +} + internal object NewMapValueWriter : ValueWriter { override fun canWrite(value: Any): Boolean { - return value is Map<*, *> + return value is Map<*, *> || (value is TreeNode && value !is ValueNode && value !is ListNode) } var isNested: Boolean = false override fun write(value: Any, context: WriterContext) { - val from = value as Map<*, *> + val node = value as? TreeNode + val from = node?.children ?: value as Map<*, *> + + context.writeComments(node, newLineAfter = false) if (hasPrimitiveValues(from)) { if (isNested) { @@ -173,10 +214,14 @@ internal object NewMapValueWriter : ValueWriter { // Render primitive types and arrays of primitive first so they are // grouped under the same table (if there is one) for ((key, value1) in from) { - val fromValue = value1 ?: continue + if (value1 == null) continue + + val fromNode = value1 as? TreeNode + val fromValue = fromNode?.value ?: value1 val valueWriter = Toml4jValueWriters.findWriterFor(fromValue) - if (valueWriter.isPrimitiveType()) { + if (valueWriter.isPrimitiveType) { + context.writeComments(fromNode) context.indent() context.write(quoteKey(key!!)).write(" = ") valueWriter.write(fromValue, context) @@ -186,6 +231,7 @@ internal object NewMapValueWriter : ValueWriter { context.write('\n') } else if (valueWriter === NewArrayValueWriter) { context.setArrayKey(key.toString()) + context.writeComments(fromNode) context.write(quoteKey(key!!)).write(" = ") valueWriter.write(fromValue, context) if (isNested) { @@ -199,9 +245,8 @@ internal object NewMapValueWriter : ValueWriter { for (key in from.keys) { val fromValue = from[key] ?: continue - val valueWriter = Toml4jValueWriters.findWriterFor(fromValue) - if (valueWriter === this) { - valueWriter.write(fromValue, context.pushTable(quoteKey(key!!))) + if (canWrite(fromValue)) { + write(fromValue, context.pushTable(quoteKey(key!!))) } } if (isNested) { @@ -228,10 +273,13 @@ internal object NewMapValueWriter : ValueWriter { private fun hasPrimitiveValues(values: Map<*, *>): Boolean { for (key in values.keys) { - val fromValue = values[key] ?: continue + val value = values[key] ?: continue + + val fromNode = value as? TreeNode + val fromValue = fromNode?.value ?: value val valueWriter = Toml4jValueWriters.findWriterFor(fromValue) - if (valueWriter.isPrimitiveType() || valueWriter === NewArrayValueWriter) { + if (valueWriter.isPrimitiveType || valueWriter === NewArrayValueWriter) { return true } } diff --git a/src/main/kotlin/com/nhubbard/konfig/BaseConfig.kt b/src/main/kotlin/com/nhubbard/konfig/BaseConfig.kt index ebbd2703..a6133df2 100644 --- a/src/main/kotlin/com/nhubbard/konfig/BaseConfig.kt +++ b/src/main/kotlin/com/nhubbard/konfig/BaseConfig.kt @@ -138,6 +138,29 @@ open class BaseConfig( (node as ItemNode).item to name } + (parent?.itemWithNames ?: listOf()) + override fun toTree(): TreeNode { + return ContainerNode(mutableMapOf()).apply { + lock.read { + itemWithNames.forEach { (item, name) -> + val value = try { + getOrNull(item, errorWhenNotFound = true).toCompatibleValue(mapper) + } catch (_: UnsetValueException) { + return@forEach + } + set(name, value.asTree(item.description)) + } + // Add spec descriptions + specs.forEach { spec -> + val path = spec.prefix.toPath() + val node = tree.getOrNull(path) + if (node != null && node.comments.isNotEmpty()) { + getOrNull(path)?.comments = node.comments + } + } + } + } + } + override fun toMap(): Map { return lock.read { itemWithNames.map { (item, name) -> @@ -175,7 +198,6 @@ open class BaseConfig( ): Any? { val valueState = lock.read { nodeByItem[item]?.value } if (valueState != null) { - @Suppress("UNCHECKED_CAST") when (valueState) { is ValueState.Unset -> if (errorWhenNotFound) { @@ -229,13 +251,13 @@ open class BaseConfig( } } } else { - if (parent != null) { - return parent!!.getOrNull(item, errorWhenNotFound, errorWhenGetDefault, lazyContext) + return if (parent != null) { + parent!!.getOrNull(item, errorWhenNotFound, errorWhenGetDefault, lazyContext) } else { if (errorWhenNotFound) { throw NoSuchItemException(item) } else { - return null + null } } } @@ -318,15 +340,15 @@ open class BaseConfig( containsInLayer(path) || (parent?.contains(path) ?: false) override fun nameOf(item: Item<*>): String { - return nameByItem[item] ?: { - val name = lock.read { tree.firstPath { it is ItemNode && it.item == item } }?.name - if (name != null) { - nameByItem[item] = name - name + return nameByItem[item] ?: run { + val name1 = lock.read { tree.firstPath { it is ItemNode && it.item == item } }?.name + if (name1 != null) { + nameByItem[item] = name1 + name1 } else { parent?.nameOf(item) ?: throw NoSuchItemException(item) } - }() + } } open fun addBeforeSetFunction(beforeSetFunction: (item: Item<*>, value: Any?) -> Unit) { @@ -638,6 +660,13 @@ open class BaseConfig( throw RepeatedItemException(name) } } + val description = spec.description + if (description.isNotEmpty()) { + val node = this.tree.getOrNull(spec.prefix.toPath()) + if (node != null && node.comments.isEmpty()) { + node.comments = description + } + } spec.innerSpecs.forEach { innerSpec -> addSpec(innerSpec.withPrefix(spec.prefix)) } @@ -730,13 +759,15 @@ open class BaseConfig( return "Config(items=${toMap()})" } - class ItemNode(override var value: ValueState, val item: Item<*>) : ValueNode + class ItemNode(override var value: ValueState, val item: Item<*>) : ValueNode { + override var comments = this.item.description + } data class Value(var value: T) sealed class ValueState { - object Unset : ValueState() - object Null : ValueState() + data object Unset : ValueState() + data object Null : ValueState() data class Lazy(val thunk: (config: ItemContainer) -> T) : ValueState() data class Value(val value: Any) : ValueState() data class Default(val value: Any?) : ValueState() diff --git a/src/main/kotlin/com/nhubbard/konfig/Config.kt b/src/main/kotlin/com/nhubbard/konfig/Config.kt index 9760968a..80ae02d8 100644 --- a/src/main/kotlin/com/nhubbard/konfig/Config.kt +++ b/src/main/kotlin/com/nhubbard/konfig/Config.kt @@ -32,11 +32,11 @@ import kotlin.reflect.KProperty * Config containing items and associated values. * * Config contains items, which can be loaded with [addSpec]. - * Config contains values, each of which is associated with corresponding item. + * Config contains values, each of which is associated with the corresponding item. * Values can be loaded from [source][Source] with [withSource] or [from]. * * Config contains read-write access operations for item. - * Items in config is in one of three states: + * The items in config are in one of three states: * - Unset. Item has not associated value in this state. * Use [unset] to change item to this state. * - Unevaluated. Item is lazy and the associated value will be evaluated when accessing. @@ -49,7 +49,7 @@ import kotlin.reflect.KProperty * The forked config is called child config, and the original config is called parent config. * A config without parent config is called root config. The new layer added by child config * is called facade layer. - * Config with ancestor configs has multiple layers. All set operation is executed in facade layer + * Config with ancestor configs has multiple layers. All set operations are executed in the facade layer * of config. * Descendant config inherits items and values in ancestor configs, and can override values for * items in ancestor configs. Overridden values in config will affect itself and its descendant @@ -57,11 +57,11 @@ import kotlin.reflect.KProperty * ancestor configs too. [invoke] can be used to create a root config, and [withLayer] can be used * to create a child config from specified config. * - * All methods in Config is thread-safe. + * All methods in Config are thread-safe. */ interface Config : ItemContainer { /** - * Associate item with specified value without type checking. + * Associate item with the specified value without type checking. * * @param item config item * @param value associated value @@ -77,7 +77,7 @@ interface Config : ItemContainer { operator fun set(item: Item, value: T) /** - * Find item with specified name, and associate it with specified value. + * Find the item with specified name, and associate it with specified value. * * @param name item name * @param value associated value @@ -102,14 +102,14 @@ interface Config : ItemContainer { fun lazySet(name: String, thunk: (config: ItemContainer) -> T) /** - * Discard associated value of specified item. + * Discard the associated value of specified item. * * @param item config item */ fun unset(item: Item<*>) /** - * Discard associated value of item with specified name. + * Discard the associated value of item with specified name. * * @param name item name */ @@ -167,12 +167,12 @@ interface Config : ItemContainer { * Returns a property that can read/set associated value for item with specified name. * * @param name item name - * @return a property that can read/set associated value for item with specified name + * @return a property that can read/set associated value for item with the specified name */ fun property(name: String): ReadWriteProperty /** - * Name of facade layer of config. + * The name of the config facade layer. * * Layer name provides information for facade layer in a cascading config. */ @@ -234,8 +234,8 @@ interface Config : ItemContainer { /** * Load item into facade layer with the specified prefix. * - * Same item cannot be added twice. - * The item cannot have same qualified name with existed items in config. + * The same item cannot be added twice. + * The item cannot have the same qualified name with existed items in config. * * @param item config item * @param prefix prefix for the config item @@ -243,10 +243,10 @@ interface Config : ItemContainer { fun addItem(item: Item<*>, prefix: String = "") /** - * Load items in specified config spec into facade layer. + * Load items in specified config spec into the facade layer. * - * Same config spec cannot be added twice. - * All items in specified config spec cannot have same qualified name with existed items in config. + * The same config spec cannot be added twice. + * All items in specified config spec cannot have the same qualified name as existing items in config. * * @param spec config spec */ @@ -269,20 +269,20 @@ interface Config : ItemContainer { fun withLayer(name: String = ""): Config /** - * Returns a child config containing values from specified source. + * Returns a child config containing values from the specified source. * - * Values from specified source will be loaded into facade layer of the returned child config + * Values from the specified source will be loaded into the facade layer of the returned child config * without affecting this config. * * @param source config source - * @return a child config containing value from specified source + * @return a child config containing value from the specified source */ fun withSource(source: Source): Config /** * Returns a child config containing values loaded by specified trigger. * - * Values loaded by specified trigger will be loaded into facade layer of + * Values loaded by specified trigger will be loaded into the facade layer of * the returned child config without affecting this config. * * @param description trigger description @@ -316,7 +316,7 @@ interface Config : ItemContainer { /** * Returns default loaders for this config. * - * It is a fluent API for loading source from default loaders. + * It is a fluent API for loading sources from default loaders. * * @return default loaders for this config */ @@ -326,7 +326,7 @@ interface Config : ItemContainer { /** * Returns default loaders for this config. * - * It is a fluent API for loading source from default loaders. + * It is a fluent API for loading sources from default loaders. */ val from: DefaultLoaders get() = DefaultLoaders(this) @@ -338,13 +338,22 @@ interface Config : ItemContainer { /** * Returns a map in key-value format for this config. * - * The returned map contains all items in this config, with item name as key and + * The returned map contains all items in this config, with item name as the key and * associated value as value. - * This map can be loaded into config as [com.uchuhimo.konf.source.base.KVSource] using + * This map can be loaded into config as [com.nhubbard.konfig.source.base.KVSource] using * `config.from.map.kv(map)`. */ fun toMap(): Map + /** + * Convert this config to a tree node. + * + * @return a tree node + */ + fun toTree(): TreeNode { + return toMap().kvToTree() + } + /** * Enables the specified feature and returns this config. * @@ -388,17 +397,17 @@ interface Config : ItemContainer { } /** - * Returns a property that can read/set associated value casted from config. + * Returns a property that can read/set, associated with the value cast from config. * - * @return a property that can read/set associated value casted from config + * @return a property that can read/set associated with the value cast from config */ inline fun Config.cast() = object : RequiredConfigProperty(this.withPrefix("root").withLayer(), name = "root") {} /** - * Returns a value casted from config. + * Returns a value cast from config. * - * @return a value casted from config + * @return a value cast from config */ inline fun Config.toValue(): T { val value by cast() @@ -445,7 +454,7 @@ open class RequiredConfigProperty( } /** - * Returns a property that can read/set associated value for specified optional item. + * Returns a property that can read/set associated value for the specified optional item. * * @param default default value returned before associating this item with specified value * @param prefix prefix for the config item @@ -488,7 +497,7 @@ open class OptionalConfigProperty( } /** - * Returns a property that can read/set associated value for specified lazy item. + * Returns a property that can read/set associated value for the specified lazy item. * * @param prefix prefix for the config item * @param name item name without prefix @@ -530,11 +539,11 @@ open class LazyConfigProperty( } } -/** - * Convert the config to a tree node. - * - * @return a tree node - */ +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") +@Deprecated( + "Use method in Config.", + replaceWith = ReplaceWith("toTree()") +) fun Config.toTree(): TreeNode { - return toMap().kvToTree() + return toTree() } diff --git a/src/main/kotlin/com/nhubbard/konfig/ConfigException.kt b/src/main/kotlin/com/nhubbard/konfig/ConfigException.kt index 1948e397..2947614b 100644 --- a/src/main/kotlin/com/nhubbard/konfig/ConfigException.kt +++ b/src/main/kotlin/com/nhubbard/konfig/ConfigException.kt @@ -26,7 +26,7 @@ open class ConfigException : RuntimeException { } /** - * Exception indicates that there is existed item with same name in config. + * Exception indicates that there is existed item with the same name in config. */ class RepeatedItemException(val name: String) : ConfigException("item $name has been added") @@ -56,7 +56,7 @@ class UnsetValueException(val name: String) : ConfigException("$name is unset") } /** - * Exception indicates that the specified item has default value. + * Exception indicates that the specified item has the default value. */ class GetDefaultValueException(val name: String) : ConfigException("$name has default value") { constructor(item: Item<*>) : this(item.asName) @@ -70,13 +70,13 @@ class NoSuchItemException(val name: String) : ConfigException("cannot find $name } /** - * Exception indicates that item cannot be added to this config because it has child layer. + * Exception indicates that item cannot be added to this config because it has the child layer. */ class LayerFrozenException(val config: Config) : ConfigException("config ${config.name} has child layer, cannot add new item") /** - * Exception indicates that expected value in specified path is not existed in the source. + * Exception indicates that the expected value in the specified path is not existed in the source. */ class NoSuchPathException(val path: String) : ConfigException("cannot find path \"$path\" in config spec") diff --git a/src/main/kotlin/com/nhubbard/konfig/ConfigSpec.kt b/src/main/kotlin/com/nhubbard/konfig/ConfigSpec.kt index 633e49c2..389e21aa 100644 --- a/src/main/kotlin/com/nhubbard/konfig/ConfigSpec.kt +++ b/src/main/kotlin/com/nhubbard/konfig/ConfigSpec.kt @@ -27,16 +27,17 @@ import com.fasterxml.jackson.module.kotlin.isKotlinClass open class ConfigSpec @JvmOverloads constructor( prefix: String? = null, items: Set> = mutableSetOf(), - innerSpecs: Set = mutableSetOf() + innerSpecs: Set = mutableSetOf(), + override val description: String = "" ) : Spec { - final override val prefix: String = prefix ?: { - if (javaClass == ConfigSpec::class.java || javaClass.isAnonymousClass) { + final override val prefix: String = + prefix ?: if (javaClass == ConfigSpec::class.java || javaClass.isAnonymousClass) { "" } else { javaClass.let { clazz -> if (this::class.isCompanion) clazz.declaringClass else clazz }.simpleName.let { name -> - if (name == null || name.contains('$')) { + if (name.contains('$')) { "" } else { name.toLittleCase() @@ -49,7 +50,6 @@ open class ConfigSpec @JvmOverloads constructor( } } } - }() init { checkPath(this.prefix) diff --git a/src/main/kotlin/com/nhubbard/konfig/Feature.kt b/src/main/kotlin/com/nhubbard/konfig/Feature.kt index be192c08..be94b573 100644 --- a/src/main/kotlin/com/nhubbard/konfig/Feature.kt +++ b/src/main/kotlin/com/nhubbard/konfig/Feature.kt @@ -26,31 +26,38 @@ enum class Feature(val enabledByDefault: Boolean) { * If enabled, an exception is thrown when loading from the source * to indicate it contains unknown paths. * - * Feature is disabled by default. + * This feature is disabled by default. */ FAIL_ON_UNKNOWN_PATH(false), /** * Feature that determines whether loading keys from sources case-insensitively. * - * Feature is disabled by default. + * This feature is disabled by default. */ LOAD_KEYS_CASE_INSENSITIVELY(false), /** * Feature that determines whether loading keys from sources as little camel case. * - * Feature is enabled by default. + * This feature is enabled by default. */ LOAD_KEYS_AS_LITTLE_CAMEL_CASE(true), /** * Feature that determines whether sources are optional by default. * - * Feature is disabled by default. + * This feature is disabled by default. */ OPTIONAL_SOURCE_BY_DEFAULT(false), /** * Feature that determines whether sources should be substituted before loaded into config. * - * Feature is enabled by default. + * This feature is enabled by default. */ - SUBSTITUTE_SOURCE_BEFORE_LOADED(true) + SUBSTITUTE_SOURCE_BEFORE_LOADED(true), + /** + * Feature that writes descriptions assigned to [Item]s as comments + * above the written configuration value. + * + * This feature is disabled by default. + */ + WRITE_DESCRIPTIONS_AS_COMMENTS(false) } diff --git a/src/main/kotlin/com/nhubbard/konfig/Item.kt b/src/main/kotlin/com/nhubbard/konfig/Item.kt index c4d9e5b4..1909b945 100644 --- a/src/main/kotlin/com/nhubbard/konfig/Item.kt +++ b/src/main/kotlin/com/nhubbard/konfig/Item.kt @@ -25,8 +25,8 @@ import com.fasterxml.jackson.databind.type.TypeFactory * * Item can be associated with value in config, containing metadata for the value. * The metadata for value includes name, path, type, description and so on. - * Item can be used as key to operate value in config, guaranteeing type safety. - * There are three kinds of item: [required item][RequiredItem], [optional item][OptionalItem] + * The item can be used as a key to operate on a value in config, guaranteeing type safety. + * There are three kinds of items: [required item][RequiredItem], [optional item][OptionalItem] * and [lazy item][LazyItem]. * * @param T type of value that can be associated with this item. @@ -65,7 +65,6 @@ sealed class Item( /** * Type of value that can be associated with this item. */ - @Suppress("LeakingThis") val type: JavaType = type ?: TypeFactory.defaultInstance().constructType(this::class.java) .findSuperType(Item::class.java).bindings.typeParameters[0] @@ -184,7 +183,7 @@ interface Handler : AutoCloseable { typealias Path = List /** - * Returns corresponding item name of the item path. + * Returns the corresponding item name of the item path. * * @receiver item path * @return item name @@ -192,7 +191,7 @@ typealias Path = List val Path.name: String get() = joinToString(".") /** - * Returns corresponding item path of the item name. + * Returns the corresponding item path of the item name. * * @receiver item name * @return item path @@ -237,8 +236,8 @@ open class RequiredItem @JvmOverloads constructor( /** * Optional item with default value. * - * Before associated with specified value, default value will be returned when accessing. - * After associated with specified value, the specified value will be returned when accessing. + * Before being associated with the specified value, the default value will be returned when accessing. + * After being associated with the specified value, the specified value will be returned when accessing. */ open class OptionalItem @JvmOverloads constructor( spec: Spec, @@ -264,9 +263,9 @@ open class OptionalItem @JvmOverloads constructor( * Lazy item evaluated value every time from thunk before associated with specified value. * * Before associated with specified value, value evaluated from thunk will be returned when accessing. - * After associated with specified value, the specified value will be returned when accessing. - * Returned value of the thunk will not be cached. The thunk will be evaluated every time - * when needed to reflect modifying of other values in config. + * After being associated with the specified value, the specified value will be returned when accessing. + * The returned value of the thunk will not be cached. + * The thunk will be evaluated every time when needed to reflect modifying of other values in config. */ open class LazyItem @JvmOverloads constructor( spec: Spec, diff --git a/src/main/kotlin/com/nhubbard/konfig/ItemContainer.kt b/src/main/kotlin/com/nhubbard/konfig/ItemContainer.kt index e759964f..7c51a317 100644 --- a/src/main/kotlin/com/nhubbard/konfig/ItemContainer.kt +++ b/src/main/kotlin/com/nhubbard/konfig/ItemContainer.kt @@ -73,7 +73,7 @@ interface ItemContainer : Iterable> { override operator fun iterator(): Iterator> /** - * Whether this item container contains specified item or not. + * Whether this item container contains the specified item or not. * * @param item config item * @return `true` if this item container contains specified item, `false` otherwise @@ -121,7 +121,7 @@ interface ItemContainer : Iterable> { } /** - * List of qualified names of items in this item container. + * List of qualified item names in this item container. */ val nameOfItems: List get() = itemWithNames.map { it.second } diff --git a/src/main/kotlin/com/nhubbard/konfig/MergedConfig.kt b/src/main/kotlin/com/nhubbard/konfig/MergedConfig.kt index 80e696da..cd3835b1 100644 --- a/src/main/kotlin/com/nhubbard/konfig/MergedConfig.kt +++ b/src/main/kotlin/com/nhubbard/konfig/MergedConfig.kt @@ -160,15 +160,15 @@ open class MergedConfig(val fallback: BaseConfig, val facade: BaseConfig) : return fallback.getOrNull(item, errorWhenNotFound, errorWhenGetDefault, lazyContext) } is GetDefaultValueException -> { - try { - return fallback.getOrNull(item, errorWhenNotFound, errorWhenGetDefault, lazyContext) + return try { + fallback.getOrNull(item, errorWhenNotFound, errorWhenGetDefault, lazyContext) } catch (ex: Exception) { when (ex) { is UnsetValueException -> { if (errorWhenGetDefault) { throw GetDefaultValueException(item) } else { - return (item as OptionalItem).default + (item as OptionalItem).default } } else -> throw ex diff --git a/src/main/kotlin/com/nhubbard/konfig/MergedMap.kt b/src/main/kotlin/com/nhubbard/konfig/MergedMap.kt index ea14187d..8ed8b87f 100644 --- a/src/main/kotlin/com/nhubbard/konfig/MergedMap.kt +++ b/src/main/kotlin/com/nhubbard/konfig/MergedMap.kt @@ -38,7 +38,7 @@ class MergedMap(val fallback: MutableMap, val facade: MutableMap> - get() = keys.map { it to getValue(it) }.toMap(LinkedHashMap()).entries + get() = keys.associateWithTo(LinkedHashMap()) { getValue(it) }.entries override val keys: MutableSet get() = facade.keys.union(fallback.keys).toMutableSet() override val values: MutableCollection diff --git a/src/main/kotlin/com/nhubbard/konfig/SizeInBytes.kt b/src/main/kotlin/com/nhubbard/konfig/SizeInBytes.kt index a77b5660..ada0b1ef 100644 --- a/src/main/kotlin/com/nhubbard/konfig/SizeInBytes.kt +++ b/src/main/kotlin/com/nhubbard/konfig/SizeInBytes.kt @@ -68,10 +68,9 @@ data class SizeInBytes( ) try { - val result: BigInteger // if the string is purely digits, parse as an integer to avoid // possible precision loss; otherwise as a double. - result = if (numberString.matches("[0-9]+".toRegex())) { + val result = if (numberString.matches("[0-9]+".toRegex())) { units.bytes.multiply(BigInteger(numberString)) } else { val resultDecimal = BigDecimal(units.bytes).multiply(BigDecimal(numberString)) diff --git a/src/main/kotlin/com/nhubbard/konfig/Spec.kt b/src/main/kotlin/com/nhubbard/konfig/Spec.kt index b9fd96cd..1fb61bd2 100644 --- a/src/main/kotlin/com/nhubbard/konfig/Spec.kt +++ b/src/main/kotlin/com/nhubbard/konfig/Spec.kt @@ -28,8 +28,8 @@ import kotlin.reflect.KProperty * * Config spec describes a group of items with common prefix, which can be loaded into config * together using [Config.addSpec]. - * Config spec also provides convenient API to specify item in it without hand-written object - * declaration. + * Config spec also provides a convenient API to specify items in it without hand-crafted object + * declarations. * * @see Config */ @@ -37,14 +37,20 @@ interface Spec { /** * Common prefix for items in this config spec. * - * An empty prefix means names of items in this config spec are unqualified. + * An empty prefix means the names of items in this config spec are unqualified. */ val prefix: String + /** + * The description of the spec. + */ + val description: String + get() = "" + /** * Qualify item name with prefix of this config spec. * - * When prefix is empty, original item name will be returned. + * When the prefix is empty, the original item name will be returned. * * @param item the config item * @return qualified item name @@ -164,15 +170,28 @@ interface Spec { return if (newPrefix.isEmpty()) { this } else { - ConfigSpec((newPrefix + prefix).name, items, innerSpecs) + ConfigSpec((newPrefix + prefix).name, items, innerSpecs, description) } } + /** + * Returns config spec with the specified description. + * + * @param description description + * @return config spec with the specified description + */ + fun withDescription(description: String): Spec { + if (this.description == description) + return this + return ConfigSpec(prefix, items, innerSpecs, description) + } + companion object { /** - * A dummy implementation for [Spec]. + * A placeholder implementation for [Spec]. * - * It will swallow all items added to it. Used for items belonged to no config spec. + * It will swallow all items added to it. + * Used for items that don't belong to a config spec. */ val dummy: Spec = object : Spec { override val prefix: String = "" diff --git a/src/main/kotlin/com/nhubbard/konfig/TreeNode.kt b/src/main/kotlin/com/nhubbard/konfig/TreeNode.kt index de3f872e..25fa78d8 100644 --- a/src/main/kotlin/com/nhubbard/konfig/TreeNode.kt +++ b/src/main/kotlin/com/nhubbard/konfig/TreeNode.kt @@ -20,7 +20,7 @@ package com.nhubbard.konfig import java.util.Collections /** - * Tree node that represents internal structure of config/source. + * Tree node that represents the internal structure of config/source. */ interface TreeNode { /** @@ -28,6 +28,11 @@ interface TreeNode { */ val children: MutableMap + /** + * The comments assigned to this tree node. + */ + var comments: String + /** * Associate path with specified node. * @@ -74,10 +79,10 @@ interface TreeNode { } /** - * Whether this tree node contains node(s) in specified path or not. + * Whether this tree node contains node(s) in the specified path or not. * * @param path item path - * @return `true` if this tree node contains node(s) in specified path, `false` otherwise + * @return `true` if this tree node contains node(s) in a specified path, `false` otherwise */ operator fun contains(path: Path): Boolean { return if (path.isEmpty()) { @@ -95,11 +100,11 @@ interface TreeNode { } /** - * Returns tree node in specified path if this tree node contains value(s) in specified path, + * Returns tree node in the specified path if this tree node contains value(s) in the specified path, * `null` otherwise. * * @param path item path - * @return tree node in specified path if this tree node contains value(s) in specified path, + * @return tree node in the specified path if this tree node contains value(s) in the specified path, * `null` otherwise */ fun getOrNull(path: Path): TreeNode? { @@ -114,11 +119,11 @@ interface TreeNode { } /** - * Returns tree node in specified path if this tree node contains value(s) in specified path, + * Returns tree node in the specified path if this tree node contains value(s) in the specified path, * `null` otherwise. * * @param path item path - * @return tree node in specified path if this tree node contains value(s) in specified path, + * @return tree node in the specified path if this tree node contains value(s) in the specified path, * `null` otherwise */ fun getOrNull(path: String): TreeNode? = getOrNull(path.toPath()) @@ -131,10 +136,10 @@ interface TreeNode { */ fun withFallback(fallback: TreeNode): TreeNode { fun traverseTree(facade: TreeNode, fallback: TreeNode, path: Path): TreeNode { - if (facade is LeafNode || fallback is LeafNode) { - return facade + return if (facade is LeafNode || fallback is LeafNode) { + facade } else { - return ContainerNode( + ContainerNode( facade.children.toMutableMap().also { map -> for ((key, child) in fallback.children) { if (key in facade.children) { @@ -261,10 +266,10 @@ interface TreeNode { val newChildren = children.mapValues { (_, child) -> child.withoutPlaceHolder() } - if (newChildren.isNotEmpty() && newChildren.all { (_, child) -> child is MapNode && child.isPlaceHolder }) { - return ContainerNode.placeHolder() + return if (newChildren.isNotEmpty() && newChildren.all { (_, child) -> child is MapNode && child.isPlaceHolder }) { + ContainerNode.placeHolder() } else { - return withMap(newChildren.filterValues { !(it is MapNode && it.isPlaceHolder) }) + withMap(newChildren.filterValues { !(it is MapNode && it.isPlaceHolder) }) } } else -> return this @@ -272,25 +277,27 @@ interface TreeNode { } fun isEmpty(): Boolean { - when (this) { - is EmptyNode -> return true + return when (this) { + is EmptyNode -> true is MapNode -> { - return children.isEmpty() || children.all { (_, child) -> child.isEmpty() } + children.isEmpty() || children.all { (_, child) -> child.isEmpty() } } - else -> return false + + else -> false } } fun isPlaceHolderNode(): Boolean { - when (this) { + return when (this) { is MapNode -> { if (isPlaceHolder) { - return true + true } else { - return children.isNotEmpty() && children.all { (_, child) -> child.isPlaceHolderNode() } + children.isNotEmpty() && children.all { (_, child) -> child.isPlaceHolderNode() } } } - else -> return false + + else -> false } } } @@ -320,16 +327,18 @@ interface ListNode : LeafNode { /** * Tree node that contains children nodes. */ -open class ContainerNode( +open class ContainerNode @JvmOverloads constructor( override val children: MutableMap, - override var isPlaceHolder: Boolean = false + override var isPlaceHolder: Boolean = false, + override var comments: String = "" ) : MapNode { + override fun withMap(map: Map): MapNode { val isPlaceHolder = map.isEmpty() && this.isPlaceHolder - if (map is MutableMap) { - return ContainerNode(map, isPlaceHolder) + return if (map is MutableMap) { + ContainerNode(map, isPlaceHolder, comments) } else { - return ContainerNode(map.toMutableMap(), isPlaceHolder) + ContainerNode(map.toMutableMap(), isPlaceHolder, comments) } } @@ -340,8 +349,9 @@ open class ContainerNode( } /** - * Tree node that represents a empty tree. + * Tree node that represents an empty tree. */ object EmptyNode : LeafNode { override val children: MutableMap = emptyMutableMap + override var comments: String = "" } diff --git a/src/main/kotlin/com/nhubbard/konfig/Utils.kt b/src/main/kotlin/com/nhubbard/konfig/Utils.kt index 40478e8a..f83d7dd4 100644 --- a/src/main/kotlin/com/nhubbard/konfig/Utils.kt +++ b/src/main/kotlin/com/nhubbard/konfig/Utils.kt @@ -18,16 +18,9 @@ package com.nhubbard.konfig import java.io.File - -/** - * Throws [UnsupportedOperationException]. - * - * @throws UnsupportedOperationException - */ -@Suppress("NOTHING_TO_INLINE") -inline fun unsupported(): Nothing { - throw UnsupportedOperationException() -} +import java.nio.file.Files.createTempDirectory +import java.nio.file.Paths +import kotlin.io.path.createTempFile internal fun getUnits(s: String): String { var i = s.length - 1 @@ -126,9 +119,17 @@ fun tempDirectory( suffix: String? = null, directory: File? = null ): File { - return createTempDir(prefix, suffix, directory) + val dirPath = directory?.toPath() ?: Paths.get(System.getProperty("java.io.tmpdir")) + val tempDir = createTempDirectory(dirPath, prefix + (suffix ?: "")) + return tempDir.toFile() } -fun tempFile(prefix: String = "tmp", suffix: String? = null, directory: File? = null): File { - return createTempFile(prefix, suffix, directory) -} +fun tempFile( + prefix: String = "tmp", + suffix: String? = null, + directory: File? = null +): File { + val dirPath = directory?.toPath() ?: Paths.get(System.getProperty("java.io.tmpdir")) + val tempFile = createTempFile(dirPath, prefix, suffix ?: "") + return tempFile.toFile() +} \ No newline at end of file diff --git a/src/main/kotlin/com/nhubbard/konfig/source/DefaultLoaders.kt b/src/main/kotlin/com/nhubbard/konfig/source/DefaultLoaders.kt index 7813baaf..18c18ab1 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/DefaultLoaders.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/DefaultLoaders.kt @@ -54,7 +54,7 @@ class DefaultLoaders( fun Provider.orMapped(): Provider = if (transform != null) this.map(transform) else this - fun Source.orMapped(): Source = transform?.invoke(this) ?: this + private fun Source.orMapped(): Source = transform?.invoke(this) ?: this /** * Returns default loaders applied the given [transform] function. @@ -381,7 +381,7 @@ class MapLoader( */ private val transform: ((Source) -> Source)? = null ) { - fun Source.orMapped(): Source = transform?.invoke(this) ?: this + private fun Source.orMapped(): Source = transform?.invoke(this) ?: this /** * Returns a child config containing values from specified hierarchical map. diff --git a/src/main/kotlin/com/nhubbard/konfig/source/DefaultProviders.kt b/src/main/kotlin/com/nhubbard/konfig/source/DefaultProviders.kt index 55af36fc..413d3ba0 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/DefaultProviders.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/DefaultProviders.kt @@ -38,13 +38,13 @@ object DefaultProviders { val json = JsonProvider /** - * Provider for properties source. + * Provider for the properties source. */ @JvmField val properties = PropertiesProvider /** - * Provider for map source. + * Provider for the map source. */ @JvmField val map = DefaultMapProviders @@ -66,11 +66,11 @@ object DefaultProviders { fun systemProperties(): Source = PropertiesProvider.system() /** - * Returns corresponding provider based on extension. + * Returns the corresponding provider based on its extension. * * @param extension the file extension * @param source the source description for error message - * @return the corresponding provider based on extension + * @return the corresponding provider based on its extension */ fun dispatchExtension(extension: String, source: String = ""): Provider = Provider.of(extension) ?: throw UnsupportedExtensionException(source) @@ -78,7 +78,7 @@ object DefaultProviders { /** * Returns a source from specified file. * - * Format of the file is auto-detected from the file extension. + * The format of the file is auto-detected from the file extension. * Supported file formats and the corresponding extensions: * - HOCON: conf * - JSON: json @@ -97,9 +97,9 @@ object DefaultProviders { fun file(file: File, optional: Boolean = false): Source = dispatchExtension(file.extension, file.name).file(file, optional) /** - * Returns a source from specified file path. + * Returns a source from the specified file path. * - * Format of the file is auto-detected from the file extension. + * The format of the file is auto-detected from the file extension. * Supported file formats and the corresponding extensions: * - HOCON: conf * - JSON: json @@ -120,7 +120,7 @@ object DefaultProviders { /** * Returns a source from specified url. * - * Format of the url is auto-detected from the url extension. + * The format of the url is auto-detected from the url extension. * Supported url formats and the corresponding extensions: * - HOCON: conf * - JSON: json @@ -142,7 +142,7 @@ object DefaultProviders { /** * Returns a source from specified url string. * - * Format of the url is auto-detected from the url extension. + * The format of the url is auto-detected from the url extension. * Supported url formats and the corresponding extensions: * - HOCON: conf * - JSON: json @@ -162,7 +162,7 @@ object DefaultProviders { } /** - * Providers for map of variant formats. + * Providers for the map of variant formats. */ object DefaultMapProviders { /** diff --git a/src/main/kotlin/com/nhubbard/konfig/source/Loader.kt b/src/main/kotlin/com/nhubbard/konfig/source/Loader.kt index 6153ef22..cb7c935d 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/Loader.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/Loader.kt @@ -20,10 +20,7 @@ package com.nhubbard.konfig.source import com.nhubbard.konfig.Config import com.nhubbard.konfig.Feature import com.nhubbard.konfig.Path -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import java.io.File import java.io.InputStream import java.io.Reader @@ -44,11 +41,11 @@ import kotlin.coroutines.CoroutineContext */ class Loader( /** - * Parent config for all child configs loading source in this loader. + * Parent config for all child configs loading the source in this loader. */ val config: Config, /** - * Source provider to provide source from various input format. + * Source provider to provide the source from various input format. */ val provider: Provider ) { @@ -83,11 +80,11 @@ class Loader( config.withSource(provider.file(file, optional)) /** - * Returns a child config containing values from specified file path. + * Returns a child config containing values from the specified file path. * * @param file specified file path * @param optional whether the source is optional - * @return a child config containing values from specified file path + * @return a child config containing values from the specified file path */ fun file(file: String, optional: Boolean = this.optional): Config = config.withSource(provider.file(file, optional)) @@ -135,7 +132,7 @@ class Loader( StandardWatchEventKinds.ENTRY_CREATE ) var digest = absoluteFile.digest - GlobalScope.launch(context) { + MainScope().launch(context) { while (true) { delay(unit.toMillis(delayTime)) if (isMac) { @@ -186,7 +183,7 @@ class Loader( } /** - * Returns a child config containing values from specified file path, + * Returns a child config containing values from the specified file path, * and reloads values when file content has been changed. * * @param file specified file path @@ -217,7 +214,7 @@ class Loader( config.withSource(provider.string(content)) /** - * Returns a child config containing values from specified byte array. + * Returns a child config containing values from the specified byte array. * * @param content specified byte array * @return a child config containing values from specified byte array @@ -226,12 +223,12 @@ class Loader( config.withSource(provider.bytes(content)) /** - * Returns a child config containing values from specified portion of byte array. + * Returns a child config containing values from the specified portion of byte array. * * @param content specified byte array * @param offset the start offset of the portion of the array to read * @param length the length of the portion of the array to read - * @return a child config containing values from specified portion of byte array + * @return a child config containing values from the specified portion of byte array */ fun bytes(content: ByteArray, offset: Int, length: Int): Config = config.withSource(provider.bytes(content, offset, length)) @@ -262,7 +259,7 @@ class Loader( * * @param url specified url * @param period reload period. The default value is 5. - * @param unit time unit of reload period. The default value is [TimeUnit.SECONDS]. + * @param unit time unit of the reload period. The default value is [TimeUnit.SECONDS]. * @param context context of the coroutine. The default value is [Dispatchers.Default]. * @param optional whether the source is optional * @param onLoad function invoked after the updated URL is loaded @@ -282,7 +279,7 @@ class Loader( load(source) } onLoad?.invoke(newConfig, source) - GlobalScope.launch(context) { + MainScope().launch(context) { while (true) { delay(unit.toMillis(period)) val newSource = provider.url(url, optional) @@ -303,7 +300,7 @@ class Loader( * * @param url specified url string * @param period reload period. The default value is 5. - * @param unit time unit of reload period. The default value is [TimeUnit.SECONDS]. + * @param unit time unit of the reload period. The default value is [TimeUnit.SECONDS]. * @param context context of the coroutine. The default value is [Dispatchers.Default]. * @param optional whether the source is optional * @param onLoad function invoked after the updated URL is loaded diff --git a/src/main/kotlin/com/nhubbard/konfig/source/MergedSource.kt b/src/main/kotlin/com/nhubbard/konfig/source/MergedSource.kt index 50d2b0c5..1c5e6dbc 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/MergedSource.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/MergedSource.kt @@ -47,40 +47,40 @@ class MergedSource(val facade: Source, val fallback: Source) : Source { override fun substituted(root: Source, enabled: Boolean, errorWhenUndefined: Boolean): Source { val substitutedFacade = facade.substituted(root, enabled, errorWhenUndefined) val substitutedFallback = fallback.substituted(root, enabled, errorWhenUndefined) - if (substitutedFacade === facade && substitutedFallback === fallback) { - return this + return if (substitutedFacade === facade && substitutedFallback === fallback) { + this } else { - return MergedSource(substitutedFacade, substitutedFallback) + MergedSource(substitutedFacade, substitutedFallback) } } override fun lowercased(enabled: Boolean): Source { val lowercasedFacade = facade.lowercased(enabled) val lowercasedFallback = fallback.lowercased(enabled) - if (lowercasedFacade === facade && lowercasedFallback === fallback) { - return this + return if (lowercasedFacade === facade && lowercasedFallback === fallback) { + this } else { - return MergedSource(lowercasedFacade, lowercasedFallback) + MergedSource(lowercasedFacade, lowercasedFallback) } } override fun littleCamelCased(enabled: Boolean): Source { val littleCamelCasedFacade = facade.littleCamelCased(enabled) val littleCamelCasedFallback = fallback.littleCamelCased(enabled) - if (littleCamelCasedFacade === facade && littleCamelCasedFallback === fallback) { - return this + return if (littleCamelCasedFacade === facade && littleCamelCasedFallback === fallback) { + this } else { - return MergedSource(littleCamelCasedFacade, littleCamelCasedFallback) + MergedSource(littleCamelCasedFacade, littleCamelCasedFallback) } } override fun normalized(lowercased: Boolean, littleCamelCased: Boolean): Source { val normalizedFacade = facade.normalized(lowercased, littleCamelCased) val normalizedFallback = fallback.normalized(lowercased, littleCamelCased) - if (normalizedFacade === facade && normalizedFallback === fallback) { - return this + return if (normalizedFacade === facade && normalizedFallback === fallback) { + this } else { - return MergedSource(normalizedFacade, normalizedFallback) + MergedSource(normalizedFacade, normalizedFallback) } } diff --git a/src/main/kotlin/com/nhubbard/konfig/source/Provider.kt b/src/main/kotlin/com/nhubbard/konfig/source/Provider.kt index 766ae9c3..f0b13c86 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/Provider.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/Provider.kt @@ -32,11 +32,11 @@ import java.net.URL import java.util.concurrent.ConcurrentHashMap /** - * Provides source from various input format. + * Provides a source from various input formats. */ interface Provider { /** - * Returns a new source from specified reader. + * Returns a new source from the specified reader. * * @param reader specified reader for reading character streams * @return a new source from specified reader @@ -52,7 +52,7 @@ interface Provider { fun inputStream(inputStream: InputStream): Source /** - * Returns a new source from specified file. + * Returns a new source from the specified file. * * @param file specified file * @param optional whether this source is optional @@ -71,11 +71,11 @@ interface Provider { } /** - * Returns a new source from specified file path. + * Returns a new source from the specified file path. * * @param file specified file path * @param optional whether this source is optional - * @return a new source from specified file path + * @return a new source from the specified file path */ fun file(file: String, optional: Boolean = false): Source = file(File(file), optional) @@ -90,10 +90,10 @@ interface Provider { } /** - * Returns a new source from specified byte array. + * Returns a new source from the specified byte array. * * @param content specified byte array - * @return a new source from specified byte array + * @return a new source from the specified byte array */ fun bytes(content: ByteArray): Source { return content.inputStream().use { @@ -102,12 +102,12 @@ interface Provider { } /** - * Returns a new source from specified portion of byte array. + * Returns a new source from the specified portion of byte array. * * @param content specified byte array * @param offset the start offset of the portion of the array to read * @param length the length of the portion of the array to read - * @return a new source from specified portion of byte array + * @return a new source from the specified portion of byte array */ fun bytes(content: ByteArray, offset: Int, length: Int): Source { return content.inputStream(offset, length).use { @@ -273,7 +273,7 @@ interface Provider { } /** - * Register extension with the corresponding provider. + * Register this extension with the corresponding provider. * * @param extension the file extension * @param provider the corresponding provider @@ -291,12 +291,12 @@ interface Provider { extensionToProvider.remove(extension) /** - * Returns corresponding provider based on extension. + * Returns the corresponding provider based on an extension. * * Returns null if the specific extension is unregistered. * * @param extension the file extension - * @return the corresponding provider based on extension + * @return the corresponding provider based on an extension */ fun of(extension: String): Provider? = extensionToProvider[extension] diff --git a/src/main/kotlin/com/nhubbard/konfig/source/Source.kt b/src/main/kotlin/com/nhubbard/konfig/source/Source.kt index ff505755..d634ef59 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/Source.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/Source.kt @@ -62,14 +62,7 @@ import java.time.YearMonth import java.time.ZoneOffset import java.time.ZonedDateTime import java.time.format.DateTimeParseException -import java.util.ArrayDeque -import java.util.Collections -import java.util.Date -import java.util.Queue -import java.util.SortedMap -import java.util.SortedSet -import java.util.TreeMap -import java.util.TreeSet +import java.util.* import java.util.regex.Pattern import kotlin.Byte import kotlin.Char @@ -79,6 +72,9 @@ import kotlin.Int import kotlin.Long import kotlin.Short import kotlin.String +import kotlin.collections.ArrayList +import kotlin.collections.HashMap +import kotlin.collections.HashSet import kotlin.reflect.KClass import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.starProjectedType @@ -88,12 +84,12 @@ import com.fasterxml.jackson.databind.node.NullNode as JacksonNullNode * Source to provide values for config. * * When config loads values from source, config will iterate all items in it, and - * retrieve value with path of each item from source. - * When source contains single value, a series of `is` operations can be used to - * judge the actual type of value, and `to` operation can be used to get the value - * with specified type. - * When source contains multiple value, `contains` operations can be used to check - * whether value(s) in specified path is in this source, and `get` operations can be used + * retrieve the value with the path of each item from source. + * When the source contains a single value, a series of `is` operations can be used to + * judge the actual type of the value, and a `to` operation can be used to get the value + * with the specified type. + * When the source contains multiple values, `contains` operations can be used to check + * whether value(s) in the specified path are in this source, and `get` operations can be used * to retrieve the corresponding sub-source. */ interface Source { @@ -108,12 +104,12 @@ interface Source { /** * Information about this source. * - * Info is in form of key-value pairs. + * Info is in the form of key-value pairs. */ val info: SourceInfo /** - * a tree node that represents internal structure of this source. + * a tree node that represents the internal structure of this source. */ val tree: TreeNode @@ -123,19 +119,19 @@ interface Source { val features: Map get() = emptyMap() /** - * Whether this source contains value(s) in specified path or not. + * Whether this source contains value(s) in the specified path or not. * * @param path item path - * @return `true` if this source contains value(s) in specified path, `false` otherwise + * @return `true` if this source contains value(s) in the specified path, `false` otherwise */ operator fun contains(path: Path): Boolean = path in tree /** - * Returns sub-source in specified path if this source contains value(s) in specified path, + * Returns sub-source in the specified path if this source contains value(s) in the specified path, * `null` otherwise. * * @param path item path - * @return sub-source in specified path if this source contains value(s) in specified path, + * @return sub-source in the specified path if this source contains value(s) in the specified path, * `null` otherwise */ fun getOrNull(path: Path): Source? { @@ -171,7 +167,7 @@ interface Source { currentKey = currentKey.toLittleCamelCase() } if (isEnabled(Feature.LOAD_KEYS_CASE_INSENSITIVELY)) { - currentKey = currentKey.toLowerCase() + currentKey = currentKey.lowercase(Locale.getDefault()) } return currentKey } @@ -182,7 +178,7 @@ interface Source { currentPath = currentPath.map { it.toLittleCamelCase() } } if (lowercased || isEnabled(Feature.LOAD_KEYS_CASE_INSENSITIVELY)) { - currentPath = currentPath.map { it.toLowerCase() } + currentPath = currentPath.map { it.lowercase(Locale.getDefault()) } } return currentPath } @@ -192,12 +188,12 @@ interface Source { } /** - * Returns sub-source in specified path. + * Returns sub-source in the specified path. * * Throws [NoSuchPathException] if there is no value in specified path. * * @param path item path - * @return sub-source in specified path + * @return sub-source in the specified path * @throws NoSuchPathException */ operator fun get(path: Path): Source = getOrNull(path) ?: throw NoSuchPathException(this, path) @@ -211,22 +207,22 @@ interface Source { operator fun contains(prefix: String): Boolean = contains(prefix.toPath()) /** - * Returns sub-source in specified path if this source contains value(s) in specified path, + * Returns sub-source in the specified path if this source contains value(s) in the specified path, * `null` otherwise. * * @param path item path - * @return sub-source in specified path if this source contains value(s) in specified path, + * @return sub-source in the specified path if this source contains value(s) in the specified path, * `null` otherwise */ fun getOrNull(path: String): Source? = getOrNull(path.toPath()) /** - * Returns sub-source in specified path. + * Returns sub-source in the specified path. * * Throws [NoSuchPathException] if there is no value in specified path. * * @param path item path - * @return sub-source in specified path + * @return sub-source in the specified path * @throws NoSuchPathException */ operator fun get(path: String): Source = get(path.toPath()) @@ -265,7 +261,7 @@ interface Source { * Returns a source backing by specified fallback source. * * When config fails to retrieve values from this source, it will try to retrieve them from - * fallback source. + * the fallback source. * * @param fallback fallback source * @return a source backing by specified fallback source @@ -288,7 +284,7 @@ interface Source { * * See [StringSubstitutor](https://commons.apache.org/proper/commons-text/apidocs/org/apache/commons/text/StringSubstitutor.html) * for detailed substitution rules. An exception is when the string is in reference format like `${path}`, - * the whole node will be replace by a reference to the sub-tree in the specified path. + * the whole node will be replaced by a reference to the subtree in the specified path. * * @param root the root source for substitution * @param enabled whether enabled or let the source decide by itself @@ -389,9 +385,9 @@ interface Source { } /** - * Returns a value casted from source. + * Returns a value cast from source. * - * @return a value casted from source + * @return a value cast from source */ inline fun Source.toValue(): T { return Config().withSource(this).toValue() @@ -445,22 +441,22 @@ private fun TreeNode.substituted( } private fun TreeNode.lowercased(): TreeNode { - if (this is ContainerNode) { - return withMap( + return if (this is ContainerNode) { + withMap( children.mapKeys { (key, _) -> - key.toLowerCase() + key.lowercase(Locale.getDefault()) }.mapValues { (_, child) -> child.lowercased() } ) } else { - return this + this } } private fun TreeNode.littleCamelCased(): TreeNode { - if (this is ContainerNode) { - return withMap( + return if (this is ContainerNode) { + withMap( children.mapKeys { (key, _) -> key.toLittleCamelCase() }.mapValues { (_, child) -> @@ -468,12 +464,12 @@ private fun TreeNode.littleCamelCased(): TreeNode { } ) } else { - return this + this } } class TreeLookup(val root: TreeNode, val source: Source, errorWhenUndefined: Boolean) : StringLookup { - val substitutor: StringSubstitutor = StringSubstitutor( + private val substitutor: StringSubstitutor = StringSubstitutor( StringLookupFactory.INSTANCE.interpolatorStringLookup(this) ).apply { isEnableSubstitutionInVariables = true @@ -519,7 +515,7 @@ open class BaseSource( ) : Source /** - * Information of source for debugging. + * Information of the source for debugging. */ class SourceInfo( private val info: MutableMap = mutableMapOf() @@ -665,8 +661,8 @@ private inline fun TreeNode.cast(source: Source): T { internal fun stringToBoolean(value: String): Boolean { return when { - value.toLowerCase() == "true" -> true - value.toLowerCase() == "false" -> false + value.lowercase(Locale.getDefault()) == "true" -> true + value.lowercase(Locale.getDefault()) == "false" -> false else -> throw ParseException("$value cannot be parsed to a boolean") } } @@ -924,18 +920,13 @@ private fun TreeNode.toValue(source: Source, type: JavaType, mapper: ObjectMappe ) } } else { - val value = castOrNull(source, clazz) - if (value != null) { - return value - } else { - try { - return mapper.readValue( - TreeTraversingParser(withoutPlaceHolder().toJsonNode(source), mapper), - type - ) - } catch (cause: JsonProcessingException) { - throw ObjectMappingException("${this.toHierarchical()} in ${source.description}", clazz, cause) - } + return castOrNull(source, clazz) ?: try { + mapper.readValue( + TreeTraversingParser(withoutPlaceHolder().toJsonNode(source), mapper), + type + ) + } catch (cause: JsonProcessingException) { + throw ObjectMappingException("${this.toHierarchical()} in ${source.description}", clazz, cause) } } } @@ -1086,13 +1077,15 @@ private fun implOf(clazz: Class<*>): Class<*> = else -> clazz } -fun Any.asTree(): TreeNode = +fun Any.asTree(): TreeNode = asTree("") + +fun Any.asTree(comment: String = ""): TreeNode = when (this) { is TreeNode -> this is Source -> this.tree is List<*> -> @Suppress("UNCHECKED_CAST") - ListSourceNode((this as List).map { it.asTree() }) + ListSourceNode((this as List).map { it.asTree() }, comments = comment) is Map<*, *> -> { when { this.size == 0 -> ContainerNode(mutableMapOf()) @@ -1101,7 +1094,7 @@ fun Any.asTree(): TreeNode = ContainerNode( (this as Map).mapValues { (_, value) -> value.asTree() - }.toMutableMap() + }.toMutableMap(), comments = comment ) } this.iterator().next().key!!::class in listOf( @@ -1116,13 +1109,13 @@ fun Any.asTree(): TreeNode = ContainerNode( (this as Map).map { (key, value) -> key.toString() to value.asTree() - }.toMap().toMutableMap() + }.toMap().toMutableMap(), comments = comment ) } - else -> ValueSourceNode(this) + else -> ValueSourceNode(this, comments = comment) } } - else -> ValueSourceNode(this) + else -> ValueSourceNode(this, comments = comment) } fun Any.asSource(type: String = "", info: SourceInfo = SourceInfo()): Source = diff --git a/src/main/kotlin/com/nhubbard/konfig/source/SourceException.kt b/src/main/kotlin/com/nhubbard/konfig/source/SourceException.kt index d3495956..7a04b611 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/SourceException.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/SourceException.kt @@ -23,7 +23,7 @@ import com.nhubbard.konfig.TreeNode import com.nhubbard.konfig.name /** - * Exception for source. + * Exception for sources. */ open class SourceException : ConfigException { constructor(message: String) : super(message) @@ -31,13 +31,13 @@ open class SourceException : ConfigException { } /** - * Exception indicates that actual type of value in source is unmatched with expected type. + * Exception indicates that the actual type of value in source is mismatched with the expected type. */ class WrongTypeException(val source: String, actual: String, expected: String) : SourceException("source $source has type $actual rather than $expected") /** - * Exception indicates that expected value in specified path is not existed in the source. + * Exception indicates that the expected value in the specified path is not existed in the source. */ class NoSuchPathException(val source: Source, val path: Path) : SourceException("cannot find path \"${path.name}\" in source ${source.description}") @@ -57,7 +57,7 @@ class UnsupportedTypeException(source: Source, clazz: Class<*>) : SourceException("value of type ${clazz.simpleName} is unsupported in source ${source.description}") /** - * Exception indicates that watch key is no longer valid for the source. + * Exception indicates that the watch key is no longer valid for the source. */ class InvalidWatchKeyException(source: Source) : SourceException("watch key for source ${source.description} is no longer valid") @@ -69,20 +69,20 @@ class InvalidRemoteRepoException(repo: String, dir: String) : SourceException("$repo is not in the remote list of $dir") /** - * Exception indicates failure to map source to value of specified class. + * Exception indicates failure to map the source to value of specified class. */ class ObjectMappingException(source: String, clazz: Class<*>, cause: Throwable) : SourceException("unable to map source $source to value of type ${clazz.simpleName}", cause) /** - * Exception indicates that value of specified class is unsupported as key of map. + * Exception indicates that the value of specified class is unsupported as a key of map. */ class UnsupportedMapKeyException(val clazz: Class<*>) : SourceException( "cannot support map with ${clazz.simpleName} key" ) /** - * Exception indicates failure to load specified path. + * Exception indicates failure to load the specified path. */ class LoadException(val path: Path, cause: Throwable) : SourceException("fail to load ${path.name}", cause) @@ -97,12 +97,12 @@ class UnknownPathsException(source: Source, val paths: List) : ) /** - * Exception indicates that specified source is not found. + * Exception indicates that the specified source is not found. */ class SourceNotFoundException(message: String) : SourceException(message) /** - * Exception indicates that specified source has unsupported extension. + * Exception indicates that the specified source has an unsupported extension. */ class UnsupportedExtensionException(source: String) : SourceException( "cannot detect supported extension for \"$source\"," + @@ -117,7 +117,7 @@ class UndefinedPathVariableException(val source: Source, val text: String) : Sou ) /** - * Exception indicates that the specified node has unsupported type. + * Exception indicates that the specified node has an unsupported type. */ class UnsupportedNodeTypeException(val source: Source, val node: TreeNode) : SourceException( "$node of type ${node::class.java.simpleName} in source ${source.description} is unsupported" diff --git a/src/main/kotlin/com/nhubbard/konfig/source/SourceNode.kt b/src/main/kotlin/com/nhubbard/konfig/source/SourceNode.kt index f3975e42..cc988370 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/SourceNode.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/SourceNode.kt @@ -26,23 +26,26 @@ interface SubstitutableNode : ValueNode { val originalValue: Any? } -class ValueSourceNode( +class ValueSourceNode @JvmOverloads constructor( override val value: Any, override val substituted: Boolean = false, - override val originalValue: Any? = null + override val originalValue: Any? = null, + override var comments: String = "" ) : SubstitutableNode { override fun substitute(value: String): TreeNode { - return ValueSourceNode(value, true, originalValue ?: this.value) + return ValueSourceNode(value, true, originalValue ?: this.value, this.comments) } } object NullSourceNode : NullNode { override val children: MutableMap = emptyMutableMap + override var comments: String = "" } -open class ListSourceNode( +open class ListSourceNode @JvmOverloads constructor( override val list: List, - override var isPlaceHolder: Boolean = false + override var isPlaceHolder: Boolean = false, + override var comments: String = "" ) : ListNode, MapNode { override val children: MutableMap get() = Collections.unmodifiableMap( @@ -50,6 +53,6 @@ open class ListSourceNode( ) override fun withList(list: List): ListNode { - return ListSourceNode(list) + return ListSourceNode(list, comments = this.comments) } } diff --git a/src/main/kotlin/com/nhubbard/konfig/source/Utils.kt b/src/main/kotlin/com/nhubbard/konfig/source/Utils.kt index 46b89c63..266f12fe 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/Utils.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/Utils.kt @@ -58,24 +58,15 @@ internal fun parseDuration(input: String): Long { unitString += "s" // note that this is deliberately case-sensitive - val units = if (unitString == "" || unitString == "ms" || unitString == "millis" || - unitString == "milliseconds" - ) { - TimeUnit.MILLISECONDS - } else if (unitString == "us" || unitString == "micros" || unitString == "microseconds") { - TimeUnit.MICROSECONDS - } else if (unitString == "ns" || unitString == "nanos" || unitString == "nanoseconds") { - TimeUnit.NANOSECONDS - } else if (unitString == "d" || unitString == "days") { - TimeUnit.DAYS - } else if (unitString == "h" || unitString == "hours") { - TimeUnit.HOURS - } else if (unitString == "s" || unitString == "seconds") { - TimeUnit.SECONDS - } else if (unitString == "m" || unitString == "minutes") { - TimeUnit.MINUTES - } else { - throw ParseException("Could not parse time unit '$originalUnitString' (try ns, us, ms, s, m, h, d)") + val units = when (unitString) { + "", "ms", "millis", "milliseconds" -> TimeUnit.MILLISECONDS + "us", "micros", "microseconds" -> TimeUnit.MICROSECONDS + "ns", "nanos", "nanoseconds" -> TimeUnit.NANOSECONDS + "d", "days" -> TimeUnit.DAYS + "h", "hours" -> TimeUnit.HOURS + "s", "seconds" -> TimeUnit.SECONDS + "m", "minutes" -> TimeUnit.MINUTES + else -> throw ParseException("Could not parse time unit '$originalUnitString' (try ns, us, ms, s, m, h, d)") } return try { diff --git a/src/main/kotlin/com/nhubbard/konfig/source/Writer.kt b/src/main/kotlin/com/nhubbard/konfig/source/Writer.kt index 2d6f8463..1afe9317 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/Writer.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/Writer.kt @@ -22,11 +22,11 @@ import java.io.OutputStream import java.io.StringWriter /** - * Save config to various output format. + * Save config to various output formats. */ interface Writer { /** - * Save to specified writer. + * Save to the specified writer. * * @param writer specified writer for writing character streams */ @@ -54,7 +54,7 @@ interface Writer { } /** - * Save to specified file path. + * Save to the specified file path. * * @param file specified file path * @param mkdirs create all parent folders before writing @@ -69,7 +69,7 @@ interface Writer { fun toText(): String = StringWriter().apply { toWriter(this) }.toString() /** - * Save to byte array. + * Save to a byte array. * * @return byte array */ diff --git a/src/main/kotlin/com/nhubbard/konfig/source/base/FlatSource.kt b/src/main/kotlin/com/nhubbard/konfig/source/base/FlatSource.kt index c964e290..9f3a157f 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/base/FlatSource.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/base/FlatSource.kt @@ -52,6 +52,7 @@ object EmptyStringNode : SubstitutableNode, ListNode { override val list: List = listOf() override val originalValue: Any? = null override val substituted: Boolean = false + override var comments: String = "" override fun substitute(value: String): TreeNode { check(value.isEmpty()) return this @@ -61,7 +62,8 @@ object EmptyStringNode : SubstitutableNode, ListNode { class SingleStringListNode( override val value: String, override val substituted: Boolean = false, - override val originalValue: Any? = null + override val originalValue: Any? = null, + override var comments: String = "" ) : SubstitutableNode, ListNode { override val children: MutableMap = Collections.unmodifiableMap( mutableMapOf("0" to value.asTree()) @@ -77,8 +79,9 @@ class SingleStringListNode( class ListStringNode( override val value: String, override val substituted: Boolean = false, - override val originalValue: Any? = null -) : ListSourceNode(value.split(',').map { ValueSourceNode(it) }), SubstitutableNode { + override val originalValue: Any? = null, + override var comments: String = "" +) : ListSourceNode(value.split(',').map { ValueSourceNode(it) }, comments = comments), SubstitutableNode { override fun substitute(value: String): TreeNode = value.promoteToList(true, originalValue ?: this.value) @@ -110,10 +113,10 @@ fun ContainerNode.promoteToList(): TreeNode { }.takeWhile { it != null }.filterNotNull().toList() - if (list.isNotEmpty() && list.toSet() == children.keys) { - return ListSourceNode(list.map { children[it]!! }) + return if (list.isNotEmpty() && list.toSet() == children.keys) { + ListSourceNode(list.map { children[it]!! }) } else { - return this + this } } @@ -121,7 +124,7 @@ fun ContainerNode.promoteToList(): TreeNode { * Returns a map in flat format for this config. * * The returned map contains all items in this config. - * This map can be loaded into config as [com.uchuhimo.konf.source.base.FlatSource] using + * This map can be loaded into config as [com.nhubbard.konfig.source.base.FlatSource] using * `config.from.map.flat(map)`. */ fun Config.toFlatMap(): Map { diff --git a/src/main/kotlin/com/nhubbard/konfig/source/base/MapSource.kt b/src/main/kotlin/com/nhubbard/konfig/source/base/MapSource.kt index d909fe9d..e5d3289a 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/base/MapSource.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/base/MapSource.kt @@ -33,7 +33,7 @@ open class MapSource( * Returns a hierarchical map for this config. * * The returned map contains all items in this config. - * This map can be loaded into config as [com.uchuhimo.konf.source.base.MapSource] using + * This map can be loaded into config as [com.nhubbard.konfig.source.base.MapSource] using * `config.from.map.hierarchical(map)`. */ @Suppress("UNCHECKED_CAST") @@ -49,10 +49,10 @@ fun Config.toHierarchicalMap(): Map { fun TreeNode.toHierarchical(): Any = withoutPlaceHolder().toHierarchicalInternal() private fun TreeNode.toHierarchicalInternal(): Any { - when (this) { - is ValueNode -> return value - is ListNode -> return list.map { it.toHierarchicalInternal() } - else -> return children.mapValues { (_, child) -> child.toHierarchicalInternal() } + return when (this) { + is ValueNode -> value + is ListNode -> list.map { it.toHierarchicalInternal() } + else -> children.mapValues { (_, child) -> child.toHierarchicalInternal() } } } diff --git a/src/main/kotlin/com/nhubbard/konfig/source/deserializer/DurationDeserializer.kt b/src/main/kotlin/com/nhubbard/konfig/source/deserializer/DurationDeserializer.kt index 08564272..9bea87bc 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/deserializer/DurationDeserializer.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/deserializer/DurationDeserializer.kt @@ -26,6 +26,8 @@ import java.time.Duration * Deserializer for [Duration]. */ object DurationDeserializer : JSR310Deserializer(Duration::class.java) { + private fun readResolve(): Any = DurationDeserializer + override fun parse(string: String): Duration { return try { Duration.parse(string) diff --git a/src/main/kotlin/com/nhubbard/konfig/source/deserializer/EmptyStringToCollectionDeserializerModifier.kt b/src/main/kotlin/com/nhubbard/konfig/source/deserializer/EmptyStringToCollectionDeserializerModifier.kt index 81b3bb99..dd47345d 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/deserializer/EmptyStringToCollectionDeserializerModifier.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/deserializer/EmptyStringToCollectionDeserializerModifier.kt @@ -28,13 +28,12 @@ import com.fasterxml.jackson.databind.type.CollectionType import com.fasterxml.jackson.databind.type.MapType object EmptyStringToCollectionDeserializerModifier : BeanDeserializerModifier() { - override fun modifyMapDeserializer( config: DeserializationConfig?, type: MapType?, beanDesc: BeanDescription?, deserializer: JsonDeserializer<*> - ): JsonDeserializer<*>? = + ): JsonDeserializer<*> = object : JsonDeserializer>(), ContextualDeserializer, ResolvableDeserializer { @Suppress("UNCHECKED_CAST") override fun deserialize(jp: JsonParser, ctx: DeserializationContext?): Map? { @@ -47,7 +46,7 @@ object EmptyStringToCollectionDeserializerModifier : BeanDeserializerModifier() override fun createContextual( ctx: DeserializationContext?, property: BeanProperty? - ): JsonDeserializer<*>? = + ): JsonDeserializer<*> = modifyMapDeserializer( config, type, @@ -66,7 +65,7 @@ object EmptyStringToCollectionDeserializerModifier : BeanDeserializerModifier() type: CollectionType?, beanDesc: BeanDescription?, deserializer: JsonDeserializer<*> - ): JsonDeserializer<*>? = + ): JsonDeserializer<*> = object : JsonDeserializer>(), ContextualDeserializer { @Suppress("UNCHECKED_CAST") override fun deserialize(jp: JsonParser, ctx: DeserializationContext?): Collection? { @@ -79,7 +78,7 @@ object EmptyStringToCollectionDeserializerModifier : BeanDeserializerModifier() override fun createContextual( ctx: DeserializationContext?, property: BeanProperty? - ): JsonDeserializer<*>? = + ): JsonDeserializer<*> = modifyCollectionDeserializer( config, type, @@ -96,7 +95,6 @@ object EmptyStringToCollectionDeserializerModifier : BeanDeserializerModifier() deserializer: JsonDeserializer<*> ): JsonDeserializer<*> = object : JsonDeserializer(), ContextualDeserializer { - @Suppress("UNCHECKED_CAST") override fun deserialize(jp: JsonParser, ctx: DeserializationContext?): Any? { if (!jp.isExpectedStartArrayToken && jp.hasToken(JsonToken.VALUE_STRING) && jp.text.isEmpty()) { val emptyValue = deserializer.getEmptyValue(ctx) @@ -112,7 +110,7 @@ object EmptyStringToCollectionDeserializerModifier : BeanDeserializerModifier() override fun createContextual( ctx: DeserializationContext?, property: BeanProperty? - ): JsonDeserializer<*>? = + ): JsonDeserializer<*> = modifyArrayDeserializer( config, valueType, diff --git a/src/main/kotlin/com/nhubbard/konfig/source/deserializer/OffsetDateTimeDeserializer.kt b/src/main/kotlin/com/nhubbard/konfig/source/deserializer/OffsetDateTimeDeserializer.kt index 30640f5c..db600d08 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/deserializer/OffsetDateTimeDeserializer.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/deserializer/OffsetDateTimeDeserializer.kt @@ -23,5 +23,6 @@ import java.time.OffsetDateTime * Deserializer for [OffsetDateTime]. */ object OffsetDateTimeDeserializer : JSR310Deserializer(OffsetDateTime::class.java) { + private fun readResolve(): Any = OffsetDateTimeDeserializer override fun parse(string: String): OffsetDateTime = OffsetDateTime.parse(string) } diff --git a/src/main/kotlin/com/nhubbard/konfig/source/deserializer/StringDeserializer.kt b/src/main/kotlin/com/nhubbard/konfig/source/deserializer/StringDeserializer.kt index 060d4bbb..719758d1 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/deserializer/StringDeserializer.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/deserializer/StringDeserializer.kt @@ -21,9 +21,12 @@ import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonToken import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.deser.impl.NullsConstantProvider import com.fasterxml.jackson.databind.deser.std.StringDeserializer as JacksonStringDeserializer object StringDeserializer : JacksonStringDeserializer() { + private fun readResolve(): Any = StringDeserializer + override fun _deserializeFromArray(p: JsonParser, ctxt: DeserializationContext): String? { val t = p.nextToken() if (t == JsonToken.END_ARRAY && ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)) { @@ -51,7 +54,7 @@ object StringDeserializer : JacksonStringDeserializer() { val str = if (t == JsonToken.VALUE_STRING) { p.text } else { - _parseString(p, ctxt) + _parseString(p, ctxt, NullsConstantProvider.nuller()) } if (sb.isEmpty()) { sb.append(str) diff --git a/src/main/kotlin/com/nhubbard/konfig/source/deserializer/ZoneDateTimeDeserializer.kt b/src/main/kotlin/com/nhubbard/konfig/source/deserializer/ZoneDateTimeDeserializer.kt index fca371c1..5133b7d8 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/deserializer/ZoneDateTimeDeserializer.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/deserializer/ZoneDateTimeDeserializer.kt @@ -23,5 +23,7 @@ import java.time.ZonedDateTime * Deserializer for [ZonedDateTime]. */ object ZoneDateTimeDeserializer : JSR310Deserializer(ZonedDateTime::class.java) { + private fun readResolve(): Any = ZoneDateTimeDeserializer + override fun parse(string: String): ZonedDateTime = ZonedDateTime.parse(string) } diff --git a/src/main/kotlin/com/nhubbard/konfig/source/git/DefaultGitLoader.kt b/src/main/kotlin/com/nhubbard/konfig/source/git/DefaultGitLoader.kt index 67ddfa1a..e67367b4 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/git/DefaultGitLoader.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/git/DefaultGitLoader.kt @@ -29,7 +29,7 @@ import kotlin.coroutines.CoroutineContext /** * Returns a child config containing values from a specified git repository. * - * Format of the url is auto-detected from the url extension. + * The format of the url is auto-detected from the url extension. * Supported url formats and the corresponding extensions: * - HOCON: conf * - JSON: json @@ -38,16 +38,15 @@ import kotlin.coroutines.CoroutineContext * - XML: xml * - YAML: yml, yaml * - * Throws [UnsupportedExtensionException] if the url extension is unsupported. + * Throws [com.nhubbard.konfig.source.UnsupportedExtensionException] if the url extension is unsupported. * * @param repo git repository * @param file file in the git repository * @param dir local directory of the git repository * @param branch the initial branch * @param optional whether the source is optional - * @param action additional action when cloning/pulling * @return a child config containing values from a specified git repository - * @throws UnsupportedExtensionException + * @throws com.nhubbard.konfig.source.UnsupportedExtensionException */ fun DefaultLoaders.git( repo: String, @@ -71,19 +70,19 @@ fun DefaultLoaders.git( * - XML: xml * - YAML: yml, yaml * - * Throws [UnsupportedExtensionException] if the url extension is unsupported. + * Throws [com.nhubbard.konfig.source.UnsupportedExtensionException] if the url extension is unsupported. * * @param repo git repository * @param file file in the git repository * @param dir local directory of the git repository * @param branch the initial branch * @param period reload period. The default value is 1. - * @param unit time unit of reload period. The default value is [TimeUnit.MINUTES]. + * @param unit time unit of the reload period. The default value is [TimeUnit.MINUTES]. * @param context context of the coroutine. The default value is [Dispatchers.Default]. * @param optional whether the source is optional * @param onLoad function invoked after the updated git file is loaded * @return a child config containing values from a specified git repository - * @throws UnsupportedExtensionException + * @throws com.nhubbard.konfig.source.UnsupportedExtensionException */ fun DefaultLoaders.watchGit( repo: String, diff --git a/src/main/kotlin/com/nhubbard/konfig/source/git/DefaultGitProvider.kt b/src/main/kotlin/com/nhubbard/konfig/source/git/DefaultGitProvider.kt index ecd42691..511edc80 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/git/DefaultGitProvider.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/git/DefaultGitProvider.kt @@ -34,7 +34,7 @@ import java.io.File * - XML: xml * - YAML: yml, yaml * - * Throws [UnsupportedExtensionException] if the url extension is unsupported. + * Throws [com.nhubbard.konfig.source.UnsupportedExtensionException] if the url extension is unsupported. * * @param repo git repository * @param file file in the git repository @@ -42,7 +42,7 @@ import java.io.File * @param branch the initial branch * @param optional whether the source is optional * @return a source from a specified git repository - * @throws UnsupportedExtensionException + * @throws com.nhubbard.konfig.source.UnsupportedExtensionException */ fun DefaultProviders.git( repo: String, diff --git a/src/main/kotlin/com/nhubbard/konfig/source/git/GitLoader.kt b/src/main/kotlin/com/nhubbard/konfig/source/git/GitLoader.kt index b88a65c9..3bec5a69 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/git/GitLoader.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/git/GitLoader.kt @@ -21,10 +21,7 @@ import com.nhubbard.konfig.Config import com.nhubbard.konfig.source.Loader import com.nhubbard.konfig.source.Source import com.nhubbard.konfig.tempDirectory -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import org.eclipse.jgit.lib.Constants import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext @@ -81,7 +78,7 @@ fun Loader.watchGit( load(source) } onLoad?.invoke(newConfig, source) - GlobalScope.launch(context) { + MainScope().launch(context) { while (true) { delay(unit.toMillis(period)) val newSource = provider.git(repo, file, directory, branch, optional) diff --git a/src/main/kotlin/com/nhubbard/konfig/source/hocon/DefaultHoconProvider.kt b/src/main/kotlin/com/nhubbard/konfig/source/hocon/DefaultHoconProvider.kt index c804905d..9d775a25 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/hocon/DefaultHoconProvider.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/hocon/DefaultHoconProvider.kt @@ -15,6 +15,8 @@ * limitations under the License. */ +@file:Suppress("UnusedReceiverParameter") + package com.nhubbard.konfig.source.hocon import com.nhubbard.konfig.source.DefaultProviders diff --git a/src/main/kotlin/com/nhubbard/konfig/source/hocon/HoconWriter.kt b/src/main/kotlin/com/nhubbard/konfig/source/hocon/HoconWriter.kt index d4cad8d4..9f0aad90 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/hocon/HoconWriter.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/hocon/HoconWriter.kt @@ -17,11 +17,12 @@ package com.nhubbard.konfig.source.hocon +import com.nhubbard.konfig.* import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigValueFactory -import com.nhubbard.konfig.Config import com.nhubbard.konfig.source.Writer import com.nhubbard.konfig.source.base.toHierarchicalMap +import com.typesafe.config.ConfigValue import java.io.OutputStream /** @@ -43,9 +44,24 @@ class HoconWriter(val config: Config) : Writer { } } + private fun TreeNode.toConfigValue(): ConfigValue { + val value = when (this) { + is ValueNode -> ConfigValueFactory.fromAnyRef(value) + is ListNode -> ConfigValueFactory.fromIterable(list.map { it.toConfigValue() }) + else -> ConfigValueFactory.fromMap(children.mapValues { (_, value) -> value.toConfigValue() }) + } + return comments.takeIf { it.isNotEmpty() }?.let { + value.withOrigin(value.origin().withComments(it.split("\n"))) + } ?: value + } + override fun toText(): String { - return ConfigValueFactory.fromMap(config.toHierarchicalMap()).render(renderOpts) - .replace("\n", System.lineSeparator()) + val output = if (config.isEnabled(Feature.WRITE_DESCRIPTIONS_AS_COMMENTS)) { + config.toTree().toConfigValue().render(renderOpts.setComments(true)) + } else { + ConfigValueFactory.fromMap(config.toHierarchicalMap()).render(renderOpts) + } + return output.replace("\n", System.lineSeparator()) } } diff --git a/src/main/kotlin/com/nhubbard/konfig/source/js/DefaultJsProvider.kt b/src/main/kotlin/com/nhubbard/konfig/source/js/DefaultJsProvider.kt index 0c554013..19255c4c 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/js/DefaultJsProvider.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/js/DefaultJsProvider.kt @@ -15,6 +15,8 @@ * limitations under the License. */ +@file:Suppress("UnusedReceiverParameter", "unused") + package com.nhubbard.konfig.source.js import com.nhubbard.konfig.source.DefaultProviders diff --git a/src/main/kotlin/com/nhubbard/konfig/source/properties/PropertiesProvider.kt b/src/main/kotlin/com/nhubbard/konfig/source/properties/PropertiesProvider.kt index d7e28dff..1b3c94d1 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/properties/PropertiesProvider.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/properties/PropertiesProvider.kt @@ -26,7 +26,7 @@ import java.io.Reader import java.util.* /** - * Provider for properties source. + * Provider for the properties source. */ object PropertiesProvider : Provider { @Suppress("UNCHECKED_CAST") diff --git a/src/main/kotlin/com/nhubbard/konfig/source/properties/PropertiesWriter.kt b/src/main/kotlin/com/nhubbard/konfig/source/properties/PropertiesWriter.kt index f4487f32..e7893327 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/properties/PropertiesWriter.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/properties/PropertiesWriter.kt @@ -44,7 +44,7 @@ private class NoCommentProperties : Properties() { override fun write(b: Int) { if (firstLineSeen) { super.write(b) - } else if (b == '\n'.toInt()) { + } else if (b == '\n'.code) { firstLineSeen = true } } @@ -67,6 +67,6 @@ private class NoCommentProperties : Properties() { } /** - * Returns writer for properties source. + * Returns writer for the properties source. */ val Config.toProperties: Writer get() = PropertiesWriter(this) diff --git a/src/main/kotlin/com/nhubbard/konfig/source/toml/DefaultTomlLoader.kt b/src/main/kotlin/com/nhubbard/konfig/source/toml/DefaultTomlLoader.kt index c23f92f5..b8a7f11e 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/toml/DefaultTomlLoader.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/toml/DefaultTomlLoader.kt @@ -21,6 +21,6 @@ import com.nhubbard.konfig.source.DefaultLoaders import com.nhubbard.konfig.source.Loader /** - * Loader for TOML source. + * Loader for the TOML source. */ val DefaultLoaders.toml get() = Loader(config, TomlProvider.orMapped()) diff --git a/src/main/kotlin/com/nhubbard/konfig/source/toml/DefaultTomlProvider.kt b/src/main/kotlin/com/nhubbard/konfig/source/toml/DefaultTomlProvider.kt index e400c765..2a391fb0 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/toml/DefaultTomlProvider.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/toml/DefaultTomlProvider.kt @@ -15,6 +15,8 @@ * limitations under the License. */ +@file:Suppress("UnusedReceiverParameter") + package com.nhubbard.konfig.source.toml import com.nhubbard.konfig.source.DefaultProviders diff --git a/src/main/kotlin/com/nhubbard/konfig/source/toml/TomlProvider.kt b/src/main/kotlin/com/nhubbard/konfig/source/toml/TomlProvider.kt index 0e3181ea..77151f8e 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/toml/TomlProvider.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/toml/TomlProvider.kt @@ -27,7 +27,7 @@ import java.io.InputStream import java.io.Reader /** - * Provider for TOML source. + * Provider for the TOML source. */ @RegisterExtension(["toml"]) object TomlProvider : Provider { diff --git a/src/main/kotlin/com/nhubbard/konfig/source/toml/TomlWriter.kt b/src/main/kotlin/com/nhubbard/konfig/source/toml/TomlWriter.kt index 165330e7..000a5f55 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/toml/TomlWriter.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/toml/TomlWriter.kt @@ -19,6 +19,7 @@ package com.nhubbard.konfig.source.toml import com.moandjiezana.toml.Toml4jWriter import com.nhubbard.konfig.Config +import com.nhubbard.konfig.Feature import com.nhubbard.konfig.source.Writer import com.nhubbard.konfig.source.base.toHierarchicalMap import java.io.OutputStream @@ -40,7 +41,12 @@ class TomlWriter(val config: Config) : Writer { } override fun toText(): String { - return toml4jWriter.write(config.toHierarchicalMap()).replace("\n", System.lineSeparator()) + val text = if (config.isEnabled(Feature.WRITE_DESCRIPTIONS_AS_COMMENTS)) { + toml4jWriter.write(config.toTree()) + } else { + toml4jWriter.write(config.toHierarchicalMap()) + } + return text.replace("\n", System.lineSeparator()) } } diff --git a/src/main/kotlin/com/nhubbard/konfig/source/xml/DefaultXmlLoader.kt b/src/main/kotlin/com/nhubbard/konfig/source/xml/DefaultXmlLoader.kt index 4394597d..154b2063 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/xml/DefaultXmlLoader.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/xml/DefaultXmlLoader.kt @@ -21,6 +21,6 @@ import com.nhubbard.konfig.source.DefaultLoaders import com.nhubbard.konfig.source.Loader /** - * Loader for XML source. + * Loader for the XML source. */ val DefaultLoaders.xml get() = Loader(config, XmlProvider.orMapped()) diff --git a/src/main/kotlin/com/nhubbard/konfig/source/xml/DefaultXmlProvider.kt b/src/main/kotlin/com/nhubbard/konfig/source/xml/DefaultXmlProvider.kt index 48d956ed..f8225a17 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/xml/DefaultXmlProvider.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/xml/DefaultXmlProvider.kt @@ -15,6 +15,8 @@ * limitations under the License. */ +@file:Suppress("UnusedReceiverParameter") + package com.nhubbard.konfig.source.xml import com.nhubbard.konfig.source.DefaultProviders diff --git a/src/main/kotlin/com/nhubbard/konfig/source/xml/XmlProvider.kt b/src/main/kotlin/com/nhubbard/konfig/source/xml/XmlProvider.kt index ab6435c2..fa064472 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/xml/XmlProvider.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/xml/XmlProvider.kt @@ -28,7 +28,7 @@ import java.io.InputStream import java.io.Reader /** - * Provider for XML source. + * Provider for the XML source. */ @RegisterExtension(["xml"]) object XmlProvider : Provider { diff --git a/src/main/kotlin/com/nhubbard/konfig/source/xml/XmlWriter.kt b/src/main/kotlin/com/nhubbard/konfig/source/xml/XmlWriter.kt index e68ae8e6..55b39a0a 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/xml/XmlWriter.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/xml/XmlWriter.kt @@ -27,7 +27,7 @@ import org.dom4j.io.XMLWriter import java.io.OutputStream /** - * Writer for XML source. + * Writer for the XML source. */ class XmlWriter(val config: Config) : Writer { private fun Map.toDocument(): Document { @@ -59,6 +59,6 @@ class XmlWriter(val config: Config) : Writer { } /** - * Returns writer for XML source. + * Returns writer for the XML source. */ val Config.toXml: Writer get() = XmlWriter(this) diff --git a/src/main/kotlin/com/nhubbard/konfig/source/yaml/DefaultYamlProvider.kt b/src/main/kotlin/com/nhubbard/konfig/source/yaml/DefaultYamlProvider.kt index 8746ad82..8af5703f 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/yaml/DefaultYamlProvider.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/yaml/DefaultYamlProvider.kt @@ -15,6 +15,8 @@ * limitations under the License. */ +@file:Suppress("UnusedReceiverParameter") + package com.nhubbard.konfig.source.yaml import com.nhubbard.konfig.source.DefaultProviders diff --git a/src/main/kotlin/com/nhubbard/konfig/source/yaml/YamlProvider.kt b/src/main/kotlin/com/nhubbard/konfig/source/yaml/YamlProvider.kt index c670c935..0fe72e61 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/yaml/YamlProvider.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/yaml/YamlProvider.kt @@ -29,6 +29,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor import org.yaml.snakeyaml.nodes.Node import org.yaml.snakeyaml.nodes.ScalarNode import org.yaml.snakeyaml.nodes.Tag +import java.io.Closeable import java.io.InputStream import java.io.Reader @@ -38,22 +39,24 @@ import java.io.Reader @RegisterExtension(["yml", "yaml"]) object YamlProvider : Provider { override fun reader(reader: Reader): Source { - val yaml = Yaml(YamlConstructor()) - val value = yaml.load(reader) - if (value == "null") { - return mapOf().asSource("YAML") - } else { - return value.asSource("YAML") - } + return load(reader) } override fun inputStream(inputStream: InputStream): Source { + return load(inputStream) + } + + private fun load(input: Any): Source { val yaml = Yaml(YamlConstructor()) - val value = yaml.load(inputStream) - if (value == "null") { - return mapOf().asSource("YAML") + val value: Any = when (input) { + is Reader -> yaml.load(input) + is InputStream -> yaml.load(input) + else -> error("This is an impossible condition.") + } + return if (value == "null") { + mapOf().asSource("YAML") } else { - return value.asSource("YAML") + value.asSource("YAML") } } diff --git a/src/main/kotlin/com/nhubbard/konfig/source/yaml/YamlWriter.kt b/src/main/kotlin/com/nhubbard/konfig/source/yaml/YamlWriter.kt index c4232689..e7ac08d3 100644 --- a/src/main/kotlin/com/nhubbard/konfig/source/yaml/YamlWriter.kt +++ b/src/main/kotlin/com/nhubbard/konfig/source/yaml/YamlWriter.kt @@ -17,31 +17,18 @@ package com.nhubbard.konfig.source.yaml -import com.nhubbard.konfig.Config +import com.nhubbard.konfig.* import com.nhubbard.konfig.source.Writer -import com.nhubbard.konfig.source.base.toHierarchicalMap -import org.yaml.snakeyaml.DumperOptions -import org.yaml.snakeyaml.LoaderOptions -import org.yaml.snakeyaml.Yaml -import org.yaml.snakeyaml.constructor.SafeConstructor -import org.yaml.snakeyaml.representer.Representer import java.io.OutputStream +import java.io.Writer as JWriter /** * Writer for YAML source. */ class YamlWriter(val config: Config) : Writer { - private val yaml = Yaml( - SafeConstructor(LoaderOptions()), - Representer(DumperOptions()), - DumperOptions().apply { - defaultFlowStyle = DumperOptions.FlowStyle.BLOCK - lineBreak = DumperOptions.LineBreak.getPlatformLineBreak() - } - ) - - override fun toWriter(writer: java.io.Writer) { - yaml.dump(config.toHierarchicalMap(), writer) + override fun toWriter(writer: JWriter) { + val nodeWriter = YamlTreeNodeWriter(writer, config.isEnabled(Feature.WRITE_DESCRIPTIONS_AS_COMMENTS)) + nodeWriter.write(config.toTree()) } override fun toOutputStream(outputStream: OutputStream) { @@ -51,6 +38,171 @@ class YamlWriter(val config: Config) : Writer { } } +private class YamlTreeNodeWriter( + private val writer: JWriter, + private val writeComments: Boolean = false +) { + private val indentSize = 2 + private var ident = 0 + + private fun increaseIndent() { + this.ident += this.indentSize + } + + private fun decreaseIndent() { + this.ident -= this.indentSize + } + + private fun writeIndent() { + repeat(this.ident) { + this.writer.write(' '.code) + } + } + + private fun write(char: Char) { + this.writer.write(char.code) + } + + private fun write(string: String) { + this.writer.write(string) + } + + private fun writeNewLine() { + write('\n') + } + + fun write(node: TreeNode) { + write(node, false) + } + + private fun write(node: TreeNode, inList: Boolean = false) { + when (node) { + is ValueNode -> writeValue(node) + is ListNode -> writeList(node, inList) + else -> writeMap(node, inList) + } + } + + private fun writeComments(node: TreeNode) { + if (!this.writeComments || node.comments.isEmpty()) + return + val comments = node.comments.split("\n") + comments.forEach { comment -> + writeIndent() + write("# $comment") + writeNewLine() + } + } + + private fun shouldWriteComments(node: TreeNode) = this.writeComments && node.comments.isNotEmpty() + + private fun writeValue(node: ValueNode) { + writeStringValue(node.value.toString()) + } + + private fun writeStringValue(string: String) { + val lines = string.split("\n") + if (lines.size > 1) { + // Multiline + write('|') + writeNewLine() + increaseIndent() + lines.forEach { line -> + writeIndent() + write(line) + writeNewLine() + } + decreaseIndent() + } else { + write(quoteValueIfNeeded(string)) + writeNewLine() + } + } + + private fun writeList(node: ListNode, inList: Boolean = false) { + val list = node.list + if (list.isEmpty()) { + write(" []") + writeNewLine() + } else { + increaseIndent() + var first = true + list.forEach { element -> + val firstListInListEntry = first && inList && !shouldWriteComments(list[0]) + if (!firstListInListEntry) { + if (first) + writeNewLine() + writeComments(element) + writeIndent() + } + first = false + write("- ") + write(element, inList = true) + } + decreaseIndent() + } + } + + private fun writeMap(node: TreeNode, inList: Boolean = false) { + val map = node.children + if (map.isEmpty()) { + write(" {}") + writeNewLine() + } else { + var first = true + if (inList) + increaseIndent() + map.forEach { (name, node) -> + writeEntry(name, node, inList, first) + first = false + } + if (inList) + decreaseIndent() + } + } + + private fun quoteString(s: String) = "\"${s.replace("\"", "\\\"")}\"" + + private fun hasQuoteChar(s: String) = '\"' in s || '\'' in s + + private fun hasTrailingWhitespace(s: String) = s.isNotEmpty() && (s.first() == ' ' || s.last() == ' ') + + private fun quoteValueIfNeeded(s: String): String { + if (s.isEmpty()) + return s + if (s.last() == ':' || hasTrailingWhitespace(s) || hasQuoteChar(s)) + return quoteString(s) + return s + } + + private fun writeEntry(name: String, node: TreeNode, first: Boolean = false, inList: Boolean = false) { + val firstListEntry = first && inList + if (!firstListEntry || shouldWriteComments(node)) { + if (firstListEntry) + writeNewLine() + writeComments(node) + writeIndent() + } + write(quoteValueIfNeeded(name)) + write(':') + when (node) { + is ValueNode -> { + write(' ') + writeValue(node) + } + is ListNode -> { + writeList(node) + } + else -> { + writeNewLine() + increaseIndent() + writeMap(node) + decreaseIndent() + } + } + } +} + /** * Returns writer for YAML source. */ diff --git a/src/snippet/kotlin/com/nhubbard/konfig/snippet/Config.kt b/src/snippet/kotlin/com/nhubbard/konfig/snippet/Config.kt index 5f22c4bd..ace1496b 100644 --- a/src/snippet/kotlin/com/nhubbard/konfig/snippet/Config.kt +++ b/src/snippet/kotlin/com/nhubbard/konfig/snippet/Config.kt @@ -15,22 +15,25 @@ * limitations under the License. */ +@file:Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + package com.nhubbard.konfig.snippet import com.nhubbard.konfig.Config import com.nhubbard.konfig.ConfigSpec -fun main(args: Array) { +fun main() { val config = Config() config.addSpec(Server) + var host: String run { - val host = config[Server.host] + host = config[Server.host] } run { - val host = config.get("server.host") + host = config["server.host"] } run { - val host = config("server.host") + host = config("server.host") } config.contains(Server.host) // or @@ -52,7 +55,7 @@ fun main(args: Array) { handler.cancel() } run { - val handler = Server.host.beforeSet { config, value -> println("the host will change to $value") } + val handler = Server.host.beforeSet { _, value -> println("the host will change to $value") } handler.cancel() } run { @@ -60,7 +63,7 @@ fun main(args: Array) { handler.cancel() } run { - val handler = Server.host.afterSet { config, value -> println("the host has changed to $value") } + val handler = Server.host.afterSet { _, value -> println("the host has changed to $value") } handler.cancel() } run { diff --git a/src/snippet/kotlin/com/nhubbard/konfig/snippet/Export.kt b/src/snippet/kotlin/com/nhubbard/konfig/snippet/Export.kt index ef9a2620..58b2acbd 100644 --- a/src/snippet/kotlin/com/nhubbard/konfig/snippet/Export.kt +++ b/src/snippet/kotlin/com/nhubbard/konfig/snippet/Export.kt @@ -15,6 +15,8 @@ * limitations under the License. */ +@file:Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + package com.nhubbard.konfig.snippet import com.nhubbard.konfig.Config @@ -23,17 +25,18 @@ import com.nhubbard.konfig.source.base.toHierarchicalMap import com.nhubbard.konfig.source.json.toJson import com.nhubbard.konfig.tempFile -fun main(args: Array) { +fun main() { val config = Config { addSpec(Server) } config[Server.tcpPort] = 1000 + var map: Map run { - val map = config.toMap() + map = config.toMap() } run { - val map = config.toHierarchicalMap() + map = config.toHierarchicalMap() } run { - val map = config.toFlatMap() + map = config.toFlatMap() } val file = tempFile(suffix = ".json") config.toJson.toFile(file) diff --git a/src/snippet/kotlin/com/nhubbard/konfig/snippet/Fork.kt b/src/snippet/kotlin/com/nhubbard/konfig/snippet/Fork.kt index 8bb2f871..44bb64e2 100644 --- a/src/snippet/kotlin/com/nhubbard/konfig/snippet/Fork.kt +++ b/src/snippet/kotlin/com/nhubbard/konfig/snippet/Fork.kt @@ -19,7 +19,7 @@ package com.nhubbard.konfig.snippet import com.nhubbard.konfig.Config -fun main(args: Array) { +fun main() { val config = Config { addSpec(Server) } config[Server.tcpPort] = 1000 // fork from parent config diff --git a/src/snippet/kotlin/com/nhubbard/konfig/snippet/Load.kt b/src/snippet/kotlin/com/nhubbard/konfig/snippet/Load.kt index 4f0f5075..6ccce469 100644 --- a/src/snippet/kotlin/com/nhubbard/konfig/snippet/Load.kt +++ b/src/snippet/kotlin/com/nhubbard/konfig/snippet/Load.kt @@ -19,7 +19,7 @@ package com.nhubbard.konfig.snippet import com.nhubbard.konfig.Config -fun main(args: Array) { +fun main() { val config = Config { addSpec(Server) } // values in source is loaded into new layer in child config val childConfig = config.from.env() diff --git a/src/snippet/kotlin/com/nhubbard/konfig/snippet/QuickStart.kt b/src/snippet/kotlin/com/nhubbard/konfig/snippet/QuickStart.kt index 5f1a2ebb..5320e129 100644 --- a/src/snippet/kotlin/com/nhubbard/konfig/snippet/QuickStart.kt +++ b/src/snippet/kotlin/com/nhubbard/konfig/snippet/QuickStart.kt @@ -30,7 +30,7 @@ object ServerSpec : ConfigSpec() { val tcpPort by required() } -fun main(args: Array) { +fun main() { val file = File("server.yml") //language=YAML file.writeText( @@ -41,13 +41,13 @@ fun main(args: Array) { """.trimIndent() ) file.deleteOnExit() - val config = Config { addSpec(ServerSpec) } + var config = Config { addSpec(ServerSpec) } .from.yaml.file("server.yml") .from.json.resource("server.json") .from.env() .from.systemProperties() run { - val config = Config { addSpec(ServerSpec) }.withSource( + config = Config { addSpec(ServerSpec) }.withSource( Source.from.yaml.file("server.yml") + Source.from.json.resource("server.json") + Source.from.env() + @@ -55,16 +55,16 @@ fun main(args: Array) { ) } run { - val config = Config { addSpec(ServerSpec) } + config = Config { addSpec(ServerSpec) } .from.yaml.watchFile("server.yml") .from.json.resource("server.json") .from.env() .from.systemProperties() } - val server = Server(config[ServerSpec.host], config[ServerSpec.tcpPort]) + var server = Server(config[ServerSpec.host], config[ServerSpec.tcpPort]) server.start() run { - val server = Config() + server = Config() .from.yaml.file("server.yml") .from.json.resource("server.json") .from.env() @@ -74,7 +74,7 @@ fun main(args: Array) { server.start() } run { - val server = ( + server = ( Source.from.yaml.file("server.yml") + Source.from.json.resource("server.json") + Source.from.env() + diff --git a/src/snippet/kotlin/com/nhubbard/konfig/snippet/Serialize.kt b/src/snippet/kotlin/com/nhubbard/konfig/snippet/Serialize.kt index 2e678f22..2db38124 100644 --- a/src/snippet/kotlin/com/nhubbard/konfig/snippet/Serialize.kt +++ b/src/snippet/kotlin/com/nhubbard/konfig/snippet/Serialize.kt @@ -22,7 +22,7 @@ import com.nhubbard.konfig.tempFile import java.io.ObjectInputStream import java.io.ObjectOutputStream -fun main(args: Array) { +fun main() { val config = Config { addSpec(Server) } config[Server.tcpPort] = 1000 val map = config.toMap()