Skip to content

Commit

Permalink
Merge pull request #2 from aSemy/convert-jvm-to-use-kmp
Browse files Browse the repository at this point in the history
Convert jvm to use kmp: implement reading/writing from Java input/output streams
  • Loading branch information
krzema12 authored Mar 7, 2024
2 parents 09fe58b + eb35f00 commit f13cc8c
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 95 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ jobs:
fetch-depth: 0

- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@v2.0.0
uses: gradle/wrapper-validation-action@v2.1.1

- name: Set up JDK
uses: actions/setup-java@v4.0.0
uses: actions/setup-java@v4.1.0
with:
java-version: 17
distribution: temurin

- name: Setup Gradle
uses: gradle/gradle-build-action@v3.0.0
uses: gradle/gradle-build-action@v3.1.0

- name: Build
run: ./gradlew assemble
Expand Down
6 changes: 3 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ kotlin {

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

Expand Down Expand Up @@ -103,7 +103,7 @@ java {

configureAssemble()
configurePublishing()
configureSpotless()
//configureSpotless()
configureTesting()
configureVersioning()
configureWrapper()
2 changes: 1 addition & 1 deletion buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ repositories {
dependencies {
implementation(group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version = "6.25.0")
implementation(group = "io.github.gradle-nexus", name = "publish-plugin", version = "1.3.0")
implementation(group = "org.ajoberstar.reckon", name = "reckon-gradle", version = "0.18.2")
implementation(group = "org.ajoberstar.reckon", name = "reckon-gradle", version = "0.18.3")
}

java {
Expand Down
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=c16d517b50dd28b3f5838f0e844b7520b8f1eb610f2f29de7e4e04a1b7c9c79b
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip
distributionSha256Sum=85719317abd2112f021d4f41f09ec370534ba288432065f4b477b6a3b652910d
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
70 changes: 38 additions & 32 deletions src/commonMain/kotlin/com/charleskorn/kaml/Yaml.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,16 @@

package com.charleskorn.kaml

import com.charleskorn.kaml.internal.StringStreamDataWriter
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.ByteString.Companion.encodeUtf8
import okio.Sink
import okio.Source
import org.snakeyaml.engine.v2.api.StreamDataWriter
import okio.buffer

public class Yaml(
override val serializersModule: SerializersModule = EmptySerializersModule(),
Expand All @@ -41,55 +42,60 @@ public class Yaml(
}

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

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

private fun <T> decodeFromReader(deserializer: DeserializationStrategy<T>, source: Source): T {
val rootNode = parseToYamlNodeFromReader(source)
public fun <T> decodeFromSource(
deserializer: DeserializationStrategy<T>,
source: Source,
): T {
val rootNode = parseToYamlNodeFromSource(source)

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

private fun parseToYamlNodeFromReader(source: Source): YamlNode {
public fun parseToYamlNode(string: String): YamlNode = parseToYamlNodeFromSource(string.bufferedSource())

internal fun parseToYamlNodeFromSource(source: Source): YamlNode {
val parser = YamlParser(source)
val reader = YamlNodeReader(parser, configuration.extensionDefinitionPrefix, configuration.allowAnchorsAndAliases)
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)
public fun <T> encodeToSink(
serializer: SerializationStrategy<T>,
value: T,
sink: Sink,
) {
val writer = encodeToStreamDataWriter(serializer, value)
writer.buffer().readAll(sink)
}

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

@OptIn(ExperimentalStdlibApi::class)
private fun <T> encodeToStreamDataWriter(serializer: SerializationStrategy<T>, value: T, writer: StreamDataWriter) {
private fun <T> encodeToStreamDataWriter(
serializer: SerializationStrategy<T>,
value: T,
): StringStreamDataWriter {
val writer = StringStreamDataWriter()
@OptIn(ExperimentalStdlibApi::class)
YamlOutput(writer, serializersModule, configuration).use { output ->
output.encodeSerializableValue(serializer, value)
}
return writer
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.charleskorn.kaml.internal

import okio.Buffer
import okio.Source
import org.snakeyaml.engine.v2.api.StreamDataWriter

internal class StringStreamDataWriter(
private val buffer: Buffer = Buffer(),
) : StreamDataWriter, Source by buffer {
override fun flush(): Unit = buffer.flush()

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

override fun write(str: String, off: Int, len: Int) {
buffer.writeUtf8(string = str, beginIndex = off, endIndex = off + len)
}
}
31 changes: 31 additions & 0 deletions src/commonMain/kotlin/com/charleskorn/kaml/internal/okio.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
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.internal

import okio.Buffer
import okio.BufferedSource
import okio.ByteString.Companion.encodeUtf8

/**
* Convert a UTF-8 String to a [BufferedSource].
*/
// https://github.com/square/okio/issues/774#issuecomment-703315013
internal fun String.bufferedSource(
// charset: String = "utf8" // TODO convert charsets?
): BufferedSource = Buffer().write(encodeUtf8())
79 changes: 34 additions & 45 deletions src/jvmMain/kotlin/com/charleskorn/kaml/JvmYamlReading.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,54 +19,43 @@
package com.charleskorn.kaml

import kotlinx.serialization.DeserializationStrategy
import okio.Buffer
import kotlinx.serialization.serializer
import okio.Source
import okio.Timeout
import okio.source
import java.io.InputStream
import java.io.InputStreamReader
import java.io.Reader
import java.nio.charset.Charset

public fun <T> Yaml.decodeFromStream(deserializer: DeserializationStrategy<T>, source: InputStream, charset: Charset = Charsets.UTF_8): T =
decodeFromReader(deserializer, InputStreamReader(source, charset))

public inline fun <reified T> Yaml.decodeFromStream(stream: InputStream): T =
TODO()

public fun Yaml.parseToYamlNode(string: String): YamlNode =
TODO()

public fun Yaml.parseToYamlNode(source: InputStream): YamlNode =
TODO()

private fun <T> Yaml.decodeFromReader(deserializer: DeserializationStrategy<T>, source: Reader): T {
val rootNode = parseToYamlNodeFromReader(source)

val input = YamlInput.createFor(rootNode, this, serializersModule, configuration, deserializer.descriptor)
return input.decodeSerializableValue(deserializer)
public fun <T> Yaml.decodeFromStream(
deserializer: DeserializationStrategy<T>,
source: InputStream,
charset: Charset = Charsets.UTF_8,
): T {
return decodeFromSource(
deserializer,
source.source(),
charset,
)
}

private fun Yaml.parseToYamlNodeFromReader(source: Reader): YamlNode {
val parser = YamlParser(source.toSource())
val reader = YamlNodeReader(parser, configuration.extensionDefinitionPrefix, configuration.allowAnchorsAndAliases)
val node = reader.read()
parser.ensureEndOfStreamReached()
return node
}

private fun Reader.toSource(): Source =
object : Source {
override fun read(sink: Buffer, byteCount: Long): Long {
val charArray = CharArray(byteCount.toInt())
return this@toSource.read(charArray, 0, byteCount.toInt()).toLong()
}

override fun timeout(): Timeout {
// TODO: use some sensible values here
return Timeout()
}

override fun close() {
this@toSource.close()
}
}
public inline fun <reified T> Yaml.decodeFromStream(
stream: InputStream
): T =
decodeFromSource(
deserializer = serializersModule.serializer<T>(),
source = stream.source(),
)

public fun Yaml.parseToYamlNode(
source: InputStream
): YamlNode =
parseToYamlNodeFromSource(source.source())

public fun <T> Yaml.decodeFromSource(
deserializer: DeserializationStrategy<T>,
source: Source,
charset: Charset = Charsets.UTF_8, // TODO convert charsets to UTF8 https://kotlinlang.slack.com/archives/C5HT9AL7Q/p1685660615754469?thread_ts=1685549089.185459&cid=C5HT9AL7Q
): T =
decodeFromSource(
deserializer,
source,
)
22 changes: 17 additions & 5 deletions src/jvmMain/kotlin/com/charleskorn/kaml/JvmYamlWriting.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,23 @@
package com.charleskorn.kaml

import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.serializer
import okio.sink
import java.io.OutputStream
import java.nio.charset.Charset

public fun <T> Yaml.encodeToStream(serializer: SerializationStrategy<T>, value: T, stream: OutputStream, charset: Charset = Charsets.UTF_8): Nothing =
TODO()

public inline fun <reified T> Yaml.encodeToStream(value: T, stream: OutputStream): Nothing =
TODO()
public fun <T> Yaml.encodeToStream(
serializer: SerializationStrategy<T>,
value: T,
stream: OutputStream,
charset: Charset = Charsets.UTF_8, // TODO convert charsets
) {
encodeToSink(serializer, value, stream.sink())
}

public inline fun <reified T> Yaml.encodeToStream(
value: T,
stream: OutputStream,
) {
encodeToStream(serializersModule.serializer(), value, stream)
}
7 changes: 3 additions & 4 deletions src/jvmTest/kotlin/com/charleskorn/kaml/JvmYamlReadingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,12 @@ package com.charleskorn.kaml
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
import kotlinx.serialization.builtins.serializer
import java.io.ByteArrayInputStream

class JvmYamlReadingTest : DescribeSpec({
describe("JVM-specific extensions for YAML reading") {
describe("parsing from a stream") {
val input = "123"
val result = Yaml.default.decodeFromStream(Int.serializer(), ByteArrayInputStream(input.toByteArray(Charsets.UTF_8)))
val result = Yaml.default.decodeFromStream(Int.serializer(), input.byteInputStream())

it("successfully deserializes values from a stream") {
result shouldBe 123
Expand All @@ -36,7 +35,7 @@ class JvmYamlReadingTest : DescribeSpec({

describe("parsing from a stream via generic extension function") {
val input = "123"
val result = Yaml.default.decodeFromStream<Int>(ByteArrayInputStream(input.toByteArray(Charsets.UTF_8)))
val result = Yaml.default.decodeFromStream<Int>(input.byteInputStream())

it("successfully deserializes values from a stream") {
result shouldBe 123
Expand All @@ -54,7 +53,7 @@ class JvmYamlReadingTest : DescribeSpec({

describe("parsing into a YamlNode from a stream") {
val input = "123"
val result = Yaml.default.parseToYamlNode(ByteArrayInputStream(input.toByteArray(Charsets.UTF_8)))
val result = Yaml.default.parseToYamlNode(input.byteInputStream())

it("successfully deserializes values from a stream") {
result shouldBe YamlScalar("123", YamlPath.root)
Expand Down

0 comments on commit f13cc8c

Please sign in to comment.