Skip to content

Commit

Permalink
Merge pull request #507 from krzema12/convert-jvm-to-use-kmp
Browse files Browse the repository at this point in the history
Convert JVM target to use KMP
  • Loading branch information
charleskorn authored Mar 20, 2024
2 parents 861c1b9 + cfc2a74 commit 24e1c02
Show file tree
Hide file tree
Showing 16 changed files with 853 additions and 1,304 deletions.
23 changes: 4 additions & 19 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,11 @@ kotlin {
}

sourceSets {
all {
languageSettings.optIn("kotlin.RequiresOptIn")
}

commonMain {
dependencies {
api("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3")
implementation("it.krzeminski:snakeyaml-engine-kmp:2.7.3")
implementation("com.squareup.okio:okio:3.8.0")
}
}

Expand All @@ -80,28 +78,15 @@ kotlin {
}
}

named("jvmMain") {
dependencies {
implementation("org.snakeyaml:snakeyaml-engine:2.7")
}
}

named("jvmTest") {
jvmTest {
dependencies {
implementation("io.kotest:kotest-runner-junit5:5.8.1")
}
}

named("jsMain") {
dependencies {
implementation("it.krzeminski:snakeyaml-engine-kmp:2.7.3")
implementation("com.squareup.okio:okio:3.9.0")
}
}
}
}

tasks.withType<KotlinCompile> {
tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_1_8)
}
Expand Down
684 changes: 373 additions & 311 deletions kotlin-js-store/yarn.lock

Large diffs are not rendered by default.

105 changes: 98 additions & 7 deletions src/commonMain/kotlin/com/charleskorn/kaml/Yaml.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,111 @@

package com.charleskorn.kaml

import com.charleskorn.kaml.internal.bufferedSource
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.StringFormat
import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule
import okio.Buffer
import okio.BufferedSink
import okio.Sink
import okio.Source
import okio.buffer
import org.snakeyaml.engine.v2.api.StreamDataWriter

