diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index 32ee7644304..c3d2bc8ac5b 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -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" diff --git a/kotest-assertions/kotest-assertions-json/build.gradle.kts b/kotest-assertions/kotest-assertions-json/build.gradle.kts index 87dec85de51..4d4201058bd 100644 --- a/kotest-assertions/kotest-assertions-json/build.gradle.kts +++ b/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") } @@ -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) } } @@ -56,8 +56,6 @@ kotlin { val jvmMain by getting { dependencies { - implementation(Libs.Jackson.databind) - implementation(Libs.Jackson.kotlin) implementation(Libs.Jayway.jsonpath) } } diff --git a/kotest-assertions/kotest-assertions-json/src/jvmMain/kotlin/io/kotest/assertions/json/JsonMatchers.kt b/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/JsonMatchers.kt similarity index 63% rename from kotest-assertions/kotest-assertions-json/src/jvmMain/kotlin/io/kotest/assertions/json/JsonMatchers.kt rename to kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/JsonMatchers.kt index 2c0c31d0649..7a5c559062b 100644 --- a/kotest-assertions/kotest-assertions-json/src/jvmMain/kotlin/io/kotest/assertions/json/JsonMatchers.kt +++ b/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/JsonMatchers.kt @@ -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. @@ -21,8 +23,8 @@ infix fun String?.shouldNotMatchJson(expected: String?) = this shouldNot matchJs fun matchJson(expected: String?) = object : Matcher { 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, @@ -39,20 +41,20 @@ fun matchJson(expected: String?) = object : Matcher { * 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 { - 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) } diff --git a/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/compare.kt b/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/compare.kt index 00e39706b1a..3efc53493fc 100644 --- a/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/compare.kt +++ b/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/compare.kt @@ -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, expected: JsonNode, actual: JsonNode, @@ -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, expected: JsonNode.ObjectNode, actual: JsonNode.ObjectNode, @@ -82,7 +81,7 @@ fun compareObjects( return null } -fun compareArrays( +internal fun compareArrays( path: List, expected: JsonNode.ArrayNode, actual: JsonNode.ArrayNode, @@ -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, expected: JsonNode.StringNode, actual: JsonNode, mode: CompareMode): JsonError? { +internal fun compareString(path: List, 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) { @@ -117,7 +116,7 @@ fun compareString(path: List, expected: JsonNode.StringNode, actual: Jso } } -fun compareStrings(path: List, expected: String, actual: String): JsonError? { +internal fun compareStrings(path: List, expected: String, actual: String): JsonError? { return when (expected) { actual -> null else -> JsonError.UnequalStrings(path, expected, actual) @@ -128,7 +127,7 @@ fun compareStrings(path: List, 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, expected: JsonNode.BooleanNode, actual: JsonNode, @@ -145,7 +144,7 @@ fun compareBoolean( } } -fun compareBooleans(path: List, expected: Boolean, actual: Boolean): JsonError? { +internal fun compareBooleans(path: List, expected: Boolean, actual: Boolean): JsonError? { return when (expected) { actual -> null else -> JsonError.UnequalBooleans(path, expected, actual) @@ -156,7 +155,7 @@ fun compareBooleans(path: List, 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, expected: JsonNode.LongNode, actual: JsonNode, mode: CompareMode): JsonError? { +internal fun compareLong(path: List, 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()) { @@ -167,7 +166,7 @@ fun compareLong(path: List, expected: JsonNode.LongNode, actual: JsonNod } } -fun compareLongs(path: List, expected: Long, actual: Long): JsonError? { +internal fun compareLongs(path: List, expected: Long, actual: Long): JsonError? { return when (expected) { actual -> null else -> JsonError.UnequalValues(path, expected, actual) @@ -178,10 +177,12 @@ fun compareLongs(path: List, 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, expected: JsonNode.DoubleNode, actual: JsonNode, mode: CompareMode): JsonError? { +internal fun compareDouble(path: List, 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) @@ -190,14 +191,56 @@ fun compareDouble(path: List, expected: JsonNode.DoubleNode, actual: Jso } } -fun compareDoubles(path: List, expected: Double, actual: Double): JsonError? { +internal fun compareDoubles(path: List, expected: Double, actual: Double): JsonError? { return when { abs(expected - actual) <= Double.MIN_VALUE -> null else -> JsonError.UnequalValues(path, expected, actual) } } -fun compareNull(path: List, b: JsonNode): JsonError? { +internal fun compareFloat(path: List, 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, 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, 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, expected: Int, actual: Int): JsonError? { + return when (expected) { + actual -> null + else -> JsonError.UnequalValues(path, expected, actual) + } +} + +internal fun compareNull(path: List, b: JsonNode): JsonError? { return when (b) { is JsonNode.NullNode -> null else -> JsonError.ExpectedNull(path, b) diff --git a/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/matchers.kt b/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/matchers.kt index 3cf97934ab7..eb6b9f7ca92 100644 --- a/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/matchers.kt +++ b/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/matchers.kt @@ -16,7 +16,9 @@ import io.kotest.matchers.MatcherResult */ fun equalJson(expected: JsonTree, mode: CompareMode, order: CompareOrder) = object : Matcher { 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", @@ -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) diff --git a/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/nodes.kt b/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/nodes.kt index 4350b991465..2b16c69ba34 100644 --- a/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/nodes.kt +++ b/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/nodes.kt @@ -9,6 +9,8 @@ sealed class JsonNode { is StringNode -> "string" is LongNode -> "long" is DoubleNode -> "double" + is IntNode -> "int" + is FloatNode -> "float" NullNode -> "null" } @@ -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 } diff --git a/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/wrappers.kt b/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/wrappers.kt new file mode 100644 index 00000000000..dc1d0199c16 --- /dev/null +++ b/kotest-assertions/kotest-assertions-json/src/commonMain/kotlin/io/kotest/assertions/json/wrappers.kt @@ -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") + } +} diff --git a/kotest-assertions/kotest-assertions-json/src/jsMain/kotlin/io.kotest.assertions.json/jsmatchers.kt b/kotest-assertions/kotest-assertions-json/src/jsMain/kotlin/io.kotest.assertions.json/jsmatchers.kt deleted file mode 100644 index a0f690f1ca3..00000000000 --- a/kotest-assertions/kotest-assertions-json/src/jsMain/kotlin/io.kotest.assertions.json/jsmatchers.kt +++ /dev/null @@ -1,31 +0,0 @@ -@file:Suppress("unused") - -package io.kotest.assertions.json - -import io.kotest.matchers.should -import io.kotest.matchers.shouldNot - -/** - * 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. - * - */ -actual 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) { - val (e, a) = parse(expected, this) - a shouldNot equalJson(e, mode, order) -} - -internal fun parse(expected: String, actual: String): Pair { - val enode = toJsonNode(JSON.parse(expected)) - val anode = toJsonNode(JSON.parse(actual)) - val e = JsonTree(enode, JSON.stringify(JSON.parse(expected), space = 2)) - val a = JsonTree(anode, JSON.stringify(JSON.parse(actual), space = 2)) - return Pair(e, a) -} diff --git a/kotest-assertions/kotest-assertions-json/src/jsMain/kotlin/io.kotest.assertions.json/wrappers.kt b/kotest-assertions/kotest-assertions-json/src/jsMain/kotlin/io.kotest.assertions.json/wrappers.kt deleted file mode 100644 index 0c820d689bb..00000000000 --- a/kotest-assertions/kotest-assertions-json/src/jsMain/kotlin/io.kotest.assertions.json/wrappers.kt +++ /dev/null @@ -1,22 +0,0 @@ -package io.kotest.assertions.json - -fun toJsonNode(any: Any?): JsonNode { - return when { - any == null -> JsonNode.NullNode - js("Array").isArray(any).unsafeCast() -> - JsonNode.ArrayNode(any.unsafeCast>().map { toJsonNode(it) }) - jsTypeOf(any) == "string" -> JsonNode.StringNode(any.toString()) - jsTypeOf(any) == "boolean" -> JsonNode.BooleanNode(any.unsafeCast()) - jsTypeOf(any) == "number" -> { - val maybeLong = any.toString().toLongOrNull() - if (maybeLong == null) JsonNode.DoubleNode(any.unsafeCast()) else JsonNode.LongNode(maybeLong) - } - jsTypeOf(any) == "object" -> { - val map = js("Object").entries(any).unsafeCast>>().map { - it[0].unsafeCast() to toJsonNode(it[1].unsafeCast()) - }.toMap() - JsonNode.ObjectNode(map) - } - else -> error("Unhandled js type $any") - } -} diff --git a/kotest-assertions/kotest-assertions-json/src/jsTest/kotlin/io.kotest.assertions.json/EqualTest.kt b/kotest-assertions/kotest-assertions-json/src/jsTest/kotlin/io.kotest.assertions.json/EqualTest.kt index d864f7969a8..f1a4b926613 100644 --- a/kotest-assertions/kotest-assertions-json/src/jsTest/kotlin/io.kotest.assertions.json/EqualTest.kt +++ b/kotest-assertions/kotest-assertions-json/src/jsTest/kotlin/io.kotest.assertions.json/EqualTest.kt @@ -337,7 +337,7 @@ actual: shouldFail { a shouldEqualJson b }.shouldHaveMessage( - """At 'b' expected null but was long + """At 'b' expected null but was int expected: { @@ -501,7 +501,7 @@ actual: test("comparing object to long") { val a = """ { "a" : "foo", "b" : { "c": true } } """ - val b = """ { "a" : "foo", "b" : 123 } """ + val b = """ { "a" : "foo", "b" : 2067120338512882656 } """ shouldFail { a shouldEqualJson b }.shouldHaveMessage( @@ -510,7 +510,7 @@ actual: expected: { "a": "foo", - "b": 123 + "b": 2067120338512882656 } actual: diff --git a/kotest-assertions/kotest-assertions-json/src/jvmMain/kotlin/io/kotest/assertions/json/resources.kt b/kotest-assertions/kotest-assertions-json/src/jvmMain/kotlin/io/kotest/assertions/json/resources.kt index b504e0b8ac7..9b84b952281 100644 --- a/kotest-assertions/kotest-assertions-json/src/jvmMain/kotlin/io/kotest/assertions/json/resources.kt +++ b/kotest-assertions/kotest-assertions-json/src/jvmMain/kotlin/io/kotest/assertions/json/resources.kt @@ -17,11 +17,14 @@ infix fun String?.shouldMatchJsonResource(resource: String) { } infix fun String.shouldNotMatchJsonResource(resource: String) = this shouldNot matchJsonResource(resource) + fun matchJsonResource(resource: String) = object : Matcher { override fun test(value: String?): MatcherResult { - val actualJson = value?.let(mapper::readTree) - val expectedJson = mapper.readTree(this.javaClass.getResourceAsStream(resource)) + val actualJson = value?.let(pretty::parseToJsonElement) + val expectedJson = this.javaClass.getResourceAsStream(resource).bufferedReader().use { + pretty.parseToJsonElement(it.readText()) + } return MatcherResult( actualJson == expectedJson, diff --git a/kotest-assertions/kotest-assertions-json/src/jvmMain/kotlin/io/kotest/assertions/json/wrappers.kt b/kotest-assertions/kotest-assertions-json/src/jvmMain/kotlin/io/kotest/assertions/json/wrappers.kt deleted file mode 100644 index 071bd5a76f5..00000000000 --- a/kotest-assertions/kotest-assertions-json/src/jvmMain/kotlin/io/kotest/assertions/json/wrappers.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.kotest.assertions.json - -import com.fasterxml.jackson.databind.JsonNode as JacksonNode -import com.fasterxml.jackson.databind.node.ArrayNode as JacksonArray -import com.fasterxml.jackson.databind.node.BooleanNode as JacksonBoolean -import com.fasterxml.jackson.databind.node.DoubleNode as JacksonDouble -import com.fasterxml.jackson.databind.node.LongNode as JacksonLong -import com.fasterxml.jackson.databind.node.ObjectNode as JacksonObject -import com.fasterxml.jackson.databind.node.TextNode as JacksonText -import com.fasterxml.jackson.databind.node.NumericNode as JacksonNumber -import com.fasterxml.jackson.databind.node.NullNode as JacksonNull - -fun JacksonNode.toJsonNode(): JsonNode = when (this) { - is JacksonText -> JsonNode.StringNode(this.textValue()) - is JacksonDouble -> JsonNode.DoubleNode(this.doubleValue()) - is JacksonLong -> JsonNode.LongNode(this.longValue()) - is JacksonBoolean -> JsonNode.BooleanNode(this.booleanValue()) - is JacksonObject -> JsonNode.ObjectNode(this.fields().asSequence().map { it.key to it.value.toJsonNode() }.toMap()) - is JacksonArray -> JsonNode.ArrayNode(this.elements().asSequence().toList().map { it.toJsonNode() }) - is JacksonNumber -> if (this.isDouble) JsonNode.DoubleNode(this.doubleValue()) else JsonNode.LongNode(this.longValue()) - is JacksonNull -> JsonNode.NullNode - else -> error("Unsupported jackson type ${this.nodeType}") -} diff --git a/kotest-assertions/kotest-assertions-json/src/jvmTest/kotlin/com/sksamuel/kotest/tests/json/EqualTest.kt b/kotest-assertions/kotest-assertions-json/src/jvmTest/kotlin/com/sksamuel/kotest/tests/json/EqualTest.kt index d250c570fa0..7ae7646bb24 100644 --- a/kotest-assertions/kotest-assertions-json/src/jvmTest/kotlin/com/sksamuel/kotest/tests/json/EqualTest.kt +++ b/kotest-assertions/kotest-assertions-json/src/jvmTest/kotlin/com/sksamuel/kotest/tests/json/EqualTest.kt @@ -2,6 +2,7 @@ package com.sksamuel.kotest.tests.json import io.kotest.assertions.json.CompareMode import io.kotest.assertions.json.CompareOrder +import io.kotest.assertions.json.pretty import io.kotest.assertions.json.shouldEqualJson import io.kotest.assertions.shouldFail import io.kotest.core.spec.style.FunSpec @@ -14,6 +15,8 @@ import io.kotest.property.arbitrary.numericDoubles import io.kotest.property.arbitrary.string import io.kotest.property.checkAll import io.kotest.property.exhaustive.boolean +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json class EqualTest : FunSpec() { init { @@ -35,14 +38,14 @@ class EqualTest : FunSpec() { expected: { - "a" : "foo", - "b" : "baz" + "a": "foo", + "b": "baz" } actual: { - "a" : "foo", - "b" : "bar" + "a": "foo", + "b": "bar" }""" ) } @@ -61,22 +64,22 @@ actual: expected: { - "a" : true, - "b" : true + "a": true, + "b": true } actual: { - "a" : true, - "b" : false + "a": true, + "b": false }""" ) } - test("comparing long in objects") { + test("comparing int in objects") { - checkAll { long -> - val a = """ { "a" : $long } """ + checkAll { i -> + val a = """ { "a" : $i } """ a shouldEqualJson a } @@ -90,14 +93,43 @@ actual: expected: { - "a" : 123, - "b" : 326 + "a": 123, + "b": 326 +} + +actual: +{ + "a": 123, + "b": 354 +}""" + ) + } + + test("comparing long in objects") { + + checkAll { long -> + val a = """ { "a" : $long } """ + a shouldEqualJson a + } + + val a = """ { "a" : 2067120338512882656, "b": 3333333333333333333 } """ + val b = """ { "a" : 2067120338512882656, "b" : 2222222222222222222 } """ + a shouldEqualJson a + shouldFail { + a shouldEqualJson b + }.shouldHaveMessage( + """At 'b' expected 2222222222222222222 but was 3333333333333333333 + +expected: +{ + "a": 2067120338512882656, + "b": 2222222222222222222 } actual: { - "a" : 123, - "b" : 354 + "a": 2067120338512882656, + "b": 3333333333333333333 }""" ) } @@ -109,23 +141,23 @@ actual: a shouldEqualJson a } - val a = """ { "a" : 123, "b": 354 } """ - val b = """ { "a" : 123, "b" : 9897 } """ + val a = """ { "a" : 6.02E23, "b": 6.626E-34 } """ + val b = """ { "a" : 6.02E23, "b" : 2.99E8 } """ shouldFail { a shouldEqualJson b }.shouldHaveMessage( - """At 'b' expected 9897 but was 354 + """At 'b' expected 2.99E8 but was 6.626E-34 expected: { - "a" : 123, - "b" : 9897 + "a": 6.02E23, + "b": 2.99E8 } actual: { - "a" : 123, - "b" : 354 + "a": 6.02E23, + "b": 6.626E-34 }""" ) } @@ -140,14 +172,14 @@ actual: expected: { - "a" : "foo", - "c" : "bar" + "a": "foo", + "c": "bar" } actual: { - "a" : "foo", - "b" : "bar" + "a": "foo", + "b": "bar" }""" ) } @@ -162,15 +194,15 @@ actual: expected: { - "a" : "foo", - "b" : "bar" + "a": "foo", + "b": "bar" } actual: { - "a" : "foo", - "b" : "bar", - "c" : "baz" + "a": "foo", + "b": "bar", + "c": "baz" }""" ) } @@ -185,16 +217,16 @@ actual: expected: { - "a" : "foo", - "b" : "bar" + "a": "foo", + "b": "bar" } actual: { - "a" : "foo", - "b" : "bar", - "c" : "baz", - "d" : true + "a": "foo", + "b": "bar", + "c": "baz", + "d": true }""" ) } @@ -209,15 +241,15 @@ actual: expected: { - "a" : "foo", - "b" : "bar", - "c" : "baz" + "a": "foo", + "b": "bar", + "c": "baz" } actual: { - "a" : "foo", - "b" : "bar" + "a": "foo", + "b": "bar" }""" ) } @@ -232,16 +264,16 @@ actual: expected: { - "a" : "foo", - "b" : "bar", - "c" : "baz", - "d" : 123 + "a": "foo", + "b": "bar", + "c": "baz", + "d": 123 } actual: { - "a" : "foo", - "b" : "bar" + "a": "foo", + "b": "bar" }""" ) } @@ -301,14 +333,14 @@ actual: expected: { - "a" : "foo", - "b" : null + "a": "foo", + "b": null } actual: { - "a" : "foo", - "b" : "bar" + "a": "foo", + "b": "bar" }""" ) } @@ -323,14 +355,14 @@ actual: expected: { - "a" : "foo", - "b" : null + "a": "foo", + "b": null } actual: { - "a" : "foo", - "b" : true + "a": "foo", + "b": true }""" ) } @@ -341,18 +373,18 @@ actual: shouldFail { a shouldEqualJson b }.shouldHaveMessage( - """At 'b' expected null but was long + """At 'b' expected null but was int expected: { - "a" : "foo", - "b" : null + "a": "foo", + "b": null } actual: { - "a" : "foo", - "b" : 234 + "a": "foo", + "b": 234 }""" ) } @@ -367,14 +399,14 @@ actual: expected: { - "a" : "foo", - "b" : null + "a": "foo", + "b": null } actual: { - "a" : "foo", - "b" : 12.34 + "a": "foo", + "b": 12.34 }""" ) } @@ -389,16 +421,16 @@ actual: expected: { - "a" : "foo", - "b" : { - "c" : true + "a": "foo", + "b": { + "c": true } } actual: { - "a" : "foo", - "b" : "bar" + "a": "foo", + "b": "bar" }""" ) } @@ -413,16 +445,16 @@ actual: expected: { - "a" : "foo", - "b" : { - "c" : true + "a": "foo", + "b": { + "c": true } } actual: { - "a" : "foo", - "b" : true + "a": "foo", + "b": true }""" ) } @@ -437,16 +469,16 @@ actual: expected: { - "a" : "foo", - "b" : { - "c" : true + "a": "foo", + "b": { + "c": true } } actual: { - "a" : "foo", - "b" : 12.45 + "a": "foo", + "b": 12.45 }""" ) } @@ -461,16 +493,20 @@ actual: expected: { - "a" : "foo", - "b" : { - "c" : true + "a": "foo", + "b": { + "c": true } } actual: { - "a" : "foo", - "b" : [ 1, 2, 3 ] + "a": "foo", + "b": [ + 1, + 2, + 3 + ] }""" ) } @@ -485,23 +521,47 @@ actual: expected: { - "a" : "foo", - "b" : true + "a": "foo", + "b": true } actual: { - "a" : "foo", - "b" : { - "c" : true + "a": "foo", + "b": { + "c": true } }""" ) } - test("comparing object to long") { + test("comparing object to int") { val a = """ { "a" : "foo", "b" : { "c": true } } """ val b = """ { "a" : "foo", "b" : 123 } """ + shouldFail { + a shouldEqualJson b + }.shouldHaveMessage( + """At 'b' expected int but was object + +expected: +{ + "a": "foo", + "b": 123 +} + +actual: +{ + "a": "foo", + "b": { + "c": true + } +}""" + ) + } + + test("comparing object to long") { + val a = """ { "a" : "foo", "b" : { "c": true } } """ + val b = """ { "a" : "foo", "b" : 2067120338512882656 } """ shouldFail { a shouldEqualJson b }.shouldHaveMessage( @@ -509,15 +569,63 @@ actual: expected: { - "a" : "foo", - "b" : 123 + "a": "foo", + "b": 2067120338512882656 +} + +actual: +{ + "a": "foo", + "b": { + "c": true + } +}""" + ) + } + + test("comparing object to float-ish should parse as a double") { + val a = """ { "a" : "foo", "b" : { "c": true } } """ + val b = """ { "a" : "foo", "b" : 6.02f } """ + shouldFail { + a shouldEqualJson b + }.shouldHaveMessage( + """At 'b' expected double but was object + +expected: +{ + "a": "foo", + "b": 6.02 +} + +actual: +{ + "a": "foo", + "b": { + "c": true + } +}""" + ) + } + + test("comparing object to double") { + val a = """ { "a" : "foo", "b" : { "c": true } } """ + val b = """ { "a" : "foo", "b" : 6.02E23 } """ + shouldFail { + a shouldEqualJson b + }.shouldHaveMessage( + """At 'b' expected double but was object + +expected: +{ + "a": "foo", + "b": 6.02E23 } actual: { - "a" : "foo", - "b" : { - "c" : true + "a": "foo", + "b": { + "c": true } }""" ) @@ -533,15 +641,15 @@ actual: expected: { - "a" : "foo", - "b" : "werqe" + "a": "foo", + "b": "werqe" } actual: { - "a" : "foo", - "b" : { - "c" : true + "a": "foo", + "b": { + "c": true } }""" ) @@ -557,20 +665,20 @@ actual: expected: { - "a" : "foo", - "b" : { - "c" : { - "d" : 534 + "a": "foo", + "b": { + "c": { + "d": 534 } } } actual: { - "a" : "foo", - "b" : { - "c" : { - "d" : 123 + "a": "foo", + "b": { + "c": { + "d": 123 } } }""" @@ -587,20 +695,28 @@ actual: expected: { - "a" : "foo", - "b" : { - "c" : { - "d" : [ 1, 2, 4 ] + "a": "foo", + "b": { + "c": { + "d": [ + 1, + 2, + 4 + ] } } } actual: { - "a" : "foo", - "b" : { - "c" : { - "d" : [ 1, 2, 3 ] + "a": "foo", + "b": { + "c": { + "d": [ + 1, + 2, + 3 + ] } } }""" @@ -617,20 +733,29 @@ actual: expected: { - "a" : "foo", - "b" : { - "c" : { - "d" : [ 1, 2, 4 ] + "a": "foo", + "b": { + "c": { + "d": [ + 1, + 2, + 4 + ] } } } actual: { - "a" : "foo", - "b" : { - "c" : { - "d" : [ 1, 2, 3, 4 ] + "a": "foo", + "b": { + "c": { + "d": [ + 1, + 2, + 3, + 4 + ] } } }""" @@ -652,12 +777,13 @@ actual: expected: { - "products" : [ { - "id" : 4815869968463, - "title" : "RIND Fitted Hat", - "handle" : "rind-fitted-hat", - "body_html" : "Flexfit Ultra fiber Cap with Air Mesh Sides
Blue with Orange Embroidery", - "published_at" : "2020-10-22T17:13:25-04:00",""" + "products": [ + { + "id": 4815869968463, + "title": "RIND Fitted Hat", + "handle": "rind-fitted-hat", + "body_html": "Flexfit Ultra fiber Cap with Air Mesh Sides
Blue with Orange Embroidery", + "published_at": "2020-10-22T17:13:25-04:00",""" ) } @@ -671,11 +797,12 @@ expected: expected: { - "products" : [ { - "id" : 4815869968463, - "title" : "RIND Fitted Hat", - "handle" : "rind-fitted-hat", - "body_html" : "Flexfit Ultra fiber Cap with Air Mesh Sides
Blue with Orange Embroidery", - "published_at" : "2020-10-22T17:13:25-04:00", - "created_at" : "2020-10-22T17:13:23-04:00",""" + "products": [ + { + "id": 4815869968463, + "title": "RIND Fitted Hat", + "handle": "rind-fitted-hat", + "body_html": "Flexfit Ultra fiber Cap with Air Mesh Sides
Blue with Orange Embroidery", + "published_at": "2020-10-22T17:13:25-04:00", + "created_at": "2020-10-22T17:13:23-04:00",""" ) } @@ -728,22 +856,22 @@ expected: expected: { - "sku" : "RIND-TOTEO-001-MCF", - "id" : 32672932069455, - "title" : "Default Title", - "requires_shipping" : true, - "taxable" : true, - "featured_image" : null + "sku": "RIND-TOTEO-001-MCF", + "id": 32672932069455, + "title": "Default Title", + "requires_shipping": true, + "taxable": true, + "featured_image": null } actual: { - "id" : 32672932069455, - "title" : "Default Title", - "sku" : "RIND-TOTEO-001-MCF", - "requires_shipping" : true, - "taxable" : true, - "featured_image" : null + "id": 32672932069455, + "title": "Default Title", + "sku": "RIND-TOTEO-001-MCF", + "requires_shipping": true, + "taxable": true, + "featured_image": null }""") } }