Skip to content

Commit

Permalink
Merge pull request #10 from EdwarDDay/feature/tagged-scalar-support
Browse files Browse the repository at this point in the history
Support tagged scalars
  • Loading branch information
charleskorn authored Sep 29, 2019
2 parents 164fef4 + cd5f837 commit 65c03d3
Show file tree
Hide file tree
Showing 10 changed files with 364 additions and 71 deletions.
3 changes: 0 additions & 3 deletions src/main/kotlin/com/charleskorn/kaml/YamlException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ open class YamlException(
val column: Int,
override val cause: Throwable? = null
) : RuntimeException(message, cause) {
constructor(message: String, event: Event, cause: Throwable? = null) : this(message, event.location, cause)
constructor(message: String, location: Location, cause: Throwable? = null) : this(message, location.line, location.column, cause)

val location: Location = Location(line, column)
Expand All @@ -46,8 +45,6 @@ class UnexpectedNullValueException(location: Location) : YamlException("Unexpect
class UnknownPropertyException(val propertyName: String, val validPropertyNames: Set<String>, location: Location) :
YamlException("Unknown property '$propertyName'. Known properties are: ${validPropertyNames.sorted().joinToString(", ")}", location)

class UnsupportedYamlFeatureException(val featureName: String, event: Event) : YamlException("Unsupported YAML feature: $featureName", event)

class YamlScalarFormatException(message: String, location: Location, val originalValue: String) : YamlException(message, location)

class IncorrectTypeException(message: String, location: Location) : YamlException(message, location)
Expand Down
26 changes: 21 additions & 5 deletions src/main/kotlin/com/charleskorn/kaml/YamlInput.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import kotlinx.serialization.CompositeDecoder.Companion.READ_DONE
import kotlinx.serialization.CompositeDecoder.Companion.UNKNOWN_NAME
import kotlinx.serialization.ElementValueDecoder
import kotlinx.serialization.KSerializer
import kotlinx.serialization.PrimitiveKind
import kotlinx.serialization.SerialDescriptor
import kotlinx.serialization.StructureKind
import kotlinx.serialization.UnionKind
Expand Down Expand Up @@ -281,9 +282,9 @@ private class YamlMapInput(val map: YamlMap, context: SerialModule, configuratio
}

readMode = when (desc.kind) {
is StructureKind.MAP -> MapReadMode.Map
is StructureKind.CLASS -> MapReadMode.Object
is StructureKind.LIST -> throw IncorrectTypeException("Expected a list, but got a map", map.location)
StructureKind.MAP -> MapReadMode.Map
StructureKind.CLASS, PrimitiveKind.UNIT -> MapReadMode.Object
StructureKind.LIST -> throw IncorrectTypeException("Expected a list, but got a map", map.location)
else -> throw YamlException("Can't decode into ${desc.kind}", map.location)
}

Expand Down Expand Up @@ -313,7 +314,7 @@ private class YamlTaggedInput(val taggedNode: YamlTaggedNode, context: SerialMod
private var isPolymorphic = false
private val childDecoder: YamlInput = createFor(taggedNode.node, context, configuration)

override fun getCurrentLocation(): Location = maybeCallOnChild(blockOnTag = { taggedNode.location }) { getCurrentLocation() }
override fun getCurrentLocation(): Location = maybeCallOnChild(blockOnTag = taggedNode::location, blockOnChild = YamlInput::getCurrentLocation)

override fun decodeElementIndex(desc: SerialDescriptor): Int {
desc.calculatePolymorphic()
Expand All @@ -323,7 +324,19 @@ private class YamlTaggedInput(val taggedNode: YamlTaggedNode, context: SerialMod
}
}

override fun decodeString(): String = maybeCallOnChild(blockOnTag = { taggedNode.tag }) { decodeString() }
override fun decodeNotNullMark(): Boolean = maybeCallOnChild(blockOnTag = { true }, blockOnChild = YamlInput::decodeNotNullMark)
override fun decodeNull(): Nothing? = maybeCallOnChild("decodeNull", blockOnChild = YamlInput::decodeNull)
override fun decodeUnit(): Unit = maybeCallOnChild("decodeUnit", blockOnChild = YamlInput::decodeUnit)
override fun decodeBoolean(): Boolean = maybeCallOnChild("decodeBoolean", blockOnChild = YamlInput::decodeBoolean)
override fun decodeByte(): Byte = maybeCallOnChild("decodeByte", blockOnChild = YamlInput::decodeByte)
override fun decodeShort(): Short = maybeCallOnChild("decodeShort", blockOnChild = YamlInput::decodeShort)
override fun decodeInt(): Int = maybeCallOnChild("decodeInt", blockOnChild = YamlInput::decodeInt)
override fun decodeLong(): Long = maybeCallOnChild("decodeLong", blockOnChild = YamlInput::decodeLong)
override fun decodeFloat(): Float = maybeCallOnChild("decodeFloat", blockOnChild = YamlInput::decodeFloat)
override fun decodeDouble(): Double = maybeCallOnChild("decodeDouble", blockOnChild = YamlInput::decodeDouble)
override fun decodeChar(): Char = maybeCallOnChild("decodeChar", blockOnChild = YamlInput::decodeChar)
override fun decodeString(): String = maybeCallOnChild(blockOnTag = taggedNode::tag, blockOnChild = YamlInput::decodeString)
override fun decodeEnum(enumDescription: EnumDescriptor): Int = maybeCallOnChild("decodeEnum") { decodeEnum(enumDescription) }

override fun beginStructure(desc: SerialDescriptor, vararg typeParams: KSerializer<*>): CompositeDecoder {
desc.calculatePolymorphic()
Expand All @@ -334,6 +347,9 @@ private class YamlTaggedInput(val taggedNode: YamlTaggedNode, context: SerialMod
isPolymorphic = kind === UnionKind.POLYMORPHIC
}

private inline fun <T> maybeCallOnChild(functionName: String, blockOnChild: YamlInput.() -> T): T =
maybeCallOnChild(blockOnTag = { throw IllegalArgumentException("can't call $functionName on tag") }, blockOnChild = blockOnChild)

private inline fun <T> maybeCallOnChild(blockOnTag: () -> T, blockOnChild: YamlInput.() -> T): T {
return if (isPolymorphic && currentIndex != 1) {
blockOnTag()
Expand Down
17 changes: 11 additions & 6 deletions src/main/kotlin/com/charleskorn/kaml/YamlOutput.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import kotlinx.serialization.CompositeEncoder
import kotlinx.serialization.ElementValueEncoder
import kotlinx.serialization.KSerializer
import kotlinx.serialization.PolymorphicSerializer
import kotlinx.serialization.PrimitiveKind
import kotlinx.serialization.SerialDescriptor
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.StructureKind
Expand Down Expand Up @@ -96,24 +97,27 @@ internal class YamlOutput(
val tag = getAndClearTag()
val implicit = !tag.isPresent
when (desc.kind) {
is StructureKind.LIST -> emitter.emit(SequenceStartEvent(Optional.empty(), tag, implicit, FlowStyle.BLOCK))
is StructureKind.MAP, StructureKind.CLASS -> emitter.emit(MappingStartEvent(Optional.empty(), tag, implicit, FlowStyle.BLOCK))
StructureKind.LIST -> emitter.emit(SequenceStartEvent(Optional.empty(), tag, implicit, FlowStyle.BLOCK))
StructureKind.MAP, StructureKind.CLASS, PrimitiveKind.UNIT -> emitter.emit(MappingStartEvent(Optional.empty(), tag, implicit, FlowStyle.BLOCK))
}

return super.beginStructure(desc, *typeParams)
}

override fun endStructure(desc: SerialDescriptor) {
when (desc.kind) {
is StructureKind.LIST -> emitter.emit(SequenceEndEvent())
is StructureKind.MAP, StructureKind.CLASS -> emitter.emit(MappingEndEvent())
StructureKind.LIST -> emitter.emit(SequenceEndEvent())
StructureKind.MAP, StructureKind.CLASS, PrimitiveKind.UNIT -> emitter.emit(MappingEndEvent())
}

super.endStructure(desc)
}

private fun emitScalar(value: String, style: ScalarStyle) =
emitter.emit(ScalarEvent(Optional.empty(), getAndClearTag(), ALL_IMPLICIT, value, style))
private fun emitScalar(value: String, style: ScalarStyle) {
val tag = getAndClearTag()
val implicit = if (tag.isPresent) ALL_EXPLICIT else ALL_IMPLICIT
emitter.emit(ScalarEvent(Optional.empty(), tag, implicit, value, style))
}

private fun getAndClearTag(): Optional<String> {
val tag = Optional.ofNullable(currentTag)
Expand All @@ -123,5 +127,6 @@ internal class YamlOutput(

companion object {
private val ALL_IMPLICIT = ImplicitTuple(true, true)
private val ALL_EXPLICIT = ImplicitTuple(false, false)
}
}
14 changes: 1 addition & 13 deletions src/main/kotlin/com/charleskorn/kaml/YamlParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package com.charleskorn.kaml

import org.snakeyaml.engine.v1.api.LoadSettingsBuilder
import org.snakeyaml.engine.v1.events.Event
import org.snakeyaml.engine.v1.events.ScalarEvent
import org.snakeyaml.engine.v1.exceptions.MarkedYamlEngineException
import org.snakeyaml.engine.v1.parser.ParserImpl
import org.snakeyaml.engine.v1.scanner.StreamReader
Expand Down Expand Up @@ -60,23 +59,12 @@ class YamlParser(yamlSource: String) {

private fun checkEvent(retrieve: () -> Event): Event {
try {
val event = retrieve()
checkForUnsupportedFeatures(event)

return event
return retrieve()
} catch (e: MarkedYamlEngineException) {
throw translateYamlEngineException(e)
}
}

private fun checkForUnsupportedFeatures(event: Event) {
if (event is ScalarEvent) {
if (event.tag.isPresent) {
throw UnsupportedYamlFeatureException("tags", event)
}
}
}

private fun translateYamlEngineException(e: MarkedYamlEngineException): MalformedYamlException {
val contextMessage = if (e.context == null) {
""
Expand Down
4 changes: 0 additions & 4 deletions src/test/kotlin/com/charleskorn/kaml/Assertions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,6 @@ fun Assert<UnknownPropertyException>.validPropertyNames(assertionCreator: Assert
property(subject::validPropertyNames).addAssertionsCreatedBy(assertionCreator)
}

fun Assert<UnsupportedYamlFeatureException>.featureName(assertionCreator: Assert<String>.() -> Unit) {
property(subject::featureName).addAssertionsCreatedBy(assertionCreator)
}

fun Assert<YamlScalarFormatException>.originalValue(assertionCreator: Assert<String>.() -> Unit) {
property(subject::originalValue).addAssertionsCreatedBy(assertionCreator)
}
18 changes: 5 additions & 13 deletions src/test/kotlin/com/charleskorn/kaml/YamlNodeReaderTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -966,21 +966,13 @@ object YamlNodeReaderTest : Spek({
}

mapOf(
"!thing" to "tags",
"!!str 'some string'" to "tags"
"!thing" to YamlTaggedNode("!thing", YamlNull(Location(1, 1))),
"!!str 'some string'" to YamlTaggedNode("tag:yaml.org,2002:str", YamlScalar("some string", Location(1, 1)))
).forEach { input, featureName ->
context("given the input '$input' which contains an unsupported YAML feature") {
context("given the input '$input' which contains a tagged node") {
describe("parsing that input") {
it("throws an appropriate exception stating that the YAML feature being used is not supported") {
assert({
val parser = YamlParser(input)
YamlNodeReader(parser).read()
}).toThrow<UnsupportedYamlFeatureException> {
message { toBe("Unsupported YAML feature: $featureName") }
line { toBe(1) }
column { toBe(1) }
featureName { toBe(featureName) }
}
it("returns the expected node") {
assert(YamlNodeReader(YamlParser(input)).read()).toBe(featureName)
}
}
}
Expand Down
75 changes: 74 additions & 1 deletion src/test/kotlin/com/charleskorn/kaml/YamlReadingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,26 @@ import ch.tutteli.atrium.api.cc.en_GB.toThrow
import ch.tutteli.atrium.verbs.assert
import com.charleskorn.kaml.testobjects.NestedObjects
import com.charleskorn.kaml.testobjects.SealedWrapper
import com.charleskorn.kaml.testobjects.SimpleBoolean
import com.charleskorn.kaml.testobjects.SimpleByte
import com.charleskorn.kaml.testobjects.SimpleChar
import com.charleskorn.kaml.testobjects.SimpleDouble
import com.charleskorn.kaml.testobjects.SimpleEnum
import com.charleskorn.kaml.testobjects.SimpleFloat
import com.charleskorn.kaml.testobjects.SimpleInt
import com.charleskorn.kaml.testobjects.SimpleLong
import com.charleskorn.kaml.testobjects.SimpleNull
import com.charleskorn.kaml.testobjects.SimpleNullableInt
import com.charleskorn.kaml.testobjects.SimpleShort
import com.charleskorn.kaml.testobjects.SimpleString
import com.charleskorn.kaml.testobjects.SimpleStructure
import com.charleskorn.kaml.testobjects.SimpleUnit
import com.charleskorn.kaml.testobjects.SimpleWrapper
import com.charleskorn.kaml.testobjects.Team
import com.charleskorn.kaml.testobjects.TestEnum
import com.charleskorn.kaml.testobjects.TestSealedStructure
import com.charleskorn.kaml.testobjects.sealedModule
import com.charleskorn.kaml.testobjects.simpleModule
import kotlinx.serialization.ContextualSerialization
import kotlinx.serialization.Decoder
import kotlinx.serialization.Encoder
Expand Down Expand Up @@ -945,6 +960,64 @@ object YamlReadingTest : Spek({
}
}

val simpleYaml = Yaml(context = simpleModule)

context("given some simple int input representing an object") {
val input = """
test: !<simpleInt> 42
""".trimIndent()

context("parsing that input") {
val result = simpleYaml.parse(SimpleWrapper.serializer(), input)
it("deserializes it to a Kotlin object") {
assert(result).toBe(SimpleWrapper(SimpleInt(42)))
}
}
}

context("given some simple inputs representing a list of object") {
val input = """
- test: !<simpleNull> null
- test: !<simpleUnit> {}
- test: !<simpleBoolean> 'false'
- test: !<simpleByte> 42
- test: !<simpleShort> 43
- test: !<simpleInt> 44
- test: !<simpleLong> 45
- test: !<simpleFloat> 4.2
- test: !<simpleDouble> 4.2
- test: !<simpleChar> 4
- test: !<simpleString> 42
- test: !<simpleEnum> TEST2
- test: !<simpleNullableInt> 4
- test: !<simpleNullableInt> null
""".trimIndent()

context("parsing that input") {
val result = simpleYaml.parse(SimpleWrapper.serializer().list, input)
it("deserializes it to a Kotlin object") {
assert(result).toBe(
listOf(
SimpleWrapper(SimpleNull),
SimpleWrapper(SimpleUnit(Unit)),
SimpleWrapper(SimpleBoolean(false)),
SimpleWrapper(SimpleByte(42)),
SimpleWrapper(SimpleShort(43)),
SimpleWrapper(SimpleInt(44)),
SimpleWrapper(SimpleLong(45L)),
SimpleWrapper(SimpleFloat(4.2f)),
SimpleWrapper(SimpleDouble(4.2)),
SimpleWrapper(SimpleChar('4')),
SimpleWrapper(SimpleString("42")),
SimpleWrapper(SimpleEnum.TEST2),
SimpleWrapper(SimpleNullableInt(4)),
SimpleWrapper(SimpleNullableInt(null))
)
)
}
}
}

context("given some input representing an object with an unknown key") {
val input = """
abc123: something
Expand Down Expand Up @@ -1013,7 +1086,7 @@ object YamlReadingTest : Spek({

context("given some input representing an object with a tagged value as a key") {
val input = """
!<sealedInt> { }: something
!<sealedInt> test: something
""".trimIndent()

context("parsing that input") {
Expand Down
Loading

0 comments on commit 65c03d3

Please sign in to comment.