-
Notifications
You must be signed in to change notification settings - Fork 51
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #437 from krzema12/implement-JS-target
Implement JS target (experimental)
- Loading branch information
Showing
11 changed files
with
3,064 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
/* | ||
Copyright 2018-2023 Charles Korn. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
https://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package com.charleskorn.kaml | ||
|
||
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.ByteString.Companion.encodeUtf8 | ||
import okio.Source | ||
import org.snakeyaml.engine.v2.api.StreamDataWriter | ||
|
||
public actual class Yaml( | ||
override val serializersModule: SerializersModule = EmptySerializersModule(), | ||
public actual val configuration: YamlConfiguration = YamlConfiguration(), | ||
) : StringFormat { | ||
public actual fun <T> decodeFromYamlNode( | ||
deserializer: DeserializationStrategy<T>, | ||
node: YamlNode, | ||
): T { | ||
val input = YamlInput.createFor(node, this, serializersModule, configuration, deserializer.descriptor) | ||
return input.decodeSerializableValue(deserializer) | ||
} | ||
|
||
public actual companion object { | ||
public actual val default: Yaml = Yaml() } | ||
|
||
override fun <T> decodeFromString(deserializer: DeserializationStrategy<T>, string: String): T { | ||
return decodeFromReader(deserializer, Buffer().write(string.encodeUtf8())) | ||
} | ||
|
||
private fun <T> decodeFromReader(deserializer: DeserializationStrategy<T>, source: Source): T { | ||
val rootNode = parseToYamlNodeFromReader(source) | ||
|
||
val input = YamlInput.createFor(rootNode, this, serializersModule, configuration, deserializer.descriptor) | ||
return input.decodeSerializableValue(deserializer) | ||
} | ||
|
||
private fun parseToYamlNodeFromReader(source: Source): YamlNode { | ||
val parser = YamlParser(source) | ||
val reader = YamlNodeReader(parser, configuration.extensionDefinitionPrefix, configuration.allowAnchorsAndAliases) | ||
val node = reader.read() | ||
parser.ensureEndOfStreamReached() | ||
return node | ||
} | ||
|
||
override fun <T> encodeToString(serializer: SerializationStrategy<T>, value: T): String { | ||
val writer = object : StreamDataWriter { | ||
private var stringBuilder = StringBuilder() | ||
|
||
override fun flush() { } | ||
|
||
override fun write(str: String) { | ||
stringBuilder.append(str) | ||
} | ||
|
||
override fun write(str: String, off: Int, len: Int) { | ||
stringBuilder.append(str.drop(off).subSequence(0, len)) | ||
} | ||
|
||
override fun toString(): String { | ||
return stringBuilder.toString() | ||
} | ||
} | ||
|
||
encodeToStreamDataWriter(serializer, value, writer) | ||
|
||
return writer.toString().trimEnd() | ||
} | ||
|
||
@OptIn(ExperimentalStdlibApi::class) | ||
private fun <T> encodeToStreamDataWriter(serializer: SerializationStrategy<T>, value: T, writer: StreamDataWriter) { | ||
YamlOutput(writer, serializersModule, configuration).use { output -> | ||
output.encodeSerializableValue(serializer, value) | ||
} | ||
} | ||
} |
210 changes: 210 additions & 0 deletions
210
src/jsMain/kotlin/com/charleskorn/kaml/YamlNodeReader.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
/* | ||
Copyright 2018-2023 Charles Korn. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
https://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package com.charleskorn.kaml | ||
|
||
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 actual class YamlNodeReader( | ||
private val parser: YamlParser, | ||
private val extensionDefinitionPrefix: String? = null, | ||
private val allowAnchorsAndAliases: Boolean = false, | ||
) { | ||
private val aliases = mutableMapOf<Anchor, YamlNode>() | ||
|
||
actual 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) | ||
} |
Oops, something went wrong.