Skip to content

Commit

Permalink
replace jackson and js native with kotlinx-serialization json parsing (
Browse files Browse the repository at this point in the history
…#2006)

* pushing initial work for WIP review

* fix pretty printing of raw json

* fix a bunch of json formatting issues

* kotlinx-serialization parses ints to ints not longs

* fix json int comparison

* add long test

* get EqualTest jvm tests passing

* move JsonMatchers into common

* collapse some of the MPP json code

* fix long/int issue in js tests

* cleanup and bump plugin version

* use kotlin version from Libs

* make compare functions internal and add a couple tests
  • Loading branch information
jschneidereit committed Jan 29, 2021
1 parent e0445fd commit 2fb3188
Show file tree
Hide file tree
Showing 13 changed files with 406 additions and 277 deletions.
5 changes: 5 additions & 0 deletions buildSrc/src/main/kotlin/Libs.kt
Expand Up @@ -154,6 +154,11 @@ object Libs {
const val test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$version"
}

object Serialization {
private const val version = "1.0.1"
const val json = "org.jetbrains.kotlinx:kotlinx-serialization-json:$version"
}

object Ktor {
private const val version = "1.5.0"
const val serverCore = "io.ktor:ktor-server-core:$version"
Expand Down
6 changes: 2 additions & 4 deletions kotest-assertions/kotest-assertions-json/build.gradle.kts
@@ -1,6 +1,7 @@
plugins {
id("java")
kotlin("multiplatform")
kotlin("plugin.serialization") version Libs.kotlinVersion
id("java-library")
id("com.adarshr.test-logger")
}
Expand Down Expand Up @@ -37,10 +38,9 @@ kotlin {

val commonMain by getting {
dependencies {
implementation(Libs.Serialization.json)
implementation(project(Projects.AssertionsShared))
implementation(kotlin("reflect"))
implementation(Libs.Jackson.databind)
implementation(Libs.Jackson.kotlin)
implementation(Libs.Jayway.jsonpath)
}
}
Expand All @@ -56,8 +56,6 @@ kotlin {

val jvmMain by getting {
dependencies {
implementation(Libs.Jackson.databind)
implementation(Libs.Jackson.kotlin)
implementation(Libs.Jayway.jsonpath)
}
}
Expand Down
@@ -1,13 +1,15 @@
package io.kotest.assertions.json

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
import io.kotest.matchers.should
import io.kotest.matchers.shouldNot
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

val mapper by lazy { ObjectMapper().registerKotlinModule() }
@OptIn(ExperimentalSerializationApi::class)
internal val pretty by lazy { Json { prettyPrint = true; prettyPrintIndent = " " } }

/**
* Verifies that the [expected] string is valid json, and that it matches this string.
Expand All @@ -21,8 +23,8 @@ infix fun String?.shouldNotMatchJson(expected: String?) = this shouldNot matchJs
fun matchJson(expected: String?) = object : Matcher<String?> {

override fun test(value: String?): MatcherResult {
val actualJson = value?.let(mapper::readTree)
val expectedJson = expected?.let(mapper::readTree)
val actualJson = value?.let(pretty::parseToJsonElement)
val expectedJson = expected?.let(pretty::parseToJsonElement)

return MatcherResult(
actualJson == expectedJson,
Expand All @@ -39,20 +41,20 @@ fun matchJson(expected: String?) = object : Matcher<String?> {
* regardless of order.
*
*/
actual fun String.shouldEqualJson(expected: String, mode: CompareMode, order: CompareOrder) {
fun String.shouldEqualJson(expected: String, mode: CompareMode, order: CompareOrder) {
val (e, a) = parse(expected, this)
a should equalJson(e, mode, order)
}

actual fun String.shouldNotEqualJson(expected: String, mode: CompareMode, order: CompareOrder) {
fun String.shouldNotEqualJson(expected: String, mode: CompareMode, order: CompareOrder) {
val (e, a) = parse(expected, this)
a shouldNot equalJson(e, mode, order)
}

internal fun parse(expected: String, actual: String): Pair<JsonTree, JsonTree> {
val enode = mapper.readTree(expected)
val anode = mapper.readTree(actual)
val e = JsonTree(enode.toJsonNode(), enode.toPrettyString())
val a = JsonTree(anode.toJsonNode(), anode.toPrettyString())
val enode = pretty.parseToJsonElement(expected)
val anode = pretty.parseToJsonElement(actual)
val e = JsonTree(enode.toJsonNode(), pretty.encodeToString(enode))
val a = JsonTree(anode.toJsonNode(), pretty.encodeToString(anode))
return Pair(e, a)
}
Expand Up @@ -15,10 +15,7 @@ enum class CompareOrder {
/**
* Compares two json trees, returning a detailed error message if they differ.
*/
fun compare(expected: JsonNode, actual: JsonNode, mode: CompareMode, order: CompareOrder) =
compare(emptyList(), expected, actual, mode, order)

fun compare(
internal fun compare(
path: List<String>,
expected: JsonNode,
actual: JsonNode,
Expand All @@ -38,11 +35,13 @@ fun compare(
is JsonNode.StringNode -> compareString(path, expected, actual, mode)
is JsonNode.LongNode -> compareLong(path, expected, actual, mode)
is JsonNode.DoubleNode -> compareDouble(path, expected, actual, mode)
is JsonNode.FloatNode -> compareFloat(path, expected, actual, mode)
is JsonNode.IntNode -> compareInt(path, expected, actual, mode)
JsonNode.NullNode -> compareNull(path, actual)
}
}

fun compareObjects(
internal fun compareObjects(
path: List<String>,
expected: JsonNode.ObjectNode,
actual: JsonNode.ObjectNode,
Expand Down Expand Up @@ -82,7 +81,7 @@ fun compareObjects(
return null
}

fun compareArrays(
internal fun compareArrays(
path: List<String>,
expected: JsonNode.ArrayNode,
actual: JsonNode.ArrayNode,
Expand All @@ -104,7 +103,7 @@ fun compareArrays(
/**
* When comparing a string, if the [mode] is [CompareMode.Lenient] we can convert the actual node to a string.
*/
fun compareString(path: List<String>, expected: JsonNode.StringNode, actual: JsonNode, mode: CompareMode): JsonError? {
internal fun compareString(path: List<String>, expected: JsonNode.StringNode, actual: JsonNode, mode: CompareMode): JsonError? {
return when {
actual is JsonNode.StringNode -> compareStrings(path, expected.value, actual.value)
mode == CompareMode.Lenient -> when (actual) {
Expand All @@ -117,7 +116,7 @@ fun compareString(path: List<String>, expected: JsonNode.StringNode, actual: Jso
}
}

fun compareStrings(path: List<String>, expected: String, actual: String): JsonError? {
internal fun compareStrings(path: List<String>, expected: String, actual: String): JsonError? {
return when (expected) {
actual -> null
else -> JsonError.UnequalStrings(path, expected, actual)
Expand All @@ -128,7 +127,7 @@ fun compareStrings(path: List<String>, expected: String, actual: String): JsonEr
* When comparing a boolean, if the [mode] is [CompareMode.Lenient] and the actual node is a text
* node with "true" or "false", then we convert.
*/
fun compareBoolean(
internal fun compareBoolean(
path: List<String>,
expected: JsonNode.BooleanNode,
actual: JsonNode,
Expand All @@ -145,7 +144,7 @@ fun compareBoolean(
}
}

fun compareBooleans(path: List<String>, expected: Boolean, actual: Boolean): JsonError? {
internal fun compareBooleans(path: List<String>, expected: Boolean, actual: Boolean): JsonError? {
return when (expected) {
actual -> null
else -> JsonError.UnequalBooleans(path, expected, actual)
Expand All @@ -156,7 +155,7 @@ fun compareBooleans(path: List<String>, expected: Boolean, actual: Boolean): Jso
* When comparing a boolean, if the [mode] is [CompareMode.Lenient] and the actual node is a text
* node with "true" or "false", then we convert.
*/
fun compareLong(path: List<String>, expected: JsonNode.LongNode, actual: JsonNode, mode: CompareMode): JsonError? {
internal fun compareLong(path: List<String>, expected: JsonNode.LongNode, actual: JsonNode, mode: CompareMode): JsonError? {
return when {
actual is JsonNode.LongNode -> compareLongs(path, expected.value, actual.value)
mode == CompareMode.Lenient && actual is JsonNode.StringNode -> when (val l = actual.value.toLongOrNull()) {
Expand All @@ -167,7 +166,7 @@ fun compareLong(path: List<String>, expected: JsonNode.LongNode, actual: JsonNod
}
}

fun compareLongs(path: List<String>, expected: Long, actual: Long): JsonError? {
internal fun compareLongs(path: List<String>, expected: Long, actual: Long): JsonError? {
return when (expected) {
actual -> null
else -> JsonError.UnequalValues(path, expected, actual)
Expand All @@ -178,10 +177,12 @@ fun compareLongs(path: List<String>, expected: Long, actual: Long): JsonError? {
* When comparing a boolean, if the [mode] is [CompareMode.Lenient] and the actual node is a text
* node with "true" or "false", then we convert.
*/
fun compareDouble(path: List<String>, expected: JsonNode.DoubleNode, actual: JsonNode, mode: CompareMode): JsonError? {
internal fun compareDouble(path: List<String>, expected: JsonNode.DoubleNode, actual: JsonNode, mode: CompareMode): JsonError? {
return when {
actual is JsonNode.DoubleNode -> compareDoubles(path, expected.value, actual.value)
actual is JsonNode.LongNode -> compareDoubles(path, expected.value, actual.value.toDouble())
actual is JsonNode.FloatNode -> compareDoubles(path, expected.value, actual.value.toDouble())
actual is JsonNode.IntNode -> compareDoubles(path, expected.value, actual.value.toDouble())
mode == CompareMode.Lenient && actual is JsonNode.StringNode -> when (val d = actual.value.toDoubleOrNull()) {
null -> JsonError.IncompatibleTypes(path, expected, actual)
else -> compareDoubles(path, expected.value, d)
Expand All @@ -190,14 +191,56 @@ fun compareDouble(path: List<String>, expected: JsonNode.DoubleNode, actual: Jso
}
}

fun compareDoubles(path: List<String>, expected: Double, actual: Double): JsonError? {
internal fun compareDoubles(path: List<String>, expected: Double, actual: Double): JsonError? {
return when {
abs(expected - actual) <= Double.MIN_VALUE -> null
else -> JsonError.UnequalValues(path, expected, actual)
}
}

fun compareNull(path: List<String>, b: JsonNode): JsonError? {
internal fun compareFloat(path: List<String>, expected: JsonNode.FloatNode, actual: JsonNode, mode: CompareMode): JsonError? {
return when {
actual is JsonNode.FloatNode -> compareFloats(path, expected.value, actual.value)
actual is JsonNode.LongNode -> compareFloats(path, expected.value, actual.value.toFloat())
actual is JsonNode.DoubleNode -> compareFloats(path, expected.value, actual.value.toFloat())
actual is JsonNode.IntNode -> compareFloats(path, expected.value, actual.value.toFloat())
mode == CompareMode.Lenient && actual is JsonNode.StringNode -> when (val d = actual.value.toFloatOrNull()) {
null -> JsonError.IncompatibleTypes(path, expected, actual)
else -> compareFloats(path, expected.value, d)
}
else -> JsonError.IncompatibleTypes(path, expected, actual)
}
}

internal fun compareFloats(path: List<String>, expected: Float, actual: Float): JsonError? {
return when {
abs(expected - actual) <= Float.MIN_VALUE -> null
else -> JsonError.UnequalValues(path, expected, actual)
}
}

internal fun compareInt(path: List<String>, expected: JsonNode.IntNode, actual: JsonNode, mode: CompareMode): JsonError? {
return when {
actual is JsonNode.IntNode -> compareInts(path, expected.value, actual.value)
actual is JsonNode.FloatNode -> compareInts(path, expected.value, actual.value.toInt())
actual is JsonNode.LongNode -> compareInts(path, expected.value, actual.value.toInt())
actual is JsonNode.DoubleNode -> compareInts(path, expected.value, actual.value.toInt())
mode == CompareMode.Lenient && actual is JsonNode.StringNode -> when (val d = actual.value.toIntOrNull()) {
null -> JsonError.IncompatibleTypes(path, expected, actual)
else -> compareInts(path, expected.value, d)
}
else -> JsonError.IncompatibleTypes(path, expected, actual)
}
}

internal fun compareInts(path: List<String>, expected: Int, actual: Int): JsonError? {
return when (expected) {
actual -> null
else -> JsonError.UnequalValues(path, expected, actual)
}
}

internal fun compareNull(path: List<String>, b: JsonNode): JsonError? {
return when (b) {
is JsonNode.NullNode -> null
else -> JsonError.ExpectedNull(path, b)
Expand Down
Expand Up @@ -16,7 +16,9 @@ import io.kotest.matchers.MatcherResult
*/
fun equalJson(expected: JsonTree, mode: CompareMode, order: CompareOrder) = object : Matcher<JsonTree> {
override fun test(value: JsonTree): MatcherResult {
val error = compare(expected.root, value.root, mode, order)?.asString()
val error = compare(
path = listOf(), expected = expected.root, actual = value.root, mode = mode, order = order
)?.asString()
return MatcherResult(
error == null,
"$error\n\nexpected:\n${expected.raw}\n\nactual:\n${value.raw}\n",
Expand Down Expand Up @@ -44,13 +46,3 @@ fun String.shouldEqualJson(expected: String, order: CompareOrder) =

fun String.shouldNotEqualJson(expected: String, order: CompareOrder) =
shouldNotEqualJson(expected, CompareMode.Strict, order)

/**
* Verifies that the [expected] string is valid json, and that it matches this string.
*
* This matcher will consider two json strings matched if they have the same key-values pairs,
* regardless of order.
*
*/
expect fun String.shouldEqualJson(expected: String, mode: CompareMode, order: CompareOrder)
expect fun String.shouldNotEqualJson(expected: String, mode: CompareMode, order: CompareOrder)
Expand Up @@ -9,6 +9,8 @@ sealed class JsonNode {
is StringNode -> "string"
is LongNode -> "long"
is DoubleNode -> "double"
is IntNode -> "int"
is FloatNode -> "float"
NullNode -> "null"
}

Expand All @@ -22,9 +24,13 @@ sealed class JsonNode {

data class StringNode(val value: String) : JsonNode(), ValueNode

data class FloatNode(val value: Float) : JsonNode(), ValueNode

data class LongNode(val value: Long) : JsonNode(), ValueNode

data class DoubleNode(val value: Double) : JsonNode(), ValueNode

data class IntNode(val value: Int) : JsonNode(), ValueNode

object NullNode : JsonNode(), ValueNode
}
@@ -0,0 +1,28 @@
package io.kotest.assertions.json

import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.doubleOrNull
import kotlinx.serialization.json.floatOrNull
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.longOrNull

fun JsonElement.toJsonNode(): JsonNode = when (this) {
JsonNull -> JsonNode.NullNode
is JsonObject -> JsonNode.ObjectNode(entries.map { it.key to it.value.toJsonNode() }.toMap())
is JsonArray -> JsonNode.ArrayNode(map { it.toJsonNode() })
is JsonPrimitive -> when {
intOrNull != null -> JsonNode.IntNode(intOrNull!!)
longOrNull != null -> JsonNode.LongNode(longOrNull!!)
doubleOrNull != null -> JsonNode.DoubleNode(doubleOrNull!!)
floatOrNull != null -> JsonNode.FloatNode(floatOrNull!!)
booleanOrNull != null -> JsonNode.BooleanNode(booleanOrNull!!)
contentOrNull != null -> JsonNode.StringNode(contentOrNull!!)
else -> error("Unsupported kotlinx-serialization type $this")
}
}

This file was deleted.

This file was deleted.

0 comments on commit 2fb3188

Please sign in to comment.