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..13816dcc 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,29 @@ 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 + } + 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 + } + } + } + } + } + @Suppress("UNCHECKED_CAST") override fun get(item: Item): T = getOrNull(item, errorWhenNotFound = true) as T @@ -574,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)) } @@ -611,7 +642,10 @@ 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) 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/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/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/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 83e9c627..fd0057ac 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. + */ + var comments: String + /** * Associate path with specified node. * @@ -317,16 +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 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) } } @@ -341,4 +348,5 @@ open class ContainerNode( */ object EmptyNode : LeafNode { override val children: MutableMap = emptyMutableMap + 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 87c4b716..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 @@ -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("") + +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..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,29 +30,33 @@ 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( list.withIndex().associate { (key, value) -> key.toString() to value }) 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..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,6 +62,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 @@ -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 var comments: String = "" ) : 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 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-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..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 @@ -22,7 +22,6 @@ 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. @@ -53,10 +52,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/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..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 @@ -17,8 +17,13 @@ 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.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 @@ -27,6 +32,7 @@ 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 +48,26 @@ 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() }) + } + val comments = 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.toTree().toConfigValue().render(renderOpts.setComments(true)) + } else { + ConfigValueFactory.fromMap(config.toHierarchicalMap()).render(renderOpts) + } + return output.replace("\n", System.lineSeparator()) } } 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..546afe77 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.isNotEmpty() }.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,63 @@ 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) { @@ -172,10 +218,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 +236,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 +249,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 +277,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()) } } 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..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 @@ -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,172 @@ 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(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. */