Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java: JSON.OBJLEN and JSON.OBJKEYS. #2492

Merged
merged 8 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* Java: Added `FT.AGGREGATE` ([#2466](https://github.com/valkey-io/valkey-glide/pull/2466))
* Java: Added `JSON.SET` and `JSON.GET` ([#2462](https://github.com/valkey-io/valkey-glide/pull/2462))
* Java: Added `JSON.ARRINSERT` and `JSON.ARRLEN` ([#2476](https://github.com/valkey-io/valkey-glide/pull/2476))
* Java: Added `JSON.OBJLEN` and `JSON.OBJKEYS` ([#2492](https://github.com/valkey-io/valkey-glide/pull/2492))
* Java: Added `FT.ALIASADD`, `FT.ALIASDEL`, `FT.ALIASUPDATE` ([#2442](https://github.com/valkey-io/valkey-glide/pull/2442))
* Core: Update routing for commands from server modules ([#2461](https://github.com/valkey-io/valkey-glide/pull/2461))
* Node: Added `JSON.SET` and `JSON.GET` ([#2427](https://github.com/valkey-io/valkey-glide/pull/2427))
Expand Down
237 changes: 237 additions & 0 deletions java/client/src/main/java/glide/api/commands/servermodules/Json.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public class Json {
private static final String JSON_ARRAPPEND = JSON_PREFIX + "ARRAPPEND";
private static final String JSON_ARRINSERT = JSON_PREFIX + "ARRINSERT";
private static final String JSON_ARRLEN = JSON_PREFIX + "ARRLEN";
private static final String JSON_OBJLEN = JSON_PREFIX + "OBJLEN";
private static final String JSON_OBJKEYS = JSON_PREFIX + "OBJKEYS";

private Json() {}

Expand Down Expand Up @@ -702,6 +704,241 @@ public static CompletableFuture<Long> arrlen(
return executeCommand(client, new GlideString[] {gs(JSON_ARRLEN), key});
}

/**
* Retrieves the number of key-value pairs in the object values at the specified <code>path</code>
* within the JSON document stored at <code>key</code>.<br>
* Equivalent to {@link #objlen(BaseClient, String, String)} with <code>path</code> set to <code>
* "."</code>.
*
* @param client The client to execute the command.
* @param key The key of the JSON document.
* @return The object length stored at the root of the document. If document root is not an
* object, an error is raised.<br>
* If <code>key</code> doesn't exist, returns <code>null</code>.
* @example
* <pre>{@code
* Json.set(client, "doc", "$", "{\"a\": 1.0, \"b\": {\"a\": {\"x\": 1, \"y\": 2}, \"b\": 2.5, \"c\": true}}").get();
* var res = Json.objlen(client, "doc").get();
* assert res == 2; // the size of object matching the path `.`, which has 2 keys: 'a' and 'b'.
* }</pre>
*/
public static CompletableFuture<Long> objlen(@NonNull BaseClient client, @NonNull String key) {
return executeCommand(client, new String[] {JSON_OBJLEN, key});
}

/**
* Retrieves the number of key-value pairs in the object values at the specified <code>path</code>
* within the JSON document stored at <code>key</code>.<br>
* Equivalent to {@link #objlen(BaseClient, GlideString, GlideString)} with <code>path</code> set
* to <code>gs(".")</code>.
*
* @param client The client to execute the command.
* @param key The key of the JSON document.
* @return The object length stored at the root of the document. If document root is not an
* object, an error is raised.<br>
* If <code>key</code> doesn't exist, returns <code>null</code>.
* @example
* <pre>{@code
* Json.set(client, "doc", "$", "{\"a\": 1.0, \"b\": {\"a\": {\"x\": 1, \"y\": 2}, \"b\": 2.5, \"c\": true}}").get();
* var res = Json.objlen(client, gs("doc"), gs(".")).get();
* assert res == 2; // the size of object matching the path `.`, which has 2 keys: 'a' and 'b'.
* }</pre>
*/
public static CompletableFuture<Long> objlen(
@NonNull BaseClient client, @NonNull GlideString key) {
return executeCommand(client, new GlideString[] {gs(JSON_OBJLEN), key});
}

/**
* Retrieves the number of key-value pairs in the object values at the specified <code>path</code>
* within the JSON document stored at <code>key</code>.
*
* @param client The client to execute the command.
* @param key The key of the JSON document.
* @param path The path within the JSON document.
* @return
* <ul>
* <li>For JSONPath (<code>path</code> starts with <code>$</code>):<br>
* Returns an <code>Object[]</code> with a list of long integers for every possible
* path, indicating the number of key-value pairs for each matching object, or <code>
* null
* </code> for JSON values matching the path that are not an object. If <code>path
* </code> does not exist, an empty array will be returned.
* <li>For legacy path (<code>path</code> doesn't start with <code>$</code>):<br>
* Returns the number of key-value pairs for the object value matching the path. If
* multiple paths are matched, returns the length of the first matching object. If
* <code>path</code> doesn't exist or the value at <code>path</code> is not an array, an
* error is raised.
* </ul>
* If <code>key</code> doesn't exist, returns <code>null</code>.
* @example
* <pre>{@code
* Json.set(client, "doc", "$", "{\"a\": 1.0, \"b\": {\"a\": {\"x\": 1, \"y\": 2}, \"b\": 2.5, \"c\": true}}").get();
* var res = Json.objlen(client, "doc", ".").get();
* assert res == 2; // the size of object matching the path `.`, which has 2 keys: 'a' and 'b'.
* res = Json.objlen(client, "doc", "$.b").get();
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
* assert Arrays.equals((Object[]) res, new Object[] { 3 }); // the length of the objects at path `$.b`
* }</pre>
*/
public static CompletableFuture<Object> objlen(
@NonNull BaseClient client, @NonNull String key, @NonNull String path) {
return executeCommand(client, new String[] {JSON_OBJLEN, key, path});
}

/**
* Retrieves the number of key-value pairs in the object values at the specified <code>path</code>
* within the JSON document stored at <code>key</code>.
*
* @param client The client to execute the command.
* @param key The key of the JSON document.
* @param path The path within the JSON document.
* @return
* <ul>
* <li>For JSONPath (<code>path</code> starts with <code>$</code>):<br>
* Returns an <code>Object[]</code> with a list of long integers for every possible
* path, indicating the number of key-value pairs for each matching object, or <code>
* null
* </code> for JSON values matching the path that are not an object. If <code>path
* </code> does not exist, an empty array will be returned.
* <li>For legacy path (<code>path</code> doesn't start with <code>$</code>):<br>
* Returns the number of key-value pairs for the object value matching the path. If
* multiple paths are matched, returns the length of the first matching object. If
* <code>path</code> doesn't exist or the value at <code>path</code> is not an array, an
* error is raised.
* </ul>
* If <code>key</code> doesn't exist, returns <code>null</code>.
* @example
* <pre>{@code
* Json.set(client, "doc", "$", "{\"a\": 1.0, \"b\": {\"a\": {\"x\": 1, \"y\": 2}, \"b\": 2.5, \"c\": true}}").get();
* var res = Json.objlen(client, gs("doc"), gs(".")).get();
* assert res == 2; // the size of object matching the path `.`, which has 2 keys: 'a' and 'b'.
* res = Json.objlen(client, gs("doc"), gs("$.b")).get();
* assert Arrays.equals((Object[]) res, new Object[] { 3 }); // the length of the objects at path `$.b`
* }</pre>
*/
public static CompletableFuture<Object> objlen(
@NonNull BaseClient client, @NonNull GlideString key, @NonNull GlideString path) {
return executeCommand(client, new GlideString[] {gs(JSON_OBJLEN), key, path});
}

/**
* Retrieves the key names in the object values at the specified <code>path</code> within the JSON
* document stored at <code>key</code>.<br>
* Equivalent to {@link #objkeys(BaseClient, String, String)} with <code>path</code> set to <code>
* "."</code>.
*
* @param client The client to execute the command.
* @param key The key of the JSON document.
* @return The object length stored at the root of the document. If document root is not an
* object, an error is raised.<br>
* If <code>key</code> doesn't exist, returns <code>null</code>.
* @example
* <pre>{@code
* Json.set(client, "doc", "$", "{\"a\": 1.0, \"b\": {\"a\": {\"x\": 1, \"y\": 2}, \"b\": 2.5, \"c\": true}}").get();
* var res = Json.objkeys(client, "doc").get();
* assert Arrays.equals((Object[]) res, new Object[] { "a", "b" }); // the keys of the object matching the path `.`, which has 2 keys: 'a' and 'b'.
* }</pre>
*/
public static CompletableFuture<Object[]> objkeys(
@NonNull BaseClient client, @NonNull String key) {
return executeCommand(client, new String[] {JSON_OBJKEYS, key});
}

/**
* Retrieves the key names in the object values at the specified <code>path</code> within the JSON
* document stored at <code>key</code>.<br>
* Equivalent to {@link #objkeys(BaseClient, GlideString, GlideString)} with <code>path</code> set
* to <code>gs(".")</code>.
*
* @param client The client to execute the command.
* @param key The key of the JSON document.
* @return The object length stored at the root of the document. If document root is not an
* object, an error is raised.<br>
* If <code>key</code> doesn't exist, returns <code>null</code>.
* @example
* <pre>{@code
* Json.set(client, "doc", "$", "{\"a\": 1.0, \"b\": {\"a\": {\"x\": 1, \"y\": 2}, \"b\": 2.5, \"c\": true}}").get();
* var res = Json.objkeys(client, gs("doc"), gs(".")).get();
* assert Arrays.equals((Object[]) res, new Object[] { gs("a"), gs("b") }); // the keys of the object matching the path `.`, which has 2 keys: 'a' and 'b'.
* }</pre>
*/
public static CompletableFuture<Object[]> objkeys(
@NonNull BaseClient client, @NonNull GlideString key) {
return executeCommand(client, new GlideString[] {gs(JSON_OBJKEYS), key});
}

/**
* Retrieves the key names in the object values at the specified <code>path</code> within the JSON
* document stored at <code>key</code>.
*
* @param client The client to execute the command.
* @param key The key of the JSON document.
* @param path The path within the JSON document.
* @return
* <ul>
* <li>For JSONPath (<code>path</code> starts with <code>$</code>):<br>
* Returns an <code>Object[][]</code> with each nested array containing key names for
* each matching object for every possible path, indicating the list of object keys for
* each matching object, or <code>null</code> for JSON values matching the path that are
* not an object. If <code>path</code> does not exist, an empty sub-array will be
* returned.
* <li>For legacy path (<code>path</code> doesn't start with <code>$</code>):<br>
* Returns an array of object keys for the object value matching the path. If multiple
* paths are matched, returns the length of the first matching object. If <code>path
* </code> doesn't exist or the value at <code>path</code> is not an array, an error is
* raised.
* </ul>
* If <code>key</code> doesn't exist, returns <code>null</code>.
* @example
* <pre>{@code
* Json.set(client, "doc", "$", "{\"a\": 1.0, \"b\": {\"a\": {\"x\": 1, \"y\": 2}, \"b\": 2.5, \"c\": true}}").get();
* var res = Json.objkeys(client, "doc", ".").get();
* assert Arrays.equals((Object[]) res, new Object[] { "a", "b" }); // key names for the object matching the path `.` as it is the only match.
* res = Json.objkeys(client, "doc", "$.b").get();
* assert Arrays.equals((Object[]) res, new Object[][] { { "a", "b", "c" } }); // key names as a nested list for objects matching the JSONPath `$.b`.
* }</pre>
*/
public static CompletableFuture<Object[]> objkeys(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

objkeys returns an array is all cases? well at least one of the commands acts decently.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object[][] for JSONPath and Object[] for legacy path. So common denominator is Object[].

@NonNull BaseClient client, @NonNull String key, @NonNull String path) {
return executeCommand(client, new String[] {JSON_OBJKEYS, key, path});
}

/**
* Retrieves the key names in the object values at the specified <code>path</code> within the JSON
* document stored at <code>key</code>.
*
* @param client The client to execute the command.
* @param key The key of the JSON document.
* @param path The path within the JSON document.
* @return
* <ul>
* <li>For JSONPath (<code>path</code> starts with <code>$</code>):<br>
* Returns an <code>Object[][]</code> with each nested array containing key names for
* each matching object for every possible path, indicating the list of object keys for
* each matching object, or <code>null</code> for JSON values matching the path that are
* not an object. If <code>path</code> does not exist, an empty sub-array will be
* returned.
* <li>For legacy path (<code>path</code> doesn't start with <code>$</code>):<br>
* Returns an array of object keys for the object value matching the path. If multiple
* paths are matched, returns the length of the first matching object. If <code>path
* </code> doesn't exist or the value at <code>path</code> is not an array, an error is
* raised.
* </ul>
* If <code>key</code> doesn't exist, returns <code>null</code>.
* @example
* <pre>{@code
* Json.set(client, "doc", "$", "{\"a\": 1.0, \"b\": {\"a\": {\"x\": 1, \"y\": 2}, \"b\": 2.5, \"c\": true}}").get();
* var res = Json.objkeys(client, gs("doc"), gs(".")).get();
* assert Arrays.equals((Object[]) res, new Object[] { "a", "b" }); // key names for the object matching the path `.` as it is the only match.
* res = Json.objkeys(client, gs("doc"), gs("$.b")).get();
* assert Arrays.equals((Object[]) res, new Object[][] { { "a", "b", "c" } }); // key names as a nested list for objects matching the JSONPath `$.b`.
* }</pre>
*/
public static CompletableFuture<Object[]> objkeys(
@NonNull BaseClient client, @NonNull GlideString key, @NonNull GlideString path) {
return executeCommand(client, new GlideString[] {gs(JSON_OBJKEYS), key, path});
}

/**
* A wrapper for custom command API.
*
Expand Down
42 changes: 42 additions & 0 deletions java/integTest/src/test/java/glide/modules/JsonTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -322,4 +322,46 @@ public void arrlen() {
res = Json.arrlen(client, key).get();
assertEquals(5L, res);
}

@Test
@SneakyThrows
public void objlen() {
String key = UUID.randomUUID().toString();

String doc = "{\"a\": 1.0, \"b\": {\"a\": {\"x\": 1, \"y\": 2}, \"b\": 2.5, \"c\": true}}";
assertEquals("OK", Json.set(client, key, "$", doc).get());

var res = Json.objlen(client, key, "$..").get();
assertArrayEquals(new Object[] {2L, 3L, 2L}, (Object[]) res);

res = Json.objlen(client, gs(key), gs("..b")).get();
assertEquals(3L, res);

// without path
res = Json.objlen(client, key).get();
assertEquals(2L, res);
res = Json.objlen(client, gs(key)).get();
assertEquals(2L, res);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we test when the path doesn't exist, or the path is not an array, or the key is not JSON?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. I don't want to start testing module instead of client.


@Test
@SneakyThrows
public void objkeys() {
String key = UUID.randomUUID().toString();

String doc = "{\"a\": 1.0, \"b\": {\"a\": {\"x\": 1, \"y\": 2}, \"b\": 2.5, \"c\": true}}";
assertEquals("OK", Json.set(client, key, "$", doc).get());

var res = Json.objkeys(client, key, "..").get();
assertArrayEquals(new Object[] {"a", "b"}, res);

res = Json.objkeys(client, gs(key), gs("$..b")).get();
assertArrayEquals(new Object[][] {{gs("a"), gs("b"), gs("c")}, {}}, res);

// without path
res = Json.objkeys(client, key).get();
assertArrayEquals(new Object[] {"a", "b"}, res);
res = Json.objkeys(client, gs(key)).get();
assertArrayEquals(new Object[] {gs("a"), gs("b")}, res);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we test when the path doesn't exist, or the path is not an array, or the key is not JSON?

}
}
Loading