From f10ac97a84e92c92158931504c67f14055fd0d50 Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Fri, 4 Sep 2026 15:59:04 +0100 Subject: [PATCH] DOC-7032: Add tested client examples for JSONPath projection expressions Convert the two illustrative CLI blocks under "Projection expressions" into runnable clients-example demos, in place. Extends the json_path_ops TCE set from the filter-operators and functions PRs; verified against a live Redis 8.10 server across all 11 clients. Documents a real finding from testing: a multi-path JSON.GET reply's key order is not guaranteed (confirmed by repeated calls against an identical document returning both orderings), so every client's test asserts structurally rather than by exact string match. Co-Authored-By: Claude Sonnet 5 --- content/develop/data-types/json/path.md | 10 +- .../go-redis/json_path_ops_test.go | 126 ++++++++++++++++++ .../jedis/JsonPathOpsExample.java | 38 ++++++ .../lettuce-async/JsonPathOpsExample.java | 75 +++++++++++ .../lettuce-reactive/JsonPathOpsExample.java | 83 ++++++++++++ .../json_path_ops/node-redis/json-path-ops.js | 40 ++++++ .../nredisstack/JsonPathOpsExample.cs | 49 +++++++ .../json_path_ops/predis/JsonPathOpsTest.php | 47 +++++++ .../json_path_ops/redis-py/json_path_ops.py | 49 +++++++ .../json_path_ops/ruby/json_path_ops.rb | 42 ++++++ .../json_path_ops/rust-async/json_path_ops.rs | 115 ++++++++++++++++ .../json_path_ops/rust-sync/json_path_ops.rs | 126 ++++++++++++++++++ 12 files changed, 795 insertions(+), 5 deletions(-) diff --git a/content/develop/data-types/json/path.md b/content/develop/data-types/json/path.md index 78e04c9ffc..5b8f3d670e 100644 --- a/content/develop/data-types/json/path.md +++ b/content/develop/data-types/json/path.md @@ -554,7 +554,7 @@ The top level of a JSONPath query can be an expression that *computes* a value A projection that evaluates to *Nothing* produces an empty reply (`[]`). -``` +{{< clients-example set="json_path_ops" step="proj_basic" description="Projection expressions: Use arithmetic, a function call, or the length of a path as a top-level query that computes a value instead of only selecting nodes" difficulty="advanced" >}} > JSON.SET doc $ '{"a":2,"b":4,"arr":[1,2,3]}' OK > JSON.GET doc '$.a + 1' @@ -567,14 +567,14 @@ OK "[3]" > JSON.GET doc '$.a / 0' "[]" -``` +{{< /clients-example >}} -When [`JSON.GET`]({{< relref "commands/json.get/" >}}) is given more than one path, projections and plain paths can be mixed, and each path becomes a key in the returned object: +When [`JSON.GET`]({{< relref "commands/json.get/" >}}) is given more than one path, projections and plain paths can be mixed, and each path becomes a key in the returned object. The order of keys in that object is not guaranteed. -``` +{{< clients-example set="json_path_ops" step="proj_multipath" description="Multi-path queries: Pass more than one path to JSON.GET to have each path's result returned under its own key in a single reply" difficulty="advanced" >}} > JSON.GET doc '$.a + 1' '$.b' "{\"$.a + 1\":[3],\"$.b\":[4]}" -``` +{{< /clients-example >}} [`JSON.MGET`]({{< relref "commands/json.mget/" >}}) evaluates the projection independently for each key. A missing key, or a per-key evaluation error, yields a null reply for that key rather than failing the whole request. diff --git a/local_examples/json_path_ops/go-redis/json_path_ops_test.go b/local_examples/json_path_ops/go-redis/json_path_ops_test.go index dccdb409e1..52fb8a61b5 100644 --- a/local_examples/json_path_ops/go-redis/json_path_ops_test.go +++ b/local_examples/json_path_ops/go-redis/json_path_ops_test.go @@ -4,6 +4,7 @@ package example_commands_test import ( "context" + "encoding/json" "fmt" "github.com/redis/go-redis/v9" @@ -948,3 +949,128 @@ func ExampleClient_json_path_ops_func_append() { // OK // [{"t":"a","price":30},{"t":"X"}] } + +func ExampleClient_json_path_ops_proj_basic() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.FlushDB(ctx) + rdb.Del(ctx, "doc") + // REMOVE_END + + // STEP_START proj_basic + res1, err := rdb.JSONSet(ctx, "doc", "$", + `{"a":2,"b":4,"arr":[1,2,3]}`, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.JSONGet(ctx, "doc", `$.a + 1`).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> [3] + + res3, err := rdb.JSONGet(ctx, "doc", `$.a * $.b`).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> [8] + + res4, err := rdb.JSONGet(ctx, "doc", `($.a + $.b) / 2`).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> [3.0] + + res5, err := rdb.JSONGet(ctx, "doc", `$.arr.length()`).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> [3] + + res6, err := rdb.JSONGet(ctx, "doc", `$.a / 0`).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6) // >>> [] + // STEP_END + + // Output: + // OK + // [3] + // [8] + // [3.0] + // [3] + // [] +} + +func ExampleClient_json_path_ops_proj_multipath() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.FlushDB(ctx) + rdb.Del(ctx, "doc") + // REMOVE_END + + // STEP_START proj_multipath + res1, err := rdb.JSONSet(ctx, "doc", "$", + `{"a":2,"b":4,"arr":[1,2,3]}`, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + raw, err := rdb.JSONGet(ctx, "doc", `$.a + 1`, `$.b`).Result() + + if err != nil { + panic(err) + } + + // A multi-path JSON.GET reply is a JSON object whose key order is not + // guaranteed, so decode it into a map rather than printing the raw + // string. Go's fmt package prints map keys in sorted order, which keeps + // this example's output deterministic regardless of the order Redis + // returns the paths in. + res2 := map[string]interface{}{} + + if err := json.Unmarshal([]byte(raw), &res2); err != nil { + panic(err) + } + + fmt.Println(res2) // >>> map[$.a + 1:[3] $.b:[4]] + // STEP_END + + // Output: + // OK + // map[$.a + 1:[3] $.b:[4]] +} diff --git a/local_examples/json_path_ops/jedis/JsonPathOpsExample.java b/local_examples/json_path_ops/jedis/JsonPathOpsExample.java index 6f35bc61ee..d3b8b1a979 100644 --- a/local_examples/json_path_ops/jedis/JsonPathOpsExample.java +++ b/local_examples/json_path_ops/jedis/JsonPathOpsExample.java @@ -348,6 +348,44 @@ public void run() { jedis.del("doc"); // REMOVE_END + // STEP_START proj_basic + String res64 = jedis.jsonSet("doc", new Path2("$"), "{\"a\":2,\"b\":4,\"arr\":[1,2,3]}"); + System.out.println(res64); // >>> OK + + Object res65 = jedis.jsonGet("doc", new Path2("$.a + 1")); + System.out.println(res65); // >>> [3] + + Object res66 = jedis.jsonGet("doc", new Path2("$.a * $.b")); + System.out.println(res66); // >>> [8] + + Object res67 = jedis.jsonGet("doc", new Path("($.a + $.b) / 2")); + System.out.println(res67); // >>> [3.0] + + Object res68 = jedis.jsonGet("doc", new Path2("$.arr.length()")); + System.out.println(res68); // >>> [3] + + Object res69 = jedis.jsonGet("doc", new Path2("$.a / 0")); + System.out.println(res69); // >>> [] + // STEP_END + // REMOVE_START + assertEquals("[3]", res65.toString()); + assertEquals("[8]", res66.toString()); + assertEquals("[3.0]", res67.toString()); + assertEquals("[3]", res68.toString()); + assertEquals("[]", res69.toString()); + // REMOVE_END + + // STEP_START proj_multipath + String res70 = jedis.jsonSet("doc", new Path2("$"), "{\"a\":2,\"b\":4,\"arr\":[1,2,3]}"); + System.out.println(res70); // >>> OK + + Object res71 = jedis.jsonGet("doc", new Path2("$.a + 1"), new Path2("$.b")); + System.out.println(res71); // >>> {"$.a + 1":[3],"$.b":[4]} (a JSON object; key order is not guaranteed) + // STEP_END + // REMOVE_START + assertTrue(((JSONObject) res71).similar(new JSONObject("{\"$.a + 1\":[3],\"$.b\":[4]}"))); + jedis.del("doc"); + // REMOVE_END // HIDE_START jedis.close(); diff --git a/local_examples/json_path_ops/lettuce-async/JsonPathOpsExample.java b/local_examples/json_path_ops/lettuce-async/JsonPathOpsExample.java index a08a956303..643285e2d7 100644 --- a/local_examples/json_path_ops/lettuce-async/JsonPathOpsExample.java +++ b/local_examples/json_path_ops/lettuce-async/JsonPathOpsExample.java @@ -603,6 +603,81 @@ public void run() { asyncCommands.del("doc").toCompletableFuture().join(); // REMOVE_END + // STEP_START proj_basic + CompletableFuture projBasicExample = asyncCommands + .jsonSet("doc", JsonPath.ROOT_PATH, + parser.createJsonValue("{\"a\":2,\"b\":4,\"arr\":[1,2,3]}")) + .thenCompose(res1 -> { + System.out.println(res1); // >>> OK + // REMOVE_START + assertThat(res1).isEqualTo("OK"); + // REMOVE_END + return asyncCommands.jsonGet("doc", JsonPath.of("$.a + 1")); + }).thenCompose(res2 -> { + System.out.println(res2); // >>> [[3]] + // REMOVE_START + assertThat(res2.toString()).isEqualTo("[[3]]"); + // REMOVE_END + return asyncCommands.jsonGet("doc", JsonPath.of("$.a * $.b")); + }).thenCompose(res3 -> { + System.out.println(res3); // >>> [[8]] + // REMOVE_START + assertThat(res3.toString()).isEqualTo("[[8]]"); + // REMOVE_END + return asyncCommands.jsonGet("doc", JsonPath.of("($.a + $.b) / 2")); + }).thenCompose(res4 -> { + System.out.println(res4); // >>> [[3.0]] + // REMOVE_START + assertThat(res4.toString()).isEqualTo("[[3.0]]"); + // REMOVE_END + return asyncCommands.jsonGet("doc", JsonPath.of("$.arr.length()")); + }).thenCompose(res5 -> { + System.out.println(res5); // >>> [[3]] + // REMOVE_START + assertThat(res5.toString()).isEqualTo("[[3]]"); + // REMOVE_END + return asyncCommands.jsonGet("doc", JsonPath.of("$.a / 0")); + }).thenAccept(res6 -> { + System.out.println(res6); // >>> [[]] + // REMOVE_START + assertThat(res6.toString()).isEqualTo("[[]]"); + // REMOVE_END + }).toCompletableFuture(); + // STEP_END + + projBasicExample.join(); + // REMOVE_START + asyncCommands.del("doc").toCompletableFuture().join(); + // REMOVE_END + + // STEP_START proj_multipath + CompletableFuture projMultipathExample = asyncCommands + .jsonSet("doc", JsonPath.ROOT_PATH, + parser.createJsonValue("{\"a\":2,\"b\":4,\"arr\":[1,2,3]}")) + .thenCompose(res7 -> { + System.out.println(res7); // >>> OK + // REMOVE_START + assertThat(res7).isEqualTo("OK"); + // REMOVE_END + return asyncCommands.jsonGet("doc", JsonPath.of("$.a + 1"), JsonPath.of("$.b")); + }).thenAccept(res8 -> { + // The multi-path JSON.GET reply is a single JSON object keyed by path, + // and Redis does not guarantee key order, so look up each path rather + // than comparing the object's rendered string. + JsonObject paths = res8.get(0).asJsonObject(); + System.out.println(paths); // >>> {"$.a + 1":[3],"$.b":[4]} + // REMOVE_START + assertThat(paths.size()).isEqualTo(2); + assertThat(paths.get("$.a + 1").toString()).isEqualTo("[3]"); + assertThat(paths.get("$.b").toString()).isEqualTo("[4]"); + // REMOVE_END + }).toCompletableFuture(); + // STEP_END + + projMultipathExample.join(); + // REMOVE_START + asyncCommands.del("doc").toCompletableFuture().join(); + // REMOVE_END } finally { redisClient.shutdown(); } diff --git a/local_examples/json_path_ops/lettuce-reactive/JsonPathOpsExample.java b/local_examples/json_path_ops/lettuce-reactive/JsonPathOpsExample.java index bf017f949b..f1ba907933 100644 --- a/local_examples/json_path_ops/lettuce-reactive/JsonPathOpsExample.java +++ b/local_examples/json_path_ops/lettuce-reactive/JsonPathOpsExample.java @@ -660,6 +660,89 @@ public void run() { reactiveCommands.del("doc").block(); // REMOVE_END + // STEP_START proj_basic + Mono projBasicExample = reactiveCommands + .jsonSet("doc", JsonPath.ROOT_PATH, parser.createJsonValue("{\"a\":2,\"b\":4,\"arr\":[1,2,3]}")) + .doOnNext(res1 -> { + System.out.println(res1); // >>> OK + // REMOVE_START + assertThat(res1).isEqualTo("OK"); + // REMOVE_END + }) + .flatMap(res1 -> reactiveCommands.jsonGet("doc", JsonPath.of("$.a + 1")).collectList()) + .doOnNext(res2 -> { + System.out.println(res2); // >>> [[3]] + // REMOVE_START + assertThat(res2.toString()).isEqualTo("[[3]]"); + // REMOVE_END + }) + .flatMap(res2 -> reactiveCommands.jsonGet("doc", JsonPath.of("$.a * $.b")).collectList()) + .doOnNext(res3 -> { + System.out.println(res3); // >>> [[8]] + // REMOVE_START + assertThat(res3.toString()).isEqualTo("[[8]]"); + // REMOVE_END + }) + .flatMap(res3 -> reactiveCommands.jsonGet("doc", JsonPath.of("($.a + $.b) / 2")).collectList()) + .doOnNext(res4 -> { + System.out.println(res4); // >>> [[3.0]] + // REMOVE_START + assertThat(res4.toString()).isEqualTo("[[3.0]]"); + // REMOVE_END + }) + .flatMap(res4 -> reactiveCommands.jsonGet("doc", JsonPath.of("$.arr.length()")).collectList()) + .doOnNext(res5 -> { + System.out.println(res5); // >>> [[3]] + // REMOVE_START + assertThat(res5.toString()).isEqualTo("[[3]]"); + // REMOVE_END + }) + .flatMap(res5 -> reactiveCommands.jsonGet("doc", JsonPath.of("$.a / 0")).collectList()) + .doOnNext(res6 -> { + System.out.println(res6); // >>> [[]] + // REMOVE_START + assertThat(res6.toString()).isEqualTo("[[]]"); + // REMOVE_END + }) + .then(); + // STEP_END + + projBasicExample.block(); + // REMOVE_START + reactiveCommands.del("doc").block(); + // REMOVE_END + + // STEP_START proj_multipath + Mono projMultipathExample = reactiveCommands + .jsonSet("doc", JsonPath.ROOT_PATH, parser.createJsonValue("{\"a\":2,\"b\":4,\"arr\":[1,2,3]}")) + .doOnNext(res7 -> { + System.out.println(res7); // >>> OK + // REMOVE_START + assertThat(res7).isEqualTo("OK"); + // REMOVE_END + }) + .flatMap(res7 -> reactiveCommands.jsonGet("doc", JsonPath.of("$.a + 1"), JsonPath.of("$.b")) + .collectList()) + .doOnNext(res8 -> { + System.out.println(res8); // >>> [{"$.a + 1":[3],"$.b":[4]}] + // REMOVE_START + // JSON.GET's multi-path reply key order is not guaranteed by the + // protocol, so compare the parsed object's entries rather than + // the serialized string. + assertThat(res8).hasSize(1); + JsonObject multiPathResult = res8.get(0).asJsonObject(); + assertThat(multiPathResult.size()).isEqualTo(2); + assertThat(multiPathResult.get("$.a + 1").toString()).isEqualTo("[3]"); + assertThat(multiPathResult.get("$.b").toString()).isEqualTo("[4]"); + // REMOVE_END + }) + .then(); + // STEP_END + + projMultipathExample.block(); + // REMOVE_START + reactiveCommands.del("doc").block(); + // REMOVE_END } finally { redisClient.shutdown(); } diff --git a/local_examples/json_path_ops/node-redis/json-path-ops.js b/local_examples/json_path_ops/node-redis/json-path-ops.js index a98795481a..6261078dca 100644 --- a/local_examples/json_path_ops/node-redis/json-path-ops.js +++ b/local_examples/json_path_ops/node-redis/json-path-ops.js @@ -349,6 +349,46 @@ assert.deepEqual(res63, [{ t: 'a', price: 30 }, { t: 'X' }]); await client.del('doc'); // REMOVE_END +// STEP_START proj_basic +const res64 = await client.json.set('doc', '$', { a: 2, b: 4, arr: [1, 2, 3] }); +console.log(res64); // >>> OK + +const res65 = await client.json.get('doc', { path: '$.a + 1' }); +console.log(res65); // >>> [ 3 ] + +const res66 = await client.json.get('doc', { path: '$.a * $.b' }); +console.log(res66); // >>> [ 8 ] + +const res67 = await client.json.get('doc', { path: '($.a + $.b) / 2' }); +console.log(res67); // >>> [ 3 ] + +const res68 = await client.json.get('doc', { path: '$.arr.length()' }); +console.log(res68); // >>> [ 3 ] + +const res69 = await client.json.get('doc', { path: '$.a / 0' }); +console.log(res69); // >>> [] +// STEP_END + +// REMOVE_START +assert.deepEqual(res65, [3]); +assert.deepEqual(res66, [8]); +assert.deepEqual(res67, [3]); +assert.deepEqual(res68, [3]); +assert.deepEqual(res69, []); +// REMOVE_END + +// STEP_START proj_multipath +const res70 = await client.json.set('doc', '$', { a: 2, b: 4, arr: [1, 2, 3] }); +console.log(res70); // >>> OK + +const res71 = await client.json.get('doc', { path: ['$.a + 1', '$.b'] }); +console.log(res71); // >>> { '$.a + 1': [ 3 ], '$.b': [ 4 ] } +// STEP_END + +// REMOVE_START +assert.deepEqual(res71, { '$.a + 1': [3], '$.b': [4] }); +await client.del('doc'); +// REMOVE_END // HIDE_START await client.close(); diff --git a/local_examples/json_path_ops/nredisstack/JsonPathOpsExample.cs b/local_examples/json_path_ops/nredisstack/JsonPathOpsExample.cs index 1d5f404cae..894369026a 100644 --- a/local_examples/json_path_ops/nredisstack/JsonPathOpsExample.cs +++ b/local_examples/json_path_ops/nredisstack/JsonPathOpsExample.cs @@ -403,6 +403,55 @@ public void Run() db.KeyDelete("doc"); // REMOVE_END + // STEP_START proj_basic + bool res64 = db.JSON().Set("doc", "$", "{\"a\":2,\"b\":4,\"arr\":[1,2,3]}"); + Console.WriteLine(res64); // >>> True + + RedisResult res65 = db.JSON().Get("doc", path: "$.a + 1"); + Console.WriteLine(res65); // >>> [3] + + RedisResult res66 = db.JSON().Get("doc", path: "$.a * $.b"); + Console.WriteLine(res66); // >>> [8] + + RedisResult res67 = db.JSON().Get("doc", path: "($.a + $.b) / 2"); + Console.WriteLine(res67); // >>> [3.0] + + RedisResult res68 = db.JSON().Get("doc", path: "$.arr.length()"); + Console.WriteLine(res68); // >>> [3] + + RedisResult res69 = db.JSON().Get("doc", path: "$.a / 0"); + Console.WriteLine(res69); // >>> [] + // STEP_END + + // REMOVE_START + Assert.True(res64); + Assert.Equal("[3]", (string?)res65); + Assert.Equal("[8]", (string?)res66); + Assert.Equal("[3.0]", (string?)res67); + Assert.Equal("[3]", (string?)res68); + Assert.Equal("[]", (string?)res69); + db.KeyDelete("doc"); + // REMOVE_END + + // STEP_START proj_multipath + bool res70 = db.JSON().Set("doc", "$", "{\"a\":2,\"b\":4,\"arr\":[1,2,3]}"); + Console.WriteLine(res70); // >>> True + + RedisResult res71 = db.JSON().Get("doc", paths: new[] { "$.a + 1", "$.b" }); + Console.WriteLine(res71); // >>> {"$.a + 1":[3],"$.b":[4]} + // STEP_END + + // REMOVE_START + Assert.True(res70); + // The key order of the multi-path JSON.GET reply is not guaranteed, + // so parse it into a dictionary and compare structurally rather than + // comparing the raw JSON text. + var res71Map = JsonSerializer.Deserialize>((string)res71!); + Assert.Equal(2, res71Map!.Count); + Assert.Equal(new[] { 3 }, res71Map["$.a + 1"]); + Assert.Equal(new[] { 4 }, res71Map["$.b"]); + db.KeyDelete("doc"); + // REMOVE_END // HIDE_START } diff --git a/local_examples/json_path_ops/predis/JsonPathOpsTest.php b/local_examples/json_path_ops/predis/JsonPathOpsTest.php index 1d17071aa4..5bbb994da9 100644 --- a/local_examples/json_path_ops/predis/JsonPathOpsTest.php +++ b/local_examples/json_path_ops/predis/JsonPathOpsTest.php @@ -428,6 +428,53 @@ public function testJsonPathOps(): void $this->redis->del('doc'); // REMOVE_END + // STEP_START proj_basic + $res64 = $this->redis->jsonset('doc', '$', json_encode( + ["a" => 2, "b" => 4, "arr" => [1, 2, 3]] + )); + echo $res64 . PHP_EOL; // >>> OK + + $res65 = $this->redis->jsonget('doc', '', '', '', '$.a + 1'); + echo $res65 . PHP_EOL; // >>> [3] + + $res66 = $this->redis->jsonget('doc', '', '', '', '$.a * $.b'); + echo $res66 . PHP_EOL; // >>> [8] + + $res67 = $this->redis->jsonget('doc', '', '', '', '($.a + $.b) / 2'); + echo $res67 . PHP_EOL; // >>> [3.0] + + $res68 = $this->redis->jsonget('doc', '', '', '', '$.arr.length()'); + echo $res68 . PHP_EOL; // >>> [3] + + $res69 = $this->redis->jsonget('doc', '', '', '', '$.a / 0'); + echo $res69 . PHP_EOL; // >>> [] + // STEP_END + + // REMOVE_START + $this->assertEquals('OK', $res64); + $this->assertEquals('[3]', $res65); + $this->assertEquals('[8]', $res66); + $this->assertEquals('[3.0]', $res67); + $this->assertEquals('[3]', $res68); + $this->assertEquals('[]', $res69); + // REMOVE_END + + // STEP_START proj_multipath + $res70 = $this->redis->jsonset('doc', '$', json_encode( + ["a" => 2, "b" => 4, "arr" => [1, 2, 3]] + )); + echo $res70 . PHP_EOL; // >>> OK + + $res71 = $this->redis->jsonget('doc', '', '', '', '$.a + 1', '$.b'); + // The reply is a JSON object keyed by path; key order is not guaranteed. + echo $res71 . PHP_EOL; // >>> {"$.a + 1":[3],"$.b":[4]} (key order not guaranteed) + // STEP_END + + // REMOVE_START + $decoded71 = json_decode($res71, true); + $this->assertEquals(['$.a + 1' => [3], '$.b' => [4]], $decoded71); + $this->redis->del('doc'); + // REMOVE_END } protected function tearDown(): void diff --git a/local_examples/json_path_ops/redis-py/json_path_ops.py b/local_examples/json_path_ops/redis-py/json_path_ops.py index d007ce31da..30f8f09567 100644 --- a/local_examples/json_path_ops/redis-py/json_path_ops.py +++ b/local_examples/json_path_ops/redis-py/json_path_ops.py @@ -405,3 +405,52 @@ assert res4 == [{"t": "a", "price": 30}, {"t": "X"}] r.delete("doc") # REMOVE_END + +# STEP_START proj_basic +res1 = r.json().set("doc", "$", {"a": 2, "b": 4, "arr": [1, 2, 3]}) +print(res1) +# >>> True + +res2 = r.json().get("doc", "$.a + 1") +print(res2) +# >>> [3] + +res3 = r.json().get("doc", "$.a * $.b") +print(res3) +# >>> [8] + +res4 = r.json().get("doc", "($.a + $.b) / 2") +print(res4) +# >>> [3.0] + +res5 = r.json().get("doc", "$.arr.length()") +print(res5) +# >>> [3] + +res6 = r.json().get("doc", "$.a / 0") +print(res6) +# >>> [] +# STEP_END + +# REMOVE_START +assert res2 == [3] +assert res3 == [8] +assert res4 == [3.0] +assert res5 == [3] +assert res6 == [] +# REMOVE_END + +# STEP_START proj_multipath +res7 = r.json().set("doc", "$", {"a": 2, "b": 4, "arr": [1, 2, 3]}) +print(res7) +# >>> True + +res8 = r.json().get("doc", "$.a + 1", "$.b") +print(res8) +# >>> {'$.a + 1': [3], '$.b': [4]} +# STEP_END + +# REMOVE_START +assert res8 == {"$.a + 1": [3], "$.b": [4]} +r.delete("doc") +# REMOVE_END diff --git a/local_examples/json_path_ops/ruby/json_path_ops.rb b/local_examples/json_path_ops/ruby/json_path_ops.rb index 7b73d4b205..5788281911 100644 --- a/local_examples/json_path_ops/ruby/json_path_ops.rb +++ b/local_examples/json_path_ops/ruby/json_path_ops.rb @@ -344,5 +344,47 @@ def assert_equal(expected, actual) assert_equal([1, 2, 3, 9], res2) assert_equal([{ 't' => 'a', 'price' => 30 }, { 't' => 'X' }], res4) r.del('doc') +# REMOVE_END + +# STEP_START proj_basic +res1 = r.json_set('doc', '$', { 'a' => 2, 'b' => 4, 'arr' => [1, 2, 3] }) +puts res1 # >>> OK + +res2 = r.json_get('doc', '$.a + 1') +p res2 # >>> [3] + +res3 = r.json_get('doc', '$.a * $.b') +p res3 # >>> [8] + +res4 = r.json_get('doc', '($.a + $.b) / 2') +p res4 # >>> [3.0] + +res5 = r.json_get('doc', '$.arr.length()') +p res5 # >>> [3] + +res6 = r.json_get('doc', '$.a / 0') +p res6 # >>> [] +# STEP_END + +# REMOVE_START +assert_equal([3], res2) +assert_equal([8], res3) +assert_equal([3.0], res4) +assert_equal([3], res5) +assert_equal([], res6) +# REMOVE_END + +# STEP_START proj_multipath +res7 = r.json_set('doc', '$', { 'a' => 2, 'b' => 4, 'arr' => [1, 2, 3] }) +puts res7 # >>> OK + +# The reply's key order is not guaranteed, so don't rely on it. +res8 = r.json_get('doc', '$.a + 1', '$.b') +p res8 +# STEP_END + +# REMOVE_START +assert_equal({ '$.a + 1' => [3], '$.b' => [4] }, res8) +r.del('doc') r.close # REMOVE_END diff --git a/local_examples/json_path_ops/rust-async/json_path_ops.rs b/local_examples/json_path_ops/rust-async/json_path_ops.rs index 0a9ae19aa3..dc88d2dfbe 100644 --- a/local_examples/json_path_ops/rust-async/json_path_ops.rs +++ b/local_examples/json_path_ops/rust-async/json_path_ops.rs @@ -893,5 +893,120 @@ mod json_path_ops_tests { // REMOVE_END // STEP_END + // STEP_START proj_basic + let _: bool = match r.json_set("doc", "$", &json!({"a":2,"b":4,"arr":[1,2,3]})).await { + Ok(v) => v, + Err(e) => { + println!("Error setting doc: {e}"); + return; + } + }; + + match r.json_get("doc", "$.a + 1").await { + Ok(res1) => { + let res1: String = res1; + println!("{res1}"); // >>> [3] + // REMOVE_START + assert_eq!(res1, "[3]"); + // REMOVE_END + }, + Err(e) => { + println!("Error getting doc: {e}"); + return; + } + } + + match r.json_get("doc", "$.a * $.b").await { + Ok(res2) => { + let res2: String = res2; + println!("{res2}"); // >>> [8] + // REMOVE_START + assert_eq!(res2, "[8]"); + // REMOVE_END + }, + Err(e) => { + println!("Error getting doc: {e}"); + return; + } + } + + match r.json_get("doc", "($.a + $.b) / 2").await { + Ok(res3) => { + let res3: String = res3; + println!("{res3}"); // >>> [3.0] + // REMOVE_START + assert_eq!(res3, "[3.0]"); + // REMOVE_END + }, + Err(e) => { + println!("Error getting doc: {e}"); + return; + } + } + + match r.json_get("doc", "$.arr.length()").await { + Ok(res4) => { + let res4: String = res4; + println!("{res4}"); // >>> [3] + // REMOVE_START + assert_eq!(res4, "[3]"); + // REMOVE_END + }, + Err(e) => { + println!("Error getting doc: {e}"); + return; + } + } + + match r.json_get("doc", "$.a / 0").await { + Ok(res5) => { + let res5: String = res5; + println!("{res5}"); // >>> [] + // REMOVE_START + assert_eq!(res5, "[]"); + // REMOVE_END + }, + Err(e) => { + println!("Error getting doc: {e}"); + return; + } + } + + // REMOVE_START + let _: Result = r.del("doc").await; + // REMOVE_END + // STEP_END + + // STEP_START proj_multipath + let _: bool = match r.json_set("doc", "$", &json!({"a":2,"b":4,"arr":[1,2,3]})).await { + Ok(v) => v, + Err(e) => { + println!("Error setting doc: {e}"); + return; + } + }; + + match r.json_get("doc", &["$.a + 1", "$.b"]).await { + Ok(res6) => { + let res6: String = res6; + // The reply's key order is not guaranteed, so parse it into a + // Value and compare that structurally rather than relying on + // the raw string's key order. + println!("{res6}"); + // REMOVE_START + let parsed: serde_json::Value = serde_json::from_str(&res6).unwrap(); + assert_eq!(parsed, json!({"$.a + 1": [3], "$.b": [4]})); + // REMOVE_END + }, + Err(e) => { + println!("Error getting doc: {e}"); + return; + } + } + + // REMOVE_START + let _: Result = r.del("doc").await; + // REMOVE_END + // STEP_END } } diff --git a/local_examples/json_path_ops/rust-sync/json_path_ops.rs b/local_examples/json_path_ops/rust-sync/json_path_ops.rs index d1bded0e14..597a92a90b 100644 --- a/local_examples/json_path_ops/rust-sync/json_path_ops.rs +++ b/local_examples/json_path_ops/rust-sync/json_path_ops.rs @@ -1017,5 +1017,131 @@ mod json_path_ops_tests { // REMOVE_END // STEP_END + // STEP_START proj_basic + match r.json_set("doc", "$", &json!({"a":2,"b":4,"arr":[1,2,3]})) { + Ok(res64) => { + let res64: bool = res64; + println!("{}", if res64 { "OK" } else { "(nil)" }); // >>> OK + // REMOVE_START + assert!(res64); + // REMOVE_END + }, + Err(e) => { + println!("Error setting doc: {e}"); + return; + } + } + + match r.json_get("doc", "$.a + 1") { + Ok(res65) => { + let res65: String = res65; + println!("{res65}"); // >>> [3] + // REMOVE_START + assert_eq!(res65, "[3]"); + // REMOVE_END + }, + Err(e) => { + println!("Error getting doc: {e}"); + return; + } + } + + match r.json_get("doc", "$.a * $.b") { + Ok(res66) => { + let res66: String = res66; + println!("{res66}"); // >>> [8] + // REMOVE_START + assert_eq!(res66, "[8]"); + // REMOVE_END + }, + Err(e) => { + println!("Error getting doc: {e}"); + return; + } + } + + match r.json_get("doc", "($.a + $.b) / 2") { + Ok(res67) => { + let res67: String = res67; + println!("{res67}"); // >>> [3.0] + // REMOVE_START + assert_eq!(res67, "[3.0]"); + // REMOVE_END + }, + Err(e) => { + println!("Error getting doc: {e}"); + return; + } + } + + match r.json_get("doc", "$.arr.length()") { + Ok(res68) => { + let res68: String = res68; + println!("{res68}"); // >>> [3] + // REMOVE_START + assert_eq!(res68, "[3]"); + // REMOVE_END + }, + Err(e) => { + println!("Error getting doc: {e}"); + return; + } + } + + match r.json_get("doc", "$.a / 0") { + Ok(res69) => { + let res69: String = res69; + println!("{res69}"); // >>> [] + // REMOVE_START + assert_eq!(res69, "[]"); + // REMOVE_END + }, + Err(e) => { + println!("Error getting doc: {e}"); + return; + } + } + // STEP_END + + // STEP_START proj_multipath + match r.json_set("doc", "$", &json!({"a":2,"b":4,"arr":[1,2,3]})) { + Ok(res70) => { + let res70: bool = res70; + println!("{}", if res70 { "OK" } else { "(nil)" }); // >>> OK + // REMOVE_START + assert!(res70); + // REMOVE_END + }, + Err(e) => { + println!("Error setting doc: {e}"); + return; + } + } + + // A multi-path JSON.GET replies with an object keyed by path, but Redis does not + // guarantee the key order in that reply, so this parses the JSON and compares it + // structurally (map equality, order-independent) instead of comparing raw strings. + match r.json_get::<_, _, String>("doc", vec!["$.a + 1", "$.b"]) { + Ok(res71) => { + let parsed: serde_json::Value = serde_json::from_str(&res71).unwrap(); + // Render with sorted keys purely for a deterministic, printable example — + // the actual reply's key order can differ from run to run. + let sorted: std::collections::BTreeMap = + serde_json::from_value(parsed.clone()).unwrap(); + println!("{}", serde_json::to_string(&sorted).unwrap()); // >>> {"$.a + 1":[3],"$.b":[4]} + // REMOVE_START + assert_eq!(parsed, json!({"$.a + 1": [3], "$.b": [4]})); + // REMOVE_END + }, + Err(e) => { + println!("Error getting doc: {e}"); + return; + } + } + + // REMOVE_START + let _: Result = r.del("doc"); + // REMOVE_END + // STEP_END } }