Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(JVM): Annotations to set the ScalarStyle on a single field. #596

Merged
merged 2 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,27 @@ import kotlinx.serialization.SerialInfo
public annotation class YamlComment(
vararg val lines: String,
)

/**
* Write a String value if it is a single line in the specified ScalarStyle.
* This overrides the value specified in the [YamlConfiguration].
*/
@OptIn(ExperimentalSerializationApi::class)
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
@SerialInfo
public annotation class YamlSingleLineStringStyle(
val singleLineStringStyle: SingleLineStringStyle,
)

/**
* Write a String value if it is a multiline in the specified ScalarStyle.
* This overrides the value specified in the [YamlConfiguration].
*/
@OptIn(ExperimentalSerializationApi::class)
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
@SerialInfo
public annotation class YamlMultiLineStringStyle(
val multiLineStringStyle: MultiLineStringStyle,
)
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ package com.charleskorn.kaml
/**
* Configuration options for parsing YAML to objects and serialising objects to YAML.
*
* * [encodeDefaults]: set to false to not write default property values to YAML (defaults to `true`)
* * [encodeDefaults]: set to `false` to not write default property values to YAML (defaults to `true`)
* * [strictMode]: set to true to throw an exception when reading an object that has an unknown property, or false to ignore unknown properties (defaults to `true`)
* * [extensionDefinitionPrefix]: prefix used on root-level keys (where document root is an object) to define extensions that can later be merged (defaults to `null`, which disables extensions altogether). See https://batect.dev/docs/reference/config#anchors-aliases-extensions-and-merging for example.
* * [polymorphismStyle]: how to read or write the type of a polymorphic object:
Expand All @@ -31,9 +31,12 @@ 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
* * [singleLineStringStyle]: the style in which a single line String value is written. Can be overruled for a specific field with the [YamlSingleLineStringStyle] annotation.
* * [multiLineStringStyle]: the style in which a multi line String value is written. Can be overruled for a specific field with the [YamlMultiLineStringStyle] annotation.
* * [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]
* * [allowAnchorsAndAliases]: set to true to allow anchors and aliases when decoding YAML (defaults to `false`)
* * [yamlNamingStrategy]: The system that converts the field names in to the names used in the Yaml.
* * [codePointLimit]: the maximum amount of code points allowed in the input YAML document (defaults to 3 MB)
*/
public data class YamlConfiguration(
Expand Down Expand Up @@ -82,6 +85,7 @@ public enum class SequenceStyle {

public enum class MultiLineStringStyle {
Literal,
Folded,
DoubleQuoted,
SingleQuoted,
Plain,
Expand Down
31 changes: 25 additions & 6 deletions src/commonMain/kotlin/com/charleskorn/kaml/YamlOutput.kt
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,24 @@ internal class YamlOutput(
override fun encodeInt(value: Int) = emitPlainScalar(value.toString())
override fun encodeLong(value: Long) = emitPlainScalar(value.toString())
override fun encodeShort(value: Short) = emitPlainScalar(value.toString())

private var forcedSingleLineScalarStyle: SingleLineStringStyle? = null
private var forcedMultiLineScalarStyle: MultiLineStringStyle? = null

override fun encodeString(value: String) {
if (shouldReadTypeName) {
currentTypeName = value
shouldReadTypeName = false
} else {
val singleLineScalarStyle = forcedSingleLineScalarStyle ?.scalarStyle ?: configuration.singleLineStringStyle.scalarStyle
val multiLineScalarStyle = forcedMultiLineScalarStyle ?.scalarStyle ?: configuration.multiLineStringStyle.scalarStyle
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)
value.contains('\n')
-> emitScalar(value, multiLineScalarStyle)
configuration.singleLineStringStyle == SingleLineStringStyle.PlainExceptAmbiguous && value.isAmbiguous()
-> emitQuotedScalar(value, configuration.ambiguousQuoteStyle.scalarStyle)
else
-> emitScalar(value, singleLineScalarStyle)
}
}
}
Expand All @@ -98,6 +107,13 @@ internal class YamlOutput(
private fun emitPlainScalar(value: String) = emitScalar(value, ScalarStyle.PLAIN)
private fun emitQuotedScalar(value: String, scalarStyle: ScalarStyle) = emitScalar(value, scalarStyle)


private inline fun <reified R> SerialDescriptor.getAnnotation(index: Int): R? {
return getElementAnnotations(index)
.filterIsInstance<R>()
.firstOrNull()
}

override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean {
encodeComment(descriptor, index)

Expand All @@ -107,6 +123,10 @@ internal class YamlOutput(
emitPlainScalar(serializedName)
}

// If this field was annotated we overrule the used ScalarStyle with the annotation
forcedSingleLineScalarStyle = descriptor.getAnnotation<YamlSingleLineStringStyle>(index)?.singleLineStringStyle
forcedMultiLineScalarStyle = descriptor.getAnnotation<YamlMultiLineStringStyle>(index)?.multiLineStringStyle

return super.encodeElement(descriptor, index)
}

Expand Down Expand Up @@ -159,9 +179,7 @@ internal class YamlOutput(
}

private fun encodeComment(descriptor: SerialDescriptor, index: Int) {
val commentAnno = descriptor.getElementAnnotations(index)
.filterIsInstance<YamlComment>()
.firstOrNull() ?: return
val commentAnno = descriptor.getAnnotation<YamlComment>(index) ?: return

for (line in commentAnno.lines) {
emitter.emit(CommentEvent(CommentType.BLOCK, " $line", null, null))
Expand Down Expand Up @@ -210,6 +228,7 @@ internal class YamlOutput(
MultiLineStringStyle.DoubleQuoted -> ScalarStyle.DOUBLE_QUOTED
MultiLineStringStyle.SingleQuoted -> ScalarStyle.SINGLE_QUOTED
MultiLineStringStyle.Literal -> ScalarStyle.LITERAL
MultiLineStringStyle.Folded -> ScalarStyle.FOLDED
MultiLineStringStyle.Plain -> ScalarStyle.PLAIN
}

Expand Down
147 changes: 147 additions & 0 deletions src/jvmTest/kotlin/com/charleskorn/kaml/JvmYamlWritingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,121 @@ class JvmYamlWritingTest : DescribeSpec({
output.toString(Charsets.UTF_8) shouldBe "\"hello world\"\n"
}
}

describe("serializing a string as an explicitly stated ScalarStyle (Single Line - ThingSL)") {
val output = ByteArrayOutputStream()
val thing = ThingSL(
"Name of Thing",
"String",
"Single Quoted",
"Double Quoted",
"Plain",
)

Yaml.default.encodeToStream<ThingSL>(thing, output)
output.toString(Charsets.UTF_8) shouldBe
"""
name: "Name of Thing"
string: "String"
singleQuoted: 'Single Quoted'
doubleQuoted: "Double Quoted"
plain: Plain

""".trimIndent()
}

describe("serializing a string as an explicitly stated ScalarStyle (Multi Line - ThingSL)") {
val output = ByteArrayOutputStream()
val thing = ThingSL(
"Name of Thing",
"String 1\nString 2\nString 3\n",
"Single Quoted 1\nSingle Quoted 2\nSingle Quoted 3\n",
"Double Quoted 1\nDouble Quoted 2\nDouble Quoted 3\n",
"Plain 1\nPlain 2\nPlain 3\n",
)

Yaml.default.encodeToStream<ThingSL>(thing, output)
output.toString(Charsets.UTF_8) shouldBe
"""
name: "Name of Thing"
string: "String 1\nString 2\nString 3\n"
singleQuoted: 'Single Quoted 1

Single Quoted 2

Single Quoted 3

'
doubleQuoted: "Double Quoted 1\nDouble Quoted 2\nDouble Quoted 3\n"
plain: 'Plain 1

Plain 2

Plain 3

'

""".trimIndent()
}

describe("serializing a string as an explicitly stated ScalarStyle (Single Line - ThingML)") {
val output = ByteArrayOutputStream()
val thing = ThingML(
"Name of Thing",
"String",
"Literal",
"Folded",
"Plain",
)

Yaml.default.encodeToStream<ThingML>(thing, output)
output.toString(Charsets.UTF_8) shouldBe
"""
name: "Name of Thing"
string: "String"
literal: "Literal"
folded: "Folded"
plain: Plain

""".trimIndent()
}

describe("serializing a string as an explicitly stated ScalarStyle (Multi Line - ThingML)") {
val output = ByteArrayOutputStream()
val thing = ThingML(
"Name of Thing",
"String 1\nString 2\nString 3\n",
"Literal 1\nLiteral 2\nLiteral 3\n",
"Folded 1\nFolded 2\nFolded 3\n",
"Plain 1\nPlain 2\nPlain 3\n",
)

Yaml.default.encodeToStream<ThingML>(thing, output)
output.toString(Charsets.UTF_8) shouldBe
"""
name: "Name of Thing"
string: "String 1\nString 2\nString 3\n"
literal: |
Literal 1
Literal 2
Literal 3
folded: >
Folded 1

Folded 2

Folded 3
plain: 'Plain 1

Plain 2

Plain 3

'

""".trimIndent()
}

}
})

Expand All @@ -190,3 +305,35 @@ sealed interface Animal {
@Serializable
data class Cat(val name: String) : Animal
}

@Serializable
data class ThingSL(
val name: String,
// Without any annotations
val string: String,
@YamlSingleLineStringStyle(SingleLineStringStyle.SingleQuoted)
@YamlMultiLineStringStyle(MultiLineStringStyle.SingleQuoted)
val singleQuoted: String,
@YamlSingleLineStringStyle(SingleLineStringStyle.DoubleQuoted)
@YamlMultiLineStringStyle(MultiLineStringStyle.DoubleQuoted)
val doubleQuoted: String,
@YamlSingleLineStringStyle(SingleLineStringStyle.Plain)
@YamlMultiLineStringStyle(MultiLineStringStyle.Plain)
val plain: String,
)

@Serializable
data class ThingML(
val name: String,
// Without any annotations
val string: String,
@YamlSingleLineStringStyle(SingleLineStringStyle.DoubleQuoted)
@YamlMultiLineStringStyle(MultiLineStringStyle.Literal)
val literal: String,
@YamlSingleLineStringStyle(SingleLineStringStyle.DoubleQuoted)
@YamlMultiLineStringStyle(MultiLineStringStyle.Folded)
val folded: String,
@YamlSingleLineStringStyle(SingleLineStringStyle.Plain)
@YamlMultiLineStringStyle(MultiLineStringStyle.Plain)
val plain: String,
)
Loading