Skip to content

Commit

Permalink
Common json support (#95)
Browse files Browse the repository at this point in the history
* Even more common JsonSupport

* Replace platform specific JsonSupport to common.

* Move JsonToPlotSpec and PlotSvgExportTest to portable.

* Fix tests - replace ' to " in plotSpec to meet JSON string specification.

* Minor refactoring.
  • Loading branch information
IKupriyanov-HORIS committed Mar 3, 2020
1 parent 644bfc4 commit 2d0c23b
Show file tree
Hide file tree
Showing 16 changed files with 571 additions and 242 deletions.
Original file line number Diff line number Diff line change
@@ -1,42 +1,10 @@
/*
* Copyright (c) 2019. JetBrains s.r.o.
* Copyright (c) 2020. JetBrains s.r.o.
* Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*/

package jetbrains.datalore.base.json

actual object JsonSupport {
actual fun parseJson(jsonString: String): MutableMap<String, Any?> {
return JsonParser().handleObject(JSON.parse(jsonString))
}

actual fun formatJson(o: Any): String {
return JsonFormatter().formatJson(o)
}
}


class JsonParser {
fun handleObject(v: dynamic): MutableMap<String, Any?> {
return js("Object").entries(v)
.unsafeCast<Array<Array<*>>>()
.map { (k, v) -> k as String to handleValue(v) }
.toMap(HashMap())
}

private fun handleArray(v: Array<*>) = v.map { handleValue(it) }

private fun handleValue(v: Any?): Any? {
return when (v) {
is String, Boolean, null -> v
is Number -> v.toDouble()
is Array<*> -> handleArray(v)
else -> handleObject(v)
}
}
}


class JsonFormatter {
private lateinit var buffer: StringBuilder

Expand Down Expand Up @@ -83,4 +51,3 @@ class JsonFormatter {
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright (c) 2020. JetBrains s.r.o.
* Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*/

package jetbrains.datalore.base.json

internal class JsonLexer(
private val input: String
) {
private var i = 0
private var tokenStart = 0
var currentToken: Token? = null
private set

private val currentChar: Char
get() = input[i]

init {
nextToken() // read first token
}

fun nextToken() {
advanceWhile { it.isWhitespace() }

if (isFinished()) {
return
}

when {
currentChar == '{' -> Token.LEFT_BRACE.also { advance() }
currentChar == '}' -> Token.RIGHT_BRACE.also { advance() }
currentChar == '[' -> Token.LEFT_BRACKET.also { advance() }
currentChar == ']' -> Token.RIGHT_BRACKET.also { advance() }
currentChar == ',' -> Token.COMMA.also { advance() }
currentChar == ':' -> Token.COLON.also { advance() }
currentChar == 't' -> Token.TRUE.also { read("true") }
currentChar == 'f' -> Token.FALSE.also { read("false") }
currentChar == 'n' -> Token.NULL.also { read("null") }
currentChar == '"' -> Token.STRING.also { readString() }
readNumber() -> Token.NUMBER
else -> error("$i:${currentChar} - unkown token")
}.also { currentToken = it }
}

fun tokenValue() = input.substring(tokenStart, i)

private fun readString() {
startToken()
advance() // opening quote
while(!(currentChar == '"')) {
if(currentChar == '\\') {
advance()
when {
currentChar == 'u' -> {
advance()
repeat(4) {
require(currentChar.isHex());
advance()
}
}
currentChar in SPECIAL_CHARS -> advance()
else -> error("Invalid escape sequence")
}
} else {
advance()
}
}
advance() // closing quote
}

private fun readNumber(): Boolean {
if (!(currentChar.isDigit() || currentChar == '-')) {
return false
}

startToken()
advanceIfCurrent('-')
advanceWhile { it.isDigit() }

advanceIfCurrent('.') {
require(currentChar.isDigit()) { "Number should have decimal part" }
advanceWhile { it.isDigit() }
}

advanceIfCurrent('e', 'E') {
advanceIfCurrent('+', '-')
advanceWhile { it.isDigit() }
}

return true
}

fun isFinished(): Boolean = i == input.length
private fun startToken() { tokenStart = i }
private fun advance() { ++i }

private fun read(str: String) {
return str.forEach {
require(currentChar == it) { "Wrong data: $str" }
require(!isFinished()) { "Unexpected end of string" }
advance()
}
}

private fun advanceWhile(pred: (Char) -> Boolean) {
while (!isFinished() && pred(currentChar)) advance()
}

private fun advanceIfCurrent(vararg expected: Char, then: () -> Unit = {}) {
if (!isFinished() && currentChar in expected) {
advance()
then()
}
}

companion object {
private val digits: CharRange = '0'..'9'
private fun Char?.isDigit() = this in digits
private fun Char.isHex(): Boolean { return isDigit() || this in 'a'..'f' || this in 'A'..'F' }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2020. JetBrains s.r.o.
* Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*/

package jetbrains.datalore.base.json

class JsonParser(
private val json: String
) {
fun parseJson(): Any? {
val lexer = JsonLexer(json)
return parseValue(lexer)
}

private fun parseValue(lexer: JsonLexer): Any? {
return when(lexer.currentToken) {
Token.STRING -> lexer.tokenValue().unescape().also { lexer.nextToken() }
Token.NUMBER -> lexer.tokenValue().toDouble().also { lexer.nextToken() }
Token.FALSE -> false.also { lexer.nextToken() }
Token.TRUE -> true.also { lexer.nextToken() }
Token.NULL -> null.also { lexer.nextToken() }
Token.LEFT_BRACE -> parseObject(lexer)
Token.LEFT_BRACKET -> parseArray(lexer)
else -> error("Invalid token: ${lexer.currentToken}")
}
}

private fun parseArray(lexer: JsonLexer): MutableList<Any?> {
fun checkCurrentToken(token: Token) { require(lexer.currentToken, token, "[Arr] ") }

val list = mutableListOf<Any?>()

checkCurrentToken(Token.LEFT_BRACKET)
lexer.nextToken()

while (lexer.currentToken != Token.RIGHT_BRACKET) {
if (list.isNotEmpty()) {
checkCurrentToken(Token.COMMA)
lexer.nextToken()
}
list.add(parseValue(lexer))
}

checkCurrentToken(Token.RIGHT_BRACKET)
lexer.nextToken()

return list
}

private fun parseObject(lexer: JsonLexer): Map<String, Any?> {
fun checkCurrentToken(token: Token) { require(lexer.currentToken, token, "[Obj] ") }

val map = mutableMapOf<String, Any?>()

checkCurrentToken(Token.LEFT_BRACE)
lexer.nextToken()

while (lexer.currentToken != Token.RIGHT_BRACE) {
if (map.isNotEmpty()) {
checkCurrentToken(Token.COMMA)
lexer.nextToken()
}

checkCurrentToken(Token.STRING)
val key = lexer.tokenValue().unescape()
lexer.nextToken()

checkCurrentToken(Token.COLON)
lexer.nextToken()

val value = parseValue(lexer)
map[key] = value
}

checkCurrentToken(Token.RIGHT_BRACE)
lexer.nextToken()

return map
}

private fun require(current: Token?, expected: Token?, messagePrefix: String? = null) {
if (current != expected) {
throw JsonException(messagePrefix + "Expected token: $expected, actual: $current")
}
}

class JsonException(message: String) : Exception(message)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2019. JetBrains s.r.o.
* Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*/

package jetbrains.datalore.base.json

object JsonSupport {
fun parseJson(jsonString: String): MutableMap<String, Any?> {
@Suppress("UNCHECKED_CAST")
return JsonParser(jsonString).parseJson() as MutableMap<String, Any?>
}
fun formatJson(o: Any): String {
return JsonFormatter().formatJson(o)
}
}


// Usefull resources:
// https://www.ietf.org/rfc/rfc4627.txt
// https://github.com/nst/JSONTestSuite

internal enum class Token {
LEFT_BRACE,
RIGHT_BRACE,
LEFT_BRACKET,
RIGHT_BRACKET,
COMMA,
COLON,
STRING,
NUMBER,
TRUE,
FALSE,
NULL,
}

internal val SPECIAL_CHARS = mapOf(
'"' to '"',
'\\' to '\\',
'/' to '/',
'b' to '\b',
'f' to '\u000C',
'n' to '\n',
'r' to '\r',
't' to '\t'
)

private val CONTROL_CHARS = (0 until 0x20).map(Int::toChar).toSet()

fun String.escape(): String {
var output: StringBuilder? = null
var i = 0

fun appendOutput(str: String) {
output = (output ?: StringBuilder(substring(0, i))).append(str)
}

while(i < length) {
when(val ch = get(i)) {
'\\' -> appendOutput("""\\""")
'"' -> appendOutput("""\"""")
'\n' -> appendOutput("""\n""")
'\r' -> appendOutput("""\r""")
'\t' -> appendOutput("""\t""")
in CONTROL_CHARS -> appendOutput("""\u${ch.toInt().toString(16).padStart(4, '0')}""")
else -> output?.append(ch)
}
i++
}
return output?.toString() ?: this
}

fun String.unescape(): String {
var output: StringBuilder? = null
val start = 1
val end = length - 1

var i = start
while(i < end) {
val ch = get(i)
if (ch == '\\') {
output = output ?: StringBuilder(substring(start, i))
when(val escapedChar = get(++i)) {
in SPECIAL_CHARS -> SPECIAL_CHARS[escapedChar].also { i++ }
'u' -> substring(i + 1, i + 5).toInt(16).toChar().also { i += 5 }
else -> throw JsonParser.JsonException("Invalid escape character: ${escapedChar}")
}.let { output.append(it) }
} else {
output?.append(ch); i++
}
}
return output?.toString() ?: substring(start, end)
}

This file was deleted.

This file was deleted.

Loading

0 comments on commit 2d0c23b

Please sign in to comment.