diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlConfiguration.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlConfiguration.kt index a92dd102..6815f2ec 100644 --- a/src/commonMain/kotlin/com/charleskorn/kaml/YamlConfiguration.kt +++ b/src/commonMain/kotlin/com/charleskorn/kaml/YamlConfiguration.kt @@ -31,6 +31,7 @@ package com.charleskorn.kaml * * [encodingIndentationSize]: number of spaces to use as indentation when encoding objects as YAML * * [breakScalarsAt]: maximum length of scalars when encoding objects as YAML (scalars exceeding this length will be split into multiple lines) * * [sequenceStyle]: how sequences (aka lists and arrays) should be formatted. See [SequenceStyle] for an example of each + * * [ambiguousQuoteStyle]: how strings should be escaped when [singleLineStringStyle] is [SingleLineStringStyle.PlainExceptAmbiguous] and the value is ambiguous * * [sequenceBlockIndent]: number of spaces to use as indentation for sequences, if [sequenceStyle] set to [SequenceStyle.Block] */ public data class YamlConfiguration constructor( @@ -44,6 +45,7 @@ public data class YamlConfiguration constructor( internal val sequenceStyle: SequenceStyle = SequenceStyle.Block, internal val singleLineStringStyle: SingleLineStringStyle = SingleLineStringStyle.DoubleQuoted, internal val multiLineStringStyle: MultiLineStringStyle = singleLineStringStyle.multiLineStringStyle, + internal val ambiguousQuoteStyle: AmbiguousQuoteStyle = AmbiguousQuoteStyle.DoubleQuoted, internal val sequenceBlockIndent: Int = 0, ) @@ -83,6 +85,15 @@ public enum class SingleLineStringStyle { DoubleQuoted, SingleQuoted, Plain, + + /** + * This is the same as [SingleLineStringStyle.Plain], except strings that could be misinterpreted as other types + * will be quoted with the escape style defined in [AmbiguousQuoteStyle]. + * + * For example, the strings "True", "0xAB", "1" and "1.2" would all be quoted, + * while "1.2.3" and "abc" would not be quoted. + */ + PlainExceptAmbiguous, ; public val multiLineStringStyle: MultiLineStringStyle @@ -90,5 +101,11 @@ public enum class SingleLineStringStyle { DoubleQuoted -> MultiLineStringStyle.DoubleQuoted SingleQuoted -> MultiLineStringStyle.SingleQuoted Plain -> MultiLineStringStyle.Plain + PlainExceptAmbiguous -> MultiLineStringStyle.Plain } } + +public enum class AmbiguousQuoteStyle { + DoubleQuoted, + SingleQuoted, +} diff --git a/src/commonTest/kotlin/com/charleskorn/kaml/YamlWritingTest.kt b/src/commonTest/kotlin/com/charleskorn/kaml/YamlWritingTest.kt index e922ce21..cba94866 100644 --- a/src/commonTest/kotlin/com/charleskorn/kaml/YamlWritingTest.kt +++ b/src/commonTest/kotlin/com/charleskorn/kaml/YamlWritingTest.kt @@ -200,6 +200,101 @@ class YamlWritingTest : DescribeSpec({ """.trimMargin() } } + + context("serializing a string with the value of an integer using SingleLineStringStyle.PlainExceptAmbiguous") { + val output = Yaml(configuration = YamlConfiguration(singleLineStringStyle = SingleLineStringStyle.PlainExceptAmbiguous)).encodeToString(String.serializer(), "12") + + it("returns the value serialized in the expected YAML form, escaping the integer") { + output shouldBe """"12"""" + } + } + + context("serializing a string with the value of a boolean using SingleLineStringStyle.PlainExceptAmbiguous") { + val output = Yaml(configuration = YamlConfiguration(singleLineStringStyle = SingleLineStringStyle.PlainExceptAmbiguous)).encodeToString(String.serializer(), "true") + + it("returns the value serialized in the expected YAML form, escaping the boolean") { + output shouldBe """"true"""" + } + } + + context("serializing a string with the value of an float using SingleLineStringStyle.PlainExceptAmbiguous") { + val output = Yaml(configuration = YamlConfiguration(singleLineStringStyle = SingleLineStringStyle.PlainExceptAmbiguous)).encodeToString(String.serializer(), "1.2") + + it("returns the value serialized in the expected YAML form, escaping the float") { + output shouldBe """"1.2"""" + } + } + + context("serializing an unambiguous numerical string using SingleLineStringStyle.PlainExceptAmbiguous") { + val output = Yaml(configuration = YamlConfiguration(singleLineStringStyle = SingleLineStringStyle.PlainExceptAmbiguous)).encodeToString(String.serializer(), "1.2.3") + + it("returns the value serialized in the expected YAML form, without being escaped") { + output shouldBe "1.2.3" + } + } + + context("serializing an int using SingleLineStringStyle.PlainExceptAmbiguous") { + val output = Yaml(configuration = YamlConfiguration(singleLineStringStyle = SingleLineStringStyle.PlainExceptAmbiguous)).encodeToString(Int.serializer(), 123) + + it("returns the value serialized in the expected YAML form, without being escaped") { + output shouldBe "123" + } + } + + context("serializing a float using SingleLineStringStyle.PlainExceptAmbiguous") { + val output = Yaml(configuration = YamlConfiguration(singleLineStringStyle = SingleLineStringStyle.PlainExceptAmbiguous)).encodeToString(Float.serializer(), 1.2f) + + it("returns the value serialized in the expected YAML form, without being escaped") { + output shouldBe "1.2" + } + } + + context("serializing a boolean using SingleLineStringStyle.PlainExceptAmbiguous") { + val output = Yaml(configuration = YamlConfiguration(singleLineStringStyle = SingleLineStringStyle.PlainExceptAmbiguous)).encodeToString(Boolean.serializer(), true) + + it("returns the value serialized in the expected YAML form, without being escaped") { + output shouldBe "true" + } + } + + context("serializing a string with the value of an integer using SingleLineStringStyle.PlainExceptAmbiguous, escaping with single-quotes") { + val output = Yaml( + configuration = YamlConfiguration( + singleLineStringStyle = SingleLineStringStyle.PlainExceptAmbiguous, + ambiguousQuoteStyle = AmbiguousQuoteStyle.SingleQuoted, + ), + ).encodeToString(String.serializer(), "12") + + it("returns the value serialized in the expected YAML form, escaping the integer with single-quotes") { + output shouldBe """'12'""" + } + } + + context("serializing a string with the value of a boolean using SingleLineStringStyle.PlainExceptAmbiguous, escaping with single-quotes") { + val output = Yaml( + configuration = YamlConfiguration( + singleLineStringStyle = SingleLineStringStyle.PlainExceptAmbiguous, + ambiguousQuoteStyle = AmbiguousQuoteStyle.SingleQuoted, + ), + ).encodeToString(String.serializer(), "true") + + it("returns the value serialized in the expected YAML form, escaping the boolean with single-quotes") { + output shouldBe """'true'""" + } + } + + context("serializing a string with the value of an float using SingleLineStringStyle.PlainExceptAmbiguous, escaping with single-quotes") { + val output = Yaml( + configuration = YamlConfiguration( + singleLineStringStyle = SingleLineStringStyle.PlainExceptAmbiguous, + ambiguousQuoteStyle = AmbiguousQuoteStyle.SingleQuoted, + ), + ).encodeToString(String.serializer(), "1.2") + + it("returns the value serialized in the expected YAML form, escaping the float with single-quotes") { + output shouldBe """'1.2'""" + } + } } describe("serializing enumeration values") { diff --git a/src/jvmMain/kotlin/com/charleskorn/kaml/YamlOutput.kt b/src/jvmMain/kotlin/com/charleskorn/kaml/YamlOutput.kt index 0b8f7d53..ee2c7d5a 100644 --- a/src/jvmMain/kotlin/com/charleskorn/kaml/YamlOutput.kt +++ b/src/jvmMain/kotlin/com/charleskorn/kaml/YamlOutput.kt @@ -88,6 +88,7 @@ internal class YamlOutput( } else { when { value.contains('\n') -> emitQuotedScalar(value, configuration.multiLineStringStyle.scalarStyle) + configuration.singleLineStringStyle == SingleLineStringStyle.PlainExceptAmbiguous && value.isAmbiguous() -> emitQuotedScalar(value, configuration.ambiguousQuoteStyle.scalarStyle) else -> emitQuotedScalar(value, configuration.singleLineStringStyle.scalarStyle) } } @@ -180,6 +181,21 @@ internal class YamlOutput( return typeName } + private fun String.isAmbiguous(): Boolean { + return when { + isEmpty() -> true + toBigIntegerOrNull() != null -> true + startsWith("0x") -> true + startsWith("0o") -> true + toDoubleOrNull() != null -> true + startsWith("#") -> true + else -> this in listOf( + "~", "-", ".inf", ".Inf", ".INF", "-.inf", "-.Inf", "-.INF", ".nan", ".NaN", ".NAN", "-.nan", "-.NaN", + "-.NAN", "null", "Null", "NULL", "true", "True", "TRUE", "false", "False", "FALSE", + ) + } + } + private val SequenceStyle.flowStyle: FlowStyle get() = when (this) { SequenceStyle.Block -> FlowStyle.BLOCK @@ -199,6 +215,13 @@ internal class YamlOutput( SingleLineStringStyle.DoubleQuoted -> ScalarStyle.DOUBLE_QUOTED SingleLineStringStyle.SingleQuoted -> ScalarStyle.SINGLE_QUOTED SingleLineStringStyle.Plain -> ScalarStyle.PLAIN + SingleLineStringStyle.PlainExceptAmbiguous -> ScalarStyle.PLAIN + } + + private val AmbiguousQuoteStyle.scalarStyle: ScalarStyle + get() = when (this) { + AmbiguousQuoteStyle.DoubleQuoted -> ScalarStyle.DOUBLE_QUOTED + AmbiguousQuoteStyle.SingleQuoted -> ScalarStyle.SINGLE_QUOTED } companion object {