Rewrote application with benchmarks using MPP plugin and added collec… (#2471)
Rewrote application with benchmarks using MPP plugin and added collecting information in json format.
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.jetbrains.report
|
||||
|
||||
import org.jetbrains.report.json.*
|
||||
|
||||
interface JsonSerializable {
|
||||
fun toJson(): String
|
||||
|
||||
// Convert iterable objects arrays, lists to json.
|
||||
fun <T> arrayToJson(data: Iterable<T>): String {
|
||||
return data.joinToString(prefix = "[", postfix = "]") {
|
||||
if (it is JsonSerializable) it.toJson() else it.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Entity can be created from json description.
|
||||
interface ConvertedFromJson {
|
||||
fun getRequiredField(data: JsonObject, fieldName: String): JsonElement {
|
||||
return data.getOrNull(fieldName) ?: error("Field '$fieldName' doesn't exist in '$data'. Please, check origin files.")
|
||||
}
|
||||
|
||||
fun getOptionalField(data: JsonObject, fieldName: String): JsonElement? {
|
||||
return data.getOrNull(fieldName)
|
||||
}
|
||||
|
||||
// Parse json array to list.
|
||||
// Takes function to convert elements in array to expected type.
|
||||
fun <T> arrayToList(array: JsonArray, convert: JsonArray.(Int) -> T?): List<T> {
|
||||
var results = mutableListOf<T>()
|
||||
var index = 0
|
||||
var current: T? = array.convert(index)
|
||||
while (current != null) {
|
||||
results.add(current)
|
||||
index++
|
||||
current = array.convert(index)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Methods for conversion to expected type with checks of possibility of such conversions.
|
||||
fun elementToDouble(element: JsonElement, name: String): Double =
|
||||
if (element is JsonPrimitive)
|
||||
element.double
|
||||
else
|
||||
error("Field '$name' in '$element' is expected to be a double number. Please, check origin files.")
|
||||
|
||||
fun elementToInt(element: JsonElement, name: String): Int =
|
||||
if (element is JsonPrimitive)
|
||||
element.int
|
||||
else
|
||||
error("Field '$name' in '$element' is expected to be an integer number. Please, check origin files.")
|
||||
|
||||
fun elementToString(element: JsonElement, name:String): String =
|
||||
if (element is JsonLiteral)
|
||||
element.unquoted()
|
||||
else
|
||||
error("Field '$name' in '$element' is expected to be a string. Please, check origin files.")
|
||||
}
|
||||
|
||||
interface EntityFromJsonFactory<T>: ConvertedFromJson {
|
||||
fun create(data: JsonElement): T
|
||||
}
|
||||
|
||||
// Class for benchcmarks report with all information of run.
|
||||
data class BenchmarksReport(val env: Environment, val benchmarks: List<BenchmarkResult>, val compiler: Compiler):
|
||||
JsonSerializable {
|
||||
|
||||
companion object: EntityFromJsonFactory<BenchmarksReport> {
|
||||
override fun create(data: JsonElement): BenchmarksReport {
|
||||
if (data is JsonObject) {
|
||||
val env = Environment.create(getRequiredField(data, "env"))
|
||||
val benchmarksObj = getRequiredField(data, "benchmarks")
|
||||
val compiler = Compiler.create(getRequiredField(data, "kotlin"))
|
||||
val benchmarks = parseBenchmarksArray(benchmarksObj)
|
||||
return BenchmarksReport(env, benchmarks, compiler)
|
||||
} else {
|
||||
error("Top level entity is expected to be an object. Please, check origin files.")
|
||||
}
|
||||
}
|
||||
|
||||
// Parse array with benchmarks to list
|
||||
fun parseBenchmarksArray(data: JsonElement): List<BenchmarkResult> {
|
||||
if (data is JsonArray) {
|
||||
return arrayToList(data.jsonArray, { index ->
|
||||
if (this.getObjectOrNull(index) != null)
|
||||
BenchmarkResult.create(this.getObjectOrNull(index) as JsonObject)
|
||||
else null
|
||||
})
|
||||
} else {
|
||||
error("Benchmarks field is expected to be an array. Please, check origin files.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun toJson(): String {
|
||||
return """
|
||||
{
|
||||
"env": ${env.toJson()},
|
||||
"kotlin": ${compiler.toJson()},
|
||||
"benchmarks": ${arrayToJson(benchmarks)}
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
// Class for kotlin compiler
|
||||
data class Compiler(val backend: Backend, val kotlinVersion: String): JsonSerializable {
|
||||
|
||||
enum class BackendType(val type: String) {
|
||||
JVM("jvm"),
|
||||
NATIVE("native")
|
||||
}
|
||||
|
||||
companion object: EntityFromJsonFactory<Compiler> {
|
||||
override fun create(data: JsonElement): Compiler {
|
||||
if (data is JsonObject) {
|
||||
val backend = Backend.create(getRequiredField(data, "backend"))
|
||||
val kotlinVersion = elementToString(getRequiredField(data, "kotlinVersion"), "kotlinVersion")
|
||||
|
||||
return Compiler(backend, kotlinVersion)
|
||||
} else {
|
||||
error("Kotlin entity is expected to be an object. Please, check origin files.")
|
||||
}
|
||||
}
|
||||
|
||||
fun backendTypeFromString(s: String): BackendType? = BackendType.values().find { it.type == s }
|
||||
}
|
||||
|
||||
// Class for compiler backend
|
||||
data class Backend(val type: BackendType, val version: String, val flags: List<String>): JsonSerializable {
|
||||
companion object: EntityFromJsonFactory<Backend> {
|
||||
override fun create(data: JsonElement): Backend {
|
||||
if (data is JsonObject) {
|
||||
val typeElement = getRequiredField(data, "type")
|
||||
if (typeElement is JsonLiteral) {
|
||||
val type = backendTypeFromString(typeElement.unquoted()) ?: error("Backend type should be 'jvm' or 'native'")
|
||||
val version = elementToString(getRequiredField(data, "version"), "version")
|
||||
val flagsArray = getOptionalField(data, "flags")
|
||||
var flags: List<String> = emptyList()
|
||||
if (flagsArray != null && flagsArray is JsonArray) {
|
||||
flags = arrayToList(flagsArray.jsonArray, { index ->
|
||||
this.getPrimitiveOrNull(index)?.toString()
|
||||
})
|
||||
}
|
||||
return Backend(type, version, flags)
|
||||
} else {
|
||||
error("Backend type should be string literal.")
|
||||
}
|
||||
} else {
|
||||
error("Backend entity is expected to be an object. Please, check origin files.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun toJson(): String {
|
||||
val result = """
|
||||
{
|
||||
"type": "${type.type}",
|
||||
"version": "${version}""""
|
||||
// Don't print flags field if there is no one.
|
||||
if (flags.isEmpty()) {
|
||||
return """$result
|
||||
}"""
|
||||
}
|
||||
else {
|
||||
return """
|
||||
$result,
|
||||
"flags": ${arrayToJson(flags)}
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun toJson(): String {
|
||||
return """
|
||||
{
|
||||
"backend": ${backend.toJson()},
|
||||
"kotlinVersion": "${kotlinVersion}"
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
// Class for description of environment of benchmarks run
|
||||
data class Environment(val machine: Machine, val jdk: JDKInstance): JsonSerializable {
|
||||
|
||||
companion object: EntityFromJsonFactory<Environment> {
|
||||
override fun create(data: JsonElement): Environment {
|
||||
if (data is JsonObject) {
|
||||
val machine = Machine.create(getRequiredField(data, "machine"))
|
||||
val jdk = JDKInstance.create(getRequiredField(data, "jdk"))
|
||||
|
||||
return Environment(machine, jdk)
|
||||
} else {
|
||||
error("Environment entity is expected to be an object. Please, check origin files.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Class for description of machine used for benchmarks run.
|
||||
data class Machine(val cpu: String, val os: String): JsonSerializable {
|
||||
companion object: EntityFromJsonFactory<Machine> {
|
||||
override fun create(data: JsonElement): Machine {
|
||||
if (data is JsonObject) {
|
||||
val cpu = elementToString(getRequiredField(data, "cpu"), "cpu")
|
||||
val os = elementToString(getRequiredField(data, "os"), "os")
|
||||
|
||||
return Machine(cpu, os)
|
||||
} else {
|
||||
error("Machine entity is expected to be an object. Please, check origin files.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun toJson(): String {
|
||||
return """
|
||||
{
|
||||
"cpu": "$cpu",
|
||||
"os": "$os"
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
// Class for description of jdk used for benchmarks run.
|
||||
data class JDKInstance(val version: String, val vendor: String): JsonSerializable {
|
||||
companion object: EntityFromJsonFactory<JDKInstance> {
|
||||
override fun create(data: JsonElement): JDKInstance {
|
||||
if (data is JsonObject) {
|
||||
val version = elementToString(getRequiredField(data, "version"), "version")
|
||||
val vendor = elementToString(getRequiredField(data, "vendor"), "vendor")
|
||||
|
||||
return JDKInstance(version, vendor)
|
||||
} else {
|
||||
error("JDK entity is expected to be an object. Please, check origin files.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun toJson(): String {
|
||||
return """
|
||||
{
|
||||
"version": "$version",
|
||||
"vendor": "$vendor"
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
override fun toJson(): String {
|
||||
return """
|
||||
{
|
||||
"machine": ${machine.toJson()},
|
||||
"jdk": ${jdk.toJson()}
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
class BenchmarkResult(val name: String, val status: Status,
|
||||
val score: Double, val runtimeInUs: Double,
|
||||
val repeat: Int, val warmup: Int): JsonSerializable {
|
||||
|
||||
companion object: EntityFromJsonFactory<BenchmarkResult> {
|
||||
|
||||
override fun create(data: JsonElement): BenchmarkResult {
|
||||
if (data is JsonObject) {
|
||||
val name = elementToString(getRequiredField(data, "name"), "name")
|
||||
val statusElement = getRequiredField(data, "status")
|
||||
if (statusElement is JsonLiteral) {
|
||||
val status = statusFromString(statusElement.unquoted())
|
||||
?: error("Status should be PASSED or FAILED")
|
||||
val score = elementToDouble(getRequiredField(data, "score"), "score")
|
||||
val runtimeInUs = elementToDouble(getRequiredField(data, "runtimeInUs"), "runtimeInUs")
|
||||
val repeat = elementToInt(getRequiredField(data, "repeat"), "repeat")
|
||||
val warmup = elementToInt(getRequiredField(data, "warmup"), "warmup")
|
||||
|
||||
return BenchmarkResult(name, status, score, runtimeInUs, repeat, warmup)
|
||||
} else {
|
||||
error("Status should be string literal.")
|
||||
}
|
||||
} else {
|
||||
error("Benchmark entity is expected to be an object. Please, check origin files.")
|
||||
}
|
||||
}
|
||||
|
||||
fun statusFromString(s: String): Status? = Status.values().find { it.value == s }
|
||||
}
|
||||
|
||||
enum class Status(val value: String) {
|
||||
PASSED("PASSED"),
|
||||
FAILED("FAILED")
|
||||
}
|
||||
|
||||
override fun toJson(): String {
|
||||
return """
|
||||
{
|
||||
"name": "$name",
|
||||
"status": "${status.value}",
|
||||
"score": ${score},
|
||||
"runtimeInUs": ${runtimeInUs},
|
||||
"repeat": ${repeat},
|
||||
"warmup": ${warmup}
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jetbrains.report.json
|
||||
|
||||
/**
|
||||
* Class representing single JSON element.
|
||||
* Can be [JsonPrimitive], [JsonArray] or [JsonObject].
|
||||
*
|
||||
* [JsonElement.toString] properly prints JSON tree as valid JSON, taking into
|
||||
* account quoted values and primitives
|
||||
*/
|
||||
sealed class JsonElement {
|
||||
|
||||
/**
|
||||
* Convenience method to get current element as [JsonPrimitive]
|
||||
* @throws JsonElementTypeMismatchException is current element is not a [JsonPrimitive]
|
||||
*/
|
||||
open val primitive: JsonPrimitive
|
||||
get() = error("JsonLiteral")
|
||||
|
||||
/**
|
||||
* Convenience method to get current element as [JsonObject]
|
||||
* @throws JsonElementTypeMismatchException is current element is not a [JsonObject]
|
||||
*/
|
||||
open val jsonObject: JsonObject
|
||||
get() = error("JsonObject")
|
||||
|
||||
/**
|
||||
* Convenience method to get current element as [JsonArray]
|
||||
* @throws JsonElementTypeMismatchException is current element is not a [JsonArray]
|
||||
*/
|
||||
open val jsonArray: JsonArray
|
||||
get() = error("JsonArray")
|
||||
|
||||
/**
|
||||
* Convenience method to get current element as [JsonNull]
|
||||
* @throws JsonElementTypeMismatchException is current element is not a [JsonNull]
|
||||
*/
|
||||
open val jsonNull: JsonNull
|
||||
get() = error("JsonPrimitive")
|
||||
|
||||
/**
|
||||
* Checks whether current element is [JsonNull]
|
||||
*/
|
||||
val isNull: Boolean
|
||||
get() = this === JsonNull
|
||||
|
||||
private fun error(element: String): Nothing =
|
||||
throw JsonElementTypeMismatchException(this::class.toString(), element)
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing JSON primitive value. Can be either [JsonLiteral] or [JsonNull].
|
||||
*/
|
||||
sealed class JsonPrimitive : JsonElement() {
|
||||
|
||||
/**
|
||||
* Content of given element without quotes. For [JsonNull] this methods returns `"null"`
|
||||
*/
|
||||
abstract val content: String
|
||||
|
||||
/**
|
||||
* Content of the given element without quotes or `null` if current element is [JsonNull]
|
||||
*/
|
||||
abstract val contentOrNull: String?
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
final override val primitive: JsonPrimitive = this
|
||||
|
||||
/**
|
||||
* Returns content of current element as int
|
||||
* @throws NumberFormatException if current element is not a valid representation of number
|
||||
*/
|
||||
val int: Int get() = content.toInt()
|
||||
|
||||
/**
|
||||
* Returns content of current element as int or `null` if current element is not a valid representation of number
|
||||
**/
|
||||
val intOrNull: Int? get() = content.toIntOrNull()
|
||||
|
||||
/**
|
||||
* Returns content of current element as long
|
||||
* @throws NumberFormatException if current element is not a valid representation of number
|
||||
*/
|
||||
val long: Long get() = content.toLong()
|
||||
|
||||
/**
|
||||
* Returns content of current element as long or `null` if current element is not a valid representation of number
|
||||
*/
|
||||
val longOrNull: Long? get() = content.toLongOrNull()
|
||||
|
||||
/**
|
||||
* Returns content of current element as double
|
||||
* @throws NumberFormatException if current element is not a valid representation of number
|
||||
*/
|
||||
val double: Double get() = content.toDouble()
|
||||
|
||||
/**
|
||||
* Returns content of current element as double or `null` if current element is not a valid representation of number
|
||||
*/
|
||||
val doubleOrNull: Double? get() = content.toDoubleOrNull()
|
||||
|
||||
/**
|
||||
* Returns content of current element as float
|
||||
* @throws NumberFormatException if current element is not a valid representation of number
|
||||
*/
|
||||
val float: Float get() = content.toFloat()
|
||||
|
||||
/**
|
||||
* Returns content of current element as float or `null` if current element is not a valid representation of number
|
||||
*/
|
||||
val floatOrNull: Float? get() = content.toFloatOrNull()
|
||||
|
||||
/**
|
||||
* Returns content of current element as boolean
|
||||
* @throws IllegalStateException if current element doesn't represent boolean
|
||||
*/
|
||||
val boolean: Boolean get() = content.toBooleanStrict()
|
||||
|
||||
/**
|
||||
* Returns content of current element as boolean or `null` if current element is not a valid representation of boolean
|
||||
*/
|
||||
val booleanOrNull: Boolean? get() = content.toBooleanStrictOrNull()
|
||||
|
||||
override fun toString() = content
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing JSON literals: numbers, booleans and string.
|
||||
* Strings are always quoted.
|
||||
*/
|
||||
data class JsonLiteral internal constructor(
|
||||
private val body: Any,
|
||||
private val isString: Boolean
|
||||
) : JsonPrimitive() {
|
||||
|
||||
override val content = body.toString()
|
||||
override val contentOrNull: String = content
|
||||
|
||||
/**
|
||||
* Creates number literal
|
||||
*/
|
||||
constructor(number: Number) : this(number, false)
|
||||
|
||||
/**
|
||||
* Creates boolean literal
|
||||
*/
|
||||
constructor(boolean: Boolean) : this(boolean, false)
|
||||
|
||||
/**
|
||||
* Creates quoted string literal
|
||||
*/
|
||||
constructor(string: String) : this(string, true)
|
||||
|
||||
override fun toString() =
|
||||
if (isString) buildString { printQuoted(content) }
|
||||
else content
|
||||
|
||||
fun unquoted() = content
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing JSON `null` value
|
||||
*/
|
||||
object JsonNull : JsonPrimitive() {
|
||||
override val jsonNull: JsonNull = this
|
||||
override val content: String = "null"
|
||||
override val contentOrNull: String? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing JSON object, consisting of name-value pairs, where value is arbitrary [JsonElement]
|
||||
*/
|
||||
data class JsonObject(val content: Map<String, JsonElement>) : JsonElement(), Map<String, JsonElement> by content {
|
||||
|
||||
override val jsonObject: JsonObject = this
|
||||
|
||||
/**
|
||||
* Returns [JsonElement] associated with given [key]
|
||||
* @throws NoSuchElementException if element is not present
|
||||
*/
|
||||
override fun get(key: String): JsonElement = content[key] ?: throw NoSuchElementException("Element $key is missing")
|
||||
|
||||
/**
|
||||
* Returns [JsonElement] associated with given [key] or `null` if element is not present
|
||||
*/
|
||||
fun getOrNull(key: String): JsonElement? = content[key]
|
||||
|
||||
/**
|
||||
* Returns [JsonPrimitive] associated with given [key]
|
||||
*
|
||||
* @throws NoSuchElementException if element is not present
|
||||
* @throws JsonElementTypeMismatchException if element is present, but has invalid type
|
||||
*/
|
||||
fun getPrimitive(key: String): JsonPrimitive = get(key) as? JsonPrimitive
|
||||
?: unexpectedJson(key, "JsonPrimitive")
|
||||
|
||||
/**
|
||||
* Returns [JsonObject] associated with given [key]
|
||||
*
|
||||
* @throws NoSuchElementException if element is not present
|
||||
* @throws JsonElementTypeMismatchException if element is present, but has invalid type
|
||||
*/
|
||||
fun getObject(key: String): JsonObject = get(key) as? JsonObject
|
||||
?: unexpectedJson(key, "JsonObject")
|
||||
|
||||
/**
|
||||
* Returns [JsonArray] associated with given [key]
|
||||
*
|
||||
* @throws NoSuchElementException if element is not present
|
||||
* @throws JsonElementTypeMismatchException if element is present, but has invalid type
|
||||
*/
|
||||
fun getArray(key: String): JsonArray = get(key) as? JsonArray
|
||||
?: unexpectedJson(key, "JsonArray")
|
||||
|
||||
/**
|
||||
* Returns [JsonPrimitive] associated with given [key] or `null` if element
|
||||
* is not present or has different type
|
||||
*/
|
||||
fun getPrimitiveOrNull(key: String): JsonPrimitive? = content[key] as? JsonPrimitive
|
||||
|
||||
/**
|
||||
* Returns [JsonObject] associated with given [key] or `null` if element
|
||||
* is not present or has different type
|
||||
*/
|
||||
fun getObjectOrNull(key: String): JsonObject? = content[key] as? JsonObject
|
||||
|
||||
/**
|
||||
* Returns [JsonArray] associated with given [key] or `null` if element
|
||||
* is not present or has different type
|
||||
*/
|
||||
fun getArrayOrNull(key: String): JsonArray? = content[key] as? JsonArray
|
||||
|
||||
/**
|
||||
* Returns [J] associated with given [key]
|
||||
*
|
||||
* @throws NoSuchElementException if element is not present
|
||||
* @throws JsonElementTypeMismatchException if element is present, but has invalid type
|
||||
*/
|
||||
inline fun <reified J : JsonElement> getAs(key: String): J = get(key) as? J
|
||||
?: unexpectedJson(key, J::class.toString())
|
||||
|
||||
/**
|
||||
* Returns [J] associated with given [key] or `null` if element
|
||||
* is not present or has different type
|
||||
*/
|
||||
inline fun <reified J : JsonElement> lookup(key: String): J? = content[key] as? J
|
||||
|
||||
override fun toString(): String {
|
||||
return content.entries.joinToString(
|
||||
prefix = "{",
|
||||
postfix = "}",
|
||||
transform = {(k, v) -> """"$k": $v"""}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class JsonArray(val content: List<JsonElement>) : JsonElement(), List<JsonElement> by content {
|
||||
|
||||
override val jsonArray: JsonArray = this
|
||||
|
||||
/**
|
||||
* Returns [index]-th element of an array as [JsonPrimitive]
|
||||
* @throws JsonElementTypeMismatchException if element has invalid type
|
||||
*/
|
||||
fun getPrimitive(index: Int) = content[index] as? JsonPrimitive
|
||||
?: unexpectedJson("at $index", "JsonPrimitive")
|
||||
|
||||
/**
|
||||
* Returns [index]-th element of an array as [JsonObject]
|
||||
* @throws JsonElementTypeMismatchException if element has invalid type
|
||||
*/
|
||||
fun getObject(index: Int) = content[index] as? JsonObject
|
||||
?: unexpectedJson("at $index", "JsonObject")
|
||||
|
||||
/**
|
||||
* Returns [index]-th element of an array as [JsonArray]
|
||||
* @throws JsonElementTypeMismatchException if element has invalid type
|
||||
*/
|
||||
fun getArray(index: Int) = content[index] as? JsonArray
|
||||
?: unexpectedJson("at $index", "JsonArray")
|
||||
|
||||
/**
|
||||
* Returns [index]-th element of an array as [JsonPrimitive] or `null` if element is missing or has different type
|
||||
*/
|
||||
fun getPrimitiveOrNull(index: Int) = content.getOrNull(index) as? JsonPrimitive
|
||||
|
||||
/**
|
||||
* Returns [index]-th element of an array as [JsonObject] or `null` if element is missing or has different type
|
||||
*/
|
||||
fun getObjectOrNull(index: Int) = content.getOrNull(index) as? JsonObject
|
||||
|
||||
/**
|
||||
* Returns [index]-th element of an array as [JsonArray] or `null` if element is missing or has different type
|
||||
*/
|
||||
fun getArrayOrNull(index: Int) = content.getOrNull(index) as? JsonArray
|
||||
|
||||
/**
|
||||
* Returns [index]-th element of an array as [J]
|
||||
* @throws JsonElementTypeMismatchException if element has invalid type
|
||||
*/
|
||||
inline fun <reified J : JsonElement> getAs(index: Int): J = content[index] as? J
|
||||
?: unexpectedJson("at $index", J::class.toString())
|
||||
|
||||
/**
|
||||
* Returns [index]-th element of an array as [J] or `null` if element is missing or has different type
|
||||
*/
|
||||
inline fun <reified J : JsonElement> getAsOrNull(index: Int): J? = content.getOrNull(index) as? J
|
||||
|
||||
override fun toString() = content.joinToString(prefix = "[", postfix = "]")
|
||||
}
|
||||
|
||||
@PublishedApi
|
||||
internal fun unexpectedJson(key: String, expected: String): Nothing =
|
||||
throw JsonElementTypeMismatchException(key, expected)
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jetbrains.report.json
|
||||
|
||||
class JsonInvalidValueInStrictModeException(value: Any, valueDescription: String) : Exception(
|
||||
"$value is not a valid $valueDescription as per JSON spec.\n" +
|
||||
"You can disable strict mode to serialize such values"
|
||||
) {
|
||||
constructor(floatValue: Float) : this(floatValue, "float")
|
||||
constructor(doubleValue: Double) : this(doubleValue, "double")
|
||||
}
|
||||
|
||||
class JsonUnknownKeyException(key: String) : Exception(
|
||||
"Strict JSON encountered unknown key: $key\n" +
|
||||
"You can disable strict mode to skip unknown keys"
|
||||
)
|
||||
|
||||
class JsonParsingException(position: Int, message: String) : Exception("Invalid JSON at $position: $message")
|
||||
|
||||
class JsonElementTypeMismatchException(key: String, expected: String) : Exception("Element $key is not a $expected")
|
||||
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jetbrains.report.json
|
||||
|
||||
import org.jetbrains.report.json.EscapeCharMappings.ESC2C
|
||||
// special strings
|
||||
internal const val NULL = "null"
|
||||
|
||||
// special chars
|
||||
internal const val COMMA = ','
|
||||
internal const val COLON = ':'
|
||||
internal const val BEGIN_OBJ = '{'
|
||||
internal const val END_OBJ = '}'
|
||||
internal const val BEGIN_LIST = '['
|
||||
internal const val END_LIST = ']'
|
||||
internal const val STRING = '"'
|
||||
internal const val STRING_ESC = '\\'
|
||||
internal const val INVALID = 0.toChar()
|
||||
internal const val UNICODE_ESC = 'u'
|
||||
// token classes
|
||||
internal const val TC_OTHER: Byte = 0
|
||||
internal const val TC_STRING: Byte = 1
|
||||
internal const val TC_STRING_ESC: Byte = 2
|
||||
internal const val TC_WS: Byte = 3
|
||||
internal const val TC_COMMA: Byte = 4
|
||||
internal const val TC_COLON: Byte = 5
|
||||
internal const val TC_BEGIN_OBJ: Byte = 6
|
||||
internal const val TC_END_OBJ: Byte = 7
|
||||
internal const val TC_BEGIN_LIST: Byte = 8
|
||||
internal const val TC_END_LIST: Byte = 9
|
||||
internal const val TC_NULL: Byte = 10
|
||||
internal const val TC_INVALID: Byte = 11
|
||||
internal const val TC_EOF: Byte = 12
|
||||
// mapping from chars to token classes
|
||||
private const val CTC_MAX = 0x7e
|
||||
// mapping from escape chars real chars
|
||||
private const val C2ESC_MAX = 0x5d
|
||||
private const val ESC2C_MAX = 0x75
|
||||
|
||||
internal val C2TC = ByteArray(CTC_MAX).apply {
|
||||
for (i in 0..0x20)
|
||||
initC2TC(i, TC_INVALID)
|
||||
initC2TC(0x09, TC_WS)
|
||||
initC2TC(0x0a, TC_WS)
|
||||
initC2TC(0x0d, TC_WS)
|
||||
initC2TC(0x20, TC_WS)
|
||||
initC2TC(COMMA, TC_COMMA)
|
||||
initC2TC(COLON, TC_COLON)
|
||||
initC2TC(BEGIN_OBJ, TC_BEGIN_OBJ)
|
||||
initC2TC(END_OBJ, TC_END_OBJ)
|
||||
initC2TC(BEGIN_LIST, TC_BEGIN_LIST)
|
||||
initC2TC(END_LIST, TC_END_LIST)
|
||||
initC2TC(STRING, TC_STRING)
|
||||
initC2TC(STRING_ESC, TC_STRING_ESC)
|
||||
}
|
||||
// object instead of @SharedImmutable because there is mutual initialization in [initC2ESC]
|
||||
internal object EscapeCharMappings {
|
||||
internal val ESC2C = CharArray(ESC2C_MAX)
|
||||
internal val C2ESC = CharArray(C2ESC_MAX).apply {
|
||||
for (i in 0x00..0x1f)
|
||||
initC2ESC(i, UNICODE_ESC)
|
||||
initC2ESC(0x08, 'b')
|
||||
initC2ESC(0x09, 't')
|
||||
initC2ESC(0x0a, 'n')
|
||||
initC2ESC(0x0c, 'f')
|
||||
initC2ESC(0x0d, 'r')
|
||||
initC2ESC('/', '/')
|
||||
initC2ESC(STRING, STRING)
|
||||
initC2ESC(STRING_ESC, STRING_ESC)
|
||||
}
|
||||
private fun CharArray.initC2ESC(c: Int, esc: Char) {
|
||||
this[c] = esc
|
||||
if (esc != UNICODE_ESC) ESC2C[esc.toInt()] = c.toChar()
|
||||
}
|
||||
private fun CharArray.initC2ESC(c: Char, esc: Char) = initC2ESC(c.toInt(), esc)
|
||||
}
|
||||
private fun ByteArray.initC2TC(c: Int, cl: Byte) {
|
||||
this[c] = cl
|
||||
}
|
||||
private fun ByteArray.initC2TC(c: Char, cl: Byte) {
|
||||
initC2TC(c.toInt(), cl)
|
||||
}
|
||||
internal fun charToTokenClass(c: Char) = if (c.toInt() < CTC_MAX) C2TC[c.toInt()] else TC_OTHER
|
||||
internal fun escapeToChar(c: Int): Char = if (c < ESC2C_MAX) ESC2C[c] else INVALID
|
||||
// JSON low level parser
|
||||
internal class Parser(val source: String) {
|
||||
var curPos: Int = 0 // position in source
|
||||
private set
|
||||
// updated by nextToken
|
||||
var tokenPos: Int = 0
|
||||
private set
|
||||
var tc: Byte = TC_EOF
|
||||
private set
|
||||
// update by nextString/nextLiteral
|
||||
private var offset = -1 // when offset >= 0 string is in source, otherwise in buf
|
||||
private var length = 0 // length of string
|
||||
private var buf = CharArray(16) // only used for strings with escapes
|
||||
init {
|
||||
nextToken()
|
||||
}
|
||||
internal inline fun requireTc(expected: Byte, lazyErrorMsg: () -> String) {
|
||||
if (tc != expected)
|
||||
fail(tokenPos, lazyErrorMsg())
|
||||
}
|
||||
val canBeginValue: Boolean
|
||||
get() = when (tc) {
|
||||
TC_BEGIN_LIST, TC_BEGIN_OBJ, TC_OTHER, TC_STRING, TC_NULL -> true
|
||||
else -> false
|
||||
}
|
||||
fun takeStr(): String {
|
||||
if (tc != TC_OTHER && tc != TC_STRING) fail(tokenPos, "Expected string or non-null literal")
|
||||
val prevStr = if (offset < 0)
|
||||
String(buf, 0, length) else
|
||||
source.substring(offset, offset + length)
|
||||
nextToken()
|
||||
return prevStr
|
||||
}
|
||||
private fun append(ch: Char) {
|
||||
if (length >= buf.size) buf = buf.copyOf(2 * buf.size)
|
||||
buf[length++] = ch
|
||||
}
|
||||
// initializes buf usage upon the first encountered escaped char
|
||||
private fun appendRange(source: String, fromIndex: Int, toIndex: Int) {
|
||||
val addLen = toIndex - fromIndex
|
||||
val oldLen = length
|
||||
val newLen = oldLen + addLen
|
||||
if (newLen > buf.size) buf = buf.copyOf(newLen.coerceAtLeast(2 * buf.size))
|
||||
for (i in 0 until addLen) buf[oldLen + i] = source[fromIndex + i]
|
||||
length += addLen
|
||||
}
|
||||
fun nextToken() {
|
||||
val source = source
|
||||
var curPos = curPos
|
||||
val maxLen = source.length
|
||||
while (true) {
|
||||
if (curPos >= maxLen) {
|
||||
tokenPos = curPos
|
||||
tc = TC_EOF
|
||||
return
|
||||
}
|
||||
val ch = source[curPos]
|
||||
val tc = charToTokenClass(ch)
|
||||
when (tc) {
|
||||
TC_WS -> curPos++ // skip whitespace
|
||||
TC_OTHER -> {
|
||||
nextLiteral(source, curPos)
|
||||
return
|
||||
}
|
||||
TC_STRING -> {
|
||||
nextString(source, curPos)
|
||||
return
|
||||
}
|
||||
else -> {
|
||||
this.tokenPos = curPos
|
||||
this.tc = tc
|
||||
this.curPos = curPos + 1
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun nextLiteral(source: String, startPos: Int) {
|
||||
tokenPos = startPos
|
||||
offset = startPos
|
||||
var curPos = startPos
|
||||
val maxLen = source.length
|
||||
while (true) {
|
||||
curPos++
|
||||
if (curPos >= maxLen || charToTokenClass(source[curPos]) != TC_OTHER) break
|
||||
}
|
||||
this.curPos = curPos
|
||||
length = curPos - offset
|
||||
tc = if (rangeEquals(source, offset, length, NULL)) TC_NULL else TC_OTHER
|
||||
}
|
||||
private fun nextString(source: String, startPos: Int) {
|
||||
tokenPos = startPos
|
||||
length = 0 // in buffer
|
||||
var curPos = startPos + 1
|
||||
var lastPos = curPos
|
||||
val maxLen = source.length
|
||||
parse@ while (true) {
|
||||
if (curPos >= maxLen) fail(curPos, "Unexpected end in string")
|
||||
if (source[curPos] == STRING) {
|
||||
break@parse
|
||||
} else if (source[curPos] == STRING_ESC) {
|
||||
appendRange(source, lastPos, curPos)
|
||||
val newPos = appendEsc(source, curPos + 1)
|
||||
curPos = newPos
|
||||
lastPos = newPos
|
||||
} else {
|
||||
curPos++
|
||||
}
|
||||
}
|
||||
if (lastPos == startPos + 1) {
|
||||
// there was no escaped chars
|
||||
this.offset = lastPos
|
||||
this.length = curPos - lastPos
|
||||
} else {
|
||||
// some escaped chars were there
|
||||
appendRange(source, lastPos, curPos)
|
||||
this.offset = -1
|
||||
}
|
||||
this.curPos = curPos + 1
|
||||
tc = TC_STRING
|
||||
}
|
||||
private fun appendEsc(source: String, startPos: Int): Int {
|
||||
var curPos = startPos
|
||||
require(curPos < source.length, curPos) { "Unexpected end after escape char" }
|
||||
val curChar = source[curPos++]
|
||||
if (curChar == UNICODE_ESC) {
|
||||
curPos = appendHex(source, curPos)
|
||||
} else {
|
||||
val c = escapeToChar(curChar.toInt())
|
||||
require(c != INVALID, curPos) { "Invalid escaped char '$curChar'" }
|
||||
append(c)
|
||||
}
|
||||
return curPos
|
||||
}
|
||||
private fun appendHex(source: String, startPos: Int): Int {
|
||||
var curPos = startPos
|
||||
append(
|
||||
((fromHexChar(source, curPos++) shl 12) +
|
||||
(fromHexChar(source, curPos++) shl 8) +
|
||||
(fromHexChar(source, curPos++) shl 4) +
|
||||
fromHexChar(source, curPos++)).toChar()
|
||||
)
|
||||
return curPos
|
||||
}
|
||||
fun skipElement() {
|
||||
if (tc != TC_BEGIN_OBJ && tc != TC_BEGIN_LIST) {
|
||||
nextToken()
|
||||
return
|
||||
}
|
||||
val tokenStack = mutableListOf<Byte>()
|
||||
do {
|
||||
when (tc) {
|
||||
TC_BEGIN_LIST, TC_BEGIN_OBJ -> tokenStack.add(tc)
|
||||
TC_END_LIST -> {
|
||||
if (tokenStack.last() != TC_BEGIN_LIST) throw JsonParsingException(curPos, "found ] instead of }")
|
||||
tokenStack.removeAt(tokenStack.size - 1)
|
||||
}
|
||||
TC_END_OBJ -> {
|
||||
if (tokenStack.last() != TC_BEGIN_OBJ) throw JsonParsingException(curPos, "found } instead of ]")
|
||||
tokenStack.removeAt(tokenStack.size - 1)
|
||||
}
|
||||
}
|
||||
nextToken()
|
||||
} while (tokenStack.isNotEmpty())
|
||||
}
|
||||
}
|
||||
// Utility functions
|
||||
private fun fromHexChar(source: String, curPos: Int): Int {
|
||||
require(curPos < source.length, curPos) { "Unexpected end in unicode escape" }
|
||||
val curChar = source[curPos]
|
||||
return when (curChar) {
|
||||
in '0'..'9' -> curChar.toInt() - '0'.toInt()
|
||||
in 'a'..'f' -> curChar.toInt() - 'a'.toInt() + 10
|
||||
in 'A'..'F' -> curChar.toInt() - 'A'.toInt() + 10
|
||||
else -> fail(curPos, "Invalid toHexChar char '$curChar' in unicode escape")
|
||||
}
|
||||
}
|
||||
private fun rangeEquals(source: String, start: Int, length: Int, str: String): Boolean {
|
||||
val n = str.length
|
||||
if (length != n) return false
|
||||
for (i in 0 until n) if (source[start + i] != str[i]) return false
|
||||
return true
|
||||
}
|
||||
internal inline fun require(condition: Boolean, pos: Int, msg: () -> String) {
|
||||
if (!condition)
|
||||
fail(pos, msg())
|
||||
}
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
internal inline fun fail(pos: Int, msg: String): Nothing {
|
||||
throw JsonParsingException(pos, msg)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
// A bit changed part of kotlinx.serialization plugin
|
||||
package org.jetbrains.report.json
|
||||
|
||||
class JsonTreeParser internal constructor(private val p: Parser) {
|
||||
|
||||
companion object {
|
||||
fun parse(input: String): JsonElement = JsonTreeParser(input).readFully()
|
||||
}
|
||||
|
||||
constructor(input: String) : this(Parser(input))
|
||||
|
||||
private fun readObject(): JsonElement {
|
||||
p.requireTc(TC_BEGIN_OBJ) { "Expected start of object" }
|
||||
p.nextToken()
|
||||
val result: MutableMap<String, JsonElement> = hashMapOf()
|
||||
while (true) {
|
||||
if (p.tc == TC_COMMA) p.nextToken()
|
||||
if (!p.canBeginValue) break
|
||||
val key = p.takeStr()
|
||||
p.requireTc(TC_COLON) { "Expected ':'" }
|
||||
p.nextToken()
|
||||
val elem = read()
|
||||
result[key] = elem
|
||||
}
|
||||
p.requireTc(TC_END_OBJ) { "Expected end of object" }
|
||||
p.nextToken()
|
||||
return JsonObject(result)
|
||||
}
|
||||
|
||||
private fun readValue(isString: Boolean): JsonElement {
|
||||
val str = p.takeStr()
|
||||
return JsonLiteral(str, isString)
|
||||
}
|
||||
|
||||
private fun readArray(): JsonElement {
|
||||
p.requireTc(TC_BEGIN_LIST) { "Expected start of array" }
|
||||
p.nextToken()
|
||||
val result: MutableList<JsonElement> = arrayListOf()
|
||||
while (true) {
|
||||
if (p.tc == TC_COMMA) p.nextToken()
|
||||
if (!p.canBeginValue) break
|
||||
val elem = read()
|
||||
result.add(elem)
|
||||
}
|
||||
p.requireTc(TC_END_LIST) { "Expected end of array" }
|
||||
p.nextToken()
|
||||
return JsonArray(result)
|
||||
}
|
||||
|
||||
fun read(): JsonElement {
|
||||
if (!p.canBeginValue) fail(p.curPos, "Can't begin reading value from here")
|
||||
val tc = p.tc
|
||||
return when (tc) {
|
||||
TC_NULL -> JsonNull.also { p.nextToken() }
|
||||
TC_STRING -> readValue(isString = true)
|
||||
TC_OTHER -> readValue(isString = false)
|
||||
TC_BEGIN_OBJ -> readObject()
|
||||
TC_BEGIN_LIST -> readArray()
|
||||
else -> fail(p.curPos, "Can't begin reading element")
|
||||
}
|
||||
}
|
||||
|
||||
fun readFully(): JsonElement {
|
||||
val r = read()
|
||||
p.requireTc(TC_EOF) { "Input wasn't consumed fully" }
|
||||
return r
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jetbrains.report.json
|
||||
|
||||
private fun toHexChar(i: Int) : Char {
|
||||
val d = i and 0xf
|
||||
return if (d < 10) (d + '0'.toInt()).toChar()
|
||||
else (d - 10 + 'a'.toInt()).toChar()
|
||||
}
|
||||
|
||||
private val ESCAPE_CHARS: Array<String?> = arrayOfNulls<String>(128).apply {
|
||||
for (c in 0..0x1f) {
|
||||
val c1 = toHexChar(c shr 12)
|
||||
val c2 = toHexChar(c shr 8)
|
||||
val c3 = toHexChar(c shr 4)
|
||||
val c4 = toHexChar(c)
|
||||
this[c] = "\\u$c1$c2$c3$c4"
|
||||
}
|
||||
this['"'.toInt()] = "\\\""
|
||||
this['\\'.toInt()] = "\\\\"
|
||||
this['\t'.toInt()] = "\\t"
|
||||
this['\b'.toInt()] = "\\b"
|
||||
this['\n'.toInt()] = "\\n"
|
||||
this['\r'.toInt()] = "\\r"
|
||||
this[0x0c] = "\\f"
|
||||
}
|
||||
|
||||
internal fun StringBuilder.printQuoted(value: String) {
|
||||
append(STRING)
|
||||
var lastPos = 0
|
||||
val length = value.length
|
||||
for (i in 0 until length) {
|
||||
val c = value[i].toInt()
|
||||
// Do not replace this constant with C2ESC_MAX (which is smaller than ESCAPE_CHARS size),
|
||||
// otherwise JIT won't eliminate range check and won't vectorize this loop
|
||||
if (c >= ESCAPE_CHARS.size) continue // no need to escape
|
||||
val esc = ESCAPE_CHARS[c] ?: continue
|
||||
append(value, lastPos, i) // flush prev
|
||||
append(esc)
|
||||
lastPos = i + 1
|
||||
}
|
||||
append(value, lastPos, length)
|
||||
append(STRING)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the contents of this string is equal to the word "true", ignoring case, `false` if content equals "false",
|
||||
* and throws [IllegalStateException] otherwise.
|
||||
*/
|
||||
fun String.toBooleanStrict(): Boolean = toBooleanStrictOrNull() ?: throw IllegalStateException("$this does not represent a Boolean")
|
||||
|
||||
/**
|
||||
* Returns `true` if the contents of this string is equal to the word "true", ignoring case, `false` if content equals "false",
|
||||
* and returns `null` otherwise.
|
||||
*/
|
||||
fun String.toBooleanStrictOrNull(): Boolean? = when {
|
||||
this.equals("true", ignoreCase = true) -> true
|
||||
this.equals("false", ignoreCase = true) -> false
|
||||
else -> null
|
||||
}
|
||||
Reference in New Issue
Block a user