Skip to content

Commit f42f6c4

Browse files
authored
Add option to return single values wrapped in an array (#30)
1 parent e8765f7 commit f42f6c4

File tree

5 files changed

+166
-10
lines changed

5 files changed

+166
-10
lines changed

jsonpath-core/src/commonMain/kotlin/com/nfeld/jsonpathkt/JsonPath.kt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package com.nfeld.jsonpathkt
22

33
import com.nfeld.jsonpathkt.json.JsonNode
4+
import com.nfeld.jsonpathkt.json.JsonType
5+
import com.nfeld.jsonpathkt.tokens.ArrayAccessorToken
6+
import com.nfeld.jsonpathkt.tokens.ObjectAccessorToken
47
import com.nfeld.jsonpathkt.tokens.Token
8+
import com.nfeld.jsonpathkt.tokens.WildcardToken
59
import kotlin.jvm.JvmInline
610

711
@JvmInline
@@ -18,9 +22,29 @@ public value class JsonPath private constructor(
1822
}
1923
}
2024

21-
public inline fun <reified T> JsonPath.resolveOrNull(node: JsonNode): T? =
25+
public inline fun <reified T> JsonPath.resolveOrNull(
26+
node: JsonNode,
27+
options: ResolutionOptions = ResolutionOptions.Default,
28+
): T? =
2229
tokens.fold(
2330
initial = node,
2431
) { valueAtPath: JsonNode?, nextToken: Token ->
2532
valueAtPath?.let(nextToken::read)
33+
}?.let {
34+
val isRoot = tokens.isEmpty()
35+
val containsWildcard = tokens.any { token -> token is WildcardToken }
36+
val lastToken = tokens.lastOrNull()
37+
val isAccessingAnObjectOrArray =
38+
lastToken is ObjectAccessorToken || lastToken is ArrayAccessorToken
39+
val isNodeAnArray = it.type == JsonType.Array
40+
41+
val wrappingRequired =
42+
options.wrapSingleValue &&
43+
!containsWildcard &&
44+
(isRoot || isAccessingAnObjectOrArray || !isNodeAnArray)
45+
46+
when {
47+
wrappingRequired -> it.copy(element = it.toJsonArray(listOf(it.element)))
48+
else -> it
49+
}
2650
}?.element as? T
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.nfeld.jsonpathkt
2+
3+
public data class ResolutionOptions(
4+
val wrapSingleValue: Boolean = false,
5+
) {
6+
public companion object {
7+
public val Default: ResolutionOptions = ResolutionOptions()
8+
}
9+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.nfeld.jsonpathkt.path
2+
3+
import com.nfeld.jsonpathkt.BOOKS_JSON
4+
import com.nfeld.jsonpathkt.LARGE_JSON
5+
import com.nfeld.jsonpathkt.ResolutionOptions
6+
import com.nfeld.jsonpathkt.SMALL_JSON
7+
import com.nfeld.jsonpathkt.SMALL_JSON_ARRAY
8+
import com.nfeld.jsonpathkt.asJson
9+
import com.nfeld.jsonpathkt.kotlinx.resolvePathOrNull
10+
import io.kotest.matchers.shouldBe
11+
import kotlinx.serialization.json.JsonElement
12+
import kotlin.test.Test
13+
14+
class WrapSingleValueTest {
15+
@Test
16+
fun parse_should_wrap_root_accessor() {
17+
SMALL_JSON.resolvePathWrappedOrNull("$") shouldBe "[${SMALL_JSON}]".asJson
18+
SMALL_JSON_ARRAY.resolvePathWrappedOrNull("$") shouldBe "[${SMALL_JSON_ARRAY}]".asJson
19+
}
20+
21+
@Test
22+
fun parse_should_wrap_array_accessor() {
23+
SMALL_JSON_ARRAY.resolvePathWrappedOrNull("$[0]") shouldBe "[1]".asJson
24+
SMALL_JSON_ARRAY.resolvePathWrappedOrNull("$[-1]") shouldBe "[${SMALL_JSON}]".asJson
25+
26+
"""
27+
|[
28+
| [
29+
| ["A"], ["B"], ["C"]
30+
| ],
31+
| [
32+
| ["D"], ["E"], ["F"]
33+
| ],
34+
| [
35+
| ["G"], ["H"], ["I"]
36+
| ]
37+
|]
38+
""".trimMargin()
39+
.resolvePathWrappedOrNull("$.[1].[2]") shouldBe """[ [ "F" ] ]""".asJson
40+
}
41+
42+
@Test
43+
fun parse_should_wrap_object_accessor() {
44+
SMALL_JSON.resolvePathWrappedOrNull("key") shouldBe "[5]".asJson
45+
SMALL_JSON_ARRAY.resolvePathWrappedOrNull("$[4].key") shouldBe "[5]".asJson
46+
LARGE_JSON.resolvePathWrappedOrNull("$[0].tags") shouldBe """[["occaecat","mollit","ullamco","labore","cillum","laboris","qui"]]""".asJson
47+
}
48+
49+
@Test
50+
fun parse_should_not_wrap_wildcard_root_accessor() {
51+
SMALL_JSON_ARRAY.resolvePathWrappedOrNull("$*") shouldBe SMALL_JSON_ARRAY.asJson
52+
SMALL_JSON_ARRAY.resolvePathWrappedOrNull("$.*") shouldBe SMALL_JSON_ARRAY.asJson
53+
}
54+
55+
@Test
56+
fun parse_should_not_wrap_wildcard_array_accessor() {
57+
"""
58+
|[
59+
| [
60+
| ["A"], ["B"], ["C"]
61+
| ],
62+
| [
63+
| ["D"], ["E"], ["F"]
64+
| ],
65+
| [
66+
| ["G"], ["H"], ["I"]
67+
| ]
68+
|]
69+
""".trimMargin()
70+
.resolvePathWrappedOrNull("$.[1].*.[0]") shouldBe """[ "D", "E", "F" ]""".asJson
71+
}
72+
73+
@Test
74+
fun parse_should_not_wrap_wildcard_object_accessor() {
75+
BOOKS_JSON.resolvePathWrappedOrNull("$.store.book.*.author") shouldBe """["Nigel Rees","Evelyn Waugh","Herman Melville","J. R. R. Tolkien"]""".asJson
76+
}
77+
78+
private val resolutionOptions = ResolutionOptions(wrapSingleValue = true)
79+
80+
private fun String.resolvePathWrappedOrNull(path: String) =
81+
asJson.resolvePathWrappedOrNull(path)
82+
83+
private fun JsonElement.resolvePathWrappedOrNull(path: String) =
84+
resolvePathOrNull(path, options = resolutionOptions)
85+
}

jsonpath-jsonjava/src/main/kotlin/com/nfeld/jsonpathkt/jsonjava/JsonJavaResolution.kt

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.nfeld.jsonpathkt.jsonjava
22

33
import com.nfeld.jsonpathkt.JsonPath
4+
import com.nfeld.jsonpathkt.ResolutionOptions
45
import com.nfeld.jsonpathkt.resolveOrNull
56
import org.json.JSONArray
67
import org.json.JSONObject
@@ -21,32 +22,56 @@ public value class JSONElement(@PublishedApi internal val element: Any) {
2122
public inline val asStringOrNull: JSONString? get() = element as? JSONString
2223
}
2324

24-
public fun JSONArray.resolveOrNull(path: JsonPath): JSONElement? = path.resolveOrNull(this)
25+
public fun JSONArray.resolveOrNull(
26+
path: JsonPath,
27+
options: ResolutionOptions = ResolutionOptions.Default,
28+
): JSONElement? = path.resolveOrNull(this, options)
29+
2530
public fun JSONArray.resolveAsStringOrNull(path: JsonPath): String? =
2631
path.resolveOrNull(this)?.asStringOrNull?.toJSONString()
2732

28-
public fun JSONArray.resolvePathOrNull(path: String): JSONElement? = JsonPath.compile(path).resolveOrNull(this)
33+
public fun JSONArray.resolvePathOrNull(
34+
path: String,
35+
options: ResolutionOptions = ResolutionOptions.Default,
36+
): JSONElement? = JsonPath.compile(path).resolveOrNull(this, options)
37+
2938
public fun JSONArray.resolvePathAsStringOrNull(path: String): String? =
3039
JsonPath.compile(path).resolveOrNull(this)?.asStringOrNull?.toJSONString()
3140

32-
public fun JSONObject.resolveOrNull(path: JsonPath): JSONElement? = path.resolveOrNull(this)
41+
public fun JSONObject.resolveOrNull(
42+
path: JsonPath,
43+
options: ResolutionOptions = ResolutionOptions.Default,
44+
): JSONElement? = path.resolveOrNull(this, options)
45+
3346
public fun JSONObject.resolveAsStringOrNull(path: JsonPath): String? =
3447
path.resolveOrNull(this)?.asStringOrNull?.toJSONString()
3548

36-
public fun JSONObject.resolvePathOrNull(path: String): JSONElement? = JsonPath.compile(path).resolveOrNull(this)
49+
public fun JSONObject.resolvePathOrNull(
50+
path: String,
51+
options: ResolutionOptions = ResolutionOptions.Default,
52+
): JSONElement? = JsonPath.compile(path).resolveOrNull(this, options)
53+
3754
public fun JSONObject.resolvePathAsStringOrNull(path: String): String? =
3855
JsonPath.compile(path).resolveOrNull(this)?.asStringOrNull?.toJSONString()
3956

40-
public fun JsonPath.resolveOrNull(json: JSONArray): JSONElement? = resolveOrNull<Any>(
57+
public fun JsonPath.resolveOrNull(
58+
json: JSONArray,
59+
options: ResolutionOptions = ResolutionOptions.Default,
60+
): JSONElement? = resolveOrNull<Any>(
4161
OrgJsonNode(json, isWildcardScope = false),
62+
options,
4263
)?.let(::JSONElement)
4364

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

48-
public fun JsonPath.resolveOrNull(json: JSONObject): JSONElement? = resolveOrNull<Any>(
69+
public fun JsonPath.resolveOrNull(
70+
json: JSONObject,
71+
options: ResolutionOptions = ResolutionOptions.Default,
72+
): JSONElement? = resolveOrNull<Any>(
4973
OrgJsonNode(json, isWildcardScope = false),
74+
options,
5075
)?.let(::JSONElement)
5176

5277
public fun JsonPath.resolveAsStringOrNull(json: JSONObject): String? = resolveOrNull<Any>(

jsonpath-kotlinx/src/commonMain/kotlin/com/nfeld/jsonpathkt/kotlinx/KotlinxResolution.kt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,39 @@
11
package com.nfeld.jsonpathkt.kotlinx
22

33
import com.nfeld.jsonpathkt.JsonPath
4+
import com.nfeld.jsonpathkt.ResolutionOptions
45
import com.nfeld.jsonpathkt.resolveOrNull
56
import kotlinx.serialization.json.JsonElement
67
import kotlinx.serialization.json.JsonNull
78
import kotlinx.serialization.json.JsonPrimitive
89
import kotlinx.serialization.json.contentOrNull
910

10-
public fun JsonElement.resolvePathOrNull(path: String): JsonElement? = JsonPath.compile(path).resolveOrNull(this)
11-
public fun JsonElement.resolveOrNull(path: JsonPath): JsonElement? = path.resolveOrNull(this)
11+
public fun JsonElement.resolvePathOrNull(
12+
path: String,
13+
options: ResolutionOptions = ResolutionOptions.Default,
14+
): JsonElement? =
15+
JsonPath.compile(path).resolveOrNull(this, options)
16+
17+
public fun JsonElement.resolveOrNull(
18+
path: JsonPath,
19+
options: ResolutionOptions = ResolutionOptions.Default,
20+
): JsonElement? = path.resolveOrNull(this, options)
1221

1322
public fun JsonElement.resolvePathAsStringOrNull(path: String): String? =
1423
JsonPath.compile(path).resolveAsStringOrNull(this)
1524

1625
public fun JsonElement.resolveAsStringOrNull(path: JsonPath): String? =
1726
path.resolveAsStringOrNull(this)
1827

19-
public fun JsonPath.resolveOrNull(json: JsonElement): JsonElement? {
28+
public fun JsonPath.resolveOrNull(
29+
json: JsonElement,
30+
options: ResolutionOptions = ResolutionOptions.Default,
31+
): JsonElement? {
2032
if (json is JsonNull) return null
2133

2234
return resolveOrNull<JsonElement>(
2335
KotlinxJsonNode(json, isWildcardScope = false),
36+
options,
2437
)
2538
}
2639

0 commit comments

Comments
 (0)