public expect class Yaml(
serializersModule: SerializersModule = EmptySerializersModule(),
configuration: YamlConfiguration = YamlConfiguration(),
public class Yaml(
override val serializersModule: SerializersModule = EmptySerializersModule(),
public val configuration: YamlConfiguration = YamlConfiguration(),
) : StringFormat {
public val configuration: YamlConfiguration

public fun <T> decodeFromYamlNode(deserializer: DeserializationStrategy<T>, node: YamlNode): T

public companion object {
public val default: Yaml
public val default: Yaml = Yaml()
}

public fun <T> decodeFromYamlNode(
deserializer: DeserializationStrategy<T>,
node: YamlNode,
): T {
val input = YamlInput.createFor(node, this, serializersModule, configuration, deserializer.descriptor)
return input.decodeSerializableValue(deserializer)
}

override fun <T> decodeFromString(
deserializer: DeserializationStrategy<T>,
string: String,
): T {
return decodeFromSource(deserializer, string.bufferedSource())
}

public fun <T> decodeFromSource(
deserializer: DeserializationStrategy<T>,
source: Source,
): T {
val rootNode = parseToYamlNode(source)

val input = YamlInput.createFor(rootNode, this, serializersModule, configuration, deserializer.descriptor)
return input.decodeSerializableValue(deserializer)
}

public fun parseToYamlNode(string: String): YamlNode =
parseToYamlNode(string.bufferedSource())

internal fun parseToYamlNode(source: Source): YamlNode {
val parser = YamlParser(source)
val reader =
YamlNodeReader(parser, configuration.extensionDefinitionPrefix, configuration.allowAnchorsAndAliases)
val node = reader.read()
parser.ensureEndOfStreamReached()
return node
}

public fun <T> encodeToSink(
serializer: SerializationStrategy<T>,
value: T,
sink: Sink,
) {
encodeToBufferedSink(serializer, value, sink.buffer())
}

override fun <T> encodeToString(
serializer: SerializationStrategy<T>,
value: T,
): String {
val buffer = Buffer()
encodeToBufferedSink(serializer, value, buffer)
return buffer.readUtf8().trimEnd()
}

@OptIn(ExperimentalStdlibApi::class)
private fun <T> encodeToBufferedSink(
serializer: SerializationStrategy<T>,
value: T,
sink: BufferedSink,
) {
BufferedSinkDataWriter(sink).use { writer ->
YamlOutput(writer, serializersModule, configuration).use { output ->
output.encodeSerializableValue(serializer, value)
}
}
}
}

@OptIn(ExperimentalStdlibApi::class)
private class BufferedSinkDataWriter(
val sink: BufferedSink,
) : StreamDataWriter, AutoCloseable {
override fun flush(): Unit = sink.flush()

override fun write(str: String) {
sink.writeUtf8(str)
}

override fun write(str: String, off: Int, len: Int) {
sink.writeUtf8(string = str, beginIndex = off, endIndex = off + len)
}

override fun close() {
flush()
}
}
191 changes: 189 additions & 2 deletions src/commonMain/kotlin/com/charleskorn/kaml/YamlNodeReader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,193 @@

package com.charleskorn.kaml

internal expect class YamlNodeReader {
fun read(): YamlNode
import org.snakeyaml.engine.v2.common.Anchor
import org.snakeyaml.engine.v2.events.AliasEvent
import org.snakeyaml.engine.v2.events.Event
import org.snakeyaml.engine.v2.events.MappingStartEvent
import org.snakeyaml.engine.v2.events.NodeEvent
import org.snakeyaml.engine.v2.events.ScalarEvent
import org.snakeyaml.engine.v2.events.SequenceStartEvent

internal class YamlNodeReader(
private val parser: YamlParser,
private val extensionDefinitionPrefix: String? = null,
private val allowAnchorsAndAliases: Boolean = false,
) {
private val aliases = mutableMapOf<Anchor, YamlNode>()

fun read(): YamlNode = readNode(YamlPath.root)

private fun readNode(path: YamlPath): YamlNode = readNodeAndAnchor(path).first

private fun readNodeAndAnchor(path: YamlPath): Pair<YamlNode, Anchor?> {
val event = parser.consumeEvent(path)
val node = readFromEvent(event, path)

if (event is NodeEvent) {
event.anchor?.let {
if (!allowAnchorsAndAliases) {
throw ForbiddenAnchorOrAliasException("Parsing anchors and aliases is disabled.", path)
}

aliases.put(it, node.withPath(YamlPath.forAliasDefinition(it.value, event.location)))
}

return node to event.anchor
}

return node to null
}

private fun readFromEvent(event: Event, path: YamlPath): YamlNode = when (event) {
is ScalarEvent -> readScalarOrNull(event, path).maybeToTaggedNode(event.tag)
is SequenceStartEvent -> readSequence(path).maybeToTaggedNode(event.tag)
is MappingStartEvent -> readMapping(path).maybeToTaggedNode(event.tag)
is AliasEvent -> readAlias(event, path)
else -> throw MalformedYamlException("Unexpected ${event.eventId}", path.withError(event.location))
}

private fun readScalarOrNull(event: ScalarEvent, path: YamlPath): YamlNode {
if ((event.value == "null" || event.value == "" || event.value == "~") && event.plain) {
return YamlNull(path)
} else {
return YamlScalar(event.value, path)
}
}

private fun readSequence(path: YamlPath): YamlList {
val items = mutableListOf<YamlNode>()

while (true) {
val event = parser.peekEvent(path)

when (event.eventId) {
Event.ID.SequenceEnd -> {
parser.consumeEventOfType(Event.ID.SequenceEnd, path)
return YamlList(items, path)
}

else -> items += readNode(path.withListEntry(items.size, event.location))
}
}
}

private fun readMapping(path: YamlPath): YamlMap {
val items = mutableMapOf<YamlScalar, YamlNode>()

while (true) {
val event = parser.peekEvent(path)

when (event.eventId) {
Event.ID.MappingEnd -> {
parser.consumeEventOfType(Event.ID.MappingEnd, path)
return YamlMap(doMerges(items), path)
}

else -> {
val keyLocation = parser.peekEvent(path).location
val key = readMapKey(path)
val keyNode = YamlScalar(key, path.withMapElementKey(key, keyLocation))

val valueLocation = parser.peekEvent(keyNode.path).location
val valuePath = if (isMerge(keyNode)) path.withMerge(valueLocation) else keyNode.path.withMapElementValue(valueLocation)
val (value, anchor) = readNodeAndAnchor(valuePath)

if (path == YamlPath.root && extensionDefinitionPrefix != null && key.startsWith(extensionDefinitionPrefix)) {
if (anchor == null) {
throw NoAnchorForExtensionException(key, extensionDefinitionPrefix, path.withError(event.location))
}
} else {
items += (keyNode to value)
}
}
}
}
}

private fun readMapKey(path: YamlPath): String {
val event = parser.peekEvent(path)

when (event.eventId) {
Event.ID.Scalar -> {
parser.consumeEventOfType(Event.ID.Scalar, path)
val scalarEvent = event as ScalarEvent
val isNullKey = (scalarEvent.value == "null" || scalarEvent.value == "~") && scalarEvent.plain

if (scalarEvent.tag != null || isNullKey) {
throw nonScalarMapKeyException(path, event)
}

return scalarEvent.value
}
else -> throw nonScalarMapKeyException(path, event)
}
}

private fun nonScalarMapKeyException(path: YamlPath, event: Event) = MalformedYamlException("Property name must not be a list, map, null or tagged value. (To use 'null' as a property name, enclose it in quotes.)", path.withError(event.location))

private fun YamlNode.maybeToTaggedNode(tag: String?): YamlNode =
tag?.let { YamlTaggedNode(it, this) } ?: this

private fun doMerges(items: Map<YamlScalar, YamlNode>): Map<YamlScalar, YamlNode> {
val mergeEntries = items.entries.filter { (key, _) -> isMerge(key) }

when (mergeEntries.count()) {
0 -> return items
1 -> when (val mappingsToMerge = mergeEntries.single().value) {
is YamlList -> return doMerges(items, mappingsToMerge.items)
else -> return doMerges(items, listOf(mappingsToMerge))
}
else -> throw MalformedYamlException("Cannot perform multiple '<<' merges into a map. Instead, combine all merges into a single '<<' entry.", mergeEntries.second().key.path)
}
}

private fun isMerge(key: YamlNode): Boolean = key is YamlScalar && key.content == "<<"

private fun doMerges(original: Map<YamlScalar, YamlNode>, others: List<YamlNode>): Map<YamlScalar, YamlNode> {
val merged = mutableMapOf<YamlScalar, YamlNode>()

original
.filterNot { (key, _) -> isMerge(key) }
.forEach { (key, value) -> merged.put(key, value) }

others
.forEach { other ->
when (other) {
is YamlNull -> throw MalformedYamlException("Cannot merge a null value into a map.", other.path)
is YamlScalar -> throw MalformedYamlException("Cannot merge a scalar value into a map.", other.path)
is YamlList -> throw MalformedYamlException("Cannot merge a list value into a map.", other.path)
is YamlTaggedNode -> throw MalformedYamlException("Cannot merge a tagged value into a map.", other.path)
is YamlMap ->
other.entries.forEach { (key, value) ->
val existingEntry = merged.entries.singleOrNull { it.key.equivalentContentTo(key) }

if (existingEntry == null) {
merged.put(key, value)
}
}
}
}

return merged
}

private fun readAlias(event: AliasEvent, path: YamlPath): YamlNode {
if (!allowAnchorsAndAliases) {
throw ForbiddenAnchorOrAliasException("Parsing anchors and aliases is disabled.", path)
}

val anchor = event.anchor!!

val resolvedNode = aliases.getOrElse(anchor) {
throw UnknownAnchorException(anchor.value, path.withError(event.location))
}

return resolvedNode.withPath(path.withAliasReference(anchor.value, event.location).withAliasDefinition(anchor.value, resolvedNode.location))
}

private fun <T> Iterable<T>.second(): T = this.drop(1).first()

private val Event.location: Location
get() = Location(startMark!!.line + 1, startMark!!.column + 1)
}
Loading

0 comments on commit 24e1c02

Please sign in to comment.