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

Common json support #95

Merged
merged 5 commits into from
Mar 3, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Move JsonToPlotSpec and PlotSvgExportTest to portable.
  • Loading branch information
IKupriyanov-HORIS committed Mar 3, 2020
commit 7f6fda9a750e9cc955f7e978a673e97991aa2485
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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 JsonFormatter {
private lateinit var buffer: StringBuilder

fun formatJson(o: Any): String {
buffer = StringBuilder()
formatMap(o as Map<*, *>)
return buffer.toString()
}

private fun formatList(list: List<*>) {
append("[")
list.headTail(::formatValue) { tail -> tail.forEach { append(","); formatValue(it) } }
append("]")
}

private fun formatMap(map: Map<*, *>) {
append("{")
map.entries.headTail(::formatPair) { tail -> tail.forEach { append(",\n"); formatPair(it) } }
append("}")
}

private fun formatValue(v: Any?) {
when (v) {
null -> append("null")
is String -> append("\"${v.escape()}\"")
is Number, Boolean -> append(v.toString())
is Array<*> -> formatList(v.asList())
is List<*> -> formatList(v)
is Map<*, *> -> formatMap(v)
else -> throw IllegalArgumentException("Can't serialize object $v")
}
}

private fun formatPair(pair: Map.Entry<Any?, Any?>) {
append("\"${pair.key}\":"); formatValue(pair.value)
}

private fun append(s: String) = buffer.append(s)

private fun <E> Collection<E>.headTail(head: (E) -> Unit, tail: (Sequence<E>) -> Unit) {
if (!isEmpty()) {
head(first())
tail(asSequence().drop(1))
}
}
}
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("Unkown token: ${currentChar}")
}.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 ESCAPING_MAP -> 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,75 @@
/*
* 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 ESCAPING_MAP = mapOf(
'"' to '"',
'\\' to '\\',
'/' to '/',
'b' to '\b',
'f' to '\u000C',
'n' to '\n',
'r' to '\r',
't' to '\t'
)

fun String.escape() =
this.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")

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(get(++i)) {
in ESCAPING_MAP -> ESCAPING_MAP[get(i)].also { i++ }
'u' -> substring(i + 1, i + 5).toInt(16).toChar().also { i += 5 }
else -> throw JsonParser.JsonException("Invalid escape character: ${get(i)}")
}.let { output.append(it) }
} else {
output?.append(ch); i++
}
}
return output?.toString() ?: substring(start, end)
}

This file was deleted.

Loading