Skip to content

Commit

Permalink
Use exceptions internally to reduce allocations and improve performance
Browse files Browse the repository at this point in the history
  • Loading branch information
xBaank committed Jun 21, 2023
1 parent 27c7ce3 commit 11826d5
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<JsonException, JsonNode> = JsonReader(this).read()

/**
* Deserialize a [String] to a [JsonNode] or return a [JsonException] if the string is not valid JSON
*/
fun BufferedSource.deserialized(): Either<JsonException, JsonNode> = JsonReader(this).read()
fun String.deserialized(): Either<JsonException, JsonNode> = JsonReader(this).read()
237 changes: 125 additions & 112 deletions simpleJson-core/src/commonMain/kotlin/simpleJson/JsonReader.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -28,208 +23,226 @@ 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<JsonException, JsonNode> = run {
readNextSkippingWhiteSpaces()
fun read(): Either<JsonException, JsonNode> = 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<JsonException, JsonNode> = 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<JsonException, JsonArray> = 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<JsonException>(JsonParseException("Expected '[' but found $current"))
if (current != JSON_LEFT_BRACKET) throw JsonParseException("Expected '[' but found $current")

val list = mutableListOf<JsonNode>()

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<JsonException>(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<JsonException, JsonObject> = either.eager {
private fun readJsonObject(): JsonObject {
skipWhiteSpaces()

if (current != JSON_LEFT_BRACE) shift<JsonException>(JsonParseException("Expected '{' but found $current"))
if (current != JSON_LEFT_BRACE) throw JsonParseException("Expected '{' but found $current")

val map = mutableMapOf<String, JsonNode>()

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<JsonException>(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<JsonException>(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<JsonException, JsonNumber> = 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<JsonException, JsonBoolean> = 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<JsonException, JsonBoolean> = 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<JsonException, JsonNull> = 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<JsonException, JsonString> = either.eager {
private fun readString(): JsonString {
val builder = StringBuilder()

if (current != JSON_DOUBLE_QUOTE) shift<JsonException>(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<JsonException>(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
}
}

private fun readUnicodeSequence(): String? {
val unicode = run {
val builder = StringBuilder()
repeat(4) {
readNext()
moveNext()
builder.append(current)
}
builder.toString()
}
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<JsonException, String> = 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
}
}

3 changes: 1 addition & 2 deletions simpleJson-core/src/jvmTest/kotlin/FileReadingTest.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import okio.BufferedSource
import okio.FileSystem
import okio.Path.Companion.toPath
import okio.buffer
Expand All @@ -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() })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,11 +22,6 @@ inline fun <reified T : Any> deserializeFromNode(json: JsonNode): Either<JsonDes
inline fun <reified T : Any> deserializeFromString(json: String): Either<JsonDeserializationException, T> =
either.eager { deserializeFromNode<T>(json.deserialized().mapException().bind()).bind() }

/**
* Deserializes an input stream to the specified type
*/
inline fun <reified T : Any> deserializeFromBuffer(buffer: BufferedSource): Either<JsonDeserializationException, T> =
either.eager { deserializeFromNode<T>(buffer.deserialized().mapException().bind()).bind() }

/**
* Deserializes a JsonNode to the specified type
Expand Down

0 comments on commit 11826d5

Please sign in to comment.