diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d5bbae376..c75e9db4a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ * Node: Added `JSON.DEL` and `JSON.FORGET` ([#2505](https://github.com/valkey-io/valkey-glide/pull/2505)) * Java: Added `JSON.TOGGLE` ([#2504](https://github.com/valkey-io/valkey-glide/pull/2504)) * Node: Added `JSON.TYPE` ([#2510](https://github.com/valkey-io/valkey-glide/pull/2510)) +* Java: Added `JSON.RESP` ([#2513](https://github.com/valkey-io/valkey-glide/pull/2513)) #### Breaking Changes diff --git a/java/client/src/main/java/glide/api/commands/servermodules/Json.java b/java/client/src/main/java/glide/api/commands/servermodules/Json.java index 80b5eaf028..bcf0daf66b 100644 --- a/java/client/src/main/java/glide/api/commands/servermodules/Json.java +++ b/java/client/src/main/java/glide/api/commands/servermodules/Json.java @@ -30,6 +30,8 @@ public class Json { private static final String JSON_DEL = JSON_PREFIX + "DEL"; private static final String JSON_FORGET = JSON_PREFIX + "FORGET"; private static final String JSON_TOGGLE = JSON_PREFIX + "TOGGLE"; + private static final String JSON_RESP = JSON_PREFIX + "RESP"; + private static final String JSON_TYPE = JSON_PREFIX + "TYPE"; private Json() {} @@ -189,11 +191,12 @@ public static CompletableFuture get( * *
  • If multiple paths are given: Returns a stringified JSON, in which each path is a key, * and it's corresponding value, is the value as if the path was executed in the command @@ -226,11 +229,12 @@ public static CompletableFuture get( *
      *
    • For JSONPath (path starts with $): Returns a stringified JSON list * replies for every possible path, or a string representation of an empty array, - * if path doesn't exist. If key doesn't exist, returns None. + * if path doesn't exist. If key doesn't exist, returns null + * . *
    • For legacy path (path doesn't start with $): Returns a string * representation of the value in paths. If paths * doesn't exist, an error is raised. If key doesn't exist, returns - * None. + * null. *
    *
  • If multiple paths are given: Returns a stringified JSON, in which each path is a key, * and it's corresponding value, is the value as if the path was executed in the command @@ -317,11 +321,12 @@ public static CompletableFuture get( *
      *
    • For JSONPath (path starts with $): Returns a stringified JSON list * replies for every possible path, or a string representation of an empty array, - * if path doesn't exist. If key doesn't exist, returns None. + * if path doesn't exist. If key doesn't exist, returns null + * . *
    • For legacy path (path doesn't start with $): Returns a string * representation of the value in paths. If paths * doesn't exist, an error is raised. If key doesn't exist, returns - * None. + * null. *
    *
  • If multiple paths are given: Returns a stringified JSON, in which each path is a key, * and it's corresponding value, is the value as if the path was executed in the command @@ -363,11 +368,12 @@ public static CompletableFuture get( *
      *
    • For JSONPath (path starts with $): Returns a stringified JSON list * replies for every possible path, or a string representation of an empty array, - * if path doesn't exist. If key doesn't exist, returns None. + * if path doesn't exist. If key doesn't exist, returns null + * . *
    • For legacy path (path doesn't start with $): Returns a string * representation of the value in paths. If paths * doesn't exist, an error is raised. If key doesn't exist, returns - * None. + * null. *
    *
  • If multiple paths are given: Returns a stringified JSON, in which each path is a key, * and it's corresponding value, is the value as if the path was executed in the command @@ -1209,6 +1215,137 @@ public static CompletableFuture toggle( client, new ArgsBuilder().add(gs(JSON_TOGGLE)).add(key).add(path).toArray()); } + /** + * Retrieves the JSON document stored at key. The returning result is in the Valkey or Redis OSS Serialization Protocol (RESP). + * + * @param client The Valkey GLIDE client to execute the command. + * @param key The key of the JSON document. + * @return Returns the JSON document in its RESP form. + * If key doesn't exist, null is returned. + * @example + *
    {@code
    +     * Json.set(client, "doc", ".", "{\"a\": [1, 2, 3], \"b\": {\"b1\": 1}, \"c\": 42}");
    +     * Object actualResult = Json.resp(client, "doc").get();
    +     * Object[] expectedResult = new Object[] {
    +     *     "{",
    +     *     "a",
    +     *     new Object[] {"[", 1L, 2L, 3L},
    +     *     "b",
    +     *     new Object[] {"{", "b1", 1L},
    +     *     "c",
    +     *     42L
    +     * };
    +     * assertInstanceOf(Object[].class, actualResult);
    +     * assertArrayEquals(expectedResult, (Object[]) actualResult);
    +     * }
    + */ + public static CompletableFuture resp(@NonNull BaseClient client, @NonNull String key) { + return executeCommand(client, new String[] {JSON_RESP, key}); + } + + /** + * Retrieves the JSON document stored at key. The returning result is in the Valkey or Redis OSS Serialization Protocol (RESP). + * + * @param client The Valkey GLIDE client to execute the command. + * @param key The key of the JSON document. + * @return Returns the JSON document in its RESP form. + * If key doesn't exist, null is returned. + * @example + *
    {@code
    +     * Json.set(client, "doc", ".", "{\"a\": [1, 2, 3], \"b\": {\"b1\": 1}, \"c\": 42}");
    +     * Object actualResultBinary = Json.resp(client, gs("doc")).get();
    +     * Object[] expectedResultBinary = new Object[] {
    +     *     gs("{"),
    +     *     gs("a"),
    +     *     new Object[] {gs("["), 1L, 2L, 3L},
    +     *     gs("b"),
    +     *     new Object[] {gs("{"), gs("b1"), 1L},
    +     *     gs("c"),
    +     *     42L
    +     * };
    +     * assertInstanceOf(Object[].class, actualResultBinary);
    +     * assertArrayEquals(expectedResultBinary, (Object[]) actualResultBinary);
    +     * }
    + */ + public static CompletableFuture resp( + @NonNull BaseClient client, @NonNull GlideString key) { + return executeCommand(client, new GlideString[] {gs(JSON_RESP), key}); + } + + /** + * Retrieve the JSON value at the specified path within the JSON document stored at + * key. The returning result is in the Valkey or Redis OSS Serialization Protocol + * (RESP). + * + * @param client The Valkey GLIDE client to execute the command. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return + *
      + *
    • For JSONPath (path starts with $): Returns a list of + * replies for every possible path, indicating the RESP form of the JSON value. If + * path doesn't exist, returns an empty list. + *
    • For legacy path (path doesn't starts with $): Returns a + * single reply for the JSON value at the specified path, in its RESP form. If multiple + * paths match, the value of the first JSON value match is returned. If path + * doesn't exist, an error is raised. + *
    + * If key doesn't exist, null is returned. + * @example + *
    {@code
    +     * Json.set(client, "doc", ".", "{\"a\": [1, 2, 3], \"b\": {\"a\": [1, 2], \"c\": {\"a\": 42}}}");
    +     * Object actualResult = Json.resp(client, "doc", "$..a").get(); // JSONPath returns all possible paths
    +     * Object[] expectedResult = new Object[] {
    +     *                 new Object[] {"[", 1L, 2L, 3L},
    +     *                 new Object[] {"[", 1L, 2L},
    +     *                 42L}
    +     * assertArrayEquals(expectedResult, (Object[]) actualResult);
    +     * // legacy path only returns the first JSON value match
    +     * assertArrayEquals(new Object[] {"[", 1L, 2L, 3L}, (Object[]) Json.resp(client, key, "..a").get());
    +     * }
    + */ + public static CompletableFuture resp( + @NonNull BaseClient client, @NonNull String key, @NonNull String path) { + return executeCommand(client, new String[] {JSON_RESP, key, path}); + } + + /** + * Retrieve the JSON value at the specified path within the JSON document stored at + * key. The returning result is in the Valkey or Redis OSS Serialization Protocol + * (RESP). + * + * @param client The Valkey GLIDE client to execute the command. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return + *
      + *
    • For JSONPath (path starts with $): Returns a list of + * replies for every possible path, indicating the RESP form of the JSON value. If + * path doesn't exist, returns an empty list. + *
    • For legacy path (path doesn't starts with $): Returns a + * single reply for the JSON value at the specified path, in its RESP form. If multiple + * paths match, the value of the first JSON value match is returned. If path + * doesn't exist, an error is raised. + *
    + * If key doesn't exist, null is returned. + * @example + *
    {@code
    +     * Json.set(client, "doc", ".", "{\"a\": [1, 2, 3], \"b\": {\"a\": [1, 2], \"c\": {\"a\": 42}}}");
    +     * Object actualResult = Json.resp(client, gs("doc"), gs("$..a")).get(); // JSONPath returns all possible paths
    +     * Object[] expectedResult = new Object[] {
    +     *                 new Object[] {gs("["), 1L, 2L, 3L},
    +     *                 new Object[] {gs("["), 1L, 2L},
    +     *                 42L}
    +     * assertArrayEquals(expectedResult, (Object[]) actualResult);
    +     * // legacy path only returns the first JSON value match
    +     * assertArrayEquals(new Object[] {gs("["), 1L, 2L, 3L}, (Object[]) Json.resp(client, gs(key), gs("..a")).get());
    +     * }
    + */ + public static CompletableFuture resp( + @NonNull BaseClient client, @NonNull GlideString key, @NonNull GlideString path) { + return executeCommand(client, new GlideString[] {gs(JSON_RESP), key, path}); + } + /** * A wrapper for custom command API. * diff --git a/java/client/src/test/java/glide/api/commands/servermodules/JsonTest.java b/java/client/src/test/java/glide/api/commands/servermodules/JsonTest.java index 0425831ea2..f05a9b2bb6 100644 --- a/java/client/src/test/java/glide/api/commands/servermodules/JsonTest.java +++ b/java/client/src/test/java/glide/api/commands/servermodules/JsonTest.java @@ -516,4 +516,86 @@ void forget_binary_with_path_returns_success() { assertEquals(expectedResponse, actualResponse); assertEquals(expectedResponseValue, actualResponseValue); } + + @Test + @SneakyThrows + void resp_without_path_returns_success() { + // setup + String key = "testKey"; + CompletableFuture expectedResponse = new CompletableFuture<>(); + String expectedResponseValue = "foo"; + expectedResponse.complete(expectedResponseValue); + when(glideClient.customCommand(eq(new String[] {"JSON.RESP", key})).thenApply(any())) + .thenReturn(expectedResponse); + + // exercise + CompletableFuture actualResponse = Json.resp(glideClient, key); + Object actualResponseValue = actualResponse.get(); + + // verify + assertEquals(expectedResponse, actualResponse); + assertEquals(expectedResponseValue, actualResponseValue); + } + + @Test + @SneakyThrows + void resp_binary_without_path_returns_success() { + // setup + GlideString key = gs("testKey"); + CompletableFuture expectedResponse = new CompletableFuture<>(); + GlideString expectedResponseValue = gs("foo"); + expectedResponse.complete(expectedResponseValue); + when(glideClient.customCommand(eq(new GlideString[] {gs("JSON.RESP"), key})).thenApply(any())) + .thenReturn(expectedResponse); + + // exercise + CompletableFuture actualResponse = Json.resp(glideClient, key); + Object actualResponseValue = actualResponse.get(); + + // verify + assertEquals(expectedResponse, actualResponse); + assertEquals(expectedResponseValue, actualResponseValue); + } + + @Test + @SneakyThrows + void resp_with_path_returns_success() { + // setup + String key = "testKey"; + CompletableFuture expectedResponse = new CompletableFuture<>(); + String expectedResponseValue = "foo"; + expectedResponse.complete(expectedResponseValue); + when(glideClient.customCommand(eq(new String[] {"JSON.RESP", key, "$"})).thenApply(any())) + .thenReturn(expectedResponse); + + // exercise + CompletableFuture actualResponse = Json.resp(glideClient, key, "$"); + Object actualResponseValue = actualResponse.get(); + + // verify + assertEquals(expectedResponse, actualResponse); + assertEquals(expectedResponseValue, actualResponseValue); + } + + @Test + @SneakyThrows + void resp_binary_with_path_returns_success() { + // setup + GlideString key = gs("testKey"); + CompletableFuture expectedResponse = new CompletableFuture<>(); + GlideString expectedResponseValue = gs("foo"); + expectedResponse.complete(expectedResponseValue); + when(glideClient + .customCommand(eq(new GlideString[] {gs("JSON.RESP"), key, gs("$")})) + .thenApply(any())) + .thenReturn(expectedResponse); + + // exercise + CompletableFuture actualResponse = Json.resp(glideClient, key, gs("$")); + Object actualResponseValue = actualResponse.get(); + + // verify + assertEquals(expectedResponse, actualResponse); + assertEquals(expectedResponseValue, actualResponseValue); + } } diff --git a/java/integTest/src/test/java/glide/modules/JsonTests.java b/java/integTest/src/test/java/glide/modules/JsonTests.java index de59753c20..17ac4530cc 100644 --- a/java/integTest/src/test/java/glide/modules/JsonTests.java +++ b/java/integTest/src/test/java/glide/modules/JsonTests.java @@ -8,6 +8,7 @@ import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleSingleNodeRoute.RANDOM; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -435,4 +436,101 @@ public void toggle() { assertThrows( ExecutionException.class, () -> Json.toggle(client, "non_existing_key", "$").get()); } + + @Test + @SneakyThrows + public void json_resp() { + String key = UUID.randomUUID().toString(); + String jsonValue = + "{\"obj\":{\"a\":1, \"b\":2}, \"arr\":[1,2,3], \"str\": \"foo\", \"bool\": true, \"int\":" + + " 42, \"float\": 3.14, \"nullVal\": null}"; + assertEquals(OK, Json.set(client, key, "$", jsonValue).get()); + + Object actualResult1 = Json.resp(client, key, "$.*").get(); + Object[] expectedResult1 = + new Object[] { + new Object[] {"{", "a", 1L, "b", 2L}, // leading "{" indicates JSON objects + new Object[] {"[", 1L, 2L, 3L}, // leading "[" indicates JSON arrays + "foo", + "true", + 42L, + 3.14, + null + }; + assertInstanceOf(Object[].class, actualResult1); + assertArrayEquals(expectedResult1, (Object[]) actualResult1); + + // multiple path match, the first will be returned + Object actualResult2 = Json.resp(client, key, "*").get(); + Object[] expectedResult2 = new Object[] {"{", "a", 1L, "b", 2L}; + assertInstanceOf(Object[].class, actualResult2); + assertArrayEquals(expectedResult2, (Object[]) actualResult2); + + Object actualResult3 = Json.resp(client, key, "$").get(); + Object[] expectedResult3 = + new Object[] { + new Object[] { + "{", + "obj", + new Object[] {"{", "a", 1L, "b", 2L}, + "arr", + new Object[] {"[", 1L, 2L, 3L}, + "str", + "foo", + "bool", + "true", + "int", + 42L, + "float", + 3.14, + "nullVal", + null + } + }; + assertInstanceOf(Object[].class, actualResult3); + assertArrayEquals(expectedResult3, (Object[]) actualResult3); + + Object actualResult4 = Json.resp(client, key, ".").get(); + Object[] expectedResult4 = + new Object[] { + "{", + "obj", + new Object[] {"{", "a", 1L, "b", 2L}, + "arr", + new Object[] {"[", 1L, 2L, 3L}, + "str", + "foo", + "bool", + "true", + "int", + 42L, + "float", + 3.14, + "nullVal", + null + }; + assertInstanceOf(Object[].class, actualResult4); + assertArrayEquals(expectedResult4, (Object[]) actualResult4); + // resp without path defaults to the same behavior of passing "." as path + Object actualResult4WithoutPath = Json.resp(client, key).get(); + assertArrayEquals(expectedResult4, (Object[]) actualResult4WithoutPath); + assertArrayEquals(expectedResult4, (Object[]) actualResult4WithoutPath); + + Object actualResult5 = Json.resp(client, gs(key), gs("$.str")).get(); + Object[] expectedResult5 = new Object[] {gs("foo")}; + assertInstanceOf(Object[].class, actualResult5); + assertArrayEquals(expectedResult5, (Object[]) actualResult5); + + Object actualResult6 = Json.resp(client, key, ".str").get(); + String expectedResult6 = "foo"; + assertEquals(expectedResult6, actualResult6); + + assertArrayEquals(new Object[] {}, (Object[]) Json.resp(client, key, "$.nonexistent").get()); + + assertThrows(ExecutionException.class, () -> Json.resp(client, key, "nonexistent").get()); + + assertNull(Json.resp(client, "nonexistent_key", "$").get()); + assertNull(Json.resp(client, "nonexistent_key", ".").get()); + assertNull(Json.resp(client, "nonexistent_key").get()); + } }