Skip to content

Commit

Permalink
Add option to return single values wrapped in an array (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
DSinge authored Nov 8, 2023
1 parent e8765f7 commit f42f6c4
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,9 +22,29 @@ public value class JsonPath private constructor(
}
}

public inline fun <reified T> JsonPath.resolveOrNull(node: JsonNode): T? =
public inline fun <reified T> 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.nfeld.jsonpathkt

public data class ResolutionOptions(
val wrapSingleValue: Boolean = false,
) {
public companion object {
public val Default: ResolutionOptions = ResolutionOptions()
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<Any>(
public fun JsonPath.resolveOrNull(
json: JSONArray,
options: ResolutionOptions = ResolutionOptions.Default,
): JSONElement? = resolveOrNull<Any>(
OrgJsonNode(json, isWildcardScope = false),
options,
)?.let(::JSONElement)

public fun JsonPath.resolveAsStringOrNull(json: JSONArray): String? = resolveOrNull<Any>(
OrgJsonNode(json, isWildcardScope = false),
)?.let(::JSONElement)?.asStringOrNull?.toJSONString()

public fun JsonPath.resolveOrNull(json: JSONObject): JSONElement? = resolveOrNull<Any>(
public fun JsonPath.resolveOrNull(
json: JSONObject,
options: ResolutionOptions = ResolutionOptions.Default,
): JSONElement? = resolveOrNull<Any>(
OrgJsonNode(json, isWildcardScope = false),
options,
)?.let(::JSONElement)

public fun JsonPath.resolveAsStringOrNull(json: JSONObject): String? = resolveOrNull<Any>(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
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)

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<JsonElement>(
KotlinxJsonNode(json, isWildcardScope = false),
options,
)
}

Expand Down

0 comments on commit f42f6c4

Please sign in to comment.