From 11826d5e5c1c237d709b946edcd79312929c4b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Bl=C3=A1zquez?= Date: Wed, 21 Jun 2023 15:36:37 +0200 Subject: [PATCH 1/2] Use exceptions internally to reduce allocations and improve performance --- .../kotlin/simpleJson/JsonExtensions.kt | 8 +- .../kotlin/simpleJson/JsonReader.kt | 237 +++++++++--------- .../src/jvmTest/kotlin/FileReadingTest.kt | 3 +- .../simpleJson/reflection/ReflectionReader.kt | 6 - 4 files changed, 127 insertions(+), 127 deletions(-) diff --git a/simpleJson-core/src/commonMain/kotlin/simpleJson/JsonExtensions.kt b/simpleJson-core/src/commonMain/kotlin/simpleJson/JsonExtensions.kt index aca1379..4e56646 100644 --- a/simpleJson-core/src/commonMain/kotlin/simpleJson/JsonExtensions.kt +++ b/simpleJson-core/src/commonMain/kotlin/simpleJson/JsonExtensions.kt @@ -6,7 +6,6 @@ import arrow.core.left import arrow.core.right import okio.Buffer import okio.BufferedSink -import okio.BufferedSource import okio.use import simpleJson.exceptions.JsonException import simpleJson.exceptions.JsonPropertyNotFoundException @@ -339,9 +338,4 @@ fun JsonNode.serializePrettyTo(buffer: BufferedSink, indent: String = " "): Uni /** * Deserialize a [String] to a [JsonNode] or return a [JsonException] if the string is not valid JSON */ -fun String.deserialized(): Either = JsonReader(this).read() - -/** - * Deserialize a [String] to a [JsonNode] or return a [JsonException] if the string is not valid JSON - */ -fun BufferedSource.deserialized(): Either = JsonReader(this).read() +fun String.deserialized(): Either = JsonReader(this).read() \ No newline at end of file diff --git a/simpleJson-core/src/commonMain/kotlin/simpleJson/JsonReader.kt b/simpleJson-core/src/commonMain/kotlin/simpleJson/JsonReader.kt index 68b5679..ee9bd2e 100644 --- a/simpleJson-core/src/commonMain/kotlin/simpleJson/JsonReader.kt +++ b/simpleJson-core/src/commonMain/kotlin/simpleJson/JsonReader.kt @@ -1,11 +1,6 @@ package simpleJson import arrow.core.Either -import arrow.core.continuations.either -import arrow.core.left -import arrow.core.right -import okio.Buffer -import okio.BufferedSource import simpleJson.exceptions.JsonEOFException import simpleJson.exceptions.JsonException import simpleJson.exceptions.JsonParseException @@ -28,153 +23,181 @@ private val NUMBERS_CHARACTERS = STARTING_NUMBERS_CHARACTERS + arrayOf('.', 'e', /** * JsonReader for the specified input stream with the specified charset */ -internal class JsonReader(val reader: BufferedSource) { - /** - * JsonReader for the specified string - */ - constructor(string: String) : this(Buffer().writeUtf8(string)) - - private var current: Char? = null +internal class JsonReader(private val data: String) { private var index = 0 + private inline val current get() = data[index] + private inline val exhausted get() = index >= data.length - //After reading all json skip all whitespace and check for no more data after + /** * Reads the input stream and returns a JsonNode * @return Either a JsonException or the JsonNode */ - fun read(): Either = run { - readNextSkippingWhiteSpaces() + fun read(): Either = Either.catch { + skipWhiteSpaces() readNode() - } - .mapLeft { JsonParseException("${it.message} at index ${index - 1}", it) } - .mapLeft { if (current == null) JsonEOFException("Unexpected EOF at index ${index - 1}") else it } - - private fun readNode(): Either = either.eager { - when (current) { - JSON_LEFT_BRACKET -> readJsonArray().bind() - JSON_LEFT_BRACE -> readJsonObject().bind() - JSON_DOUBLE_QUOTE -> readString().bind() - JSON_TRUE -> readBooleanTrue().bind() - JSON_FALSE -> readBooleanFalse().bind() - JSON_NULL -> readNull().bind() - in STARTING_NUMBERS_CHARACTERS -> readNumber().bind() - else -> shift(JsonParseException("Unexpected character $current")) + }.mapLeft { + when (it) { + is JsonException -> it + is IndexOutOfBoundsException -> JsonEOFException("Unexpected EOF at index ${index - 1}") + else -> JsonParseException(it.message ?: "Unknown error", it) } } - private fun readJsonArray(): Either = either.eager { + private fun readNode() = when (current) { + JSON_LEFT_BRACKET -> readJsonArray() + JSON_LEFT_BRACE -> readJsonObject() + JSON_DOUBLE_QUOTE -> readString() + JSON_TRUE -> readBooleanTrue() + JSON_FALSE -> readBooleanFalse() + JSON_NULL -> readNull() + in STARTING_NUMBERS_CHARACTERS -> readNumber() + else -> throw JsonParseException("Unexpected character $current") + } + + + private fun readJsonArray(): JsonArray { skipWhiteSpaces() - if (current != JSON_LEFT_BRACKET) shift(JsonParseException("Expected '[' but found $current")) + if (current != JSON_LEFT_BRACKET) throw JsonParseException("Expected '[' but found $current") val list = mutableListOf() - readNextSkippingWhiteSpaces() + moveNext() + skipWhiteSpaces() + if (current == JSON_RIGHT_BRACKET) { - readNextSkippingWhiteSpaces() - return@eager list + moveNext() + skipWhiteSpaces() + return JsonArray(list) } - do readNode().bind() - .also { list.add(it) } - .also { skipWhiteSpaces() } - .let { if (current == JSON_RIGHT_BRACKET) null else Unit } - ?.also { if (current != JSON_COMMA) shift(JsonParseException("Expected ',' or ']' but found $current")) } - ?.also { readNextSkippingWhiteSpaces() } - while (current != JSON_RIGHT_BRACKET) + do { + readNode() + .also { list.add(it) } + .also { skipWhiteSpaces() } - readNextSkippingWhiteSpaces() - list - }.map(::JsonArray) + if (current == JSON_RIGHT_BRACKET) break + if (current == JSON_COMMA) { + moveNext() + skipWhiteSpaces() + continue + } + throw JsonParseException("Expected ',' or ']' but found $current") + + } while (current != JSON_RIGHT_BRACKET) + + moveNext() + skipWhiteSpaces() + return JsonArray(list) + } - private fun readJsonObject(): Either = either.eager { + private fun readJsonObject(): JsonObject { skipWhiteSpaces() - if (current != JSON_LEFT_BRACE) shift(JsonParseException("Expected '{' but found $current")) + if (current != JSON_LEFT_BRACE) throw JsonParseException("Expected '{' but found $current") val map = mutableMapOf() - readNextSkippingWhiteSpaces() + moveNext() + skipWhiteSpaces() + if (current == JSON_RIGHT_BRACE) { - readNextSkippingWhiteSpaces() - return@eager map + moveNext() + skipWhiteSpaces() + return JsonObject(map) } - do readString().bind() - .also { if (current != JSON_COLON) shift(JsonParseException("Expected ':' but found $current")) } - .also { readNextSkippingWhiteSpaces() } - .let { map[it.value] = readNode().bind() } - .let { if (current == JSON_RIGHT_BRACE) null else Unit } - ?.also { if (current != JSON_COMMA) shift(JsonParseException("Expected ',' or '}' but found $current")) } - ?.also { readNextSkippingWhiteSpaces() } - while (current != JSON_RIGHT_BRACE) + do { + readString() + .also { skipWhiteSpaces() } + .also { if (current != JSON_COLON) throw JsonParseException("Expected ':' but found $current") } + .also { moveNext() } + .also { skipWhiteSpaces() } + .let { map[it.value] = readNode() } + .also { skipWhiteSpaces() } + + if (current == JSON_RIGHT_BRACE) break + if (current == JSON_COMMA) { + moveNext() + skipWhiteSpaces() + continue + } + throw JsonParseException("Expected ',' or '}' but found $current") - readNextSkippingWhiteSpaces() - map - }.map(::JsonObject) + } while (current != JSON_RIGHT_BRACE) + + moveNext() + skipWhiteSpaces() + return JsonObject(map) + } - private fun readNumber(): Either = either.eager { + private fun readNumber(): JsonNumber { val resultBuilder = StringBuilder() while (current in NUMBERS_CHARACTERS) { resultBuilder.append(current) - readNextSkippingWhiteSpaces() + moveNext() } val result = resultBuilder.toString() val parsed = result.toLongOrNull() ?: result.toDoubleOrNull() - ?: shift(JsonParseException("Expected number but found $result")) + ?: throw JsonParseException("Expected number but found $result") - JsonNumber(parsed) + return JsonNumber(parsed) } - private fun readBooleanTrue(): Either = either.eager { - val exceptionFunc: (String) -> JsonException = { JsonParseException("Expected true but found $it") } - readOrThrow("true".length - 1, exceptionFunc) { it == "true" }.bind() + private fun readBooleanTrue(): JsonBoolean { + val exceptionFunc: (String) -> Nothing = { throw JsonParseException("Expected true but found $it") } + readOrThrow("true".length - 1, exceptionFunc) { it == "true" } + moveNext() - JsonBoolean(true) + return JsonBoolean(true) } - private fun readBooleanFalse(): Either = either.eager { - val exceptionFunc: (String) -> JsonException = { JsonParseException("Expected false but found $it") } - readOrThrow("false".length - 1, exceptionFunc) { it == "false" }.bind() + private fun readBooleanFalse(): JsonBoolean { + val exceptionFunc: (String) -> Nothing = { throw JsonParseException("Expected false but found $it") } + readOrThrow("false".length - 1, exceptionFunc) { it == "false" } + moveNext() - JsonBoolean(false) + return JsonBoolean(false) } - private fun readNull(): Either = either.eager { - val exceptionFunc: (String) -> JsonException = { JsonParseException("Expected null but found $it") } - readOrThrow("null".length - 1, exceptionFunc) { it == "null" }.bind() + private fun readNull(): JsonNull { + val exceptionFunc: (String) -> Nothing = { throw JsonParseException("Expected null but found $it") } + readOrThrow("null".length - 1, exceptionFunc) { it == "null" } + moveNext() - JsonNull + return JsonNull } - private fun readString(): Either = either.eager { + private fun readString(): JsonString { val builder = StringBuilder() - if (current != JSON_DOUBLE_QUOTE) shift(JsonParseException("Expected '\"' but found $current")) + if (current != JSON_DOUBLE_QUOTE) throw JsonParseException("Expected '\"' but found $current") - fun readChar() = either.eager { + fun readChar() = if (current == '\\') { - readEscapeSequence() ?: shift(JsonParseException("Invalid escape sequence")) - } else current!! - } + readEscapeSequence() ?: throw JsonParseException("Invalid escape sequence") + } else current + - do readNext() - .let { if (current == JSON_DOUBLE_QUOTE) null else it } - ?.let { readChar().bind() } - ?.also { builder.append(it) } - while (current != JSON_DOUBLE_QUOTE) + moveNext() + do { + if (current == JSON_DOUBLE_QUOTE) break + builder.append(readChar()) + moveNext() + } while (current != JSON_DOUBLE_QUOTE) - readNextSkippingWhiteSpaces() - JsonString(builder.toString()) + moveNext() + return JsonString(builder.toString()) } private fun readEscapeSequence(): String? { - readNext() + moveNext() return when (val escaped = current) { 'u' -> readUnicodeSequence() - else -> CONTROL_CHARACTERS[escaped].also { current = null } //This is a hack, current can be " here + else -> CONTROL_CHARACTERS[escaped] //This is a hack, current can be " here } } @@ -182,7 +205,7 @@ internal class JsonReader(val reader: BufferedSource) { val unicode = run { val builder = StringBuilder() repeat(4) { - readNext() + moveNext() builder.append(current) } builder.toString() @@ -190,46 +213,36 @@ internal class JsonReader(val reader: BufferedSource) { return unicode.toIntOrNull(16)?.toChar()?.toString() } - private fun readNext() { - if (reader.exhausted()) { - current = null - return - } - current = reader.readUtf8CodePoint().toChar() + private fun moveNext() { index++ } private tailrec fun skipWhiteSpaces() { - if (current != null && current !in WHITESPACE) return - readNext() - if (current == null) return - skipWhiteSpaces() - } - - private fun readNextSkippingWhiteSpaces() { - readNext() + if (!exhausted && current !in WHITESPACE) return + moveNext() + if (exhausted) return skipWhiteSpaces() } private inline fun readOrThrow( length: Int, - exception: (String) -> JsonException, + exception: (String) -> Nothing, predicate: (String) -> Boolean - ): Either = with(reader) { + ): String { val result = StringBuilder().append(current) repeat(length) { - readNext() + moveNext() result.append(current) } val resultString = result.toString() if (!predicate(resultString)) { - return exception(resultString).left() + exception(resultString) } - readNextSkippingWhiteSpaces() - return resultString.right() + skipWhiteSpaces() + return resultString } } diff --git a/simpleJson-core/src/jvmTest/kotlin/FileReadingTest.kt b/simpleJson-core/src/jvmTest/kotlin/FileReadingTest.kt index d8c5edf..693f1c0 100644 --- a/simpleJson-core/src/jvmTest/kotlin/FileReadingTest.kt +++ b/simpleJson-core/src/jvmTest/kotlin/FileReadingTest.kt @@ -1,4 +1,3 @@ -import okio.BufferedSource import okio.FileSystem import okio.Path.Companion.toPath import okio.buffer @@ -11,7 +10,7 @@ class FileReadingTest { @Test fun should_read_json_from_file() { val path = "src/jvmTest/resources/photos.json" - val source: BufferedSource = FileSystem.SYSTEM.source(path.toPath()).buffer() + val source = FileSystem.SYSTEM.source(path.toPath()).buffer().readUtf8() val json = source.deserialized().asArray() assert(json.getOrThrow().size == 5000) assert(json.getOrThrow().all { it["albumId"].isRightOrThrow() }) diff --git a/simpleJson-reflection/src/commonMain/kotlin/simpleJson/reflection/ReflectionReader.kt b/simpleJson-reflection/src/commonMain/kotlin/simpleJson/reflection/ReflectionReader.kt index a1201bf..6ec6155 100644 --- a/simpleJson-reflection/src/commonMain/kotlin/simpleJson/reflection/ReflectionReader.kt +++ b/simpleJson-reflection/src/commonMain/kotlin/simpleJson/reflection/ReflectionReader.kt @@ -2,7 +2,6 @@ package simpleJson.reflection import arrow.core.Either import arrow.core.continuations.either -import okio.BufferedSource import simpleJson.* import simpleJson.exceptions.JsonException import kotlin.reflect.KClass @@ -23,11 +22,6 @@ inline fun deserializeFromNode(json: JsonNode): Either deserializeFromString(json: String): Either = either.eager { deserializeFromNode(json.deserialized().mapException().bind()).bind() } -/** - * Deserializes an input stream to the specified type - */ -inline fun deserializeFromBuffer(buffer: BufferedSource): Either = - either.eager { deserializeFromNode(buffer.deserialized().mapException().bind()).bind() } /** * Deserializes a JsonNode to the specified type From a970c56b85482b9b7d27a36342aa5ddb1fcf736c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Bl=C3=A1zquez?= Date: Sat, 26 Aug 2023 15:55:30 +0200 Subject: [PATCH 2/2] Add nullable parameter for JsonNode --- .../src/commonMain/kotlin/simpleJson/JsonObjectBuilder.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/simpleJson-core/src/commonMain/kotlin/simpleJson/JsonObjectBuilder.kt b/simpleJson-core/src/commonMain/kotlin/simpleJson/JsonObjectBuilder.kt index 2a5eb33..5ce6b34 100644 --- a/simpleJson-core/src/commonMain/kotlin/simpleJson/JsonObjectBuilder.kt +++ b/simpleJson-core/src/commonMain/kotlin/simpleJson/JsonObjectBuilder.kt @@ -32,7 +32,8 @@ class JsonObjectBuilder { /** * Adds the [value] to the object with [String] as its key. */ - infix fun String.to(value: JsonNode) = map.put(this, value) + infix fun String.to(value: JsonNode?) = + if (value == null) map.put(this, JsonNull) else map.put(this, value) /** * Adds the [value] to the object with [String] as its key. @@ -67,7 +68,7 @@ class JsonObjectBuilder { /** * Adds the [value] to the object with [String] as its key. */ - operator fun String.plusAssign(value: JsonNode) { + operator fun String.plusAssign(value: JsonNode?) { this to value }