From ae79a9b4a41595e7817f025df129592cc2d26a2d Mon Sep 17 00:00:00 2001 From: Seppe Volkaerts Date: Sun, 2 Feb 2020 16:13:58 +0100 Subject: [PATCH 1/7] Add support for descriptions as config comments for hocon. --- .../kotlin/com/uchuhimo/konf/BaseConfig.kt | 24 ++++++++- .../main/kotlin/com/uchuhimo/konf/Config.kt | 18 ++++--- .../main/kotlin/com/uchuhimo/konf/Feature.kt | 9 +++- .../main/kotlin/com/uchuhimo/konf/TreeNode.kt | 19 +++++-- .../kotlin/com/uchuhimo/konf/source/Source.kt | 15 +++--- .../com/uchuhimo/konf/source/SourceNode.kt | 16 ++++-- .../uchuhimo/konf/source/base/FlatSource.kt | 9 ++-- .../uchuhimo/konf/source/base/MapSource.kt | 51 +++++++++++++++++-- .../uchuhimo/konf/source/hocon/HoconWriter.kt | 27 +++++++++- 9 files changed, 156 insertions(+), 32 deletions(-) diff --git a/konf-core/src/main/kotlin/com/uchuhimo/konf/BaseConfig.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/BaseConfig.kt index 2a684174..ab55b147 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/BaseConfig.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/BaseConfig.kt @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.uchuhimo.konf.source.Source +import com.uchuhimo.konf.source.asTree import com.uchuhimo.konf.source.base.EmptyMapSource import com.uchuhimo.konf.source.deserializer.DurationDeserializer import com.uchuhimo.konf.source.deserializer.EmptyStringToCollectionDeserializerModifier @@ -164,6 +165,23 @@ open class BaseConfig( } } + 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 + } + val description = item.description + val comment = if (description.isEmpty()) null else description + set(name, value.asTree(comment)) + } + } + } + } + @Suppress("UNCHECKED_CAST") override fun get(item: Item): T = getOrNull(item, errorWhenNotFound = true) as T @@ -611,7 +629,11 @@ 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 val comments: String? + get() = if (this.item.description.isEmpty()) null else this.item.description + } data class Value(var value: T) diff --git a/konf-core/src/main/kotlin/com/uchuhimo/konf/Config.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/Config.kt index 4b6f2c34..70608a11 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/Config.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/Config.kt @@ -307,6 +307,15 @@ interface Config : ItemContainer { */ 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. * @@ -477,11 +486,8 @@ open class LazyConfigProperty( } } -/** - * Convert the config to a tree node. - * - * @return a tree node - */ +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") +@Deprecated(message = "Use method in Config.", replaceWith = ReplaceWith("toTree()")) fun Config.toTree(): TreeNode { - return toMap().kvToTree() + return toTree() } diff --git a/konf-core/src/main/kotlin/com/uchuhimo/konf/Feature.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/Feature.kt index 75e0ffb2..74a686e9 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/Feature.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/Feature.kt @@ -45,5 +45,12 @@ enum class Feature(val enabledByDefault: Boolean) { * * 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. + * + * Feature is disabled by default. + */ + WRITE_DESCRIPTIONS_AS_COMMENTS(false) } diff --git a/konf-core/src/main/kotlin/com/uchuhimo/konf/TreeNode.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/TreeNode.kt index 83e9c627..a20071a6 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/TreeNode.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/TreeNode.kt @@ -292,9 +292,17 @@ interface TreeNode { } } -interface LeafNode : TreeNode +/** + * A node which can hold a comment, if any. + */ +interface CommentableNode : TreeNode { + + val comments: String? +} -interface MapNode : TreeNode { +interface LeafNode : TreeNode, CommentableNode + +interface MapNode : TreeNode, CommentableNode { fun withMap(map: Map): MapNode = throw NotImplementedError() var isPlaceHolder: Boolean } @@ -319,8 +327,12 @@ interface ListNode : LeafNode { */ open class ContainerNode( override val children: MutableMap, - override var isPlaceHolder: Boolean = false + override var isPlaceHolder: Boolean = false, + override val comments: String? = null ) : MapNode { + + constructor(children: MutableMap, isPlaceHolder: Boolean = false) : this(children, isPlaceHolder, null) + override fun withMap(map: Map): MapNode { val isPlaceHolder = map.isEmpty() && this.isPlaceHolder if (map is MutableMap) { @@ -341,4 +353,5 @@ open class ContainerNode( */ object EmptyNode : LeafNode { override val children: MutableMap = emptyMutableMap + override val comments: String? = null } diff --git a/konf-core/src/main/kotlin/com/uchuhimo/konf/source/Source.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/source/Source.kt index 87c4b716..989fe39f 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/source/Source.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/source/Source.kt @@ -56,7 +56,6 @@ import com.uchuhimo.konf.annotation.JavaApi import com.uchuhimo.konf.source.base.ListStringNode import com.uchuhimo.konf.source.base.toHierarchical import com.uchuhimo.konf.toPath -import com.uchuhimo.konf.toTree import com.uchuhimo.konf.toValue import java.lang.reflect.InvocationTargetException import java.math.BigDecimal @@ -973,13 +972,15 @@ private fun implOf(clazz: Class<*>): Class<*> = else -> clazz } -fun Any.asTree(): TreeNode = +fun Any.asTree(): TreeNode = asTree(null) + +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()) @@ -987,7 +988,7 @@ fun Any.asTree(): TreeNode = @Suppress("UNCHECKED_CAST") (ContainerNode((this as Map).mapValues { (_, value) -> value.asTree() - }.toMutableMap())) + }.toMutableMap(), comments = comment)) } this.iterator().next().key!!::class in listOf( Char::class, @@ -1000,12 +1001,12 @@ fun Any.asTree(): TreeNode = @Suppress("UNCHECKED_CAST") (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/konf-core/src/main/kotlin/com/uchuhimo/konf/source/SourceNode.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/source/SourceNode.kt index 45eb771c..be24a554 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/source/SourceNode.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/source/SourceNode.kt @@ -33,26 +33,34 @@ interface SubstitutableNode : ValueNode { class ValueSourceNode( override val value: Any, override val substituted: Boolean = false, - override val originalValue: Any? = null + override val originalValue: Any? = null, + override val comments: String? = null ) : SubstitutableNode { + + constructor(value: Any, substituted: Boolean = false, originalValue: Any? = null) : this(value, substituted, originalValue, null) + 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 val comments: String? = null } open class ListSourceNode( override val list: List, - override var isPlaceHolder: Boolean = false + override var isPlaceHolder: Boolean = false, + override val comments: String? = null ) : ListNode, MapNode { override val children: MutableMap get() = Collections.unmodifiableMap( list.withIndex().associate { (key, value) -> key.toString() to value }) + constructor(list: List, isPlaceHolder: Boolean = false) : this(list, isPlaceHolder, null) + override fun withList(list: List): ListNode { - return ListSourceNode(list) + return ListSourceNode(list, comments = this.comments) } } diff --git a/konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/FlatSource.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/FlatSource.kt index b4507d01..2d0dc655 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/FlatSource.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/FlatSource.kt @@ -62,6 +62,7 @@ object EmptyStringNode : SubstitutableNode, ListNode { override val list: List = listOf() override val originalValue: Any? = null override val substituted: Boolean = false + override val comments: String? = null override fun substitute(value: String): TreeNode { check(value.isEmpty()) return this @@ -71,7 +72,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 val comments: String? = null ) : SubstitutableNode, ListNode { override val children: MutableMap = Collections.unmodifiableMap( mutableMapOf("0" to value.asTree())) @@ -83,8 +85,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 val comments: String? = null +) : ListSourceNode(value.split(',').map { ValueSourceNode(it) }, comments = comments), SubstitutableNode { override fun substitute(value: String): TreeNode = value.promoteToList(true, originalValue ?: this.value) diff --git a/konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/MapSource.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/MapSource.kt index ada21d69..f9bd90db 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/MapSource.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/MapSource.kt @@ -16,13 +16,13 @@ package com.uchuhimo.konf.source.base +import com.uchuhimo.konf.CommentableNode import com.uchuhimo.konf.Config import com.uchuhimo.konf.ListNode import com.uchuhimo.konf.TreeNode import com.uchuhimo.konf.ValueNode import com.uchuhimo.konf.notEmptyOr import com.uchuhimo.konf.source.SourceInfo -import com.uchuhimo.konf.toTree /** * Source from a hierarchical map. @@ -45,6 +45,17 @@ fun Config.toHierarchicalMap(): Map { return toTree().toHierarchical() as Map } +/** + * 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 + * `config.from.map.hierarchical(map)`. + */ +fun Config.toHierarchicalMapNode(): HierarchicalTreeNode.Map { + return toTree().toHierarchicalNode() as HierarchicalTreeNode.Map +} + /** * Returns a hierarchical value for this tree node. * @@ -52,14 +63,44 @@ fun Config.toHierarchicalMap(): Map { */ fun TreeNode.toHierarchical(): Any = withoutPlaceHolder().toHierarchicalInternal() +/** + * Returns a hierarchical value for this tree node. + * + * The returned value contains all items in this tree node. + */ +fun TreeNode.toHierarchicalNode(): HierarchicalTreeNode = withoutPlaceHolder().toHierarchicalNodeInternal() + 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() } } } +private fun TreeNode.toHierarchicalNodeInternal(): HierarchicalTreeNode { + return when (this) { + is ValueNode -> HierarchicalTreeNode.Value(value, this.comments) + is ListNode -> HierarchicalTreeNode.List(list.map { it.toHierarchicalNodeInternal() }, this.comments) + else -> HierarchicalTreeNode.Map(children.mapValues { (_, child) -> child.toHierarchicalNodeInternal() }, + if (this is CommentableNode) this.comments else null) + } +} + +/** + * Represents a value of a hierarchical map. + */ +sealed class HierarchicalTreeNode { + + abstract val comments: String? + + class Value(val value: Any, override val comments: String?) : HierarchicalTreeNode() + + class Map(val map: kotlin.collections.Map, override val comments: String?) : HierarchicalTreeNode() + + class List(val list: kotlin.collections.List, override val comments: String?) : HierarchicalTreeNode() +} + /** * Source from an empty map. */ diff --git a/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt b/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt index bebaf995..aa287af3 100644 --- a/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt +++ b/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt @@ -17,16 +17,21 @@ package com.uchuhimo.konf.source.hocon import com.typesafe.config.ConfigRenderOptions +import com.typesafe.config.ConfigValue import com.typesafe.config.ConfigValueFactory import com.uchuhimo.konf.Config +import com.uchuhimo.konf.Feature import com.uchuhimo.konf.source.Writer +import com.uchuhimo.konf.source.base.HierarchicalTreeNode import com.uchuhimo.konf.source.base.toHierarchicalMap +import com.uchuhimo.konf.source.base.toHierarchicalMapNode import java.io.OutputStream /** * Writer for HOCON source. */ class HoconWriter(val config: Config) : Writer { + private val renderOpts = ConfigRenderOptions.defaults() .setOriginComments(false) .setComments(false) @@ -42,9 +47,27 @@ class HoconWriter(val config: Config) : Writer { } } + private fun HierarchicalTreeNode.toConfigValue(): ConfigValue { + val value = when (this) { + is HierarchicalTreeNode.Value -> ConfigValueFactory.fromAnyRef(this.value) + is HierarchicalTreeNode.Map -> ConfigValueFactory.fromMap( + this.map.mapValues { (_, value) -> value.toConfigValue() }) + is HierarchicalTreeNode.List -> ConfigValueFactory.fromIterable(this.list.map { it.toConfigValue() }) + } + val comments = this.comments + if (comments != null) { + return value.withOrigin(value.origin().withComments(comments.split("\n"))) + } + return 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.toHierarchicalMapNode().toConfigValue().render(renderOpts.setComments(true)) + } else { + ConfigValueFactory.fromMap(config.toHierarchicalMap()).render(renderOpts) + } + return output.replace("\n", System.lineSeparator()) } } From 30cfe59bf6abb87d977f4e1df47519f3cd1a63a0 Mon Sep 17 00:00:00 2001 From: Seppe Volkaerts Date: Sun, 2 Feb 2020 16:17:38 +0100 Subject: [PATCH 2/7] Cleanup. --- .../uchuhimo/konf/source/base/MapSource.kt | 42 ------------------- .../uchuhimo/konf/source/hocon/HoconWriter.kt | 25 ++++++----- 2 files changed, 14 insertions(+), 53 deletions(-) diff --git a/konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/MapSource.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/MapSource.kt index f9bd90db..58290c7a 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/MapSource.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/MapSource.kt @@ -16,7 +16,6 @@ package com.uchuhimo.konf.source.base -import com.uchuhimo.konf.CommentableNode import com.uchuhimo.konf.Config import com.uchuhimo.konf.ListNode import com.uchuhimo.konf.TreeNode @@ -45,17 +44,6 @@ fun Config.toHierarchicalMap(): Map { return toTree().toHierarchical() as Map } -/** - * 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 - * `config.from.map.hierarchical(map)`. - */ -fun Config.toHierarchicalMapNode(): HierarchicalTreeNode.Map { - return toTree().toHierarchicalNode() as HierarchicalTreeNode.Map -} - /** * Returns a hierarchical value for this tree node. * @@ -63,13 +51,6 @@ fun Config.toHierarchicalMapNode(): HierarchicalTreeNode.Map { */ fun TreeNode.toHierarchical(): Any = withoutPlaceHolder().toHierarchicalInternal() -/** - * Returns a hierarchical value for this tree node. - * - * The returned value contains all items in this tree node. - */ -fun TreeNode.toHierarchicalNode(): HierarchicalTreeNode = withoutPlaceHolder().toHierarchicalNodeInternal() - private fun TreeNode.toHierarchicalInternal(): Any { return when (this) { is ValueNode -> value @@ -78,29 +59,6 @@ private fun TreeNode.toHierarchicalInternal(): Any { } } -private fun TreeNode.toHierarchicalNodeInternal(): HierarchicalTreeNode { - return when (this) { - is ValueNode -> HierarchicalTreeNode.Value(value, this.comments) - is ListNode -> HierarchicalTreeNode.List(list.map { it.toHierarchicalNodeInternal() }, this.comments) - else -> HierarchicalTreeNode.Map(children.mapValues { (_, child) -> child.toHierarchicalNodeInternal() }, - if (this is CommentableNode) this.comments else null) - } -} - -/** - * Represents a value of a hierarchical map. - */ -sealed class HierarchicalTreeNode { - - abstract val comments: String? - - class Value(val value: Any, override val comments: String?) : HierarchicalTreeNode() - - class Map(val map: kotlin.collections.Map, override val comments: String?) : HierarchicalTreeNode() - - class List(val list: kotlin.collections.List, override val comments: String?) : HierarchicalTreeNode() -} - /** * Source from an empty map. */ diff --git a/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt b/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt index aa287af3..d73a9354 100644 --- a/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt +++ b/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt @@ -19,12 +19,14 @@ package com.uchuhimo.konf.source.hocon import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigValue import com.typesafe.config.ConfigValueFactory +import com.uchuhimo.konf.CommentableNode import com.uchuhimo.konf.Config import com.uchuhimo.konf.Feature +import com.uchuhimo.konf.ListNode +import com.uchuhimo.konf.TreeNode +import com.uchuhimo.konf.ValueNode import com.uchuhimo.konf.source.Writer -import com.uchuhimo.konf.source.base.HierarchicalTreeNode import com.uchuhimo.konf.source.base.toHierarchicalMap -import com.uchuhimo.konf.source.base.toHierarchicalMapNode import java.io.OutputStream /** @@ -47,23 +49,24 @@ class HoconWriter(val config: Config) : Writer { } } - private fun HierarchicalTreeNode.toConfigValue(): ConfigValue { + private fun TreeNode.toConfigValue(): ConfigValue { val value = when (this) { - is HierarchicalTreeNode.Value -> ConfigValueFactory.fromAnyRef(this.value) - is HierarchicalTreeNode.Map -> ConfigValueFactory.fromMap( - this.map.mapValues { (_, value) -> value.toConfigValue() }) - is HierarchicalTreeNode.List -> ConfigValueFactory.fromIterable(this.list.map { it.toConfigValue() }) + is ValueNode -> ConfigValueFactory.fromAnyRef(this.value) + is ListNode -> ConfigValueFactory.fromIterable(this.list.map { it.toConfigValue() }) + else -> ConfigValueFactory.fromMap(this.children.mapValues { (_, value) -> value.toConfigValue() }) } - val comments = this.comments - if (comments != null) { - return value.withOrigin(value.origin().withComments(comments.split("\n"))) + if (this is CommentableNode) { + val comments = this.comments + if (comments != null) { + return value.withOrigin(value.origin().withComments(comments.split("\n"))) + } } return value } override fun toText(): String { val output = if (config.isEnabled(Feature.WRITE_DESCRIPTIONS_AS_COMMENTS)) { - config.toHierarchicalMapNode().toConfigValue().render(renderOpts.setComments(true)) + config.toTree().toConfigValue().render(renderOpts.setComments(true)) } else { ConfigValueFactory.fromMap(config.toHierarchicalMap()).render(renderOpts) } From 4216600d4076942c1f78b91a06bc27f563b5c862 Mon Sep 17 00:00:00 2001 From: Seppe Volkaerts Date: Sun, 2 Feb 2020 16:21:04 +0100 Subject: [PATCH 3/7] Remove CommentableNode. --- .../main/kotlin/com/uchuhimo/konf/TreeNode.kt | 17 +++++++---------- .../uchuhimo/konf/source/hocon/HoconWriter.kt | 9 +++------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/konf-core/src/main/kotlin/com/uchuhimo/konf/TreeNode.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/TreeNode.kt index a20071a6..2dbe5cdd 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/TreeNode.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/TreeNode.kt @@ -27,6 +27,11 @@ interface TreeNode { */ val children: MutableMap + /** + * The comments assigned to this tree node. + */ + val comments: String? + /** * Associate path with specified node. * @@ -292,17 +297,9 @@ interface TreeNode { } } -/** - * A node which can hold a comment, if any. - */ -interface CommentableNode : TreeNode { - - val comments: String? -} - -interface LeafNode : TreeNode, CommentableNode +interface LeafNode : TreeNode -interface MapNode : TreeNode, CommentableNode { +interface MapNode : TreeNode { fun withMap(map: Map): MapNode = throw NotImplementedError() var isPlaceHolder: Boolean } diff --git a/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt b/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt index d73a9354..cdfdfd02 100644 --- a/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt +++ b/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt @@ -19,7 +19,6 @@ package com.uchuhimo.konf.source.hocon import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigValue import com.typesafe.config.ConfigValueFactory -import com.uchuhimo.konf.CommentableNode import com.uchuhimo.konf.Config import com.uchuhimo.konf.Feature import com.uchuhimo.konf.ListNode @@ -55,11 +54,9 @@ class HoconWriter(val config: Config) : Writer { is ListNode -> ConfigValueFactory.fromIterable(this.list.map { it.toConfigValue() }) else -> ConfigValueFactory.fromMap(this.children.mapValues { (_, value) -> value.toConfigValue() }) } - if (this is CommentableNode) { - val comments = this.comments - if (comments != null) { - return value.withOrigin(value.origin().withComments(comments.split("\n"))) - } + val comments = this.comments + if (comments != null) { + return value.withOrigin(value.origin().withComments(comments.split("\n"))) } return value } From 86e4469782bd4f305e44dfbf05599a0638c88b8a Mon Sep 17 00:00:00 2001 From: Seppe Volkaerts Date: Sun, 2 Feb 2020 18:12:11 +0100 Subject: [PATCH 4/7] Add comment support for toml. --- .../uchuhimo/konf/source/hocon/HoconWriter.kt | 8 +- .../com/moandjiezana/toml/Toml4jWriter.kt | 84 +++++++++++++++---- .../uchuhimo/konf/source/toml/TomlWriter.kt | 8 +- 3 files changed, 78 insertions(+), 22 deletions(-) diff --git a/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt b/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt index cdfdfd02..f63ef03d 100644 --- a/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt +++ b/konf-hocon/src/main/kotlin/com/uchuhimo/konf/source/hocon/HoconWriter.kt @@ -50,11 +50,11 @@ class HoconWriter(val config: Config) : Writer { private fun TreeNode.toConfigValue(): ConfigValue { val value = when (this) { - is ValueNode -> ConfigValueFactory.fromAnyRef(this.value) - is ListNode -> ConfigValueFactory.fromIterable(this.list.map { it.toConfigValue() }) - else -> ConfigValueFactory.fromMap(this.children.mapValues { (_, value) -> value.toConfigValue() }) + is ValueNode -> ConfigValueFactory.fromAnyRef(value) + is ListNode -> ConfigValueFactory.fromIterable(list.map { it.toConfigValue() }) + else -> ConfigValueFactory.fromMap(children.mapValues { (_, value) -> value.toConfigValue() }) } - val comments = this.comments + val comments = comments if (comments != null) { return value.withOrigin(value.origin().withComments(comments.split("\n"))) } diff --git a/konf-toml/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt b/konf-toml/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt index 6399435e..80c30d17 100644 --- a/konf-toml/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt +++ b/konf-toml/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt @@ -20,6 +20,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.uchuhimo.konf.ListNode +import com.uchuhimo.konf.TreeNode +import com.uchuhimo.konf.ValueNode import java.io.IOException import java.io.StringWriter import java.io.Writer @@ -92,7 +95,7 @@ internal object Toml4jValueWriters { return valueWriter } } - return NewMapValueWriter + error("Can't find writer for ${value::class.qualifiedName}") } private val VALUE_WRITERS = arrayOf( @@ -106,23 +109,38 @@ 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.filter { it is TreeNode && it.comments != null }.any() + 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 + @@ -130,35 +148,60 @@ 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) { + val comments = node?.comments?.split("\n") ?: return + comments.forEach { comment -> + indent() + write("\n# $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) { @@ -172,10 +215,15 @@ 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) @@ -185,6 +233,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) { @@ -197,10 +246,8 @@ internal object NewMapValueWriter : ValueWriter { // Now render (sub)tables and arrays of tables 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) { @@ -227,10 +274,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/konf-toml/src/main/kotlin/com/uchuhimo/konf/source/toml/TomlWriter.kt b/konf-toml/src/main/kotlin/com/uchuhimo/konf/source/toml/TomlWriter.kt index f887abe4..0bb4217f 100644 --- a/konf-toml/src/main/kotlin/com/uchuhimo/konf/source/toml/TomlWriter.kt +++ b/konf-toml/src/main/kotlin/com/uchuhimo/konf/source/toml/TomlWriter.kt @@ -18,6 +18,7 @@ package com.uchuhimo.konf.source.toml import com.moandjiezana.toml.Toml4jWriter import com.uchuhimo.konf.Config +import com.uchuhimo.konf.Feature import com.uchuhimo.konf.source.Writer import com.uchuhimo.konf.source.base.toHierarchicalMap import java.io.OutputStream @@ -39,7 +40,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()) } } From a01bee94f88b8a66bdc367313dad4a1976bb28f7 Mon Sep 17 00:00:00 2001 From: Seppe Volkaerts Date: Sun, 2 Feb 2020 19:42:43 +0100 Subject: [PATCH 5/7] Add spec descriptions. --- .../kotlin/com/uchuhimo/konf/BaseConfig.kt | 22 ++++++++++++++----- .../kotlin/com/uchuhimo/konf/ConfigSpec.kt | 3 ++- .../src/main/kotlin/com/uchuhimo/konf/Spec.kt | 20 ++++++++++++++++- .../main/kotlin/com/uchuhimo/konf/TreeNode.kt | 16 ++++++-------- .../kotlin/com/uchuhimo/konf/source/Source.kt | 4 ++-- .../com/uchuhimo/konf/source/SourceNode.kt | 14 +++++------- .../uchuhimo/konf/source/base/FlatSource.kt | 6 ++--- .../com/moandjiezana/toml/Toml4jWriter.kt | 6 +++-- 8 files changed, 59 insertions(+), 32 deletions(-) diff --git a/konf-core/src/main/kotlin/com/uchuhimo/konf/BaseConfig.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/BaseConfig.kt index ab55b147..13816dcc 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/BaseConfig.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/BaseConfig.kt @@ -174,9 +174,15 @@ open class BaseConfig( } catch (_: UnsetValueException) { return@forEach } - val description = item.description - val comment = if (description.isEmpty()) null else description - set(name, value.asTree(comment)) + 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 + } } } } @@ -592,6 +598,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)) } @@ -631,8 +644,7 @@ open class BaseConfig( class ItemNode(override var value: ValueState, val item: Item<*>) : ValueNode { - override val comments: String? - get() = if (this.item.description.isEmpty()) null else this.item.description + override var comments = this.item.description } data class Value(var value: T) diff --git a/konf-core/src/main/kotlin/com/uchuhimo/konf/ConfigSpec.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/ConfigSpec.kt index 5fe7f972..658eac7f 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/ConfigSpec.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/ConfigSpec.kt @@ -26,7 +26,8 @@ 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) { diff --git a/konf-core/src/main/kotlin/com/uchuhimo/konf/Spec.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/Spec.kt index d2e4a007..f1c55dd7 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/Spec.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/Spec.kt @@ -39,6 +39,12 @@ interface Spec { */ val prefix: String + /** + * The description of the spec. + */ + val description: String + get() = "" + /** * Qualify item name with prefix of this config spec. * @@ -162,10 +168,22 @@ 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]. diff --git a/konf-core/src/main/kotlin/com/uchuhimo/konf/TreeNode.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/TreeNode.kt index 2dbe5cdd..fd0057ac 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/TreeNode.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/TreeNode.kt @@ -30,7 +30,7 @@ interface TreeNode { /** * The comments assigned to this tree node. */ - val comments: String? + var comments: String /** * Associate path with specified node. @@ -322,20 +322,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 val comments: String? = null + override var comments: String = "" ) : MapNode { - constructor(children: MutableMap, isPlaceHolder: Boolean = false) : this(children, isPlaceHolder, null) - 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) } } @@ -350,5 +348,5 @@ open class ContainerNode( */ object EmptyNode : LeafNode { override val children: MutableMap = emptyMutableMap - override val comments: String? = null + override var comments: String = "" } diff --git a/konf-core/src/main/kotlin/com/uchuhimo/konf/source/Source.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/source/Source.kt index 989fe39f..3480e800 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/source/Source.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/source/Source.kt @@ -972,9 +972,9 @@ private fun implOf(clazz: Class<*>): Class<*> = else -> clazz } -fun Any.asTree(): TreeNode = asTree(null) +fun Any.asTree(): TreeNode = asTree("") -fun Any.asTree(comment: String?): TreeNode = +fun Any.asTree(comment: String = ""): TreeNode = when (this) { is TreeNode -> this is Source -> this.tree diff --git a/konf-core/src/main/kotlin/com/uchuhimo/konf/source/SourceNode.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/source/SourceNode.kt index be24a554..7d1e57ad 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/source/SourceNode.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/source/SourceNode.kt @@ -30,15 +30,13 @@ 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 comments: String? = null + override var comments: String = "" ) : SubstitutableNode { - constructor(value: Any, substituted: Boolean = false, originalValue: Any? = null) : this(value, substituted, originalValue, null) - override fun substitute(value: String): TreeNode { return ValueSourceNode(value, true, originalValue ?: this.value, this.comments) } @@ -46,20 +44,18 @@ class ValueSourceNode( object NullSourceNode : NullNode { override val children: MutableMap = emptyMutableMap - override val comments: String? = null + override var comments: String = "" } -open class ListSourceNode( +open class ListSourceNode @JvmOverloads constructor( override val list: List, override var isPlaceHolder: Boolean = false, - override val comments: String? = null + override var comments: String = "" ) : ListNode, MapNode { override val children: MutableMap get() = Collections.unmodifiableMap( list.withIndex().associate { (key, value) -> key.toString() to value }) - constructor(list: List, isPlaceHolder: Boolean = false) : this(list, isPlaceHolder, null) - override fun withList(list: List): ListNode { return ListSourceNode(list, comments = this.comments) } diff --git a/konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/FlatSource.kt b/konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/FlatSource.kt index 2d0dc655..bea2fb07 100644 --- a/konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/FlatSource.kt +++ b/konf-core/src/main/kotlin/com/uchuhimo/konf/source/base/FlatSource.kt @@ -62,7 +62,7 @@ object EmptyStringNode : SubstitutableNode, ListNode { override val list: List = listOf() override val originalValue: Any? = null override val substituted: Boolean = false - override val comments: String? = null + override var comments: String = "" override fun substitute(value: String): TreeNode { check(value.isEmpty()) return this @@ -73,7 +73,7 @@ class SingleStringListNode( override val value: String, override val substituted: Boolean = false, override val originalValue: Any? = null, - override val comments: String? = null + override var comments: String = "" ) : SubstitutableNode, ListNode { override val children: MutableMap = Collections.unmodifiableMap( mutableMapOf("0" to value.asTree())) @@ -86,7 +86,7 @@ class ListStringNode( override val value: String, override val substituted: Boolean = false, override val originalValue: Any? = null, - override val comments: String? = 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) diff --git a/konf-toml/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt b/konf-toml/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt index 80c30d17..ea2c56cc 100644 --- a/konf-toml/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt +++ b/konf-toml/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt @@ -122,7 +122,7 @@ internal object NewArrayValueWriter : ArrayValueWriter() { var first = true var firstWriter: ValueWriter? = null - val hasAnyComments = values.filter { it is TreeNode && it.comments != null }.any() + val hasAnyComments = values.filter { it is TreeNode && it.comments.isNotEmpty() }.any() if (hasAnyComments) context.write('\n') @@ -180,7 +180,9 @@ private val TreeNode.value: Any } private fun WriterContext.writeComments(node: TreeNode?, newLineAfter: Boolean = true) { - val comments = node?.comments?.split("\n") ?: return + if (node == null || node.comments.isEmpty()) + return + val comments = node.comments.split("\n") comments.forEach { comment -> indent() write("\n# $comment") From 3af860827f4aadecff08279b7bf3ffa80074ca50 Mon Sep 17 00:00:00 2001 From: Seppe Volkaerts Date: Mon, 3 Feb 2020 02:03:39 +0100 Subject: [PATCH 6/7] Add comment support for yaml. --- .../uchuhimo/konf/source/yaml/YamlWriter.kt | 175 +++++++++++++++++- 1 file changed, 165 insertions(+), 10 deletions(-) diff --git a/konf-yaml/src/main/kotlin/com/uchuhimo/konf/source/yaml/YamlWriter.kt b/konf-yaml/src/main/kotlin/com/uchuhimo/konf/source/yaml/YamlWriter.kt index 3190d721..5035fca9 100644 --- a/konf-yaml/src/main/kotlin/com/uchuhimo/konf/source/yaml/YamlWriter.kt +++ b/konf-yaml/src/main/kotlin/com/uchuhimo/konf/source/yaml/YamlWriter.kt @@ -17,25 +17,21 @@ package com.uchuhimo.konf.source.yaml import com.uchuhimo.konf.Config +import com.uchuhimo.konf.Feature +import com.uchuhimo.konf.ListNode +import com.uchuhimo.konf.TreeNode +import com.uchuhimo.konf.ValueNode import com.uchuhimo.konf.source.Writer -import com.uchuhimo.konf.source.base.toHierarchicalMap import java.io.OutputStream -import org.yaml.snakeyaml.DumperOptions -import org.yaml.snakeyaml.Yaml -import org.yaml.snakeyaml.constructor.SafeConstructor -import org.yaml.snakeyaml.representer.Representer /** * Writer for YAML source. */ class YamlWriter(val config: Config) : Writer { - private val yaml = Yaml(SafeConstructor(), Representer(), DumperOptions().apply { - defaultFlowStyle = DumperOptions.FlowStyle.BLOCK - lineBreak = DumperOptions.LineBreak.getPlatformLineBreak() - }) override fun toWriter(writer: java.io.Writer) { - yaml.dump(config.toHierarchicalMap(), writer) + val nodeWriter = YamlTreeNodeWriter(writer, config.isEnabled(Feature.WRITE_DESCRIPTIONS_AS_COMMENTS)) + nodeWriter.write(config.toTree()) } override fun toOutputStream(outputStream: OutputStream) { @@ -45,6 +41,165 @@ class YamlWriter(val config: Config) : Writer { } } +private class YamlTreeNodeWriter( + private val writer: java.io.Writer, + 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(' '.toInt()) + } + } + + private fun write(char: Char) { + this.writer.write(char.toInt()) + } + + 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(quoteStringIfNeeded(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 quoteStringIfNeeded(s: String): String { + if (':' in s || '\"' in s || '\'' in s) { + return "\"${s.replace("\"", "\\\"")}\"" + } + 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(quoteStringIfNeeded(name)) + write(':') + when (node) { + is ValueNode -> { + write(' ') + writeValue(node) + } + is ListNode -> { + writeList(node) + } + else -> { + writeNewLine() + increaseIndent() + writeMap(node) + decreaseIndent() + } + } + } +} + /** * Returns writer for YAML source. */ From aa0b1658f23e7a58eb14b50801ff771986269c56 Mon Sep 17 00:00:00 2001 From: Seppe Volkaerts Date: Mon, 3 Feb 2020 16:23:46 +0100 Subject: [PATCH 7/7] A few tweaks. --- .../com/moandjiezana/toml/Toml4jWriter.kt | 3 ++- .../uchuhimo/konf/source/yaml/YamlWriter.kt | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/konf-toml/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt b/konf-toml/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt index ea2c56cc..546afe77 100644 --- a/konf-toml/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt +++ b/konf-toml/src/main/kotlin/com/moandjiezana/toml/Toml4jWriter.kt @@ -184,8 +184,9 @@ private fun WriterContext.writeComments(node: TreeNode?, newLineAfter: Boolean = return val comments = node.comments.split("\n") comments.forEach { comment -> + write('\n') indent() - write("\n# $comment") + write("# $comment") } if (newLineAfter) write('\n') diff --git a/konf-yaml/src/main/kotlin/com/uchuhimo/konf/source/yaml/YamlWriter.kt b/konf-yaml/src/main/kotlin/com/uchuhimo/konf/source/yaml/YamlWriter.kt index 5035fca9..9d0288b1 100644 --- a/konf-yaml/src/main/kotlin/com/uchuhimo/konf/source/yaml/YamlWriter.kt +++ b/konf-yaml/src/main/kotlin/com/uchuhimo/konf/source/yaml/YamlWriter.kt @@ -118,7 +118,7 @@ private class YamlTreeNodeWriter( } decreaseIndent() } else { - write(quoteStringIfNeeded(string)) + write(quoteValueIfNeeded(string)) writeNewLine() } } @@ -165,10 +165,17 @@ private class YamlTreeNodeWriter( } } - private fun quoteStringIfNeeded(s: String): String { - if (':' in s || '\"' in s || '\'' in s) { - return "\"${s.replace("\"", "\\\"")}\"" - } + 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 } @@ -180,7 +187,7 @@ private class YamlTreeNodeWriter( writeComments(node) writeIndent() } - write(quoteStringIfNeeded(name)) + write(quoteValueIfNeeded(name)) write(':') when (node) { is ValueNode -> {