From f42f6c49accee21b58e640838c6f99fc9da55c32 Mon Sep 17 00:00:00 2001 From: DSinge Date: Wed, 8 Nov 2023 16:40:37 -0500 Subject: [PATCH] Add option to return single values wrapped in an array (#30) --- .../kotlin/com/nfeld/jsonpathkt/JsonPath.kt | 26 +++++- .../com/nfeld/jsonpathkt/ResolutionOptions.kt | 9 ++ .../jsonpathkt/path/WrapSingleValueTest.kt | 85 +++++++++++++++++++ .../jsonpathkt/jsonjava/JsonJavaResolution.kt | 37 ++++++-- .../jsonpathkt/kotlinx/KotlinxResolution.kt | 19 ++++- 5 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 jsonpath-core/src/commonMain/kotlin/com/nfeld/jsonpathkt/ResolutionOptions.kt create mode 100644 jsonpath-core/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/WrapSingleValueTest.kt diff --git a/jsonpath-core/src/commonMain/kotlin/com/nfeld/jsonpathkt/JsonPath.kt b/jsonpath-core/src/commonMain/kotlin/com/nfeld/jsonpathkt/JsonPath.kt index 43a5203..97b2bcc 100644 --- a/jsonpath-core/src/commonMain/kotlin/com/nfeld/jsonpathkt/JsonPath.kt +++ b/jsonpath-core/src/commonMain/kotlin/com/nfeld/jsonpathkt/JsonPath.kt @@ -1,7 +1,11 @@ package com.nfeld.jsonpathkt import com.nfeld.jsonpathkt.json.JsonNode +import com.nfeld.jsonpathkt.json.JsonType +import com.nfeld.jsonpathkt.tokens.ArrayAccessorToken +import com.nfeld.jsonpathkt.tokens.ObjectAccessorToken import com.nfeld.jsonpathkt.tokens.Token +import com.nfeld.jsonpathkt.tokens.WildcardToken import kotlin.jvm.JvmInline @JvmInline @@ -18,9 +22,29 @@ public value class JsonPath private constructor( } } -public inline fun JsonPath.resolveOrNull(node: JsonNode): T? = +public inline fun JsonPath.resolveOrNull( + node: JsonNode, + options: ResolutionOptions = ResolutionOptions.Default, +): T? = tokens.fold( initial = node, ) { valueAtPath: JsonNode?, nextToken: Token -> valueAtPath?.let(nextToken::read) + }?.let { + val isRoot = tokens.isEmpty() + val containsWildcard = tokens.any { token -> token is WildcardToken } + val lastToken = tokens.lastOrNull() + val isAccessingAnObjectOrArray = + lastToken is ObjectAccessorToken || lastToken is ArrayAccessorToken + val isNodeAnArray = it.type == JsonType.Array + + val wrappingRequired = + options.wrapSingleValue && + !containsWildcard && + (isRoot || isAccessingAnObjectOrArray || !isNodeAnArray) + + when { + wrappingRequired -> it.copy(element = it.toJsonArray(listOf(it.element))) + else -> it + } }?.element as? T diff --git a/jsonpath-core/src/commonMain/kotlin/com/nfeld/jsonpathkt/ResolutionOptions.kt b/jsonpath-core/src/commonMain/kotlin/com/nfeld/jsonpathkt/ResolutionOptions.kt new file mode 100644 index 0000000..c9defd8 --- /dev/null +++ b/jsonpath-core/src/commonMain/kotlin/com/nfeld/jsonpathkt/ResolutionOptions.kt @@ -0,0 +1,9 @@ +package com.nfeld.jsonpathkt + +public data class ResolutionOptions( + val wrapSingleValue: Boolean = false, +) { + public companion object { + public val Default: ResolutionOptions = ResolutionOptions() + } +} diff --git a/jsonpath-core/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/WrapSingleValueTest.kt b/jsonpath-core/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/WrapSingleValueTest.kt new file mode 100644 index 0000000..e7d6599 --- /dev/null +++ b/jsonpath-core/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/WrapSingleValueTest.kt @@ -0,0 +1,85 @@ +package com.nfeld.jsonpathkt.path + +import com.nfeld.jsonpathkt.BOOKS_JSON +import com.nfeld.jsonpathkt.LARGE_JSON +import com.nfeld.jsonpathkt.ResolutionOptions +import com.nfeld.jsonpathkt.SMALL_JSON +import com.nfeld.jsonpathkt.SMALL_JSON_ARRAY +import com.nfeld.jsonpathkt.asJson +import com.nfeld.jsonpathkt.kotlinx.resolvePathOrNull +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.JsonElement +import kotlin.test.Test + +class WrapSingleValueTest { + @Test + fun parse_should_wrap_root_accessor() { + SMALL_JSON.resolvePathWrappedOrNull("$") shouldBe "[${SMALL_JSON}]".asJson + SMALL_JSON_ARRAY.resolvePathWrappedOrNull("$") shouldBe "[${SMALL_JSON_ARRAY}]".asJson + } + + @Test + fun parse_should_wrap_array_accessor() { + SMALL_JSON_ARRAY.resolvePathWrappedOrNull("$[0]") shouldBe "[1]".asJson + SMALL_JSON_ARRAY.resolvePathWrappedOrNull("$[-1]") shouldBe "[${SMALL_JSON}]".asJson + + """ + |[ + | [ + | ["A"], ["B"], ["C"] + | ], + | [ + | ["D"], ["E"], ["F"] + | ], + | [ + | ["G"], ["H"], ["I"] + | ] + |] + """.trimMargin() + .resolvePathWrappedOrNull("$.[1].[2]") shouldBe """[ [ "F" ] ]""".asJson + } + + @Test + fun parse_should_wrap_object_accessor() { + SMALL_JSON.resolvePathWrappedOrNull("key") shouldBe "[5]".asJson + SMALL_JSON_ARRAY.resolvePathWrappedOrNull("$[4].key") shouldBe "[5]".asJson + LARGE_JSON.resolvePathWrappedOrNull("$[0].tags") shouldBe """[["occaecat","mollit","ullamco","labore","cillum","laboris","qui"]]""".asJson + } + + @Test + fun parse_should_not_wrap_wildcard_root_accessor() { + SMALL_JSON_ARRAY.resolvePathWrappedOrNull("$*") shouldBe SMALL_JSON_ARRAY.asJson + SMALL_JSON_ARRAY.resolvePathWrappedOrNull("$.*") shouldBe SMALL_JSON_ARRAY.asJson + } + + @Test + fun parse_should_not_wrap_wildcard_array_accessor() { + """ + |[ + | [ + | ["A"], ["B"], ["C"] + | ], + | [ + | ["D"], ["E"], ["F"] + | ], + | [ + | ["G"], ["H"], ["I"] + | ] + |] + """.trimMargin() + .resolvePathWrappedOrNull("$.[1].*.[0]") shouldBe """[ "D", "E", "F" ]""".asJson + } + + @Test + fun parse_should_not_wrap_wildcard_object_accessor() { + BOOKS_JSON.resolvePathWrappedOrNull("$.store.book.*.author") shouldBe """["Nigel Rees","Evelyn Waugh","Herman Melville","J. R. R. Tolkien"]""".asJson + } + + private val resolutionOptions = ResolutionOptions(wrapSingleValue = true) + + private fun String.resolvePathWrappedOrNull(path: String) = + asJson.resolvePathWrappedOrNull(path) + + private fun JsonElement.resolvePathWrappedOrNull(path: String) = + resolvePathOrNull(path, options = resolutionOptions) +} diff --git a/jsonpath-jsonjava/src/main/kotlin/com/nfeld/jsonpathkt/jsonjava/JsonJavaResolution.kt b/jsonpath-jsonjava/src/main/kotlin/com/nfeld/jsonpathkt/jsonjava/JsonJavaResolution.kt index 3c9796e..ad677ff 100644 --- a/jsonpath-jsonjava/src/main/kotlin/com/nfeld/jsonpathkt/jsonjava/JsonJavaResolution.kt +++ b/jsonpath-jsonjava/src/main/kotlin/com/nfeld/jsonpathkt/jsonjava/JsonJavaResolution.kt @@ -1,6 +1,7 @@ package com.nfeld.jsonpathkt.jsonjava import com.nfeld.jsonpathkt.JsonPath +import com.nfeld.jsonpathkt.ResolutionOptions import com.nfeld.jsonpathkt.resolveOrNull import org.json.JSONArray import org.json.JSONObject @@ -21,32 +22,56 @@ public value class JSONElement(@PublishedApi internal val element: Any) { public inline val asStringOrNull: JSONString? get() = element as? JSONString } -public fun JSONArray.resolveOrNull(path: JsonPath): JSONElement? = path.resolveOrNull(this) +public fun JSONArray.resolveOrNull( + path: JsonPath, + options: ResolutionOptions = ResolutionOptions.Default, +): JSONElement? = path.resolveOrNull(this, options) + public fun JSONArray.resolveAsStringOrNull(path: JsonPath): String? = path.resolveOrNull(this)?.asStringOrNull?.toJSONString() -public fun JSONArray.resolvePathOrNull(path: String): JSONElement? = JsonPath.compile(path).resolveOrNull(this) +public fun JSONArray.resolvePathOrNull( + path: String, + options: ResolutionOptions = ResolutionOptions.Default, +): JSONElement? = JsonPath.compile(path).resolveOrNull(this, options) + public fun JSONArray.resolvePathAsStringOrNull(path: String): String? = JsonPath.compile(path).resolveOrNull(this)?.asStringOrNull?.toJSONString() -public fun JSONObject.resolveOrNull(path: JsonPath): JSONElement? = path.resolveOrNull(this) +public fun JSONObject.resolveOrNull( + path: JsonPath, + options: ResolutionOptions = ResolutionOptions.Default, +): JSONElement? = path.resolveOrNull(this, options) + public fun JSONObject.resolveAsStringOrNull(path: JsonPath): String? = path.resolveOrNull(this)?.asStringOrNull?.toJSONString() -public fun JSONObject.resolvePathOrNull(path: String): JSONElement? = JsonPath.compile(path).resolveOrNull(this) +public fun JSONObject.resolvePathOrNull( + path: String, + options: ResolutionOptions = ResolutionOptions.Default, +): JSONElement? = JsonPath.compile(path).resolveOrNull(this, options) + public fun JSONObject.resolvePathAsStringOrNull(path: String): String? = JsonPath.compile(path).resolveOrNull(this)?.asStringOrNull?.toJSONString() -public fun JsonPath.resolveOrNull(json: JSONArray): JSONElement? = resolveOrNull( +public fun JsonPath.resolveOrNull( + json: JSONArray, + options: ResolutionOptions = ResolutionOptions.Default, +): JSONElement? = resolveOrNull( OrgJsonNode(json, isWildcardScope = false), + options, )?.let(::JSONElement) public fun JsonPath.resolveAsStringOrNull(json: JSONArray): String? = resolveOrNull( OrgJsonNode(json, isWildcardScope = false), )?.let(::JSONElement)?.asStringOrNull?.toJSONString() -public fun JsonPath.resolveOrNull(json: JSONObject): JSONElement? = resolveOrNull( +public fun JsonPath.resolveOrNull( + json: JSONObject, + options: ResolutionOptions = ResolutionOptions.Default, +): JSONElement? = resolveOrNull( OrgJsonNode(json, isWildcardScope = false), + options, )?.let(::JSONElement) public fun JsonPath.resolveAsStringOrNull(json: JSONObject): String? = resolveOrNull( diff --git a/jsonpath-kotlinx/src/commonMain/kotlin/com/nfeld/jsonpathkt/kotlinx/KotlinxResolution.kt b/jsonpath-kotlinx/src/commonMain/kotlin/com/nfeld/jsonpathkt/kotlinx/KotlinxResolution.kt index f45d841..35eb437 100644 --- a/jsonpath-kotlinx/src/commonMain/kotlin/com/nfeld/jsonpathkt/kotlinx/KotlinxResolution.kt +++ b/jsonpath-kotlinx/src/commonMain/kotlin/com/nfeld/jsonpathkt/kotlinx/KotlinxResolution.kt @@ -1,14 +1,23 @@ package com.nfeld.jsonpathkt.kotlinx import com.nfeld.jsonpathkt.JsonPath +import com.nfeld.jsonpathkt.ResolutionOptions import com.nfeld.jsonpathkt.resolveOrNull import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.contentOrNull -public fun JsonElement.resolvePathOrNull(path: String): JsonElement? = JsonPath.compile(path).resolveOrNull(this) -public fun JsonElement.resolveOrNull(path: JsonPath): JsonElement? = path.resolveOrNull(this) +public fun JsonElement.resolvePathOrNull( + path: String, + options: ResolutionOptions = ResolutionOptions.Default, +): JsonElement? = + JsonPath.compile(path).resolveOrNull(this, options) + +public fun JsonElement.resolveOrNull( + path: JsonPath, + options: ResolutionOptions = ResolutionOptions.Default, +): JsonElement? = path.resolveOrNull(this, options) public fun JsonElement.resolvePathAsStringOrNull(path: String): String? = JsonPath.compile(path).resolveAsStringOrNull(this) @@ -16,11 +25,15 @@ public fun JsonElement.resolvePathAsStringOrNull(path: String): String? = public fun JsonElement.resolveAsStringOrNull(path: JsonPath): String? = path.resolveAsStringOrNull(this) -public fun JsonPath.resolveOrNull(json: JsonElement): JsonElement? { +public fun JsonPath.resolveOrNull( + json: JsonElement, + options: ResolutionOptions = ResolutionOptions.Default, +): JsonElement? { if (json is JsonNull) return null return resolveOrNull( KotlinxJsonNode(json, isWildcardScope = false), + options, ) }