Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3.0.0 #13

Merged
merged 2 commits into from
Aug 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand Down
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
Loading
Loading