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 } }