From f03f593a0db4bdde58198f2930b7b815cea95c93 Mon Sep 17 00:00:00 2001 From: Tihomir Krasimirov Mateev Date: Wed, 25 Sep 2024 10:36:37 +0300 Subject: [PATCH] Introducing JSON to Lettuce (#2933) * Most of the API layer * Add the JSON.TYPE command * Kotlin coroutines added; Started JSON README.md; Parser registry is now part of the Connection; formatting; chained all commands; extracted commands in their own unit RedisJsonCommandBuilder; * Implemented 90% of commands from top 10 * All but SET are implemented * Integrated Jackson, finished up the SET command * Left out a few files by mistake * Adding some JavaDoc * Formatting * Implemented all JSON commands * Introducing test containers to the testing fw * Complete coverage with integration tests, straight scenarios * Added Pathv1 tests * Added RedisCE cluster support for the JSON.MGET and JSON.MSET commands * Handle null values * No longer using K for the JSON object keys * Polishing * JsonType introduced to help typization * Remove the RedisCodec from the JsonValue/JsonParser abstraction, add configuration for custom parsers * Extend API surface with methods that reduce the amount of required arguments * Adding unit tests, addressing changes to README.md * Implemented object-mapping functionality * Addresses Ali's comments * Addressed last bunch of comments from Ali, changed ports to not colide with existing infra * Forgot to change ports and stop debug log * Polishing touches --- pom.xml | 23 + .../core/AbstractRedisAsyncCommands.java | 193 +++++- .../core/AbstractRedisReactiveCommands.java | 197 +++++- .../java/io/lettuce/core/AclSetuserArgs.java | 6 +- .../java/io/lettuce/core/ClientOptions.java | 41 +- .../core/FutureSyncInvocationHandler.java | 4 +- .../lettuce/core/RedisAsyncCommandsImpl.java | 5 +- .../java/io/lettuce/core/RedisClient.java | 5 +- .../io/lettuce/core/RedisCommandBuilder.java | 152 ----- .../lettuce/core/RedisJsonCommandBuilder.java | 329 +++++++++ .../core/RedisReactiveCommandsImpl.java | 5 +- .../core/StatefulRedisConnectionImpl.java | 26 +- .../core/api/async/RedisAsyncCommands.java | 9 +- .../api/async/RedisJsonAsyncCommands.java | 441 +++++++++++++ .../reactive/BaseRedisReactiveCommands.java | 7 + .../reactive/RedisJsonReactiveCommands.java | 442 +++++++++++++ .../api/reactive/RedisReactiveCommands.java | 2 +- .../lettuce/core/api/sync/RedisCommands.java | 17 +- .../core/api/sync/RedisJsonCommands.java | 440 +++++++++++++ .../ClusterDistributionChannelWriter.java | 4 +- .../io/lettuce/core/cluster/CommandSet.java | 2 +- ...RedisAdvancedClusterAsyncCommandsImpl.java | 101 ++- ...isAdvancedClusterReactiveCommandsImpl.java | 15 +- .../core/cluster/RedisClusterClient.java | 14 +- .../StatefulRedisClusterConnectionImpl.java | 16 +- .../api/async/NodeSelectionAsyncCommands.java | 11 +- .../async/NodeSelectionJsonAsyncCommands.java | 440 +++++++++++++ .../api/async/RedisClusterAsyncCommands.java | 9 +- .../api/sync/NodeSelectionJsonCommands.java | 440 +++++++++++++ .../api/sync/RedisClusterCommands.java | 18 +- .../dynamic/DefaultCommandMethodVerifier.java | 6 +- .../core/dynamic/segment/CommandSegments.java | 9 +- .../lettuce/core/json/DefaultJsonParser.java | 91 +++ .../lettuce/core/json/DelegateJsonArray.java | 108 +++ .../lettuce/core/json/DelegateJsonObject.java | 61 ++ .../lettuce/core/json/DelegateJsonValue.java | 122 ++++ .../java/io/lettuce/core/json/JsonArray.java | 90 +++ .../java/io/lettuce/core/json/JsonObject.java | 52 ++ .../java/io/lettuce/core/json/JsonParser.java | 78 +++ .../java/io/lettuce/core/json/JsonPath.java | 124 ++++ .../java/io/lettuce/core/json/JsonType.java | 40 ++ .../java/io/lettuce/core/json/JsonValue.java | 109 +++ src/main/java/io/lettuce/core/json/README.md | 87 +++ .../lettuce/core/json/RedisJsonException.java | 24 + .../core/json/UnproccessedJsonValue.java | 166 +++++ .../core/json/arguments/JsonGetArgs.java | 137 ++++ .../core/json/arguments/JsonMsetArgs.java | 67 ++ .../core/json/arguments/JsonRangeArgs.java | 116 ++++ .../core/json/arguments/JsonSetArgs.java | 110 ++++ .../io/lettuce/core/json/package-info.java | 4 + .../masterreplica/AutodiscoveryConnector.java | 2 +- .../MasterReplicaChannelWriter.java | 4 +- .../core/masterreplica/SentinelConnector.java | 2 +- ...tefulRedisMasterReplicaConnectionImpl.java | 6 +- .../StaticMasterReplicaConnector.java | 2 +- .../core/metrics/CommandLatencyId.java | 2 +- .../MicrometerCommandLatencyRecorder.java | 4 +- .../core/metrics/MicrometerOptions.java | 2 +- .../core/output/JsonTypeListOutput.java | 50 ++ .../core/output/JsonValueListOutput.java | 56 ++ .../lettuce/core/output/NumberListOutput.java | 76 +++ .../protocol/BaseRedisCommandBuilder.java | 170 ++++- .../io/lettuce/core/protocol/CommandArgs.java | 2 +- .../lettuce/core/protocol/CommandHandler.java | 2 +- .../lettuce/core/protocol/CommandKeyword.java | 4 +- .../io/lettuce/core/protocol/CommandType.java | 33 + .../core/protocol/ProtocolKeyword.java | 2 +- .../core/pubsub/PubSubCommandHandler.java | 2 +- .../lettuce/core/pubsub/PubSubEndpoint.java | 11 +- .../pubsub/RedisPubSubAsyncCommandsImpl.java | 2 +- .../RedisPubSubReactiveCommandsImpl.java | 2 +- .../StatefulRedisPubSubConnectionImpl.java | 2 +- .../RedisSentinelReactiveCommandsImpl.java | 5 +- .../StatefulRedisSentinelConnectionImpl.java | 6 +- .../io/lettuce/core/tracing/BraveTracing.java | 2 +- .../DefaultLettuceObservationConvention.java | 6 +- .../api/coroutines/RedisCoroutinesCommands.kt | 4 +- .../coroutines/RedisCoroutinesCommandsImpl.kt | 8 +- .../coroutines/RedisJsonCoroutinesCommands.kt | 444 +++++++++++++ .../RedisJsonCoroutinesCommandsImpl.kt | 136 ++++ .../lettuce/core/api/RedisJsonCommands.java | 440 +++++++++++++ .../MyExtendedRedisClusterClient.java | 6 +- .../MyRedisClusterConnection.java | 5 +- .../io/lettuce/apigenerator/Constants.java | 3 +- .../lettuce/core/ClientOptionsUnitTests.java | 49 ++ .../core/RedisContainerIntegrationTests.java | 57 ++ .../RedisJsonCommandBuilderUnitTests.java | 270 ++++++++ .../ClusterReadOnlyCommandsUnitTests.java | 2 +- .../ServerCommandIntegrationTests.java | 2 +- ...CommandSegmentCommandFactoryUnitTests.java | 2 +- ...otationCommandSegmentFactoryUnitTests.java | 12 +- .../core/json/DefaultJsonParserUnitTests.java | 104 +++ .../core/json/DelegateJsonArrayUnitTests.java | 173 +++++ .../json/DelegateJsonObjectUnitTests.java | 69 ++ .../core/json/DelegateJsonValueUnitTests.java | 145 ++++ .../RedisJsonClusterIntegrationTests.java | 433 ++++++++++++ .../core/json/RedisJsonIntegrationTests.java | 623 ++++++++++++++++++ .../json/UnproccessedJsonValueUnitTests.java | 139 ++++ .../metrics/CommandLatencyIdUnitTests.java | 2 +- .../output/JsonTypeListOutputUnitTests.java | 48 ++ .../output/JsonValueListOutputUnitTests.java | 36 + .../output/NumberListOutputUnitTests.java | 52 ++ .../core/tracing/BraveTracingUnitTests.java | 4 +- src/test/java/io/lettuce/test/CliParser.java | 4 +- .../dynamic/RedisCommandFactoryBenchmark.java | 3 +- src/test/resources/bike-inventory.json | 46 ++ src/test/resources/docker/Dockerfile | 23 + .../docker/cluster-nodes/nodes-36379.conf | 7 + .../docker/cluster-nodes/nodes-36380.conf | 7 + .../docker/cluster-nodes/nodes-36381.conf | 7 + .../docker/cluster-nodes/nodes-36382.conf | 7 + .../docker/cluster-nodes/nodes-36383.conf | 7 + .../docker/cluster-nodes/nodes-36384.conf | 7 + .../docker/cluster-nodes/redis-36379.conf | 13 + .../docker/cluster-nodes/redis-36380.conf | 13 + .../docker/cluster-nodes/redis-36381.conf | 13 + .../docker/cluster-nodes/redis-36382.conf | 13 + .../docker/cluster-nodes/redis-36383.conf | 13 + .../docker/cluster-nodes/redis-36384.conf | 13 + src/test/resources/docker/docker-compose.yml | 17 + src/test/resources/docker/start_cluster.sh | 31 + 121 files changed, 8673 insertions(+), 311 deletions(-) create mode 100644 src/main/java/io/lettuce/core/RedisJsonCommandBuilder.java create mode 100644 src/main/java/io/lettuce/core/api/async/RedisJsonAsyncCommands.java create mode 100644 src/main/java/io/lettuce/core/api/reactive/RedisJsonReactiveCommands.java create mode 100644 src/main/java/io/lettuce/core/api/sync/RedisJsonCommands.java create mode 100644 src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionJsonAsyncCommands.java create mode 100644 src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionJsonCommands.java create mode 100644 src/main/java/io/lettuce/core/json/DefaultJsonParser.java create mode 100644 src/main/java/io/lettuce/core/json/DelegateJsonArray.java create mode 100644 src/main/java/io/lettuce/core/json/DelegateJsonObject.java create mode 100644 src/main/java/io/lettuce/core/json/DelegateJsonValue.java create mode 100644 src/main/java/io/lettuce/core/json/JsonArray.java create mode 100644 src/main/java/io/lettuce/core/json/JsonObject.java create mode 100644 src/main/java/io/lettuce/core/json/JsonParser.java create mode 100644 src/main/java/io/lettuce/core/json/JsonPath.java create mode 100644 src/main/java/io/lettuce/core/json/JsonType.java create mode 100644 src/main/java/io/lettuce/core/json/JsonValue.java create mode 100644 src/main/java/io/lettuce/core/json/README.md create mode 100644 src/main/java/io/lettuce/core/json/RedisJsonException.java create mode 100644 src/main/java/io/lettuce/core/json/UnproccessedJsonValue.java create mode 100644 src/main/java/io/lettuce/core/json/arguments/JsonGetArgs.java create mode 100644 src/main/java/io/lettuce/core/json/arguments/JsonMsetArgs.java create mode 100644 src/main/java/io/lettuce/core/json/arguments/JsonRangeArgs.java create mode 100644 src/main/java/io/lettuce/core/json/arguments/JsonSetArgs.java create mode 100644 src/main/java/io/lettuce/core/json/package-info.java create mode 100644 src/main/java/io/lettuce/core/output/JsonTypeListOutput.java create mode 100644 src/main/java/io/lettuce/core/output/JsonValueListOutput.java create mode 100644 src/main/java/io/lettuce/core/output/NumberListOutput.java create mode 100644 src/main/kotlin/io/lettuce/core/api/coroutines/RedisJsonCoroutinesCommands.kt create mode 100644 src/main/kotlin/io/lettuce/core/api/coroutines/RedisJsonCoroutinesCommandsImpl.kt create mode 100644 src/main/templates/io/lettuce/core/api/RedisJsonCommands.java create mode 100644 src/test/java/io/lettuce/core/RedisContainerIntegrationTests.java create mode 100644 src/test/java/io/lettuce/core/RedisJsonCommandBuilderUnitTests.java create mode 100644 src/test/java/io/lettuce/core/json/DefaultJsonParserUnitTests.java create mode 100644 src/test/java/io/lettuce/core/json/DelegateJsonArrayUnitTests.java create mode 100644 src/test/java/io/lettuce/core/json/DelegateJsonObjectUnitTests.java create mode 100644 src/test/java/io/lettuce/core/json/DelegateJsonValueUnitTests.java create mode 100644 src/test/java/io/lettuce/core/json/RedisJsonClusterIntegrationTests.java create mode 100644 src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java create mode 100644 src/test/java/io/lettuce/core/json/UnproccessedJsonValueUnitTests.java create mode 100644 src/test/java/io/lettuce/core/output/JsonTypeListOutputUnitTests.java create mode 100644 src/test/java/io/lettuce/core/output/JsonValueListOutputUnitTests.java create mode 100644 src/test/java/io/lettuce/core/output/NumberListOutputUnitTests.java create mode 100644 src/test/resources/bike-inventory.json create mode 100644 src/test/resources/docker/Dockerfile create mode 100644 src/test/resources/docker/cluster-nodes/nodes-36379.conf create mode 100644 src/test/resources/docker/cluster-nodes/nodes-36380.conf create mode 100644 src/test/resources/docker/cluster-nodes/nodes-36381.conf create mode 100644 src/test/resources/docker/cluster-nodes/nodes-36382.conf create mode 100644 src/test/resources/docker/cluster-nodes/nodes-36383.conf create mode 100644 src/test/resources/docker/cluster-nodes/nodes-36384.conf create mode 100644 src/test/resources/docker/cluster-nodes/redis-36379.conf create mode 100644 src/test/resources/docker/cluster-nodes/redis-36380.conf create mode 100644 src/test/resources/docker/cluster-nodes/redis-36381.conf create mode 100644 src/test/resources/docker/cluster-nodes/redis-36382.conf create mode 100644 src/test/resources/docker/cluster-nodes/redis-36383.conf create mode 100644 src/test/resources/docker/cluster-nodes/redis-36384.conf create mode 100644 src/test/resources/docker/docker-compose.yml create mode 100644 src/test/resources/docker/start_cluster.sh diff --git a/pom.xml b/pom.xml index 1a6c2e4c47..bc3225af6a 100644 --- a/pom.xml +++ b/pom.xml @@ -50,6 +50,7 @@ 5.13.11 3.13.0 2.12.0 + 2.17.0 1.3.2 4.0.1 5.10.2 @@ -238,6 +239,13 @@ true + + com.fasterxml.jackson.core + jackson-databind + ${jackson-version} + true + + @@ -523,6 +531,21 @@ test + + + + org.testcontainers + testcontainers + 1.20.1 + test + + + org.testcontainers + junit-jupiter + 1.20.1 + test + + diff --git a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java index 60a0096644..5789812671 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java @@ -26,6 +26,14 @@ import io.lettuce.core.codec.Base16; import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.json.JsonParser; +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; import io.lettuce.core.models.stream.ClaimedMessages; import io.lettuce.core.models.stream.PendingMessage; import io.lettuce.core.models.stream.PendingMessages; @@ -71,21 +79,27 @@ public abstract class AbstractRedisAsyncCommands implements RedisAclAsyncC RedisKeyAsyncCommands, RedisStringAsyncCommands, RedisListAsyncCommands, RedisSetAsyncCommands, RedisSortedSetAsyncCommands, RedisScriptingAsyncCommands, RedisServerAsyncCommands, RedisHLLAsyncCommands, BaseRedisAsyncCommands, RedisTransactionalAsyncCommands, - RedisGeoAsyncCommands, RedisClusterAsyncCommands { + RedisGeoAsyncCommands, RedisClusterAsyncCommands, RedisJsonAsyncCommands { private final StatefulConnection connection; private final RedisCommandBuilder commandBuilder; + private final RedisJsonCommandBuilder jsonCommandBuilder; + + private final JsonParser parser; + /** * Initialize a new instance. * * @param connection the connection to operate on * @param codec the codec for command encoding */ - public AbstractRedisAsyncCommands(StatefulConnection connection, RedisCodec codec) { + public AbstractRedisAsyncCommands(StatefulConnection connection, RedisCodec codec, JsonParser parser) { + this.parser = parser; this.connection = connection; this.commandBuilder = new RedisCommandBuilder<>(codec); + this.jsonCommandBuilder = new RedisJsonCommandBuilder<>(codec, parser); } @Override @@ -1453,6 +1467,176 @@ public boolean isOpen() { return connection.isOpen(); } + @Override + public RedisFuture> jsonArrappend(K key, JsonPath jsonPath, JsonValue... values) { + return dispatch(jsonCommandBuilder.jsonArrappend(key, jsonPath, values)); + } + + @Override + public RedisFuture> jsonArrappend(K key, JsonValue... values) { + return dispatch(jsonCommandBuilder.jsonArrappend(key, JsonPath.ROOT_PATH, values)); + } + + @Override + public RedisFuture> jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range) { + return dispatch(jsonCommandBuilder.jsonArrindex(key, jsonPath, value, range)); + } + + @Override + public RedisFuture> jsonArrindex(K key, JsonPath jsonPath, JsonValue value) { + return dispatch(jsonCommandBuilder.jsonArrindex(key, jsonPath, value, JsonRangeArgs.Builder.defaults())); + } + + @Override + public RedisFuture> jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values) { + return dispatch(jsonCommandBuilder.jsonArrinsert(key, jsonPath, index, values)); + } + + @Override + public RedisFuture> jsonArrlen(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonArrlen(key, jsonPath)); + } + + @Override + public RedisFuture> jsonArrlen(K key) { + return dispatch(jsonCommandBuilder.jsonArrlen(key, JsonPath.ROOT_PATH)); + } + + @Override + public RedisFuture> jsonArrpop(K key, JsonPath jsonPath, int index) { + return dispatch(jsonCommandBuilder.jsonArrpop(key, jsonPath, index)); + } + + @Override + public RedisFuture> jsonArrpop(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonArrpop(key, jsonPath, -1)); + } + + @Override + public RedisFuture> jsonArrpop(K key) { + return dispatch(jsonCommandBuilder.jsonArrpop(key, JsonPath.ROOT_PATH, -1)); + } + + @Override + public RedisFuture> jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range) { + return dispatch(jsonCommandBuilder.jsonArrtrim(key, jsonPath, range)); + } + + @Override + public RedisFuture jsonClear(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonClear(key, jsonPath)); + } + + @Override + public RedisFuture jsonClear(K key) { + return dispatch(jsonCommandBuilder.jsonClear(key, JsonPath.ROOT_PATH)); + } + + @Override + public RedisFuture jsonDel(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonDel(key, jsonPath)); + } + + @Override + public RedisFuture jsonDel(K key) { + return dispatch(jsonCommandBuilder.jsonDel(key, JsonPath.ROOT_PATH)); + } + + @Override + public RedisFuture> jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths) { + return dispatch(jsonCommandBuilder.jsonGet(key, options, jsonPaths)); + } + + @Override + public RedisFuture> jsonGet(K key, JsonPath... jsonPaths) { + return dispatch(jsonCommandBuilder.jsonGet(key, JsonGetArgs.Builder.defaults(), jsonPaths)); + } + + @Override + public RedisFuture jsonMerge(K key, JsonPath jsonPath, JsonValue value) { + return dispatch(jsonCommandBuilder.jsonMerge(key, jsonPath, value)); + } + + @Override + public RedisFuture> jsonMGet(JsonPath jsonPath, K... keys) { + return dispatch(jsonCommandBuilder.jsonMGet(jsonPath, keys)); + } + + @Override + public RedisFuture jsonMSet(List> arguments) { + return dispatch(jsonCommandBuilder.jsonMSet(arguments)); + } + + @Override + public RedisFuture> jsonNumincrby(K key, JsonPath jsonPath, Number number) { + return dispatch(jsonCommandBuilder.jsonNumincrby(key, jsonPath, number)); + } + + @Override + public RedisFuture> jsonObjkeys(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonObjkeys(key, jsonPath)); + } + + @Override + public RedisFuture> jsonObjkeys(K key) { + return dispatch(jsonCommandBuilder.jsonObjkeys(key, JsonPath.ROOT_PATH)); + } + + @Override + public RedisFuture> jsonObjlen(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonObjlen(key, jsonPath)); + } + + @Override + public RedisFuture> jsonObjlen(K key) { + return dispatch(jsonCommandBuilder.jsonObjlen(key, JsonPath.ROOT_PATH)); + } + + @Override + public RedisFuture jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options) { + return dispatch(jsonCommandBuilder.jsonSet(key, jsonPath, value, options)); + } + + @Override + public RedisFuture jsonSet(K key, JsonPath jsonPath, JsonValue value) { + return dispatch(jsonCommandBuilder.jsonSet(key, jsonPath, value, JsonSetArgs.Builder.defaults())); + } + + @Override + public RedisFuture> jsonStrappend(K key, JsonPath jsonPath, JsonValue value) { + return dispatch(jsonCommandBuilder.jsonStrappend(key, jsonPath, value)); + } + + @Override + public RedisFuture> jsonStrappend(K key, JsonValue value) { + return dispatch(jsonCommandBuilder.jsonStrappend(key, JsonPath.ROOT_PATH, value)); + } + + @Override + public RedisFuture> jsonStrlen(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonStrlen(key, jsonPath)); + } + + @Override + public RedisFuture> jsonStrlen(K key) { + return dispatch(jsonCommandBuilder.jsonStrlen(key, JsonPath.ROOT_PATH)); + } + + @Override + public RedisFuture> jsonToggle(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonToggle(key, jsonPath)); + } + + @Override + public RedisFuture> jsonType(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonType(key, jsonPath)); + } + + @Override + public RedisFuture> jsonType(K key) { + return dispatch(jsonCommandBuilder.jsonType(key, JsonPath.ROOT_PATH)); + } + @Override public RedisFuture> keys(K pattern) { return dispatch(commandBuilder.keys(pattern)); @@ -3194,6 +3378,11 @@ public RedisFuture>> clusterLinks() { return dispatch(commandBuilder.clusterLinks()); } + @Override + public JsonParser getJsonParser() { + return this.parser; + } + private byte[] encodeFunction(String functionCode) { LettuceAssert.notNull(functionCode, "Function code must not be null"); LettuceAssert.notEmpty(functionCode, "Function code script must not be empty"); diff --git a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java index 8614a6eb59..350fbce601 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java @@ -26,6 +26,14 @@ import io.lettuce.core.codec.Base16; import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.json.JsonParser; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; import io.lettuce.core.models.stream.ClaimedMessages; import io.lettuce.core.models.stream.PendingMessage; import io.lettuce.core.models.stream.PendingMessages; @@ -74,6 +82,7 @@ * @author dengliming * @author Andrey Shlykov * @author Ali Takavci + * @author Tihomir Mateev * @since 4.0 */ public abstract class AbstractRedisReactiveCommands @@ -81,12 +90,16 @@ public abstract class AbstractRedisReactiveCommands RedisStringReactiveCommands, RedisListReactiveCommands, RedisSetReactiveCommands, RedisSortedSetReactiveCommands, RedisScriptingReactiveCommands, RedisServerReactiveCommands, RedisHLLReactiveCommands, BaseRedisReactiveCommands, RedisTransactionalReactiveCommands, - RedisGeoReactiveCommands, RedisClusterReactiveCommands { + RedisGeoReactiveCommands, RedisClusterReactiveCommands, RedisJsonReactiveCommands { private final StatefulConnection connection; private final RedisCommandBuilder commandBuilder; + private final RedisJsonCommandBuilder jsonCommandBuilder; + + private final JsonParser parser; + private final ClientResources clientResources; private final boolean tracingEnabled; @@ -99,9 +112,11 @@ public abstract class AbstractRedisReactiveCommands * @param connection the connection to operate on. * @param codec the codec for command encoding. */ - public AbstractRedisReactiveCommands(StatefulConnection connection, RedisCodec codec) { + public AbstractRedisReactiveCommands(StatefulConnection connection, RedisCodec codec, JsonParser parser) { this.connection = connection; + this.parser = parser; this.commandBuilder = new RedisCommandBuilder<>(codec); + this.jsonCommandBuilder = new RedisJsonCommandBuilder<>(codec, parser); this.clientResources = connection.getResources(); this.tracingEnabled = clientResources.tracing().isEnabled(); } @@ -122,6 +137,11 @@ private EventExecutorGroup getScheduler() { return this.scheduler = schedulerToUse; } + @Override + public JsonParser getJsonParser() { + return parser; + } + @Override public Mono> aclCat() { return createMono(commandBuilder::aclCat); @@ -1515,6 +1535,179 @@ public boolean isOpen() { return connection.isOpen(); } + @Override + public Flux jsonArrappend(K key, JsonPath jsonPath, JsonValue... values) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrappend(key, jsonPath, values)); + } + + @Override + public Flux jsonArrappend(K key, JsonValue... values) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrappend(key, JsonPath.ROOT_PATH, values)); + } + + @Override + public Flux jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrindex(key, jsonPath, value, range)); + } + + @Override + public Flux jsonArrindex(K key, JsonPath jsonPath, JsonValue value) { + final JsonRangeArgs args = JsonRangeArgs.Builder.defaults(); + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrindex(key, jsonPath, value, args)); + } + + @Override + public Flux jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrinsert(key, jsonPath, index, values)); + } + + @Override + public Flux jsonArrlen(K key, JsonPath jsonPath) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrlen(key, jsonPath)); + } + + @Override + public Flux jsonArrlen(K key) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrlen(key, JsonPath.ROOT_PATH)); + } + + @Override + public Flux jsonArrpop(K key, JsonPath jsonPath, int index) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrpop(key, jsonPath, index)); + } + + @Override + public Flux jsonArrpop(K key, JsonPath jsonPath) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrpop(key, jsonPath, -1)); + } + + @Override + public Flux jsonArrpop(K key) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrpop(key, JsonPath.ROOT_PATH, -1)); + } + + @Override + public Flux jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrtrim(key, jsonPath, range)); + } + + @Override + public Mono jsonClear(K key, JsonPath jsonPath) { + return createMono(() -> jsonCommandBuilder.jsonClear(key, jsonPath)); + } + + @Override + public Mono jsonClear(K key) { + return createMono(() -> jsonCommandBuilder.jsonClear(key, JsonPath.ROOT_PATH)); + } + + @Override + public Mono jsonDel(K key, JsonPath jsonPath) { + return createMono(() -> jsonCommandBuilder.jsonDel(key, jsonPath)); + } + + @Override + public Mono jsonDel(K key) { + return createMono(() -> jsonCommandBuilder.jsonDel(key, JsonPath.ROOT_PATH)); + } + + @Override + public Flux jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonGet(key, options, jsonPaths)); + } + + @Override + public Flux jsonGet(K key, JsonPath... jsonPaths) { + final JsonGetArgs args = JsonGetArgs.Builder.defaults(); + return createDissolvingFlux(() -> jsonCommandBuilder.jsonGet(key, args, jsonPaths)); + } + + @Override + public Mono jsonMerge(K key, JsonPath jsonPath, JsonValue value) { + return createMono(() -> jsonCommandBuilder.jsonMerge(key, jsonPath, value)); + } + + @Override + public Flux jsonMGet(JsonPath jsonPath, K... keys) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonMGet(jsonPath, keys)); + } + + @Override + public Mono jsonMSet(List> arguments) { + return createMono(() -> jsonCommandBuilder.jsonMSet(arguments)); + } + + @Override + public Flux jsonNumincrby(K key, JsonPath jsonPath, Number number) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonNumincrby(key, jsonPath, number)); + } + + @Override + public Flux jsonObjkeys(K key, JsonPath jsonPath) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonObjkeys(key, jsonPath)); + } + + @Override + public Flux jsonObjkeys(K key) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonObjkeys(key, JsonPath.ROOT_PATH)); + } + + @Override + public Flux jsonObjlen(K key, JsonPath jsonPath) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonObjlen(key, jsonPath)); + } + + @Override + public Flux jsonObjlen(K key) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonObjlen(key, JsonPath.ROOT_PATH)); + } + + @Override + public Mono jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options) { + return createMono(() -> jsonCommandBuilder.jsonSet(key, jsonPath, value, options)); + } + + @Override + public Mono jsonSet(K key, JsonPath jsonPath, JsonValue value) { + final JsonSetArgs args = JsonSetArgs.Builder.defaults(); + return createMono(() -> jsonCommandBuilder.jsonSet(key, jsonPath, value, args)); + } + + @Override + public Flux jsonStrappend(K key, JsonPath jsonPath, JsonValue value) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonStrappend(key, jsonPath, value)); + } + + @Override + public Flux jsonStrappend(K key, JsonValue value) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonStrappend(key, JsonPath.ROOT_PATH, value)); + } + + @Override + public Flux jsonStrlen(K key, JsonPath jsonPath) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonStrlen(key, jsonPath)); + } + + @Override + public Flux jsonStrlen(K key) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonStrlen(key, JsonPath.ROOT_PATH)); + } + + @Override + public Flux jsonToggle(K key, JsonPath jsonPath) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonToggle(key, jsonPath)); + } + + @Override + public Flux jsonType(K key, JsonPath jsonPath) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonType(key, jsonPath)); + } + + @Override + public Flux jsonType(K key) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonType(key, JsonPath.ROOT_PATH)); + } + @Override public Flux keys(K pattern) { return createDissolvingFlux(() -> commandBuilder.keys(pattern)); diff --git a/src/main/java/io/lettuce/core/AclSetuserArgs.java b/src/main/java/io/lettuce/core/AclSetuserArgs.java index 4b3e27a69f..c64145c100 100644 --- a/src/main/java/io/lettuce/core/AclSetuserArgs.java +++ b/src/main/java/io/lettuce/core/AclSetuserArgs.java @@ -606,7 +606,7 @@ public void build(CommandArgs args) { @Override public String toString() { - return getClass().getSimpleName() + ": " + value.name(); + return getClass().getSimpleName() + ": " + value.toString(); } } @@ -716,7 +716,7 @@ public void build(CommandArgs args) { if (command.getSubCommand() == null) { args.add("+" + command.getCommand().name()); } else { - args.add("+" + command.getCommand().name() + "|" + command.getSubCommand().name()); + args.add("+" + command.getCommand().name() + "|" + command.getSubCommand().toString()); } } @@ -735,7 +735,7 @@ public void build(CommandArgs args) { if (command.getSubCommand() == null) { args.add("-" + command.getCommand().name()); } else { - args.add("-" + command.getCommand().name() + "|" + command.getSubCommand().name()); + args.add("-" + command.getCommand().name() + "|" + command.getSubCommand().toString()); } } diff --git a/src/main/java/io/lettuce/core/ClientOptions.java b/src/main/java/io/lettuce/core/ClientOptions.java index 3e32967bf1..9d8aeb4ad9 100644 --- a/src/main/java/io/lettuce/core/ClientOptions.java +++ b/src/main/java/io/lettuce/core/ClientOptions.java @@ -25,6 +25,8 @@ import io.lettuce.core.api.StatefulConnection; import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.json.DefaultJsonParser; +import io.lettuce.core.json.JsonParser; import io.lettuce.core.protocol.DecodeBufferPolicies; import io.lettuce.core.protocol.DecodeBufferPolicy; import io.lettuce.core.protocol.ProtocolVersion; @@ -69,6 +71,8 @@ public class ClientOptions implements Serializable { public static final TimeoutOptions DEFAULT_TIMEOUT_OPTIONS = TimeoutOptions.enabled(); + public static final JsonParser DEFAULT_JSON_PARSER = DefaultJsonParser.INSTANCE; + private final boolean autoReconnect; private final boolean cancelCommandsOnReconnectFailure; @@ -89,6 +93,8 @@ public class ClientOptions implements Serializable { private final Charset scriptCharset; + private final JsonParser jsonParser; + private final SocketOptions socketOptions; private final SslOptions sslOptions; @@ -108,6 +114,7 @@ protected ClientOptions(Builder builder) { this.readOnlyCommands = builder.readOnlyCommands; this.requestQueueSize = builder.requestQueueSize; this.scriptCharset = builder.scriptCharset; + this.jsonParser = builder.jsonParser; this.socketOptions = builder.socketOptions; this.sslOptions = builder.sslOptions; this.suspendReconnectOnProtocolFailure = builder.suspendReconnectOnProtocolFailure; @@ -125,6 +132,7 @@ protected ClientOptions(ClientOptions original) { this.readOnlyCommands = original.getReadOnlyCommands(); this.requestQueueSize = original.getRequestQueueSize(); this.scriptCharset = original.getScriptCharset(); + this.jsonParser = original.getJsonParser(); this.socketOptions = original.getSocketOptions(); this.sslOptions = original.getSslOptions(); this.suspendReconnectOnProtocolFailure = original.isSuspendReconnectOnProtocolFailure(); @@ -184,6 +192,8 @@ public static class Builder { private Charset scriptCharset = DEFAULT_SCRIPT_CHARSET; + private JsonParser jsonParser = DEFAULT_JSON_PARSER; + private SocketOptions socketOptions = DEFAULT_SOCKET_OPTIONS; private SslOptions sslOptions = DEFAULT_SSL_OPTIONS; @@ -369,6 +379,21 @@ public Builder scriptCharset(Charset scriptCharset) { return this; } + /** + * Set a custom implementation for the {@link JsonParser} to use. Defaults to {@link DefaultJsonParser}. + * + * @param parser must not be {@code null}. + * @return {@code this} + * @see JsonParser + * @since 6.5 + */ + public Builder jsonParser(JsonParser parser) { + + LettuceAssert.notNull(parser, "JsonParser must not be null"); + this.jsonParser = parser; + return this; + } + /** * Sets the low-level {@link SocketOptions} for the connections kept to Redis servers. See * {@link #DEFAULT_SOCKET_OPTIONS}. @@ -449,9 +474,9 @@ public ClientOptions.Builder mutate() { .decodeBufferPolicy(getDecodeBufferPolicy()).disconnectedBehavior(getDisconnectedBehavior()) .readOnlyCommands(getReadOnlyCommands()).publishOnScheduler(isPublishOnScheduler()) .pingBeforeActivateConnection(isPingBeforeActivateConnection()).protocolVersion(getConfiguredProtocolVersion()) - .requestQueueSize(getRequestQueueSize()).scriptCharset(getScriptCharset()).socketOptions(getSocketOptions()) - .sslOptions(getSslOptions()).suspendReconnectOnProtocolFailure(isSuspendReconnectOnProtocolFailure()) - .timeoutOptions(getTimeoutOptions()); + .requestQueueSize(getRequestQueueSize()).scriptCharset(getScriptCharset()).jsonParser(getJsonParser()) + .socketOptions(getSocketOptions()).sslOptions(getSslOptions()) + .suspendReconnectOnProtocolFailure(isSuspendReconnectOnProtocolFailure()).timeoutOptions(getTimeoutOptions()); return builder; } @@ -609,6 +634,16 @@ public Charset getScriptCharset() { return scriptCharset; } + /** + * Returns the currently set up {@link JsonParser}. + * + * @return the implementation of the {@link JsonParser} to use. + * @since 6.5 + */ + public JsonParser getJsonParser() { + return jsonParser; + } + /** * Returns the {@link SocketOptions}. * diff --git a/src/main/java/io/lettuce/core/FutureSyncInvocationHandler.java b/src/main/java/io/lettuce/core/FutureSyncInvocationHandler.java index a080ead113..c251a35310 100644 --- a/src/main/java/io/lettuce/core/FutureSyncInvocationHandler.java +++ b/src/main/java/io/lettuce/core/FutureSyncInvocationHandler.java @@ -107,8 +107,8 @@ private static boolean isTxControlMethod(String methodName, Object[] args) { if (methodName.equals("dispatch") && args.length > 0 && args[0] instanceof ProtocolKeyword) { ProtocolKeyword keyword = (ProtocolKeyword) args[0]; - if (keyword.name().equals(CommandType.MULTI.name()) || keyword.name().equals(CommandType.EXEC.name()) - || keyword.name().equals(CommandType.DISCARD.name())) { + if (keyword.toString().equals(CommandType.MULTI.name()) || keyword.toString().equals(CommandType.EXEC.name()) + || keyword.toString().equals(CommandType.DISCARD.name())) { return true; } } diff --git a/src/main/java/io/lettuce/core/RedisAsyncCommandsImpl.java b/src/main/java/io/lettuce/core/RedisAsyncCommandsImpl.java index aeb17c53e8..23ded92df9 100644 --- a/src/main/java/io/lettuce/core/RedisAsyncCommandsImpl.java +++ b/src/main/java/io/lettuce/core/RedisAsyncCommandsImpl.java @@ -4,6 +4,7 @@ import io.lettuce.core.api.async.RedisAsyncCommands; import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands; import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonParser; /** * An asynchronous and thread-safe API for a Redis connection. @@ -22,8 +23,8 @@ public class RedisAsyncCommandsImpl extends AbstractRedisAsyncCommands connection, RedisCodec codec) { - super(connection, codec); + public RedisAsyncCommandsImpl(StatefulRedisConnection connection, RedisCodec codec, JsonParser parser) { + super(connection, codec, parser); } @Override diff --git a/src/main/java/io/lettuce/core/RedisClient.java b/src/main/java/io/lettuce/core/RedisClient.java index 550c5bf104..4a2c3e7bd3 100644 --- a/src/main/java/io/lettuce/core/RedisClient.java +++ b/src/main/java/io/lettuce/core/RedisClient.java @@ -38,6 +38,7 @@ import io.lettuce.core.internal.ExceptionFactory; import io.lettuce.core.internal.Futures; import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.json.JsonParser; import io.lettuce.core.masterreplica.MasterReplica; import io.lettuce.core.protocol.CommandExpiryWriter; import io.lettuce.core.protocol.CommandHandler; @@ -656,7 +657,7 @@ protected StatefulRedisPubSubConnectionImpl newStatefulRedisPubSubC */ protected StatefulRedisSentinelConnectionImpl newStatefulRedisSentinelConnection( RedisChannelWriter channelWriter, RedisCodec codec, Duration timeout) { - return new StatefulRedisSentinelConnectionImpl<>(channelWriter, codec, timeout); + return new StatefulRedisSentinelConnectionImpl<>(channelWriter, codec, timeout, getOptions().getJsonParser()); } /** @@ -674,7 +675,7 @@ protected StatefulRedisSentinelConnectionImpl newStatefulRedisSenti */ protected StatefulRedisConnectionImpl newStatefulRedisConnection(RedisChannelWriter channelWriter, PushHandler pushHandler, RedisCodec codec, Duration timeout) { - return new StatefulRedisConnectionImpl<>(channelWriter, pushHandler, codec, timeout); + return new StatefulRedisConnectionImpl<>(channelWriter, pushHandler, codec, timeout, getOptions().getJsonParser()); } /** diff --git a/src/main/java/io/lettuce/core/RedisCommandBuilder.java b/src/main/java/io/lettuce/core/RedisCommandBuilder.java index a192690d9e..d7db66b4df 100644 --- a/src/main/java/io/lettuce/core/RedisCommandBuilder.java +++ b/src/main/java/io/lettuce/core/RedisCommandBuilder.java @@ -62,16 +62,6 @@ @SuppressWarnings({ "unchecked", "varargs" }) class RedisCommandBuilder extends BaseRedisCommandBuilder { - private static final String MUST_NOT_CONTAIN_NULL_ELEMENTS = "must not contain null elements"; - - private static final String MUST_NOT_BE_EMPTY = "must not be empty"; - - private static final String MUST_NOT_BE_NULL = "must not be null"; - - private static final byte[] MINUS_BYTES = { '-' }; - - private static final byte[] PLUS_BYTES = { '+' }; - RedisCommandBuilder(RedisCodec codec) { super(codec); } @@ -4438,148 +4428,6 @@ Command>> clusterLinks() { return createCommand(CLUSTER, (CommandOutput) new ObjectOutput<>(StringCodec.UTF8), args); } - private boolean allElementsInstanceOf(Object[] objects, Class expectedAssignableType) { - - for (Object object : objects) { - if (!expectedAssignableType.isAssignableFrom(object.getClass())) { - return false; - } - } - - return true; - } - - private byte[] maxValue(Range range) { - - Boundary upper = range.getUpper(); - - if (upper.getValue() == null) { - return PLUS_BYTES; - } - - ByteBuffer encoded = codec.encodeValue(upper.getValue()); - ByteBuffer allocated = ByteBuffer.allocate(encoded.remaining() + 1); - allocated.put(upper.isIncluding() ? (byte) '[' : (byte) '(').put(encoded); - - return allocated.array(); - } - - private byte[] minValue(Range range) { - - Boundary lower = range.getLower(); - - if (lower.getValue() == null) { - return MINUS_BYTES; - } - - ByteBuffer encoded = codec.encodeValue(lower.getValue()); - ByteBuffer allocated = ByteBuffer.allocate(encoded.remaining() + 1); - allocated.put(lower.isIncluding() ? (byte) '[' : (byte) '(').put(encoded); - - return allocated.array(); - } - - static void notNull(ScoredValueStreamingChannel channel) { - LettuceAssert.notNull(channel, "ScoredValueStreamingChannel " + MUST_NOT_BE_NULL); - } - - static void notNull(KeyStreamingChannel channel) { - LettuceAssert.notNull(channel, "KeyValueStreamingChannel " + MUST_NOT_BE_NULL); - } - - static void notNull(ValueStreamingChannel channel) { - LettuceAssert.notNull(channel, "ValueStreamingChannel " + MUST_NOT_BE_NULL); - } - - static void notNull(KeyValueStreamingChannel channel) { - LettuceAssert.notNull(channel, "KeyValueStreamingChannel " + MUST_NOT_BE_NULL); - } - - static void notNullMinMax(String min, String max) { - LettuceAssert.notNull(min, "Min " + MUST_NOT_BE_NULL); - LettuceAssert.notNull(max, "Max " + MUST_NOT_BE_NULL); - } - - private static void addLimit(CommandArgs args, Limit limit) { - - if (limit.isLimited()) { - args.add(LIMIT).add(limit.getOffset()).add(limit.getCount()); - } - } - - private static void assertNodeId(String nodeId) { - LettuceAssert.notNull(nodeId, "NodeId " + MUST_NOT_BE_NULL); - LettuceAssert.notEmpty(nodeId, "NodeId " + MUST_NOT_BE_EMPTY); - } - - private static String max(Range range) { - - Boundary upper = range.getUpper(); - - if (upper.getValue() == null - || upper.getValue() instanceof Double && upper.getValue().doubleValue() == Double.POSITIVE_INFINITY) { - return "+inf"; - } - - if (!upper.isIncluding()) { - return "(" + upper.getValue(); - } - - return upper.getValue().toString(); - } - - private static String min(Range range) { - - Boundary lower = range.getLower(); - - if (lower.getValue() == null - || lower.getValue() instanceof Double && lower.getValue().doubleValue() == Double.NEGATIVE_INFINITY) { - return "-inf"; - } - - if (!lower.isIncluding()) { - return "(" + lower.getValue(); - } - - return lower.getValue().toString(); - } - - private static void notEmpty(Object[] keys) { - LettuceAssert.notNull(keys, "Keys " + MUST_NOT_BE_NULL); - LettuceAssert.notEmpty(keys, "Keys " + MUST_NOT_BE_EMPTY); - } - - private static void notEmptySlots(int[] slots) { - LettuceAssert.notNull(slots, "Slots " + MUST_NOT_BE_NULL); - LettuceAssert.notEmpty(slots, "Slots " + MUST_NOT_BE_EMPTY); - } - - private static void notEmptyValues(Object[] values) { - LettuceAssert.notNull(values, "Values " + MUST_NOT_BE_NULL); - LettuceAssert.notEmpty(values, "Values " + MUST_NOT_BE_EMPTY); - } - - private static void notNullKey(Object key) { - LettuceAssert.notNull(key, "Key " + MUST_NOT_BE_NULL); - } - - private static void keyAndFieldsProvided(Object key, Object[] fields) { - LettuceAssert.notNull(key, "Key " + MUST_NOT_BE_NULL); - LettuceAssert.notEmpty(fields, "Fields " + MUST_NOT_BE_EMPTY); - } - - private static void notNullLimit(Limit limit) { - LettuceAssert.notNull(limit, "Limit " + MUST_NOT_BE_NULL); - } - - private static void notNullRange(Range range) { - LettuceAssert.notNull(range, "Range " + MUST_NOT_BE_NULL); - } - - private static void notEmptyRanges(Range[] ranges) { - LettuceAssert.notEmpty(ranges, "Ranges " + MUST_NOT_BE_NULL); - } - enum LongCodec implements RedisCodec { INSTANCE; diff --git a/src/main/java/io/lettuce/core/RedisJsonCommandBuilder.java b/src/main/java/io/lettuce/core/RedisJsonCommandBuilder.java new file mode 100644 index 0000000000..fb1e580179 --- /dev/null +++ b/src/main/java/io/lettuce/core/RedisJsonCommandBuilder.java @@ -0,0 +1,329 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core; + +import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonParser; +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; +import io.lettuce.core.output.*; +import io.lettuce.core.protocol.BaseRedisCommandBuilder; +import io.lettuce.core.protocol.Command; +import io.lettuce.core.protocol.CommandArgs; + +import java.util.List; + +import static io.lettuce.core.protocol.CommandType.*; + +/** + * Implementation of the {@link BaseRedisCommandBuilder} handling JSON commands. + * + * @author Tihomir Mateev + * @since 6.5 + */ +class RedisJsonCommandBuilder extends BaseRedisCommandBuilder { + + private final JsonParser parser; + + RedisJsonCommandBuilder(RedisCodec codec, JsonParser theParser) { + super(codec); + parser = theParser; + } + + Command> jsonArrappend(K key, JsonPath jsonPath, JsonValue... jsonValues) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + for (JsonValue value : jsonValues) { + args.add(value.asByteBuffer().array()); + } + + return createCommand(JSON_ARRAPPEND, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command> jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + args.add(value.asByteBuffer().array()); + + if (range != null) { + // OPTIONAL as per API + range.build(args); + } + + return createCommand(JSON_ARRINDEX, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command> jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + args.add(index); + + for (JsonValue value : values) { + args.add(value.asByteBuffer().array()); + } + + return createCommand(JSON_ARRINSERT, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command> jsonArrlen(K key, JsonPath jsonPath) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + return createCommand(JSON_ARRLEN, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command> jsonArrpop(K key, JsonPath jsonPath, int index) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null) { + args.add(jsonPath.toString()); + + if (index != -1) { + args.add(index); + } + } + + return createCommand(JSON_ARRPOP, new JsonValueListOutput<>(codec, parser), args); + } + + Command> jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range) { + + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + if (range != null) { + range.build(args); + } + + return createCommand(JSON_ARRTRIM, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command jsonClear(K key, JsonPath jsonPath) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + return createCommand(JSON_CLEAR, new IntegerOutput<>(codec), args); + } + + Command> jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (options != null) { + options.build(args); + } + + if (jsonPaths != null) { + for (JsonPath jsonPath : jsonPaths) { + if (jsonPath != null) { + args.add(jsonPath.toString()); + } + } + } + + return createCommand(JSON_GET, new JsonValueListOutput<>(codec, parser), args); + } + + Command jsonMerge(K key, JsonPath jsonPath, JsonValue value) { + + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + args.add(value.asByteBuffer().array()); + + return createCommand(JSON_MERGE, new StatusOutput<>(codec), args); + } + + Command> jsonMGet(JsonPath jsonPath, K... keys) { + notEmpty(keys); + + CommandArgs args = new CommandArgs<>(codec).addKeys(keys); + + if (jsonPath != null) { + args.add(jsonPath.toString()); + } + + return createCommand(JSON_MGET, new JsonValueListOutput<>(codec, parser), args); + } + + Command jsonMSet(List> arguments) { + + notEmpty(arguments.toArray()); + + CommandArgs args = new CommandArgs<>(codec); + + for (JsonMsetArgs argument : arguments) { + argument.build(args); + } + + return createCommand(JSON_MSET, new StatusOutput<>(codec), args); + } + + Command> jsonNumincrby(K key, JsonPath jsonPath, Number number) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + args.add(number.toString()); + + return createCommand(JSON_NUMINCRBY, new NumberListOutput<>(codec), args); + } + + Command> jsonObjkeys(K key, JsonPath jsonPath) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + return createCommand(JSON_OBJKEYS, new ValueListOutput<>(codec), args); + } + + Command> jsonObjlen(K key, JsonPath jsonPath) { + + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + return createCommand(JSON_OBJLEN, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + args.add(jsonPath.toString()); + + args.add(value.asByteBuffer().array()); + + if (options != null) { + options.build(args); + } + + return createCommand(JSON_SET, new StatusOutput<>(codec), args); + } + + Command> jsonStrappend(K key, JsonPath jsonPath, JsonValue value) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + args.add(value.asByteBuffer().array()); + + return createCommand(JSON_STRAPPEND, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command> jsonStrlen(K key, JsonPath jsonPath) { + + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + return createCommand(JSON_STRLEN, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command> jsonToggle(K key, JsonPath jsonPath) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + return createCommand(JSON_TOGGLE, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command jsonDel(K key, JsonPath jsonPath) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + return createCommand(JSON_DEL, new IntegerOutput<>(codec), args); + } + + Command> jsonType(K key, JsonPath jsonPath) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + return createCommand(JSON_TYPE, new JsonTypeListOutput<>(codec), args); + } + +} diff --git a/src/main/java/io/lettuce/core/RedisReactiveCommandsImpl.java b/src/main/java/io/lettuce/core/RedisReactiveCommandsImpl.java index 6b28b8e051..23ffd71ef2 100644 --- a/src/main/java/io/lettuce/core/RedisReactiveCommandsImpl.java +++ b/src/main/java/io/lettuce/core/RedisReactiveCommandsImpl.java @@ -4,6 +4,7 @@ import io.lettuce.core.api.reactive.RedisReactiveCommands; import io.lettuce.core.cluster.api.reactive.RedisClusterReactiveCommands; import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonParser; /** * A reactive and thread-safe API for a Redis Sentinel connection. @@ -22,8 +23,8 @@ public class RedisReactiveCommandsImpl extends AbstractRedisReactiveComman * @param codec the codec for command encoding. * */ - public RedisReactiveCommandsImpl(StatefulRedisConnection connection, RedisCodec codec) { - super(connection, codec); + public RedisReactiveCommandsImpl(StatefulRedisConnection connection, RedisCodec codec, JsonParser parser) { + super(connection, codec, parser); } @Override diff --git a/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java b/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java index 5f64e272e8..1948062a59 100644 --- a/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java +++ b/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java @@ -36,6 +36,7 @@ import io.lettuce.core.cluster.api.sync.RedisClusterCommands; import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.json.JsonParser; import io.lettuce.core.output.MultiOutput; import io.lettuce.core.output.StatusOutput; import io.lettuce.core.protocol.*; @@ -64,6 +65,8 @@ public class StatefulRedisConnectionImpl extends RedisChannelHandler private final PushHandler pushHandler; + private final JsonParser parser; + protected MultiOutput multi; /** @@ -75,12 +78,13 @@ public class StatefulRedisConnectionImpl extends RedisChannelHandler * @param timeout Maximum time to wait for a response. */ public StatefulRedisConnectionImpl(RedisChannelWriter writer, PushHandler pushHandler, RedisCodec codec, - Duration timeout) { + Duration timeout, JsonParser parser) { super(writer, timeout); this.pushHandler = pushHandler; this.codec = codec; + this.parser = parser; this.async = newRedisAsyncCommandsImpl(); this.sync = newRedisSyncCommandsImpl(); this.reactive = newRedisReactiveCommandsImpl(); @@ -110,7 +114,7 @@ protected RedisCommands newRedisSyncCommandsImpl() { * @return a new instance */ protected RedisAsyncCommandsImpl newRedisAsyncCommandsImpl() { - return new RedisAsyncCommandsImpl<>(this, codec); + return new RedisAsyncCommandsImpl<>(this, codec, parser); } @Override @@ -124,7 +128,7 @@ public RedisReactiveCommands reactive() { * @return a new instance */ protected RedisReactiveCommandsImpl newRedisReactiveCommandsImpl() { - return new RedisReactiveCommandsImpl<>(this, codec); + return new RedisReactiveCommandsImpl<>(this, codec, parser); } @Override @@ -184,7 +188,7 @@ public RedisCommand dispatch(RedisCommand command) { private void potentiallyEnableMulti(RedisCommand command) { - if (command.getType().name().equals(MULTI.name())) { + if (command.getType().toString().equals(MULTI.name())) { multi = (multi == null ? new MultiOutput<>(codec) : multi); @@ -202,7 +206,7 @@ protected RedisCommand preProcessCommand(RedisCommand comm RedisCommand local = command; - if (local.getType().name().equals(AUTH.name())) { + if (local.getType().toString().equals(AUTH.name())) { local = attachOnComplete(local, status -> { if ("OK".equals(status)) { @@ -219,7 +223,7 @@ protected RedisCommand preProcessCommand(RedisCommand comm }); } - if (local.getType().name().equals(SELECT.name())) { + if (local.getType().toString().equals(SELECT.name())) { local = attachOnComplete(local, status -> { if ("OK".equals(status)) { Long db = CommandArgsAccessor.getFirstInteger(command.getArgs()); @@ -230,7 +234,7 @@ protected RedisCommand preProcessCommand(RedisCommand comm }); } - if (local.getType().name().equals(READONLY.name())) { + if (local.getType().toString().equals(READONLY.name())) { local = attachOnComplete(local, status -> { if ("OK".equals(status)) { state.setReadOnly(true); @@ -238,7 +242,7 @@ protected RedisCommand preProcessCommand(RedisCommand comm }); } - if (local.getType().name().equals(READWRITE.name())) { + if (local.getType().toString().equals(READWRITE.name())) { local = attachOnComplete(local, status -> { if ("OK".equals(status)) { state.setReadOnly(false); @@ -246,14 +250,14 @@ protected RedisCommand preProcessCommand(RedisCommand comm }); } - if (local.getType().name().equals(DISCARD.name())) { + if (local.getType().toString().equals(DISCARD.name())) { if (multi != null) { multi.cancel(); multi = null; } } - if (local.getType().name().equals(EXEC.name())) { + if (local.getType().toString().equals(EXEC.name())) { MultiOutput multiOutput = this.multi; this.multi = null; if (multiOutput == null) { @@ -262,7 +266,7 @@ protected RedisCommand preProcessCommand(RedisCommand comm local.setOutput((MultiOutput) multiOutput); } - if (multi != null && !local.getType().name().equals(MULTI.name())) { + if (multi != null && !local.getType().toString().equals(MULTI.name())) { local = new TransactionalCommand<>(local); multi.add(local); } diff --git a/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java index be442dd70b..6ff3ef9ad1 100644 --- a/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java +++ b/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java @@ -22,6 +22,7 @@ import io.lettuce.core.RedisFuture; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands; +import io.lettuce.core.json.JsonParser; /** * A complete asynchronous and thread-safe Redis API with 400+ Methods. @@ -36,7 +37,7 @@ public interface RedisAsyncCommands extends BaseRedisAsyncCommands, RedisHashAsyncCommands, RedisHLLAsyncCommands, RedisKeyAsyncCommands, RedisListAsyncCommands, RedisScriptingAsyncCommands, RedisServerAsyncCommands, RedisSetAsyncCommands, RedisSortedSetAsyncCommands, RedisStreamAsyncCommands, RedisStringAsyncCommands, - RedisTransactionalAsyncCommands { + RedisTransactionalAsyncCommands, RedisJsonAsyncCommands { /** * Authenticate to the server. @@ -81,4 +82,10 @@ public interface RedisAsyncCommands extends BaseRedisAsyncCommands, @Deprecated StatefulRedisConnection getStatefulConnection(); + /** + * @return the currently configured instance of the {@link JsonParser} + * @since 6.5 + */ + JsonParser getJsonParser(); + } diff --git a/src/main/java/io/lettuce/core/api/async/RedisJsonAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisJsonAsyncCommands.java new file mode 100644 index 0000000000..74494e2718 --- /dev/null +++ b/src/main/java/io/lettuce/core/api/async/RedisJsonAsyncCommands.java @@ -0,0 +1,441 @@ +/* + * Copyright 2017-2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.api.async; + +import java.util.List; +import io.lettuce.core.RedisFuture; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; + +/** + * Asynchronous executed commands for JSON documents + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @see Redis JSON + * @since 6.5 + * @generated by io.lettuce.apigenerator.CreateAsyncApi + */ +public interface RedisJsonAsyncCommands { + + /** + * Append the JSON values into the array at a given {@link JsonPath} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonArrappend(K key, JsonPath jsonPath, JsonValue... values); + + /** + * Append the JSON values into the array at the {@link JsonPath#ROOT_PATH} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonArrappend(K key, JsonValue... values); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @param range the {@link JsonRangeArgs} to search within. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + RedisFuture> jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. This + * method uses defaults for the start and end indexes, see {@link JsonRangeArgs#DEFAULT_START_INDEX} and + * {@link JsonRangeArgs#DEFAULT_END_INDEX}. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + RedisFuture> jsonArrindex(K key, JsonPath jsonPath, JsonValue value); + + /** + * Insert the {@link JsonValue}s into the array at a given {@link JsonPath} before the provided index, shifting the existing + * elements to the right + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index before which the new elements will be inserted. + * @param values one or more {@link JsonValue}s to be inserted. + * @return Long the resulting size of the arrays after the new data was inserted, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values); + + /** + * Report the length of the JSON array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonArrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON array at a the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonArrlen(K key); + + /** + * Remove and return {@link JsonValue} at a given index in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index of the element to be removed. Default is -1, meaning the last element. Out-of-range indexes round + * to their respective array ends. Popping an empty array returns null. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + RedisFuture> jsonArrpop(K key, JsonPath jsonPath, int index); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + RedisFuture> jsonArrpop(K key, JsonPath jsonPath); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + RedisFuture> jsonArrpop(K key); + + /** + * Trim an array at a given {@link JsonPath} so that it contains only the specified inclusive range of elements. All + * elements with indexes smaller than the start range and all elements with indexes bigger than the end range are trimmed. + *

+ * Behavior as of RedisJSON v2.0: + *

    + *
  • If start is larger than the array's size or start > stop, returns 0 and an empty array.
  • + *
  • If start is < 0, then start from the end of the array.
  • + *
  • If stop is larger than the end of the array, it is treated like the last element.
  • + *
+ * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param range the {@link JsonRangeArgs} to trim by. + * @return Long the resulting size of the arrays after the trimming, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + RedisFuture jsonClear(K key, JsonPath jsonPath); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + RedisFuture jsonClear(K key); + + /** + * Deletes a value inside the JSON document at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + RedisFuture jsonDel(K key, JsonPath jsonPath); + + /** + * Deletes a value inside the JSON document at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + RedisFuture jsonDel(K key); + + /** + * Return the value at the specified path in JSON serialized form. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param options the {@link JsonGetArgs} to use. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths); + + /** + * Return the value at the specified path in JSON serialized form. Uses defaults for the {@link JsonGetArgs}. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonGet(K key, JsonPath... jsonPaths); + + /** + * Merge a given {@link JsonValue} with the value matching {@link JsonPath}. Consequently, JSON values at matching paths are + * updated, deleted, or expanded with new children. + *

+ * Merging is done according to the following rules per JSON value in the value argument while considering the corresponding + * original value if it exists: + *

    + *
  • merging an existing object key with a null value deletes the key
  • + *
  • merging an existing object key with non-null value updates the value
  • + *
  • merging a non-existing object key adds the key and value
  • + *
  • merging an existing array with any merged value, replaces the entire array with the value
  • + *
+ *

+ * This command complies with RFC7396 "Json Merge Patch" + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to merge. + * @param value the {@link JsonValue} to merge. + * @return String "OK" if the set was successful, error if the operation failed. + * @since 6.5 + * @see RFC7396 + */ + RedisFuture jsonMerge(K key, JsonPath jsonPath, JsonValue value); + + /** + * Return the values at the specified path from multiple key arguments. + * + * @param jsonPath the {@link JsonPath} pointing to the value to fetch. + * @param keys the keys holding the {@link JsonValue}s to fetch. + * @return List the values at path, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonMGet(JsonPath jsonPath, K... keys); + + /** + * Set or update one or more JSON values according to the specified {@link JsonMsetArgs} + *

+ * JSON.MSET is atomic, hence, all given additions or updates are either applied or not. It is not possible for clients to + * see that some keys were updated while others are unchanged. + *

+ * A JSON value is a hierarchical structure. If you change a value in a specific path - nested values are affected. + * + * @param arguments the {@link JsonMsetArgs} specifying the values to change. + * @return "OK" if the operation was successful, error otherwise + * @since 6.5 + */ + RedisFuture jsonMSet(List> arguments); + + /** + * Increment the number value stored at the specified {@link JsonPath} in the JSON document by the provided increment. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to increment. + * @param number the increment value. + * @return a {@link List} of the new values after the increment. + * @since 6.5 + */ + RedisFuture> jsonNumincrby(K key, JsonPath jsonPath, Number number); + + /** + * Return the keys in the JSON document that are referenced by the given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + RedisFuture> jsonObjkeys(K key, JsonPath jsonPath); + + /** + * Return the keys in the JSON document that are referenced by the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + RedisFuture> jsonObjkeys(K key); + + /** + * Report the number of keys in the JSON object at the specified {@link JsonPath} and for the provided key + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want to count + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonObjlen(K key, JsonPath jsonPath); + + /** + * Report the number of keys in the JSON object at the {@link JsonPath#ROOT_PATH} and for the provided key + * + * @param key the key holding the JSON document. + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonObjlen(K key); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document. + *

+ * For new Redis keys, the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @param options the {@link JsonSetArgs} the options for setting the value. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + RedisFuture jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document using defaults for the {@link JsonSetArgs}. + *

+ * For new Redis keys the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + RedisFuture jsonSet(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to append the value. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + RedisFuture> jsonStrappend(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + RedisFuture> jsonStrappend(K key, JsonValue value); + + /** + * Report the length of the JSON String at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + RedisFuture> jsonStrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON String at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + RedisFuture> jsonStrlen(K key); + + /** + * Toggle a Boolean value stored at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the new value after the toggle, 0 for false, 1 for true or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonToggle(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + RedisFuture> jsonType(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + RedisFuture> jsonType(K key); + +} diff --git a/src/main/java/io/lettuce/core/api/reactive/BaseRedisReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/BaseRedisReactiveCommands.java index b0d089b237..098500c1e5 100644 --- a/src/main/java/io/lettuce/core/api/reactive/BaseRedisReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/BaseRedisReactiveCommands.java @@ -21,6 +21,7 @@ import java.util.Map; +import io.lettuce.core.json.JsonParser; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import io.lettuce.core.output.CommandOutput; @@ -228,4 +229,10 @@ public interface BaseRedisReactiveCommands { @Deprecated void flushCommands(); + /** + * @return the currently configured instance of the {@link JsonParser} + * @since 6.5 + */ + JsonParser getJsonParser(); + } diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisJsonReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisJsonReactiveCommands.java new file mode 100644 index 0000000000..de5e060125 --- /dev/null +++ b/src/main/java/io/lettuce/core/api/reactive/RedisJsonReactiveCommands.java @@ -0,0 +1,442 @@ +/* + * Copyright 2017-2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.api.reactive; + +import java.util.List; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Reactive executed commands for JSON documents + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @see Redis JSON + * @since 6.5 + * @generated by io.lettuce.apigenerator.CreateReactiveApi + */ +public interface RedisJsonReactiveCommands { + + /** + * Append the JSON values into the array at a given {@link JsonPath} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonArrappend(K key, JsonPath jsonPath, JsonValue... values); + + /** + * Append the JSON values into the array at the {@link JsonPath#ROOT_PATH} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonArrappend(K key, JsonValue... values); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @param range the {@link JsonRangeArgs} to search within. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + Flux jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. This + * method uses defaults for the start and end indexes, see {@link JsonRangeArgs#DEFAULT_START_INDEX} and + * {@link JsonRangeArgs#DEFAULT_END_INDEX}. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + Flux jsonArrindex(K key, JsonPath jsonPath, JsonValue value); + + /** + * Insert the {@link JsonValue}s into the array at a given {@link JsonPath} before the provided index, shifting the existing + * elements to the right + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index before which the new elements will be inserted. + * @param values one or more {@link JsonValue}s to be inserted. + * @return Long the resulting size of the arrays after the new data was inserted, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values); + + /** + * Report the length of the JSON array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonArrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON array at a the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonArrlen(K key); + + /** + * Remove and return {@link JsonValue} at a given index in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index of the element to be removed. Default is -1, meaning the last element. Out-of-range indexes round + * to their respective array ends. Popping an empty array returns null. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + Flux jsonArrpop(K key, JsonPath jsonPath, int index); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + Flux jsonArrpop(K key, JsonPath jsonPath); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + Flux jsonArrpop(K key); + + /** + * Trim an array at a given {@link JsonPath} so that it contains only the specified inclusive range of elements. All + * elements with indexes smaller than the start range and all elements with indexes bigger than the end range are trimmed. + *

+ * Behavior as of RedisJSON v2.0: + *

    + *
  • If start is larger than the array's size or start > stop, returns 0 and an empty array.
  • + *
  • If start is < 0, then start from the end of the array.
  • + *
  • If stop is larger than the end of the array, it is treated like the last element.
  • + *
+ * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param range the {@link JsonRangeArgs} to trim by. + * @return Long the resulting size of the arrays after the trimming, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + Mono jsonClear(K key, JsonPath jsonPath); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + Mono jsonClear(K key); + + /** + * Deletes a value inside the JSON document at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + Mono jsonDel(K key, JsonPath jsonPath); + + /** + * Deletes a value inside the JSON document at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + Mono jsonDel(K key); + + /** + * Return the value at the specified path in JSON serialized form. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param options the {@link JsonGetArgs} to use. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths); + + /** + * Return the value at the specified path in JSON serialized form. Uses defaults for the {@link JsonGetArgs}. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonGet(K key, JsonPath... jsonPaths); + + /** + * Merge a given {@link JsonValue} with the value matching {@link JsonPath}. Consequently, JSON values at matching paths are + * updated, deleted, or expanded with new children. + *

+ * Merging is done according to the following rules per JSON value in the value argument while considering the corresponding + * original value if it exists: + *

    + *
  • merging an existing object key with a null value deletes the key
  • + *
  • merging an existing object key with non-null value updates the value
  • + *
  • merging a non-existing object key adds the key and value
  • + *
  • merging an existing array with any merged value, replaces the entire array with the value
  • + *
+ *

+ * This command complies with RFC7396 "Json Merge Patch" + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to merge. + * @param value the {@link JsonValue} to merge. + * @return String "OK" if the set was successful, error if the operation failed. + * @since 6.5 + * @see RFC7396 + */ + Mono jsonMerge(K key, JsonPath jsonPath, JsonValue value); + + /** + * Return the values at the specified path from multiple key arguments. + * + * @param jsonPath the {@link JsonPath} pointing to the value to fetch. + * @param keys the keys holding the {@link JsonValue}s to fetch. + * @return List the values at path, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonMGet(JsonPath jsonPath, K... keys); + + /** + * Set or update one or more JSON values according to the specified {@link JsonMsetArgs} + *

+ * JSON.MSET is atomic, hence, all given additions or updates are either applied or not. It is not possible for clients to + * see that some keys were updated while others are unchanged. + *

+ * A JSON value is a hierarchical structure. If you change a value in a specific path - nested values are affected. + * + * @param arguments the {@link JsonMsetArgs} specifying the values to change. + * @return "OK" if the operation was successful, error otherwise + * @since 6.5 + */ + Mono jsonMSet(List> arguments); + + /** + * Increment the number value stored at the specified {@link JsonPath} in the JSON document by the provided increment. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to increment. + * @param number the increment value. + * @return a {@link List} of the new values after the increment. + * @since 6.5 + */ + Flux jsonNumincrby(K key, JsonPath jsonPath, Number number); + + /** + * Return the keys in the JSON document that are referenced by the given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + Flux jsonObjkeys(K key, JsonPath jsonPath); + + /** + * Return the keys in the JSON document that are referenced by the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + Flux jsonObjkeys(K key); + + /** + * Report the number of keys in the JSON object at the specified {@link JsonPath} and for the provided key + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want to count + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonObjlen(K key, JsonPath jsonPath); + + /** + * Report the number of keys in the JSON object at the {@link JsonPath#ROOT_PATH} and for the provided key + * + * @param key the key holding the JSON document. + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonObjlen(K key); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document. + *

+ * For new Redis keys, the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @param options the {@link JsonSetArgs} the options for setting the value. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + Mono jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document using defaults for the {@link JsonSetArgs}. + *

+ * For new Redis keys the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + Mono jsonSet(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to append the value. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + Flux jsonStrappend(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + Flux jsonStrappend(K key, JsonValue value); + + /** + * Report the length of the JSON String at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + Flux jsonStrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON String at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + Flux jsonStrlen(K key); + + /** + * Toggle a Boolean value stored at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the new value after the toggle, 0 for false, 1 for true or null if the path does not exist. + * @since 6.5 + */ + Flux jsonToggle(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + Flux jsonType(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + Flux jsonType(K key); + +} diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java index 346f79b4bf..2f75efcc92 100644 --- a/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java @@ -36,7 +36,7 @@ public interface RedisReactiveCommands extends BaseRedisReactiveCommands, RedisHLLReactiveCommands, RedisKeyReactiveCommands, RedisListReactiveCommands, RedisScriptingReactiveCommands, RedisServerReactiveCommands, RedisSetReactiveCommands, RedisSortedSetReactiveCommands, RedisStreamReactiveCommands, - RedisStringReactiveCommands, RedisTransactionalReactiveCommands { + RedisStringReactiveCommands, RedisTransactionalReactiveCommands, RedisJsonReactiveCommands { /** * Authenticate to the server. diff --git a/src/main/java/io/lettuce/core/api/sync/RedisCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisCommands.java index efc6005bb0..98f21b4cb2 100644 --- a/src/main/java/io/lettuce/core/api/sync/RedisCommands.java +++ b/src/main/java/io/lettuce/core/api/sync/RedisCommands.java @@ -21,6 +21,7 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.cluster.api.sync.RedisClusterCommands; +import io.lettuce.core.json.JsonParser; /** * @@ -31,11 +32,11 @@ * @author Mark Paluch * @since 3.0 */ -public interface RedisCommands - extends BaseRedisCommands, RedisAclCommands, RedisClusterCommands, RedisFunctionCommands, - RedisGeoCommands, RedisHashCommands, RedisHLLCommands, RedisKeyCommands, - RedisListCommands, RedisScriptingCommands, RedisServerCommands, RedisSetCommands, - RedisSortedSetCommands, RedisStreamCommands, RedisStringCommands, RedisTransactionalCommands { +public interface RedisCommands extends BaseRedisCommands, RedisAclCommands, RedisClusterCommands, + RedisFunctionCommands, RedisGeoCommands, RedisHashCommands, RedisHLLCommands, + RedisKeyCommands, RedisListCommands, RedisScriptingCommands, RedisServerCommands, + RedisSetCommands, RedisSortedSetCommands, RedisStreamCommands, RedisStringCommands, + RedisTransactionalCommands, RedisJsonCommands { /** * Authenticate to the server. @@ -80,4 +81,10 @@ public interface RedisCommands @Deprecated StatefulRedisConnection getStatefulConnection(); + /** + * @return the currently configured instance of the {@link JsonParser} + * @since 6.5 + */ + JsonParser getJsonParser(); + } diff --git a/src/main/java/io/lettuce/core/api/sync/RedisJsonCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisJsonCommands.java new file mode 100644 index 0000000000..006e382283 --- /dev/null +++ b/src/main/java/io/lettuce/core/api/sync/RedisJsonCommands.java @@ -0,0 +1,440 @@ +/* + * Copyright 2017-2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.api.sync; + +import java.util.List; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; + +/** + * Synchronous executed commands for JSON documents + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @see Redis JSON + * @since 6.5 + * @generated by io.lettuce.apigenerator.CreateSyncApi + */ +public interface RedisJsonCommands { + + /** + * Append the JSON values into the array at a given {@link JsonPath} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrappend(K key, JsonPath jsonPath, JsonValue... values); + + /** + * Append the JSON values into the array at the {@link JsonPath#ROOT_PATH} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrappend(K key, JsonValue... values); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @param range the {@link JsonRangeArgs} to search within. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. This + * method uses defaults for the start and end indexes, see {@link JsonRangeArgs#DEFAULT_START_INDEX} and + * {@link JsonRangeArgs#DEFAULT_END_INDEX}. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrindex(K key, JsonPath jsonPath, JsonValue value); + + /** + * Insert the {@link JsonValue}s into the array at a given {@link JsonPath} before the provided index, shifting the existing + * elements to the right + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index before which the new elements will be inserted. + * @param values one or more {@link JsonValue}s to be inserted. + * @return Long the resulting size of the arrays after the new data was inserted, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values); + + /** + * Report the length of the JSON array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON array at a the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrlen(K key); + + /** + * Remove and return {@link JsonValue} at a given index in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index of the element to be removed. Default is -1, meaning the last element. Out-of-range indexes round + * to their respective array ends. Popping an empty array returns null. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrpop(K key, JsonPath jsonPath, int index); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrpop(K key, JsonPath jsonPath); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrpop(K key); + + /** + * Trim an array at a given {@link JsonPath} so that it contains only the specified inclusive range of elements. All + * elements with indexes smaller than the start range and all elements with indexes bigger than the end range are trimmed. + *

+ * Behavior as of RedisJSON v2.0: + *

    + *
  • If start is larger than the array's size or start > stop, returns 0 and an empty array.
  • + *
  • If start is < 0, then start from the end of the array.
  • + *
  • If stop is larger than the end of the array, it is treated like the last element.
  • + *
+ * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param range the {@link JsonRangeArgs} to trim by. + * @return Long the resulting size of the arrays after the trimming, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + Long jsonClear(K key, JsonPath jsonPath); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + Long jsonClear(K key); + + /** + * Deletes a value inside the JSON document at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + Long jsonDel(K key, JsonPath jsonPath); + + /** + * Deletes a value inside the JSON document at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + Long jsonDel(K key); + + /** + * Return the value at the specified path in JSON serialized form. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param options the {@link JsonGetArgs} to use. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + List jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths); + + /** + * Return the value at the specified path in JSON serialized form. Uses defaults for the {@link JsonGetArgs}. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + List jsonGet(K key, JsonPath... jsonPaths); + + /** + * Merge a given {@link JsonValue} with the value matching {@link JsonPath}. Consequently, JSON values at matching paths are + * updated, deleted, or expanded with new children. + *

+ * Merging is done according to the following rules per JSON value in the value argument while considering the corresponding + * original value if it exists: + *

    + *
  • merging an existing object key with a null value deletes the key
  • + *
  • merging an existing object key with non-null value updates the value
  • + *
  • merging a non-existing object key adds the key and value
  • + *
  • merging an existing array with any merged value, replaces the entire array with the value
  • + *
+ *

+ * This command complies with RFC7396 "Json Merge Patch" + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to merge. + * @param value the {@link JsonValue} to merge. + * @return String "OK" if the set was successful, error if the operation failed. + * @since 6.5 + * @see RFC7396 + */ + String jsonMerge(K key, JsonPath jsonPath, JsonValue value); + + /** + * Return the values at the specified path from multiple key arguments. + * + * @param jsonPath the {@link JsonPath} pointing to the value to fetch. + * @param keys the keys holding the {@link JsonValue}s to fetch. + * @return List the values at path, or null if the path does not exist. + * @since 6.5 + */ + List jsonMGet(JsonPath jsonPath, K... keys); + + /** + * Set or update one or more JSON values according to the specified {@link JsonMsetArgs} + *

+ * JSON.MSET is atomic, hence, all given additions or updates are either applied or not. It is not possible for clients to + * see that some keys were updated while others are unchanged. + *

+ * A JSON value is a hierarchical structure. If you change a value in a specific path - nested values are affected. + * + * @param arguments the {@link JsonMsetArgs} specifying the values to change. + * @return "OK" if the operation was successful, error otherwise + * @since 6.5 + */ + String jsonMSet(List> arguments); + + /** + * Increment the number value stored at the specified {@link JsonPath} in the JSON document by the provided increment. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to increment. + * @param number the increment value. + * @return a {@link List} of the new values after the increment. + * @since 6.5 + */ + List jsonNumincrby(K key, JsonPath jsonPath, Number number); + + /** + * Return the keys in the JSON document that are referenced by the given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + List jsonObjkeys(K key, JsonPath jsonPath); + + /** + * Return the keys in the JSON document that are referenced by the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + List jsonObjkeys(K key); + + /** + * Report the number of keys in the JSON object at the specified {@link JsonPath} and for the provided key + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want to count + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + List jsonObjlen(K key, JsonPath jsonPath); + + /** + * Report the number of keys in the JSON object at the {@link JsonPath#ROOT_PATH} and for the provided key + * + * @param key the key holding the JSON document. + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + List jsonObjlen(K key); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document. + *

+ * For new Redis keys, the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @param options the {@link JsonSetArgs} the options for setting the value. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + String jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document using defaults for the {@link JsonSetArgs}. + *

+ * For new Redis keys the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + String jsonSet(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to append the value. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + List jsonStrappend(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + List jsonStrappend(K key, JsonValue value); + + /** + * Report the length of the JSON String at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + List jsonStrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON String at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + List jsonStrlen(K key); + + /** + * Toggle a Boolean value stored at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the new value after the toggle, 0 for false, 1 for true or null if the path does not exist. + * @since 6.5 + */ + List jsonToggle(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + List jsonType(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + List jsonType(K key); + +} diff --git a/src/main/java/io/lettuce/core/cluster/ClusterDistributionChannelWriter.java b/src/main/java/io/lettuce/core/cluster/ClusterDistributionChannelWriter.java index a0a80609f1..983ca013eb 100644 --- a/src/main/java/io/lettuce/core/cluster/ClusterDistributionChannelWriter.java +++ b/src/main/java/io/lettuce/core/cluster/ClusterDistributionChannelWriter.java @@ -141,13 +141,13 @@ private RedisCommand doWrite(RedisCommand command) { clusterEventListener.onMovedRedirection(); asking = false; - publish(new MovedRedirectionEvent(clusterCommand.getType().name(), keyAsString, slot, + publish(new MovedRedirectionEvent(clusterCommand.getType().toString(), keyAsString, slot, clusterCommand.getError())); } else { target = getAskTarget(clusterCommand.getError()); asking = true; clusterEventListener.onAskRedirection(); - publish(new AskRedirectionEvent(clusterCommand.getType().name(), keyAsString, slot, + publish(new AskRedirectionEvent(clusterCommand.getType().toString(), keyAsString, slot, clusterCommand.getError())); } diff --git a/src/main/java/io/lettuce/core/cluster/CommandSet.java b/src/main/java/io/lettuce/core/cluster/CommandSet.java index 788da79d30..2bc0d625f9 100644 --- a/src/main/java/io/lettuce/core/cluster/CommandSet.java +++ b/src/main/java/io/lettuce/core/cluster/CommandSet.java @@ -59,7 +59,7 @@ public boolean hasCommand(ProtocolKeyword commandName) { return availableCommands.contains(commandName); } - return commands.containsKey(commandName.name().toLowerCase()); + return commands.containsKey(commandName.toString().toLowerCase()); } } diff --git a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java index f4d8fc63e9..c0ba71e48a 100644 --- a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java +++ b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java @@ -36,6 +36,7 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; import io.lettuce.core.*; import io.lettuce.core.api.StatefulRedisConnection; @@ -52,6 +53,10 @@ import io.lettuce.core.cluster.models.partitions.Partitions; import io.lettuce.core.cluster.models.partitions.RedisClusterNode; import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonParser; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonMsetArgs; import io.lettuce.core.output.IntegerOutput; import io.lettuce.core.output.KeyStreamingChannel; import io.lettuce.core.output.KeyValueStreamingChannel; @@ -67,6 +72,7 @@ * @param Value type. * @author Mark Paluch * @author Jon Chambers + * @author Tihomir Mateev * @since 3.3 */ @SuppressWarnings("unchecked") @@ -80,11 +86,13 @@ public class RedisAdvancedClusterAsyncCommandsImpl extends AbstractRedisAs * * @param connection the stateful connection * @param codec Codec used to encode/decode keys and values. - * @deprecated since 5.1, use {@link #RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection, RedisCodec)}. + * @deprecated since 5.1, use + * {@link #RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection, RedisCodec, JsonParser)}. */ @Deprecated - public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnectionImpl connection, RedisCodec codec) { - super(connection, codec); + public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnectionImpl connection, RedisCodec codec, + JsonParser parser) { + super(connection, codec, parser); this.codec = codec; } @@ -94,8 +102,9 @@ public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnectionImpl< * @param connection the stateful connection * @param codec Codec used to encode/decode keys and values. */ - public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection connection, RedisCodec codec) { - super(connection, codec); + public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection connection, RedisCodec codec, + JsonParser parser) { + super(connection, codec, parser); this.codec = codec; } @@ -282,6 +291,54 @@ public RedisFuture keys(KeyStreamingChannel channel, K pattern) { return MultiNodeExecution.aggregateAsync(executions); } + @Override + public RedisFuture> jsonMGet(JsonPath jsonPath, K... keys) { + Map> partitioned = SlotHash.partition(codec, Arrays.asList(keys)); + + if (partitioned.size() < 2) { + return super.jsonMGet(jsonPath, keys); + } + + // For a given partition, maps the key to its index within the List in partitioned for faster lookups below + Map> keysToIndexes = mapKeyToIndex(partitioned); + Map slots = SlotHash.getSlots(partitioned); + Map>> executions = new HashMap<>(partitioned.size()); + + for (Map.Entry> entry : partitioned.entrySet()) { + K[] partitionKeys = entry.getValue().toArray((K[]) new Object[entry.getValue().size()]); + RedisFuture> jsonMget = super.jsonMGet(jsonPath, partitionKeys); + executions.put(entry.getKey(), jsonMget); + } + + // restore order of key + return new PipelinedRedisFuture<>(executions, objectPipelinedRedisFuture -> { + List result = new ArrayList<>(slots.size()); + for (K opKey : keys) { + int slot = slots.get(opKey); + + int position = keysToIndexes.get(slot).get(opKey); + RedisFuture> listRedisFuture = executions.get(slot); + result.add(MultiNodeExecution.execute(() -> listRedisFuture.get().get(position))); + } + + return result; + }); + } + + private Map> mapKeyToIndex(Map> partitioned) { + Map> result = new HashMap<>(partitioned.size()); + for (Integer partition : partitioned.keySet()) { + List keysForPartition = partitioned.get(partition); + Map keysToIndexes = new HashMap<>(keysForPartition.size()); + for (int i = 0; i < keysForPartition.size(); i++) { + keysToIndexes.put(keysForPartition.get(i), i); + } + result.put(partition, keysToIndexes); + } + + return result; + } + @Override public RedisFuture>> mget(K... keys) { return mget(Arrays.asList(keys)); @@ -296,15 +353,7 @@ public RedisFuture>> mget(Iterable keys) { } // For a given partition, maps the key to its index within the List in partitioned for faster lookups below - Map> partitionedKeysToIndexes = new HashMap<>(partitioned.size()); - for (Integer partition : partitioned.keySet()) { - List keysForPartition = partitioned.get(partition); - Map keysToIndexes = new HashMap<>(keysForPartition.size()); - for (int i = 0; i < keysForPartition.size(); i++) { - keysToIndexes.put(keysForPartition.get(i), i); - } - partitionedKeysToIndexes.put(partition, keysToIndexes); - } + Map> partitionedKeysToIndexes = mapKeyToIndex(partitioned); Map slots = SlotHash.getSlots(partitioned); Map>>> executions = new HashMap<>(partitioned.size()); @@ -351,6 +400,30 @@ public RedisFuture mget(KeyValueStreamingChannel channel, Iterable jsonMSet(List> arguments) { + List keys = arguments.stream().map(JsonMsetArgs::getKey).collect(Collectors.toList()); + Map>> argsPerKey = arguments.stream().collect(Collectors.groupingBy(JsonMsetArgs::getKey)); + Map> partitioned = SlotHash.partition(codec, keys); + + if (partitioned.size() < 2) { + return super.jsonMSet(arguments); + } + + Map> executions = new HashMap<>(); + + for (Map.Entry> entry : partitioned.entrySet()) { + + List> op = new ArrayList<>(); + entry.getValue().forEach(k -> op.addAll(argsPerKey.get(k))); + + RedisFuture mset = super.jsonMSet(op); + executions.put(entry.getKey(), mset); + } + + return MultiNodeExecution.firstOfAsync(executions); + } + @Override public RedisFuture mset(Map map) { diff --git a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java index 2df05e254b..bd8d6f671a 100644 --- a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java +++ b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java @@ -34,6 +34,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import io.lettuce.core.json.JsonParser; import org.reactivestreams.Publisher; import io.lettuce.core.*; @@ -75,12 +76,13 @@ public class RedisAdvancedClusterReactiveCommandsImpl extends AbstractRedi * * @param connection the stateful connection. * @param codec Codec used to encode/decode keys and values. - * @deprecated since 5.2, use {@link #RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection, RedisCodec)}. + * @deprecated since 5.2, use + * {@link #RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection, RedisCodec, JsonParser)}. */ @Deprecated - public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnectionImpl connection, - RedisCodec codec) { - super(connection, codec); + public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnectionImpl connection, RedisCodec codec, + JsonParser parser) { + super(connection, codec, parser); this.codec = codec; } @@ -90,8 +92,9 @@ public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnectionIm * @param connection the stateful connection. * @param codec Codec used to encode/decode keys and values. */ - public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection connection, RedisCodec codec) { - super(connection, codec); + public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection connection, RedisCodec codec, + JsonParser parser) { + super(connection, codec, parser); this.codec = codec; } diff --git a/src/main/java/io/lettuce/core/cluster/RedisClusterClient.java b/src/main/java/io/lettuce/core/cluster/RedisClusterClient.java index a60dfd0d82..499e3cd6da 100644 --- a/src/main/java/io/lettuce/core/cluster/RedisClusterClient.java +++ b/src/main/java/io/lettuce/core/cluster/RedisClusterClient.java @@ -59,6 +59,7 @@ import io.lettuce.core.internal.Futures; import io.lettuce.core.internal.LettuceAssert; import io.lettuce.core.internal.LettuceLists; +import io.lettuce.core.json.JsonParser; import io.lettuce.core.output.KeyValueStreamingChannel; import io.lettuce.core.protocol.CommandExpiryWriter; import io.lettuce.core.protocol.CommandHandler; @@ -553,7 +554,7 @@ ConnectionFuture> connectToNodeAsync(RedisC } StatefulRedisConnectionImpl connection = newStatefulRedisConnection(writer, endpoint, codec, - getFirstUri().getTimeout()); + getFirstUri().getTimeout(), getClusterClientOptions().getJsonParser()); ConnectionFuture> connectionFuture = connectStatefulAsync(connection, endpoint, getFirstUri(), socketAddressSupplier, @@ -580,8 +581,8 @@ ConnectionFuture> connectToNodeAsync(RedisC * @return new instance of StatefulRedisConnectionImpl */ protected StatefulRedisConnectionImpl newStatefulRedisConnection(RedisChannelWriter channelWriter, - PushHandler pushHandler, RedisCodec codec, Duration timeout) { - return new StatefulRedisConnectionImpl<>(channelWriter, pushHandler, codec, timeout); + PushHandler pushHandler, RedisCodec codec, Duration timeout, JsonParser parser) { + return new StatefulRedisConnectionImpl<>(channelWriter, pushHandler, codec, timeout, parser); } /** @@ -667,7 +668,7 @@ private CompletableFuture> connectCl clusterWriter.setClusterConnectionProvider(pooledClusterConnectionProvider); StatefulRedisClusterConnectionImpl connection = newStatefulRedisClusterConnection(clusterWriter, - pooledClusterConnectionProvider, codec, getFirstUri().getTimeout()); + pooledClusterConnectionProvider, codec, getFirstUri().getTimeout(), getClusterClientOptions().getJsonParser()); connection.setReadFrom(ReadFrom.UPSTREAM); connection.setPartitions(partitions); @@ -704,8 +705,9 @@ private CompletableFuture> connectCl * @return new instance of StatefulRedisClusterConnectionImpl */ protected StatefulRedisClusterConnectionImpl newStatefulRedisClusterConnection( - RedisChannelWriter channelWriter, ClusterPushHandler pushHandler, RedisCodec codec, Duration timeout) { - return new StatefulRedisClusterConnectionImpl(channelWriter, pushHandler, codec, timeout); + RedisChannelWriter channelWriter, ClusterPushHandler pushHandler, RedisCodec codec, Duration timeout, + JsonParser parser) { + return new StatefulRedisClusterConnectionImpl(channelWriter, pushHandler, codec, timeout, parser); } private Mono connect(Mono socketAddressSupplier, DefaultEndpoint endpoint, diff --git a/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java b/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java index 3e89689016..e248f36822 100644 --- a/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java +++ b/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java @@ -52,6 +52,7 @@ import io.lettuce.core.cluster.models.partitions.RedisClusterNode; import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.json.JsonParser; import io.lettuce.core.protocol.CommandArgsAccessor; import io.lettuce.core.protocol.CompleteableCommand; import io.lettuce.core.protocol.ConnectionIntent; @@ -74,6 +75,8 @@ public class StatefulRedisClusterConnectionImpl extends RedisChannelHandle protected final RedisCodec codec; + protected final JsonParser parser; + protected final RedisAdvancedClusterCommands sync; protected final RedisAdvancedClusterAsyncCommandsImpl async; @@ -93,11 +96,12 @@ public class StatefulRedisClusterConnectionImpl extends RedisChannelHandle * @param timeout Maximum time to wait for a response. */ public StatefulRedisClusterConnectionImpl(RedisChannelWriter writer, ClusterPushHandler pushHandler, RedisCodec codec, - Duration timeout) { + Duration timeout, JsonParser parser) { super(writer, timeout); this.pushHandler = pushHandler; this.codec = codec; + this.parser = parser; this.async = newRedisAdvancedClusterAsyncCommandsImpl(); this.sync = newRedisAdvancedClusterCommandsImpl(); @@ -105,7 +109,7 @@ public StatefulRedisClusterConnectionImpl(RedisChannelWriter writer, ClusterPush } protected RedisAdvancedClusterReactiveCommandsImpl newRedisAdvancedClusterReactiveCommandsImpl() { - return new RedisAdvancedClusterReactiveCommandsImpl<>((StatefulRedisClusterConnection) this, codec); + return new RedisAdvancedClusterReactiveCommandsImpl<>((StatefulRedisClusterConnection) this, codec, parser); } protected RedisAdvancedClusterCommands newRedisAdvancedClusterCommandsImpl() { @@ -117,7 +121,7 @@ protected T clusterSyncHandler(Class... interfaces) { } protected RedisAdvancedClusterAsyncCommandsImpl newRedisAdvancedClusterAsyncCommandsImpl() { - return new RedisAdvancedClusterAsyncCommandsImpl((StatefulRedisClusterConnection) this, codec); + return new RedisAdvancedClusterAsyncCommandsImpl((StatefulRedisClusterConnection) this, codec, parser); } @Override @@ -235,7 +239,7 @@ private RedisCommand preProcessCommand(RedisCommand comman RedisCommand local = command; - if (local.getType().name().equals(AUTH.name())) { + if (local.getType().toString().equals(AUTH.name())) { local = attachOnComplete(local, status -> { if (status.equals("OK")) { List args = CommandArgsAccessor.getCharArrayArguments(command.getArgs()); @@ -252,7 +256,7 @@ private RedisCommand preProcessCommand(RedisCommand comman }); } - if (local.getType().name().equals(READONLY.name())) { + if (local.getType().toString().equals(READONLY.name())) { local = attachOnComplete(local, status -> { if (status.equals("OK")) { this.connectionState.setReadOnly(true); @@ -260,7 +264,7 @@ private RedisCommand preProcessCommand(RedisCommand comman }); } - if (local.getType().name().equals(READWRITE.name())) { + if (local.getType().toString().equals(READWRITE.name())) { local = attachOnComplete(local, status -> { if (status.equals("OK")) { this.connectionState.setReadOnly(false); diff --git a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionAsyncCommands.java index 0d7894b5ca..dbf9b679fa 100644 --- a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionAsyncCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionAsyncCommands.java @@ -8,9 +8,10 @@ * * @author Mark Paluch */ -public interface NodeSelectionAsyncCommands extends BaseNodeSelectionAsyncCommands, - NodeSelectionFunctionAsyncCommands, NodeSelectionGeoAsyncCommands, NodeSelectionHashAsyncCommands, - NodeSelectionHLLAsyncCommands, NodeSelectionKeyAsyncCommands, NodeSelectionListAsyncCommands, - NodeSelectionScriptingAsyncCommands, NodeSelectionServerAsyncCommands, NodeSelectionSetAsyncCommands, - NodeSelectionSortedSetAsyncCommands, NodeSelectionStreamCommands, NodeSelectionStringAsyncCommands { +public interface NodeSelectionAsyncCommands + extends BaseNodeSelectionAsyncCommands, NodeSelectionFunctionAsyncCommands, + NodeSelectionGeoAsyncCommands, NodeSelectionHashAsyncCommands, NodeSelectionHLLAsyncCommands, + NodeSelectionKeyAsyncCommands, NodeSelectionListAsyncCommands, NodeSelectionScriptingAsyncCommands, + NodeSelectionServerAsyncCommands, NodeSelectionSetAsyncCommands, NodeSelectionSortedSetAsyncCommands, + NodeSelectionStreamCommands, NodeSelectionStringAsyncCommands, NodeSelectionJsonAsyncCommands { } diff --git a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionJsonAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionJsonAsyncCommands.java new file mode 100644 index 0000000000..bc4d9229dd --- /dev/null +++ b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionJsonAsyncCommands.java @@ -0,0 +1,440 @@ +/* + * Copyright 2017-2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.cluster.api.async; + +import java.util.List; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; + +/** + * Asynchronous executed commands on a node selection for JSON documents + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @see Redis JSON + * @since 6.5 + * @generated by io.lettuce.apigenerator.CreateAsyncNodeSelectionClusterApi + */ +public interface NodeSelectionJsonAsyncCommands { + + /** + * Append the JSON values into the array at a given {@link JsonPath} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonArrappend(K key, JsonPath jsonPath, JsonValue... values); + + /** + * Append the JSON values into the array at the {@link JsonPath#ROOT_PATH} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonArrappend(K key, JsonValue... values); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @param range the {@link JsonRangeArgs} to search within. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + AsyncExecutions> jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. This + * method uses defaults for the start and end indexes, see {@link JsonRangeArgs#DEFAULT_START_INDEX} and + * {@link JsonRangeArgs#DEFAULT_END_INDEX}. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + AsyncExecutions> jsonArrindex(K key, JsonPath jsonPath, JsonValue value); + + /** + * Insert the {@link JsonValue}s into the array at a given {@link JsonPath} before the provided index, shifting the existing + * elements to the right + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index before which the new elements will be inserted. + * @param values one or more {@link JsonValue}s to be inserted. + * @return Long the resulting size of the arrays after the new data was inserted, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values); + + /** + * Report the length of the JSON array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonArrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON array at a the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonArrlen(K key); + + /** + * Remove and return {@link JsonValue} at a given index in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index of the element to be removed. Default is -1, meaning the last element. Out-of-range indexes round + * to their respective array ends. Popping an empty array returns null. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + AsyncExecutions> jsonArrpop(K key, JsonPath jsonPath, int index); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + AsyncExecutions> jsonArrpop(K key, JsonPath jsonPath); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + AsyncExecutions> jsonArrpop(K key); + + /** + * Trim an array at a given {@link JsonPath} so that it contains only the specified inclusive range of elements. All + * elements with indexes smaller than the start range and all elements with indexes bigger than the end range are trimmed. + *

+ * Behavior as of RedisJSON v2.0: + *

    + *
  • If start is larger than the array's size or start > stop, returns 0 and an empty array.
  • + *
  • If start is < 0, then start from the end of the array.
  • + *
  • If stop is larger than the end of the array, it is treated like the last element.
  • + *
+ * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param range the {@link JsonRangeArgs} to trim by. + * @return Long the resulting size of the arrays after the trimming, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + AsyncExecutions jsonClear(K key, JsonPath jsonPath); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + AsyncExecutions jsonClear(K key); + + /** + * Deletes a value inside the JSON document at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + AsyncExecutions jsonDel(K key, JsonPath jsonPath); + + /** + * Deletes a value inside the JSON document at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + AsyncExecutions jsonDel(K key); + + /** + * Return the value at the specified path in JSON serialized form. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param options the {@link JsonGetArgs} to use. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths); + + /** + * Return the value at the specified path in JSON serialized form. Uses defaults for the {@link JsonGetArgs}. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonGet(K key, JsonPath... jsonPaths); + + /** + * Merge a given {@link JsonValue} with the value matching {@link JsonPath}. Consequently, JSON values at matching paths are + * updated, deleted, or expanded with new children. + *

+ * Merging is done according to the following rules per JSON value in the value argument while considering the corresponding + * original value if it exists: + *

    + *
  • merging an existing object key with a null value deletes the key
  • + *
  • merging an existing object key with non-null value updates the value
  • + *
  • merging a non-existing object key adds the key and value
  • + *
  • merging an existing array with any merged value, replaces the entire array with the value
  • + *
+ *

+ * This command complies with RFC7396 "Json Merge Patch" + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to merge. + * @param value the {@link JsonValue} to merge. + * @return String "OK" if the set was successful, error if the operation failed. + * @since 6.5 + * @see RFC7396 + */ + AsyncExecutions jsonMerge(K key, JsonPath jsonPath, JsonValue value); + + /** + * Return the values at the specified path from multiple key arguments. + * + * @param jsonPath the {@link JsonPath} pointing to the value to fetch. + * @param keys the keys holding the {@link JsonValue}s to fetch. + * @return List the values at path, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonMGet(JsonPath jsonPath, K... keys); + + /** + * Set or update one or more JSON values according to the specified {@link JsonMsetArgs} + *

+ * JSON.MSET is atomic, hence, all given additions or updates are either applied or not. It is not possible for clients to + * see that some keys were updated while others are unchanged. + *

+ * A JSON value is a hierarchical structure. If you change a value in a specific path - nested values are affected. + * + * @param arguments the {@link JsonMsetArgs} specifying the values to change. + * @return "OK" if the operation was successful, error otherwise + * @since 6.5 + */ + AsyncExecutions jsonMSet(List> arguments); + + /** + * Increment the number value stored at the specified {@link JsonPath} in the JSON document by the provided increment. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to increment. + * @param number the increment value. + * @return a {@link List} of the new values after the increment. + * @since 6.5 + */ + AsyncExecutions> jsonNumincrby(K key, JsonPath jsonPath, Number number); + + /** + * Return the keys in the JSON document that are referenced by the given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + AsyncExecutions> jsonObjkeys(K key, JsonPath jsonPath); + + /** + * Return the keys in the JSON document that are referenced by the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + AsyncExecutions> jsonObjkeys(K key); + + /** + * Report the number of keys in the JSON object at the specified {@link JsonPath} and for the provided key + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want to count + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonObjlen(K key, JsonPath jsonPath); + + /** + * Report the number of keys in the JSON object at the {@link JsonPath#ROOT_PATH} and for the provided key + * + * @param key the key holding the JSON document. + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonObjlen(K key); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document. + *

+ * For new Redis keys, the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @param options the {@link JsonSetArgs} the options for setting the value. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + AsyncExecutions jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document using defaults for the {@link JsonSetArgs}. + *

+ * For new Redis keys the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + AsyncExecutions jsonSet(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to append the value. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + AsyncExecutions> jsonStrappend(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + AsyncExecutions> jsonStrappend(K key, JsonValue value); + + /** + * Report the length of the JSON String at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + AsyncExecutions> jsonStrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON String at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + AsyncExecutions> jsonStrlen(K key); + + /** + * Toggle a Boolean value stored at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the new value after the toggle, 0 for false, 1 for true or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonToggle(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + AsyncExecutions> jsonType(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + AsyncExecutions> jsonType(K key); + +} diff --git a/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java index 6f9e1e9020..9fb75c8b43 100644 --- a/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java @@ -26,6 +26,7 @@ import io.lettuce.core.Range; import io.lettuce.core.RedisFuture; import io.lettuce.core.api.async.*; +import io.lettuce.core.json.JsonParser; /** * A complete asynchronous and thread-safe cluster Redis API with 400+ Methods. @@ -382,9 +383,15 @@ public interface RedisClusterAsyncCommands extends BaseRedisAsyncCommands< /** * Retrieves information about the TCP links between nodes in a Redis Cluster. - * + * * @return List of maps containing attributes and values for each peer link. */ RedisFuture>> clusterLinks(); + /** + * @return the currently configured instance of the {@link JsonParser} + * @since 6.5 + */ + JsonParser getJsonParser(); + } diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionJsonCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionJsonCommands.java new file mode 100644 index 0000000000..0ca886a8b8 --- /dev/null +++ b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionJsonCommands.java @@ -0,0 +1,440 @@ +/* + * Copyright 2017-2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.cluster.api.sync; + +import java.util.List; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; + +/** + * Synchronous executed commands on a node selection for JSON documents + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @see Redis JSON + * @since 6.5 + * @generated by io.lettuce.apigenerator.CreateSyncNodeSelectionClusterApi + */ +public interface NodeSelectionJsonCommands { + + /** + * Append the JSON values into the array at a given {@link JsonPath} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonArrappend(K key, JsonPath jsonPath, JsonValue... values); + + /** + * Append the JSON values into the array at the {@link JsonPath#ROOT_PATH} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonArrappend(K key, JsonValue... values); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @param range the {@link JsonRangeArgs} to search within. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + Executions> jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. This + * method uses defaults for the start and end indexes, see {@link JsonRangeArgs#DEFAULT_START_INDEX} and + * {@link JsonRangeArgs#DEFAULT_END_INDEX}. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + Executions> jsonArrindex(K key, JsonPath jsonPath, JsonValue value); + + /** + * Insert the {@link JsonValue}s into the array at a given {@link JsonPath} before the provided index, shifting the existing + * elements to the right + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index before which the new elements will be inserted. + * @param values one or more {@link JsonValue}s to be inserted. + * @return Long the resulting size of the arrays after the new data was inserted, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values); + + /** + * Report the length of the JSON array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonArrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON array at a the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonArrlen(K key); + + /** + * Remove and return {@link JsonValue} at a given index in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index of the element to be removed. Default is -1, meaning the last element. Out-of-range indexes round + * to their respective array ends. Popping an empty array returns null. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + Executions> jsonArrpop(K key, JsonPath jsonPath, int index); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + Executions> jsonArrpop(K key, JsonPath jsonPath); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + Executions> jsonArrpop(K key); + + /** + * Trim an array at a given {@link JsonPath} so that it contains only the specified inclusive range of elements. All + * elements with indexes smaller than the start range and all elements with indexes bigger than the end range are trimmed. + *

+ * Behavior as of RedisJSON v2.0: + *

    + *
  • If start is larger than the array's size or start > stop, returns 0 and an empty array.
  • + *
  • If start is < 0, then start from the end of the array.
  • + *
  • If stop is larger than the end of the array, it is treated like the last element.
  • + *
+ * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param range the {@link JsonRangeArgs} to trim by. + * @return Long the resulting size of the arrays after the trimming, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + Executions jsonClear(K key, JsonPath jsonPath); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + Executions jsonClear(K key); + + /** + * Deletes a value inside the JSON document at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + Executions jsonDel(K key, JsonPath jsonPath); + + /** + * Deletes a value inside the JSON document at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + Executions jsonDel(K key); + + /** + * Return the value at the specified path in JSON serialized form. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param options the {@link JsonGetArgs} to use. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths); + + /** + * Return the value at the specified path in JSON serialized form. Uses defaults for the {@link JsonGetArgs}. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonGet(K key, JsonPath... jsonPaths); + + /** + * Merge a given {@link JsonValue} with the value matching {@link JsonPath}. Consequently, JSON values at matching paths are + * updated, deleted, or expanded with new children. + *

+ * Merging is done according to the following rules per JSON value in the value argument while considering the corresponding + * original value if it exists: + *

    + *
  • merging an existing object key with a null value deletes the key
  • + *
  • merging an existing object key with non-null value updates the value
  • + *
  • merging a non-existing object key adds the key and value
  • + *
  • merging an existing array with any merged value, replaces the entire array with the value
  • + *
+ *

+ * This command complies with RFC7396 "Json Merge Patch" + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to merge. + * @param value the {@link JsonValue} to merge. + * @return String "OK" if the set was successful, error if the operation failed. + * @since 6.5 + * @see RFC7396 + */ + Executions jsonMerge(K key, JsonPath jsonPath, JsonValue value); + + /** + * Return the values at the specified path from multiple key arguments. + * + * @param jsonPath the {@link JsonPath} pointing to the value to fetch. + * @param keys the keys holding the {@link JsonValue}s to fetch. + * @return List the values at path, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonMGet(JsonPath jsonPath, K... keys); + + /** + * Set or update one or more JSON values according to the specified {@link JsonMsetArgs} + *

+ * JSON.MSET is atomic, hence, all given additions or updates are either applied or not. It is not possible for clients to + * see that some keys were updated while others are unchanged. + *

+ * A JSON value is a hierarchical structure. If you change a value in a specific path - nested values are affected. + * + * @param arguments the {@link JsonMsetArgs} specifying the values to change. + * @return "OK" if the operation was successful, error otherwise + * @since 6.5 + */ + Executions jsonMSet(List> arguments); + + /** + * Increment the number value stored at the specified {@link JsonPath} in the JSON document by the provided increment. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to increment. + * @param number the increment value. + * @return a {@link List} of the new values after the increment. + * @since 6.5 + */ + Executions> jsonNumincrby(K key, JsonPath jsonPath, Number number); + + /** + * Return the keys in the JSON document that are referenced by the given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + Executions> jsonObjkeys(K key, JsonPath jsonPath); + + /** + * Return the keys in the JSON document that are referenced by the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + Executions> jsonObjkeys(K key); + + /** + * Report the number of keys in the JSON object at the specified {@link JsonPath} and for the provided key + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want to count + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonObjlen(K key, JsonPath jsonPath); + + /** + * Report the number of keys in the JSON object at the {@link JsonPath#ROOT_PATH} and for the provided key + * + * @param key the key holding the JSON document. + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonObjlen(K key); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document. + *

+ * For new Redis keys, the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @param options the {@link JsonSetArgs} the options for setting the value. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + Executions jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document using defaults for the {@link JsonSetArgs}. + *

+ * For new Redis keys the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + Executions jsonSet(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to append the value. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + Executions> jsonStrappend(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + Executions> jsonStrappend(K key, JsonValue value); + + /** + * Report the length of the JSON String at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + Executions> jsonStrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON String at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + Executions> jsonStrlen(K key); + + /** + * Toggle a Boolean value stored at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the new value after the toggle, 0 for false, 1 for true or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonToggle(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + Executions> jsonType(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + Executions> jsonType(K key); + +} diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java index ec02e83a9b..988975740c 100644 --- a/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java @@ -25,6 +25,7 @@ import io.lettuce.core.Range; import io.lettuce.core.api.sync.*; +import io.lettuce.core.json.JsonParser; /** * A complete synchronous and thread-safe Redis Cluster API with 400+ Methods. @@ -35,10 +36,11 @@ * @author dengliming * @since 4.0 */ -public interface RedisClusterCommands extends BaseRedisCommands, RedisAclCommands, - RedisFunctionCommands, RedisGeoCommands, RedisHashCommands, RedisHLLCommands, - RedisKeyCommands, RedisListCommands, RedisScriptingCommands, RedisServerCommands, - RedisSetCommands, RedisSortedSetCommands, RedisStreamCommands, RedisStringCommands { +public interface RedisClusterCommands + extends BaseRedisCommands, RedisAclCommands, RedisFunctionCommands, RedisGeoCommands, + RedisHashCommands, RedisHLLCommands, RedisKeyCommands, RedisListCommands, + RedisScriptingCommands, RedisServerCommands, RedisSetCommands, RedisSortedSetCommands, + RedisStreamCommands, RedisStringCommands, RedisJsonCommands { /** * Set the default timeout for operations. A zero timeout value indicates to not time out. @@ -371,9 +373,15 @@ public interface RedisClusterCommands extends BaseRedisCommands, Red /** * Retrieves information about the TCP links between nodes in a Redis Cluster. - * + * * @return List of maps containing attributes and values for each peer link. */ List> clusterLinks(); + /** + * @return the currently configured instance of the {@link JsonParser} + * @since 6.5 + */ + JsonParser getJsonParser(); + } diff --git a/src/main/java/io/lettuce/core/dynamic/DefaultCommandMethodVerifier.java b/src/main/java/io/lettuce/core/dynamic/DefaultCommandMethodVerifier.java index ea49a9f916..dc44640ac0 100644 --- a/src/main/java/io/lettuce/core/dynamic/DefaultCommandMethodVerifier.java +++ b/src/main/java/io/lettuce/core/dynamic/DefaultCommandMethodVerifier.java @@ -55,10 +55,10 @@ public DefaultCommandMethodVerifier(List commandDetails) { @Override public void validate(CommandSegments commandSegments, CommandMethod commandMethod) throws CommandMethodSyntaxException { - LettuceAssert.notEmpty(commandSegments.getCommandType().name(), "Command name must not be empty"); + LettuceAssert.notEmpty(commandSegments.getCommandType().toString(), "Command name must not be empty"); - CommandDetail commandDetail = findCommandDetail(commandSegments.getCommandType().name()) - .orElseThrow(() -> syntaxException(commandSegments.getCommandType().name(), commandMethod)); + CommandDetail commandDetail = findCommandDetail(commandSegments.getCommandType().toString()) + .orElseThrow(() -> syntaxException(commandSegments.getCommandType().toString(), commandMethod)); validateParameters(commandDetail, commandSegments, commandMethod); } diff --git a/src/main/java/io/lettuce/core/dynamic/segment/CommandSegments.java b/src/main/java/io/lettuce/core/dynamic/segment/CommandSegments.java index 155edc4b65..5cade513d3 100644 --- a/src/main/java/io/lettuce/core/dynamic/segment/CommandSegments.java +++ b/src/main/java/io/lettuce/core/dynamic/segment/CommandSegments.java @@ -80,14 +80,9 @@ public byte[] getBytes() { return commandTypeBytes; } - @Override - public String name() { - return commandType; - } - @Override public String toString() { - return name(); + return commandType; } @Override @@ -112,7 +107,7 @@ public int hashCode() { @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append(getCommandType().name()); + sb.append(getCommandType().toString()); for (CommandSegment segment : segments) { sb.append(' ').append(segment); diff --git a/src/main/java/io/lettuce/core/json/DefaultJsonParser.java b/src/main/java/io/lettuce/core/json/DefaultJsonParser.java new file mode 100644 index 0000000000..a002d9782a --- /dev/null +++ b/src/main/java/io/lettuce/core/json/DefaultJsonParser.java @@ -0,0 +1,91 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Default implementation of the {@link JsonParser} that should fit most use cases. Utilizes the Jackson library for maintaining + * the JSON tree model and provides the ability to create new instances of the {@link JsonValue}, {@link JsonArray} and + * {@link JsonObject}. + * + * @since 6.5 + * @author Tihomir Mateev + */ +public class DefaultJsonParser implements JsonParser { + + public static final DefaultJsonParser INSTANCE = new DefaultJsonParser(); + + private DefaultJsonParser() { + } + + @Override + public JsonValue loadJsonValue(ByteBuffer bytes) { + return new UnproccessedJsonValue(bytes, this); + } + + @Override + public JsonValue createJsonValue(ByteBuffer bytes) { + return parse(bytes); + } + + @Override + public JsonValue createJsonValue(String value) { + return parse(value); + } + + @Override + public JsonObject createJsonObject() { + return new DelegateJsonObject(); + } + + @Override + public JsonArray createJsonArray() { + return new DelegateJsonArray(); + } + + @Override + public JsonValue fromObject(Object object) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + JsonNode root = objectMapper.valueToTree(object); + return DelegateJsonValue.wrap(root); + } catch (IllegalArgumentException e) { + throw new RedisJsonException("Failed to process the provided object as JSON", e); + } + } + + private JsonValue parse(String value) { + ObjectMapper mapper = new ObjectMapper(); + try { + JsonNode root = mapper.readTree(value); + return DelegateJsonValue.wrap(root); + } catch (JsonProcessingException e) { + throw new RedisJsonException( + "Failed to process the provided value as JSON: " + String.format("%.50s", value) + "...", e); + } + } + + private JsonValue parse(ByteBuffer byteBuffer) { + ObjectMapper mapper = new ObjectMapper(); + try { + byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + JsonNode root = mapper.readTree(bytes); + return DelegateJsonValue.wrap(root); + } catch (IOException e) { + throw new RedisJsonException("Failed to process the provided value as JSON", e); + } + } + +} diff --git a/src/main/java/io/lettuce/core/json/DelegateJsonArray.java b/src/main/java/io/lettuce/core/json/DelegateJsonArray.java new file mode 100644 index 0000000000..39d63ac75f --- /dev/null +++ b/src/main/java/io/lettuce/core/json/DelegateJsonArray.java @@ -0,0 +1,108 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import io.lettuce.core.internal.LettuceAssert; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Implementation of the {@link DelegateJsonArray} that delegates most of its' functionality to the Jackson {@link ArrayNode}. + * + * @author Tihomir Mateev + */ +class DelegateJsonArray extends DelegateJsonValue implements JsonArray { + + DelegateJsonArray() { + super(new ArrayNode(JsonNodeFactory.instance)); + } + + DelegateJsonArray(JsonNode node) { + super(node); + } + + @Override + public JsonArray add(JsonValue element) { + JsonNode newNode = null; + + if (element != null) { + newNode = ((DelegateJsonValue) element).getNode(); + } + + ((ArrayNode) node).add(newNode); + + return this; + } + + @Override + public void addAll(JsonArray element) { + LettuceAssert.notNull(element, "Element must not be null"); + + ArrayNode otherArray = (ArrayNode) ((DelegateJsonValue) element).getNode(); + ((ArrayNode) node).addAll(otherArray); + } + + @Override + public List asList() { + List result = new ArrayList<>(); + + for (JsonNode jsonNode : node) { + result.add(new DelegateJsonValue(jsonNode)); + } + + return result; + } + + @Override + public JsonValue get(int index) { + JsonNode jsonNode = node.get(index); + + return jsonNode == null ? null : wrap(jsonNode); + } + + @Override + public JsonValue getFirst() { + return get(0); + } + + @Override + public Iterator iterator() { + return asList().iterator(); + } + + @Override + public JsonValue remove(int index) { + JsonNode jsonNode = ((ArrayNode) node).remove(index); + + return wrap(jsonNode); + } + + @Override + public JsonValue replace(int index, JsonValue newElement) { + JsonNode replaceWith = ((DelegateJsonValue) newElement).getNode(); + JsonNode replaced = ((ArrayNode) node).set(index, replaceWith); + + return wrap(replaced); + } + + @Override + public int size() { + return node.size(); + } + + @Override + public JsonArray asJsonArray() { + return this; + } + +} diff --git a/src/main/java/io/lettuce/core/json/DelegateJsonObject.java b/src/main/java/io/lettuce/core/json/DelegateJsonObject.java new file mode 100644 index 0000000000..8159726f61 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/DelegateJsonObject.java @@ -0,0 +1,61 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Implementation of the {@link DelegateJsonObject} that delegates most of its functionality to the Jackson {@link ObjectNode}. + * + * @author Tihomir Mateev + */ +class DelegateJsonObject extends DelegateJsonValue implements JsonObject { + + DelegateJsonObject() { + super(new ObjectNode(JsonNodeFactory.instance)); + } + + DelegateJsonObject(JsonNode node) { + super(node); + } + + @Override + public JsonObject put(String key, JsonValue element) { + JsonNode newNode = ((DelegateJsonValue) element).getNode(); + + ((ObjectNode) node).replace(key, newNode); + return this; + } + + @Override + public JsonValue get(String key) { + JsonNode value = node.get(key); + + return value == null ? null : wrap(value); + } + + @Override + public JsonValue remove(String key) { + JsonNode value = ((ObjectNode) node).remove(key); + + return wrap(value); + } + + @Override + public int size() { + return node.size(); + } + + @Override + public JsonObject asJsonObject() { + return this; + } + +} diff --git a/src/main/java/io/lettuce/core/json/DelegateJsonValue.java b/src/main/java/io/lettuce/core/json/DelegateJsonValue.java new file mode 100644 index 0000000000..3ca65eb7a7 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/DelegateJsonValue.java @@ -0,0 +1,122 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.nio.ByteBuffer; + +/** + * Implementation of the {@link JsonValue} that delegates most of its functionality to the Jackson {@link JsonNode}. + * + * @author Tihomir Mateev + */ +class DelegateJsonValue implements JsonValue { + + protected JsonNode node; + + DelegateJsonValue(JsonNode node) { + this.node = node; + } + + @Override + public String toString() { + return node.toString(); + } + + @Override + public ByteBuffer asByteBuffer() { + byte[] result = node.toString().getBytes(); + return ByteBuffer.wrap(result); + } + + @Override + public boolean isJsonArray() { + return node.isArray(); + } + + @Override + public JsonArray asJsonArray() { + return null; + } + + @Override + public boolean isJsonObject() { + return node.isObject(); + } + + @Override + public JsonObject asJsonObject() { + return null; + } + + @Override + public boolean isString() { + return node.isTextual(); + } + + @Override + public String asString() { + return node.isTextual() ? node.asText() : null; + } + + @Override + public boolean isNumber() { + return node.isNumber(); + } + + @Override + public Boolean asBoolean() { + + return node.isBoolean() ? node.asBoolean() : null; + } + + @Override + public boolean isBoolean() { + return node.isBoolean(); + } + + public boolean isNull() { + return node.isNull(); + } + + @Override + public Number asNumber() { + if (node.isNull()) { + return null; + } + return node.numberValue(); + } + + protected JsonNode getNode() { + return node; + } + + @Override + public T toObject(Class type) { + ObjectMapper mapper = new ObjectMapper(); + try { + return mapper.treeToValue(node, type); + } catch (IllegalArgumentException | JsonProcessingException e) { + throw new RedisJsonException("Unable to map the provided JsonValue to " + type.getName(), e); + } + } + + static JsonValue wrap(JsonNode root) { + if (root.isObject()) { + return new DelegateJsonObject(root); + } else if (root.isArray()) { + return new DelegateJsonArray(root); + } + + return new DelegateJsonValue(root); + } + +} diff --git a/src/main/java/io/lettuce/core/json/JsonArray.java b/src/main/java/io/lettuce/core/json/JsonArray.java new file mode 100644 index 0000000000..687dbec22f --- /dev/null +++ b/src/main/java/io/lettuce/core/json/JsonArray.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import java.util.Iterator; +import java.util.List; + +/** + * Representation of a JSON array as per RFC 8259 - The + * JavaScript Object Notation (JSON) Data Interchange Format, Section 5. Arrays + *

+ * + * @see JsonValue + * @author Tihomir Mateev + * @since 6.5 + */ +public interface JsonArray extends JsonValue { + + /** + * Add a new {@link JsonValue} to the array. Supports chaining of calls. + * + * @param element the value to add + * @return the updated {@link JsonArray} to allow call chaining + */ + JsonArray add(JsonValue element); + + /** + * Add all elements from the provided {@link JsonArray} to this array. + * + * @param element the array to add all elements from + */ + void addAll(JsonArray element); + + /** + * Get all the {@link JsonValue}s in the array as a {@link List}. + * + * @return the {@link List} of {@link JsonValue}s in the array + */ + List asList(); + + /** + * Get the {@link JsonValue} at the provided index. + * + * @param index the index to get the value for + * @return the {@link JsonValue} at the provided index or {@code null} if no value is found + */ + JsonValue get(int index); + + /** + * Get the first {@link JsonValue} in the array. + * + * @return the first {@link JsonValue} in the array or {@code null} if the array is empty + */ + JsonValue getFirst(); + + /** + * Get an {@link Iterator} allowing access to all the {@link JsonValue}s in the array. + * + * @return the last {@link JsonValue} in the array or {@code null} if the array is empty + */ + Iterator iterator(); + + /** + * Remove the {@link JsonValue} at the provided index. + * + * @param index the index to remove the value for + * @return the removed {@link JsonValue} or {@code null} if no value is found + */ + JsonValue remove(int index); + + /** + * Replace the {@link JsonValue} at the provided index with the provided new {@link JsonValue}. + * + * @param index the index to replace the value for + * @param newElement the new value to replace the old one with + * @return the updated {@link JsonArray} to allow call chaining + */ + JsonValue replace(int index, JsonValue newElement); + + /** + * @return the number of elements in this {@link JsonArray} + */ + int size(); + +} diff --git a/src/main/java/io/lettuce/core/json/JsonObject.java b/src/main/java/io/lettuce/core/json/JsonObject.java new file mode 100644 index 0000000000..4ab4ce0cc8 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/JsonObject.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +/** + * Representation of a JSON object as per RFC 8259 - The + * JavaScript Object Notation (JSON) Data Interchange Format, Section 4. Objects + *

+ * + * @see JsonValue + * @author Tihomir Mateev + * @since 6.5 + */ +public interface JsonObject extends JsonValue { + + /** + * Add (if there is no value with the same key already) or replace (if there is) a new {@link JsonValue} to the object under + * the provided key. Supports chaining of calls. + * + * @param key the key of the {@link JsonValue} to add or replace + * @param element the value to add or replace + * @return the updated {@link JsonObject} to allow call chaining + */ + JsonObject put(String key, JsonValue element); + + /** + * Get the {@link JsonValue} under the provided key. + * + * @param key the key to get the value for + * @return the {@link JsonValue} under the provided key or {@code null} if no value is found + */ + JsonValue get(String key); + + /** + * Remove the {@link JsonValue} under the provided key. + * + * @param key the key to remove the value for + * @return the removed {@link JsonValue} or {@code null} if no value is found + */ + JsonValue remove(String key); + + /** + * @return the number of key-value pairs in this {@link JsonObject} + */ + int size(); + +} diff --git a/src/main/java/io/lettuce/core/json/JsonParser.java b/src/main/java/io/lettuce/core/json/JsonParser.java new file mode 100644 index 0000000000..3e6122f3b2 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/JsonParser.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import java.nio.ByteBuffer; + +/** + * The JsonParser is an abstraction that allows transforming a JSON document from a {@link ByteBuffer} to implementations of the + * {@link JsonValue} interface and vice versa. Underneath there could be different implementations that use different JSON + * parser libraries or custom implementations. Respectively the {@link JsonParser} is responsible for building new instances of + * the {@link JsonArray} and {@link JsonObject} interfaces, as they are ultimately tightly coupled with the specific JSON parser + * that is being used. + *

+ * A custom implementation of the {@link JsonParser} can be provided to the {@link io.lettuce.core.ClientOptions} in case the + * default implementation does not fit the requirements. + * + * @since 6.5 + * @author Tihomir Mateev + */ +public interface JsonParser { + + /** + * Loads the provided {@link ByteBuffer} in a new {@link JsonValue}. Does not start the actual processing of the + * {@link ByteBuffer} until a method of the {@link JsonValue} is called. + * + * @param bytes the {@link ByteBuffer} to create the {@link JsonValue} from + * @return the created {@link JsonValue} + * @throws RedisJsonException if the provided {@link ByteBuffer} is not a valid JSON document + */ + JsonValue loadJsonValue(ByteBuffer bytes); + + /** + * Create a new {@link JsonValue} from the provided {@link ByteBuffer}. + * + * @param bytes the {@link ByteBuffer} to create the {@link JsonValue} from + * @return the created {@link JsonValue} + * @throws RedisJsonException if the provided {@link ByteBuffer} is not a valid JSON document + */ + JsonValue createJsonValue(ByteBuffer bytes); + + /** + * Create a new {@link JsonValue} from the provided value. + * + * @param value the value to create the {@link JsonValue} from + * @return the created {@link JsonValue} + * @throws RedisJsonException if the provided value is not a valid JSON document + */ + JsonValue createJsonValue(String value); + + /** + * Create a new empty {@link JsonObject}. + * + * @return the created {@link JsonObject} + */ + JsonObject createJsonObject(); + + /** + * Create a new empty {@link JsonArray}. + * + * @return the created {@link JsonArray} + */ + JsonArray createJsonArray(); + + /** + * Create a new {@link JsonValue} from the provided object. + * + * @param object the object to create the {@link JsonValue} from + * @return the created {@link JsonValue} + * @throws RedisJsonException if the provided object is not a valid JSON document + */ + JsonValue fromObject(Object object); + +} diff --git a/src/main/java/io/lettuce/core/json/JsonPath.java b/src/main/java/io/lettuce/core/json/JsonPath.java new file mode 100644 index 0000000000..abb8afc513 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/JsonPath.java @@ -0,0 +1,124 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +/** + * Describes a path to a certain {@link JsonValue} inside a JSON document. + *

+ *

+ * The Redis server implements its own JSONPath implementation, based on existing technologies. The generic rules to build a + * path string are: + *

    + *
  • $ - The root (outermost JSON element), starts the path.
  • + *
  • . or [] - Selects a child element.
  • + *
  • .. - Recursively descends through the JSON document.
  • + *
  • * - Wildcard, returns all elements.
  • + *
  • [] - Subscript operator, accesses an array element.
  • + *
  • [,]] - Union, selects multiple elements.
  • + *
  • [start:end:step] - Array slice where start, end, and step are indexes.
  • + *
  • ?() - Filters a JSON object or array. Supports comparison operators (==, !=, <, <=, >, >=, =~), logical + * operators (&&, ||), and parenthesis ((, )).
  • + *
  • () - Script expression.
  • + *
  • @ - The current element, used in filter or script expressions.
  • + *
+ *

+ * For example, given the following JSON document: + * + *

+ * {
+ *     "inventory": {
+ *         "mountain_bikes": [
+ *             {
+ *                 "id": "bike:1",
+ *                 "model": "Phoebe",
+ *                 "description": "This is a mid-travel trail slayer that is a fantastic daily...",
+ *                 "price": 1920,
+ *                 "specs": {"material": "carbon", "weight": 13.1},
+ *                 "colors": ["black", "silver"],
+ *             },
+ *             ...
+ *         }
+ *     }
+ *}
+ * 
+ *

+ * To get a list of all the {@code mountain_bikes} inside the {@code inventory} you would write something like: + *

+ * {@code JSON.GET store '$.inventory["mountain_bikes"]' } + * + * @author Tihomir Mateev + * @since 6.5 + * @see JSON Path in Redis docs + */ +public class JsonPath { + + /** + * The root path {@code $} as defined by the second version of the RedisJSON implementation. + *

+ * + * @since 6.5 + */ + public static final JsonPath ROOT_PATH = new JsonPath("$"); + + /** + * The legacy root path {@code .} as defined by the first version of the RedisJSON implementation. + * + * @deprecated since 6.5, use {@link #ROOT_PATH} instead. + */ + public static final JsonPath ROOT_PATH_LEGACY = new JsonPath("."); + + private final String path; + + /** + * Create a new {@link JsonPath} given a path string. + * + * @param pathString the path string, must not be {@literal null} or empty. + */ + public JsonPath(final String pathString) { + + if (pathString == null) { + throw new IllegalArgumentException("Path cannot be null."); + } + + if (pathString.isEmpty()) { + throw new IllegalArgumentException("Path cannot be empty."); + } + + this.path = pathString; + } + + @Override + public String toString() { + return path; + } + + /** + * Create a new {@link JsonPath} given a path string. + * + * @param path the path string, must not be {@literal null} or empty. + * @return the {@link JsonPath}. + */ + public static JsonPath of(final String path) { + return new JsonPath(path); + } + + @Override + public boolean equals(Object obj) { + return this.path.equals(obj); + } + + @Override + public int hashCode() { + return path.hashCode(); + } + + public boolean isRootPath() { + return ROOT_PATH.toString().equals(path) || ROOT_PATH_LEGACY.toString().equals(path); + } + +} diff --git a/src/main/java/io/lettuce/core/json/JsonType.java b/src/main/java/io/lettuce/core/json/JsonType.java new file mode 100644 index 0000000000..0344e8cee2 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/JsonType.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +/** + * JSON types as returned by the JSON.TYPE command + * + * @see io.lettuce.core.api.sync.RedisCommands#jsonType + * @since 6.5 + * @author Tihomir Mateev + */ +public enum JsonType { + + OBJECT, ARRAY, STRING, INTEGER, NUMBER, BOOLEAN, UNKNOWN; + + public static JsonType fromString(String s) { + switch (s) { + case "object": + return OBJECT; + case "array": + return ARRAY; + case "string": + return STRING; + case "integer": + return INTEGER; + case "number": + return NUMBER; + case "boolean": + return BOOLEAN; + default: + return UNKNOWN; + } + } + +} diff --git a/src/main/java/io/lettuce/core/json/JsonValue.java b/src/main/java/io/lettuce/core/json/JsonValue.java new file mode 100644 index 0000000000..2ba6ac20a0 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/JsonValue.java @@ -0,0 +1,109 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import java.nio.ByteBuffer; + +/** + * Representation of a JSON text as per the RFC 8259 - + * The JavaScript Object Notation (JSON) Data Interchange Format, Section 3. Values + *

+ * Implementations of this interface need to make sure parsing of the JSON is not done inside the event loop thread, used to + * process the data coming from the Redis server; otherwise larger JSON documents might cause performance degradation that spans + * across all threads using the driver. + * + * @see JsonObject + * @see JsonArray + * @see RFC 8259 - The JavaScript Object Notation (JSON) Data + * Interchange Format + * @author Tihomir Mateev + * @since 6.5 + */ +public interface JsonValue { + + /** + * Execute any {@link io.lettuce.core.codec.RedisCodec} decoding and fetch the result. + * + * @return the {@link String} representation of this {@link JsonValue} + */ + String toString(); + + /** + * @return the raw JSON text as a {@link ByteBuffer} + */ + ByteBuffer asByteBuffer(); + + /** + * @return {@code true} if this {@link JsonValue} represents a JSON array + */ + boolean isJsonArray(); + + /** + * @return the {@link JsonArray} representation of this {@link JsonValue}, null if this is not a JSON array + * @see #isJsonArray() + */ + JsonArray asJsonArray(); + + /** + * @return {@code true} if this {@link JsonValue} represents a JSON object + */ + boolean isJsonObject(); + + /** + * @return the {@link JsonObject} representation of this {@link JsonValue}, null if this is not a JSON object + * @see #isJsonObject() + */ + JsonObject asJsonObject(); + + /** + * @return {@code true} if this {@link JsonValue} represents a JSON string + */ + boolean isString(); + + /** + * @return the {@link String} representation of this {@link JsonValue}, null if this is not a JSON string + * @see #isString() + */ + String asString(); + + /** + * @return {@code true} if this {@link JsonValue} represents a JSON number + */ + boolean isNumber(); + + /** + * @return the {@link Number} representation of this {@link JsonValue}, null if this is not a JSON number + * @see #isNumber() + */ + Number asNumber(); + + /** + * @return {@code true} if this {@link JsonValue} represents a JSON boolean value + */ + boolean isBoolean(); + + /** + * @return the {@link Boolean} representation of this {@link JsonValue}, null if this is not a JSON boolean value + * @see #isNumber() + */ + Boolean asBoolean(); + + /** + * @return {@code true} if this {@link JsonValue} represents the value of null + */ + boolean isNull(); + + /** + * Given a {@link Class} type, this method will attempt to convert the JSON value to the provided type. + * + * @return the newly created instance of the provided type with the data from the JSON value + * @throws RedisJsonException if the provided type is not a valid JSON document + */ + T toObject(Class type); + +} diff --git a/src/main/java/io/lettuce/core/json/README.md b/src/main/java/io/lettuce/core/json/README.md new file mode 100644 index 0000000000..5c6905bd73 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/README.md @@ -0,0 +1,87 @@ +# RedisJSON support in Lettuce + +Lettuce supports [RedisJSON](https://oss.redis.com/redisjson/) starting from [Lettuce 6.5.0.RELEASE](https://github.com/redis/lettuce/releases/tag/6.5.0.RELEASE). + +The driver generally allows three distinct ways of working with the RedisJSON module: +* (Default mode) - default JSON parsing using Jackson behind the scenes +* (Advanced mode) - custom JSON parsing using a user-provided JSON parser +* (Power-user mode) - unprocessed JSON documents that have not gone through any process of deserialization or serialization + +> [!IMPORTANT]\ +> In all the above modes, the driver would refrain from processing the JSON document in the main event loop and instead +delegate this to the user thread. This behaviour is consistent when both receiving and sending JSON documents - when +receiving the parsing is done lazily whenever a method is called that requires the JSON to be parsed; when sending the +JSON is serialized immediately after it is passed to any of the commands, but before dispatching the command to the +event loop. + + +## Default mode +Best for: +* Most typical use-cases where the JSON document is parsed and processed + +### Example usage: + +```java +RedisURI redisURI = RedisURI.Builder.redis("acme.com").build(); +RedisClient redisClient = RedisClient.create(redisURI); +try (StatefulRedisConnection connect = redisClient.connect()){ + redis = connect.async(); + JsonPath path = JsonPath.of("$..mountain_bikes[0:2].model"); + + JsonParser parser = redis.getJsonParser(); + JsonObject bikeRecord = parser.createJsonObject(); + JsonObject bikeSpecs = parser.createJsonObject(); + JsonArray bikeColors = parser.createJsonArray(); + + bikeSpecs.put("material", parser.createJsonValue("\"wood\"")); + bikeSpecs.put("weight", parser.createJsonValue("19")); + + bikeColors.add(parser.createJsonValue("\"walnut\"")); + bikeColors.add(parser.createJsonValue("\"chestnut\"")); + + bikeRecord.put("id", parser.createJsonValue("\"bike:13\"")); + bikeRecord.put("model", parser.createJsonValue("\"Woody\"")); + bikeRecord.put("description", parser.createJsonValue("\"The Woody is an environmentally-friendly wooden bike\"")); + bikeRecord.put("price", parser.createJsonValue("\"1112\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + String result = redis.jsonSet("bikes:inventory", path, bikeRecord).get(); +} +``` + +## Advanced mode +Best for: +* Applications that want to handle parsing manually - either by using another library or by implementing their own parser + +### Example usage: + +```java +RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build(); + +try (RedisClient client = RedisClient.create(redisURI)) { + client.setOptions(ClientOptions.builder().jsonParser(new CustomParser()).build()); + StatefulRedisConnection connection = client.connect(StringCodec.UTF8); + RedisCommands redis = connection.sync(); +} +``` + +## Power-user mode +Best for: +* Applications that do little to no processing on the Java layer + +### Example usage: + +```java +JsonPath myPath = JsonPath.of("$..mountain_bikes"); +RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build(); +try (RedisClient client = RedisClient.create(redisURI)) { + RedisAsyncCommands redis = client.connect().async(); + RedisFuture> bikes = redis.jsonGet("bikes:inventory", myPath); + + CompletionStage> stage = bikes.thenApply( + fetchedBikes -> redis.jsonSet("service_bikes", JsonPath.ROOT_PATH, fetchedBikes.get(0))); + + String result = stage.toCompletableFuture().get().get(); +} +``` diff --git a/src/main/java/io/lettuce/core/json/RedisJsonException.java b/src/main/java/io/lettuce/core/json/RedisJsonException.java new file mode 100644 index 0000000000..c394ab8f53 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/RedisJsonException.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +public class RedisJsonException extends RuntimeException { + + public RedisJsonException(String message) { + super(message); + } + + public RedisJsonException(String message, Throwable cause) { + super(message, cause); + } + + public RedisJsonException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/io/lettuce/core/json/UnproccessedJsonValue.java b/src/main/java/io/lettuce/core/json/UnproccessedJsonValue.java new file mode 100644 index 0000000000..cfeda343b1 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/UnproccessedJsonValue.java @@ -0,0 +1,166 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import io.lettuce.core.codec.StringCodec; + +import java.nio.ByteBuffer; + +/** + * A wrapper around any of the implementations of the {@link JsonValue} provided by the implementation of the {@link JsonParser} + * that is currently being used. The purpose of this class is to provide a lazy initialization mechanism and avoid any + * deserialization in the event loop that processes the data coming from the Redis server. + *

+ * This class is thread-safe and can be used in a multi-threaded environment. + * + * @author Tihomir Mateev + */ +class UnproccessedJsonValue implements JsonValue { + + private volatile JsonValue jsonValue; + + private final JsonParser parser; + + private final ByteBuffer unprocessedData; + + /** + * Create a new instance of the {@link UnproccessedJsonValue}. + * + * @param bytes the raw JSON data + * @param theParser the {@link JsonParser} that works with the current instance + */ + public UnproccessedJsonValue(ByteBuffer bytes, JsonParser theParser) { + unprocessedData = bytes; + parser = theParser; + } + + @Override + public String toString() { + if (isDeserialized()) { + return jsonValue.toString(); + } + + synchronized (this) { + if (isDeserialized()) { + return jsonValue.toString(); + } + + // if no deserialization took place, so no modification took place + // in this case we can decode the source data as is + return StringCodec.UTF8.decodeValue(unprocessedData); + } + } + + @Override + public ByteBuffer asByteBuffer() { + if (isDeserialized()) { + return jsonValue.asByteBuffer(); + } + + synchronized (this) { + if (isDeserialized()) { + return jsonValue.asByteBuffer(); + } + + // if no deserialization took place, so no modification took place + // in this case we can decode the source data as is + return unprocessedData; + } + } + + @Override + public boolean isJsonArray() { + lazilyDeserialize(); + return jsonValue.isJsonArray(); + } + + @Override + public JsonArray asJsonArray() { + lazilyDeserialize(); + return jsonValue.asJsonArray(); + } + + @Override + public boolean isJsonObject() { + lazilyDeserialize(); + return jsonValue.isJsonObject(); + } + + @Override + public JsonObject asJsonObject() { + lazilyDeserialize(); + return jsonValue.asJsonObject(); + } + + @Override + public boolean isString() { + lazilyDeserialize(); + return jsonValue.isString(); + } + + @Override + public String asString() { + lazilyDeserialize(); + return jsonValue.asString(); + } + + @Override + public boolean isNumber() { + lazilyDeserialize(); + return jsonValue.isNumber(); + } + + @Override + public Number asNumber() { + lazilyDeserialize(); + return jsonValue.asNumber(); + } + + @Override + public boolean isBoolean() { + lazilyDeserialize(); + return jsonValue.isBoolean(); + } + + @Override + public Boolean asBoolean() { + lazilyDeserialize(); + return jsonValue.asBoolean(); + } + + @Override + public boolean isNull() { + lazilyDeserialize(); + return jsonValue.isNull(); + } + + @Override + public T toObject(Class targetType) { + lazilyDeserialize(); + return jsonValue.toObject(targetType); + } + + private void lazilyDeserialize() { + if (!isDeserialized()) { + synchronized (this) { + if (!isDeserialized()) { + jsonValue = parser.createJsonValue(unprocessedData); + unprocessedData.clear(); + } + } + } + } + + /** + * @return {@code true} if the data has been deserialized + */ + boolean isDeserialized() { + return jsonValue != null; + } + +} diff --git a/src/main/java/io/lettuce/core/json/arguments/JsonGetArgs.java b/src/main/java/io/lettuce/core/json/arguments/JsonGetArgs.java new file mode 100644 index 0000000000..6e3b619956 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/arguments/JsonGetArgs.java @@ -0,0 +1,137 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json.arguments; + +import io.lettuce.core.CompositeArgument; +import io.lettuce.core.protocol.CommandArgs; +import io.lettuce.core.protocol.CommandKeyword; + +/** + * Argument list builder for the Redis JSON.GET command. + *

+ * {@link JsonGetArgs} is a mutable object and instances should be used only once to avoid shared mutable state. + * + * @author Tihomir Mateev + * @since 6.5 + */ +public class JsonGetArgs implements CompositeArgument { + + private String indent; + + private String newline; + + private String space; + + /** + * Builder entry points for {@link JsonGetArgs}. + */ + public static class Builder { + + /** + * Utility constructor. + */ + private Builder() { + } + + /** + * Creates new {@link JsonGetArgs} and sets the string used for indentation. + * + * @return new {@link JsonGetArgs} with indentation set. + */ + public static JsonGetArgs indent(String indent) { + return new JsonGetArgs().indent(indent); + } + + /** + * Creates new {@link JsonGetArgs} and sets the string used for newline. + * + * @return new {@link JsonGetArgs} with newline set. + */ + public static JsonGetArgs newline(String newline) { + return new JsonGetArgs().newline(newline); + } + + /** + * Creates new {@link JsonGetArgs} and sets the string used for spacing. + * + * @return new {@link JsonGetArgs} with spacing set. + */ + public static JsonGetArgs space(String space) { + return new JsonGetArgs().space(space); + } + + /** + * Creates new {@link JsonGetArgs} empty arguments. + * + * @return new {@link JsonGetArgs} with empty arguments set. + */ + public static JsonGetArgs defaults() { + return new JsonGetArgs().defaults(); + } + + } + + /** + * Set the string used for indentation. + * + * @return {@code this}. + */ + public JsonGetArgs indent(String indent) { + + this.indent = indent; + return this; + } + + /** + * Set the string used for newline. + * + * @return {@code this}. + */ + public JsonGetArgs newline(String newline) { + + this.newline = newline; + return this; + } + + /** + * Set the string used for spacing. + * + * @return {@code this}. + */ + public JsonGetArgs space(String space) { + + this.space = space; + return this; + } + + /** + * Set empty arguments. + * + * @return {@code this}. + */ + public JsonGetArgs defaults() { + return this; + } + + @Override + public void build(CommandArgs args) { + + if (indent != null) { + args.add(CommandKeyword.INDENT).add(indent); + } + + if (newline != null) { + args.add(CommandKeyword.NEWLINE).add(newline); + } + + if (space != null) { + args.add(CommandKeyword.SPACE).add(space); + } + } + +} diff --git a/src/main/java/io/lettuce/core/json/arguments/JsonMsetArgs.java b/src/main/java/io/lettuce/core/json/arguments/JsonMsetArgs.java new file mode 100644 index 0000000000..eb5f827aab --- /dev/null +++ b/src/main/java/io/lettuce/core/json/arguments/JsonMsetArgs.java @@ -0,0 +1,67 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json.arguments; + +import io.lettuce.core.CompositeArgument; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.protocol.CommandArgs; + +/** + * Argument list builder for the Redis JSON.MSET command. + *

+ * + * @author Tihomir Mateev + * @since 6.5 + */ +public class JsonMsetArgs implements CompositeArgument { + + private final K key; + + private final JsonPath path; + + private final JsonValue element; + + /** + * Creates a new {@link JsonMsetArgs} given a {@code key}, {@code path} and {@code element}. + * + * @param key the key to set the value for + * @param path the path to set the value for + * @param element the value to set + */ + public JsonMsetArgs(K key, JsonPath path, JsonValue element) { + this.key = key; + this.path = path; + this.element = element; + } + + /** + * Return the key associated with this {@link JsonMsetArgs}. + */ + public K getKey() { + return key; + } + + @SuppressWarnings("unchecked") + @Override + public void build(CommandArgs args) { + + if (key != null) { + args.addKey((K) key); + } + + if (path != null) { + args.add(path.toString()); + } + + if (element != null) { + args.add(element.asByteBuffer().array()); + } + } + +} diff --git a/src/main/java/io/lettuce/core/json/arguments/JsonRangeArgs.java b/src/main/java/io/lettuce/core/json/arguments/JsonRangeArgs.java new file mode 100644 index 0000000000..d9a579222c --- /dev/null +++ b/src/main/java/io/lettuce/core/json/arguments/JsonRangeArgs.java @@ -0,0 +1,116 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json.arguments; + +import io.lettuce.core.CompositeArgument; +import io.lettuce.core.protocol.CommandArgs; + +/** + * Argument list builder for the RedisJSON commands that require ranges. By default, start and end indexes are set to 0. + * Modifying these values might have different effects depending on the command they are supplied to. + *

+ * {@link JsonRangeArgs} is a mutable object and instances should be used only once to avoid shared mutable state. + * + * @author Tihomir Mateev + * @since 6.5 + * @see JSON.ARRINDEX + * @see JSON.ARRTRIM + */ +public class JsonRangeArgs implements CompositeArgument { + + /** + * Default start index to indicate where to start slicing the array + */ + public static final int DEFAULT_START_INDEX = 0; + + /** + * Default end index to indicate where to stop slicing the array + */ + public static final int DEFAULT_END_INDEX = 0; + + private long start = DEFAULT_START_INDEX; + + private long stop = DEFAULT_END_INDEX; + + /** + * Builder entry points for {@link JsonRangeArgs}. + */ + public static class Builder { + + /** + * Utility constructor. + */ + private Builder() { + } + + /** + * Creates new {@link JsonRangeArgs} and sets the start index. + * + * @return new {@link JsonRangeArgs} with the start index set. + */ + public static JsonRangeArgs start(long start) { + return new JsonRangeArgs().start(start); + } + + /** + * Creates new {@link JsonRangeArgs} and sets the end index. + * + * @return new {@link JsonRangeArgs} with the end index set. + */ + public static JsonRangeArgs stop(long stop) { + return new JsonRangeArgs().stop(stop); + } + + /** + * Creates new {@link JsonRangeArgs} and sets default values. + *

+ * The default start index is 0 and the default end index is 0. + * + * @return new {@link JsonRangeArgs} with the end index set. + */ + public static JsonRangeArgs defaults() { + return new JsonRangeArgs(); + } + + } + + /** + * Set the start index. + * + * @return {@code this}. + */ + public JsonRangeArgs start(long start) { + + this.start = start; + return this; + } + + /** + * Set the end index. + * + * @return {@code this}. + */ + public JsonRangeArgs stop(long stop) { + + this.stop = stop; + return this; + } + + @Override + public void build(CommandArgs args) { + + if (start != DEFAULT_START_INDEX || stop != DEFAULT_END_INDEX) { + args.add(start); + } + + if (stop != DEFAULT_END_INDEX) { + args.add(stop); + } + } + +} diff --git a/src/main/java/io/lettuce/core/json/arguments/JsonSetArgs.java b/src/main/java/io/lettuce/core/json/arguments/JsonSetArgs.java new file mode 100644 index 0000000000..53925bdf49 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/arguments/JsonSetArgs.java @@ -0,0 +1,110 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json.arguments; + +import io.lettuce.core.CompositeArgument; +import io.lettuce.core.protocol.CommandArgs; +import io.lettuce.core.protocol.CommandKeyword; + +/** + * Argument list builder for the Redis JSON.SET command. + *

+ * {@link JsonSetArgs} is a mutable object and instances should be used only once to avoid shared mutable state. + * + * @author Mark Paluch + * @since 6.5 + */ +public class JsonSetArgs implements CompositeArgument { + + private boolean nx; + + private boolean xx; + + /** + * Builder entry points for {@link JsonSetArgs}. + */ + public static class Builder { + + /** + * Utility constructor. + */ + private Builder() { + } + + /** + * Creates new {@link JsonSetArgs} and sets {@literal NX}. + * + * @return new {@link JsonSetArgs} with {@literal NX} set. + */ + public static JsonSetArgs nx() { + return new JsonSetArgs().nx(); + } + + /** + * Creates new {@link JsonSetArgs} and sets {@literal XX}. + * + * @return new {@link JsonSetArgs} with {@literal XX} set. + */ + public static JsonSetArgs xx() { + return new JsonSetArgs().xx(); + } + + /** + * Creates new empty {@link JsonSetArgs} + * + * @return new {@link JsonSetArgs} with nothing set. + */ + public static JsonSetArgs defaults() { + return new JsonSetArgs().defaults(); + } + + } + + /** + * Set the key only if it does not already exist. + * + * @return {@code this}. + */ + public JsonSetArgs nx() { + + this.nx = true; + return this; + } + + /** + * Set the key only if it already exists. + * + * @return {@code this}. + */ + public JsonSetArgs xx() { + + this.xx = true; + return this; + } + + /** + * Set the key only if it already exists. + * + * @return {@code this}. + */ + public JsonSetArgs defaults() { + + return this; + } + + @Override + public void build(CommandArgs args) { + + if (xx) { + args.add(CommandKeyword.XX); + } else if (nx) { + args.add(CommandKeyword.NX); + } + } + +} diff --git a/src/main/java/io/lettuce/core/json/package-info.java b/src/main/java/io/lettuce/core/json/package-info.java new file mode 100644 index 0000000000..a7b24f7830 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/package-info.java @@ -0,0 +1,4 @@ +/** + * Support for the JSON Redis Module. + */ +package io.lettuce.core.json; diff --git a/src/main/java/io/lettuce/core/masterreplica/AutodiscoveryConnector.java b/src/main/java/io/lettuce/core/masterreplica/AutodiscoveryConnector.java index aad151858c..cb17ec886a 100644 --- a/src/main/java/io/lettuce/core/masterreplica/AutodiscoveryConnector.java +++ b/src/main/java/io/lettuce/core/masterreplica/AutodiscoveryConnector.java @@ -134,7 +134,7 @@ private Mono> initializeConnection(Re redisClient.getResources(), redisClient.getOptions()); StatefulRedisMasterReplicaConnectionImpl connection = new StatefulRedisMasterReplicaConnectionImpl<>( - channelWriter, codec, redisURI.getTimeout()); + channelWriter, codec, redisURI.getTimeout(), redisClient.getOptions().getJsonParser()); connection.setOptions(redisClient.getOptions()); diff --git a/src/main/java/io/lettuce/core/masterreplica/MasterReplicaChannelWriter.java b/src/main/java/io/lettuce/core/masterreplica/MasterReplicaChannelWriter.java index bfa4b4bd28..9ad7a8e451 100644 --- a/src/main/java/io/lettuce/core/masterreplica/MasterReplicaChannelWriter.java +++ b/src/main/java/io/lettuce/core/masterreplica/MasterReplicaChannelWriter.java @@ -271,11 +271,11 @@ private static boolean isSuccessfullyCompleted(CompletableFuture connectFutur } private static boolean isStartTransaction(ProtocolKeyword command) { - return command.name().equals("MULTI"); + return command.toString().equals("MULTI"); } private boolean isEndTransaction(ProtocolKeyword command) { - return command.name().equals("EXEC") || command.name().equals("DISCARD"); + return command.toString().equals("EXEC") || command.toString().equals("DISCARD"); } } diff --git a/src/main/java/io/lettuce/core/masterreplica/SentinelConnector.java b/src/main/java/io/lettuce/core/masterreplica/SentinelConnector.java index afda7cc573..ba44025af9 100644 --- a/src/main/java/io/lettuce/core/masterreplica/SentinelConnector.java +++ b/src/main/java/io/lettuce/core/masterreplica/SentinelConnector.java @@ -98,7 +98,7 @@ public CompletableFuture closeAsync() { }; StatefulRedisMasterReplicaConnectionImpl connection = new StatefulRedisMasterReplicaConnectionImpl<>( - channelWriter, codec, redisURI.getTimeout()); + channelWriter, codec, redisURI.getTimeout(), redisClient.getOptions().getJsonParser()); connection.setOptions(redisClient.getOptions()); CompletionStage bind = sentinelTopologyRefresh.bind(runnable); diff --git a/src/main/java/io/lettuce/core/masterreplica/StatefulRedisMasterReplicaConnectionImpl.java b/src/main/java/io/lettuce/core/masterreplica/StatefulRedisMasterReplicaConnectionImpl.java index 47d793fac4..974b18a045 100644 --- a/src/main/java/io/lettuce/core/masterreplica/StatefulRedisMasterReplicaConnectionImpl.java +++ b/src/main/java/io/lettuce/core/masterreplica/StatefulRedisMasterReplicaConnectionImpl.java @@ -5,6 +5,7 @@ import io.lettuce.core.ReadFrom; import io.lettuce.core.StatefulRedisConnectionImpl; import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonParser; /** * @author Mark Paluch @@ -19,8 +20,9 @@ class StatefulRedisMasterReplicaConnectionImpl extends StatefulRedisConnec * @param codec Codec used to encode/decode keys and values. * @param timeout Maximum time to wait for a response. */ - StatefulRedisMasterReplicaConnectionImpl(MasterReplicaChannelWriter writer, RedisCodec codec, Duration timeout) { - super(writer, NoOpPushHandler.INSTANCE, codec, timeout); + StatefulRedisMasterReplicaConnectionImpl(MasterReplicaChannelWriter writer, RedisCodec codec, Duration timeout, + JsonParser parser) { + super(writer, NoOpPushHandler.INSTANCE, codec, timeout, parser); } @Override diff --git a/src/main/java/io/lettuce/core/masterreplica/StaticMasterReplicaConnector.java b/src/main/java/io/lettuce/core/masterreplica/StaticMasterReplicaConnector.java index df9a33eeca..3b504d1f75 100644 --- a/src/main/java/io/lettuce/core/masterreplica/StaticMasterReplicaConnector.java +++ b/src/main/java/io/lettuce/core/masterreplica/StaticMasterReplicaConnector.java @@ -90,7 +90,7 @@ private Mono> initializeConnection(Re redisClient.getResources(), redisClient.getOptions()); StatefulRedisMasterReplicaConnectionImpl connection = new StatefulRedisMasterReplicaConnectionImpl<>( - channelWriter, codec, seedNode.getTimeout()); + channelWriter, codec, seedNode.getTimeout(), redisClient.getOptions().getJsonParser()); connection.setOptions(redisClient.getOptions()); return Mono.just(connection); diff --git a/src/main/java/io/lettuce/core/metrics/CommandLatencyId.java b/src/main/java/io/lettuce/core/metrics/CommandLatencyId.java index 0cf213ab0d..8e3b4d45fd 100644 --- a/src/main/java/io/lettuce/core/metrics/CommandLatencyId.java +++ b/src/main/java/io/lettuce/core/metrics/CommandLatencyId.java @@ -31,7 +31,7 @@ protected CommandLatencyId(SocketAddress localAddress, SocketAddress remoteAddre this.localAddress = localAddress; this.remoteAddress = remoteAddress; this.commandType = commandType; - this.commandName = commandType.name(); + this.commandName = commandType.toString(); } /** diff --git a/src/main/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorder.java b/src/main/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorder.java index fcd47c7fb9..0cb992029c 100644 --- a/src/main/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorder.java +++ b/src/main/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorder.java @@ -115,7 +115,7 @@ protected Timer completionTimer(CommandLatencyId commandLatencyId) { Timer.Builder timer = Timer.builder(METRIC_COMPLETION) .description("Latency between command send and command completion (complete response received") - .tag(LABEL_COMMAND, commandLatencyId.commandType().name()) + .tag(LABEL_COMMAND, commandLatencyId.commandType().toString()) .tag(LABEL_LOCAL, commandLatencyId.localAddress().toString()) .tag(LABEL_REMOTE, commandLatencyId.remoteAddress().toString()).tags(options.tags()); @@ -131,7 +131,7 @@ protected Timer firstResponseTimer(CommandLatencyId commandLatencyId) { Timer.Builder timer = Timer.builder(METRIC_FIRST_RESPONSE) .description("Latency between command send and first response (first response received)") - .tag(LABEL_COMMAND, commandLatencyId.commandType().name()) + .tag(LABEL_COMMAND, commandLatencyId.commandType().toString()) .tag(LABEL_LOCAL, commandLatencyId.localAddress().toString()) .tag(LABEL_REMOTE, commandLatencyId.remoteAddress().toString()).tags(options.tags()); diff --git a/src/main/java/io/lettuce/core/metrics/MicrometerOptions.java b/src/main/java/io/lettuce/core/metrics/MicrometerOptions.java index 86886f1a99..185358fca3 100644 --- a/src/main/java/io/lettuce/core/metrics/MicrometerOptions.java +++ b/src/main/java/io/lettuce/core/metrics/MicrometerOptions.java @@ -217,7 +217,7 @@ public Builder enabledCommands(CommandType... commands) { enabledCommands.add(enabledCommand.name()); } - return metricsFilter(command -> enabledCommands.contains(command.getType().name())); + return metricsFilter(command -> enabledCommands.contains(command.getType().toString())); } /** diff --git a/src/main/java/io/lettuce/core/output/JsonTypeListOutput.java b/src/main/java/io/lettuce/core/output/JsonTypeListOutput.java new file mode 100644 index 0000000000..96f942f280 --- /dev/null +++ b/src/main/java/io/lettuce/core/output/JsonTypeListOutput.java @@ -0,0 +1,50 @@ +/* + * Copyright 2011-Present, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.output; + +import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonType; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; + +/** + * {@link List} of {@link JsonType} output. + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @since 6.5 + */ +public class JsonTypeListOutput extends CommandOutput> { + + private boolean initialized; + + public JsonTypeListOutput(RedisCodec codec) { + super(codec, Collections.emptyList()); + } + + @Override + public void set(ByteBuffer bytes) { + if (!initialized) { + multi(1); + } + + output.add(JsonType.fromString(decodeAscii(bytes))); + } + + @Override + public void multi(int count) { + + if (!initialized) { + output = OutputFactory.newList(count); + initialized = true; + } + } + +} diff --git a/src/main/java/io/lettuce/core/output/JsonValueListOutput.java b/src/main/java/io/lettuce/core/output/JsonValueListOutput.java new file mode 100644 index 0000000000..389b696624 --- /dev/null +++ b/src/main/java/io/lettuce/core/output/JsonValueListOutput.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.output; + +import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.JsonParser; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; + +/** + * {@link List} of string output. + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @since 6.5 + */ +public class JsonValueListOutput extends CommandOutput> { + + private boolean initialized; + + private final JsonParser parser; + + public JsonValueListOutput(RedisCodec codec, JsonParser theParser) { + super(codec, Collections.emptyList()); + parser = theParser; + } + + @Override + public void set(ByteBuffer bytes) { + if (!initialized) { + multi(1); + } + + ByteBuffer fetched = ByteBuffer.allocate(bytes.remaining()); + fetched.put(bytes); + fetched.flip(); + output.add(parser.loadJsonValue(fetched)); + } + + @Override + public void multi(int count) { + if (!initialized) { + output = OutputFactory.newList(count); + initialized = true; + } + } + +} diff --git a/src/main/java/io/lettuce/core/output/NumberListOutput.java b/src/main/java/io/lettuce/core/output/NumberListOutput.java new file mode 100644 index 0000000000..cbbb9bd9b3 --- /dev/null +++ b/src/main/java/io/lettuce/core/output/NumberListOutput.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020-Present, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.output; + +import io.lettuce.core.codec.RedisCodec; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.nio.ByteBuffer; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; + +/** + * {@link List} of Number output. + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @since 6.5 + */ +public class NumberListOutput extends CommandOutput> { + + private static final InternalLogger LOG = InternalLoggerFactory.getInstance(NumberListOutput.class); + + private boolean initialized; + + public NumberListOutput(RedisCodec codec) { + super(codec, new ArrayList<>()); + } + + @Override + public void set(ByteBuffer bytes) { + output.add(bytes != null ? parseNumber(bytes) : null); + } + + @Override + public void set(double number) { + output.add(number); + } + + @Override + public void set(long integer) { + output.add(integer); + } + + @Override + public void setBigNumber(ByteBuffer bytes) { + output.add(bytes != null ? parseNumber(bytes) : null); + } + + @Override + public void multi(int count) { + if (!initialized) { + output = OutputFactory.newList(count); + initialized = true; + } + } + + private Number parseNumber(ByteBuffer bytes) { + Number result = 0; + try { + result = NumberFormat.getNumberInstance().parse(decodeAscii(bytes)); + } catch (ParseException e) { + LOG.warn("Failed to parse " + bytes, e); + } + + return result; + } + +} diff --git a/src/main/java/io/lettuce/core/protocol/BaseRedisCommandBuilder.java b/src/main/java/io/lettuce/core/protocol/BaseRedisCommandBuilder.java index fb149d4ac0..98fc293ea2 100644 --- a/src/main/java/io/lettuce/core/protocol/BaseRedisCommandBuilder.java +++ b/src/main/java/io/lettuce/core/protocol/BaseRedisCommandBuilder.java @@ -1,22 +1,36 @@ package io.lettuce.core.protocol; +import io.lettuce.core.Limit; +import io.lettuce.core.Range; import io.lettuce.core.RedisException; import io.lettuce.core.ScriptOutputType; import io.lettuce.core.codec.RedisCodec; -import io.lettuce.core.output.BooleanOutput; -import io.lettuce.core.output.CommandOutput; -import io.lettuce.core.output.IntegerOutput; -import io.lettuce.core.output.NestedMultiOutput; -import io.lettuce.core.output.ObjectOutput; -import io.lettuce.core.output.StatusOutput; -import io.lettuce.core.output.ValueOutput; +import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.output.*; + +import java.nio.ByteBuffer; + +import static io.lettuce.core.protocol.CommandKeyword.LIMIT; /** + * Common utility methods shared by all implementations of the Redis command builder. + * * @author Mark Paluch + * @author Tihomir Mateev * @since 3.0 */ public class BaseRedisCommandBuilder { + protected static final String MUST_NOT_CONTAIN_NULL_ELEMENTS = "must not contain null elements"; + + protected static final String MUST_NOT_BE_EMPTY = "must not be empty"; + + protected static final String MUST_NOT_BE_NULL = "must not be null"; + + protected static final byte[] MINUS_BYTES = { '-' }; + + protected static final byte[] PLUS_BYTES = { '+' }; + protected final RedisCodec codec; public BaseRedisCommandBuilder(RedisCodec codec) { @@ -66,4 +80,146 @@ protected CommandOutput newScriptOutput(RedisCodec codec, Scr } } + protected boolean allElementsInstanceOf(Object[] objects, Class expectedAssignableType) { + + for (Object object : objects) { + if (!expectedAssignableType.isAssignableFrom(object.getClass())) { + return false; + } + } + + return true; + } + + protected byte[] maxValue(Range range) { + + Range.Boundary upper = range.getUpper(); + + if (upper.getValue() == null) { + return PLUS_BYTES; + } + + ByteBuffer encoded = codec.encodeValue(upper.getValue()); + ByteBuffer allocated = ByteBuffer.allocate(encoded.remaining() + 1); + allocated.put(upper.isIncluding() ? (byte) '[' : (byte) '(').put(encoded); + + return allocated.array(); + } + + protected byte[] minValue(Range range) { + + Range.Boundary lower = range.getLower(); + + if (lower.getValue() == null) { + return MINUS_BYTES; + } + + ByteBuffer encoded = codec.encodeValue(lower.getValue()); + ByteBuffer allocated = ByteBuffer.allocate(encoded.remaining() + 1); + allocated.put(lower.isIncluding() ? (byte) '[' : (byte) '(').put(encoded); + + return allocated.array(); + } + + protected static void notNull(ScoredValueStreamingChannel channel) { + LettuceAssert.notNull(channel, "ScoredValueStreamingChannel " + MUST_NOT_BE_NULL); + } + + protected static void notNull(KeyStreamingChannel channel) { + LettuceAssert.notNull(channel, "KeyValueStreamingChannel " + MUST_NOT_BE_NULL); + } + + protected static void notNull(ValueStreamingChannel channel) { + LettuceAssert.notNull(channel, "ValueStreamingChannel " + MUST_NOT_BE_NULL); + } + + protected static void notNull(KeyValueStreamingChannel channel) { + LettuceAssert.notNull(channel, "KeyValueStreamingChannel " + MUST_NOT_BE_NULL); + } + + protected static void notNullMinMax(String min, String max) { + LettuceAssert.notNull(min, "Min " + MUST_NOT_BE_NULL); + LettuceAssert.notNull(max, "Max " + MUST_NOT_BE_NULL); + } + + protected static void addLimit(CommandArgs args, Limit limit) { + + if (limit.isLimited()) { + args.add(LIMIT).add(limit.getOffset()).add(limit.getCount()); + } + } + + protected static void assertNodeId(String nodeId) { + LettuceAssert.notNull(nodeId, "NodeId " + MUST_NOT_BE_NULL); + LettuceAssert.notEmpty(nodeId, "NodeId " + MUST_NOT_BE_EMPTY); + } + + protected static String max(Range range) { + + Range.Boundary upper = range.getUpper(); + + if (upper.getValue() == null + || upper.getValue() instanceof Double && upper.getValue().doubleValue() == Double.POSITIVE_INFINITY) { + return "+inf"; + } + + if (!upper.isIncluding()) { + return "(" + upper.getValue(); + } + + return upper.getValue().toString(); + } + + protected static String min(Range range) { + + Range.Boundary lower = range.getLower(); + + if (lower.getValue() == null + || lower.getValue() instanceof Double && lower.getValue().doubleValue() == Double.NEGATIVE_INFINITY) { + return "-inf"; + } + + if (!lower.isIncluding()) { + return "(" + lower.getValue(); + } + + return lower.getValue().toString(); + } + + protected static void notEmpty(Object[] keys) { + LettuceAssert.notNull(keys, "Keys " + MUST_NOT_BE_NULL); + LettuceAssert.notEmpty(keys, "Keys " + MUST_NOT_BE_EMPTY); + } + + protected static void notEmptySlots(int[] slots) { + LettuceAssert.notNull(slots, "Slots " + MUST_NOT_BE_NULL); + LettuceAssert.notEmpty(slots, "Slots " + MUST_NOT_BE_EMPTY); + } + + protected static void notEmptyValues(Object[] values) { + LettuceAssert.notNull(values, "Values " + MUST_NOT_BE_NULL); + LettuceAssert.notEmpty(values, "Values " + MUST_NOT_BE_EMPTY); + } + + protected static void notNullKey(Object key) { + LettuceAssert.notNull(key, "Key " + MUST_NOT_BE_NULL); + } + + protected static void keyAndFieldsProvided(Object key, Object[] fields) { + LettuceAssert.notNull(key, "Key " + MUST_NOT_BE_NULL); + LettuceAssert.notEmpty(fields, "Fields " + MUST_NOT_BE_EMPTY); + } + + protected static void notNullLimit(Limit limit) { + LettuceAssert.notNull(limit, "Limit " + MUST_NOT_BE_NULL); + } + + protected static void notNullRange(Range range) { + LettuceAssert.notNull(range, "Range " + MUST_NOT_BE_NULL); + } + + protected static void notEmptyRanges(Range[] ranges) { + LettuceAssert.notEmpty(ranges, "Ranges " + MUST_NOT_BE_NULL); + } + } diff --git a/src/main/java/io/lettuce/core/protocol/CommandArgs.java b/src/main/java/io/lettuce/core/protocol/CommandArgs.java index c34ed70e1c..da5f897852 100644 --- a/src/main/java/io/lettuce/core/protocol/CommandArgs.java +++ b/src/main/java/io/lettuce/core/protocol/CommandArgs.java @@ -441,7 +441,7 @@ static BytesArgument of(ProtocolKeyword protocolKeyword) { @Override public String toString() { - return protocolKeyword.name(); + return protocolKeyword.toString(); } } diff --git a/src/main/java/io/lettuce/core/protocol/CommandHandler.java b/src/main/java/io/lettuce/core/protocol/CommandHandler.java index 83bf304020..d6bdacadf1 100644 --- a/src/main/java/io/lettuce/core/protocol/CommandHandler.java +++ b/src/main/java/io/lettuce/core/protocol/CommandHandler.java @@ -476,7 +476,7 @@ private void attachTracing(ChannelHandlerContext ctx, RedisCommand comm TraceContext context = provider.getTraceContext(); Tracer.Span span = tracer.nextSpan(context); - span.name(command.getType().name()); + span.name(command.getType().toString()); if (tracedEndpoint != null) { span.remoteEndpoint(tracedEndpoint); diff --git a/src/main/java/io/lettuce/core/protocol/CommandKeyword.java b/src/main/java/io/lettuce/core/protocol/CommandKeyword.java index bd56b713dc..8cd1f0f9d3 100644 --- a/src/main/java/io/lettuce/core/protocol/CommandKeyword.java +++ b/src/main/java/io/lettuce/core/protocol/CommandKeyword.java @@ -49,11 +49,11 @@ public enum CommandKeyword implements ProtocolKeyword { MIGRATING, IMPORTING, SAVE, SKIPME, SLAVES, STREAM, STORE, SUM, SEGFAULT, SETUSER, TAKEOVER, TRACKING, TRACKINGINFO, TYPE, UNBLOCK, USERS, USAGE, WEIGHTS, WHOAMI, - WITHSCORE, WITHSCORES, WITHVALUES, XOR, XX, YES; + WITHSCORE, WITHSCORES, WITHVALUES, XOR, XX, YES, INDENT, NEWLINE, SPACE; public final byte[] bytes; - private CommandKeyword() { + CommandKeyword() { bytes = name().getBytes(StandardCharsets.US_ASCII); } diff --git a/src/main/java/io/lettuce/core/protocol/CommandType.java b/src/main/java/io/lettuce/core/protocol/CommandType.java index 4a44d6087e..7eb949e5d3 100644 --- a/src/main/java/io/lettuce/core/protocol/CommandType.java +++ b/src/main/java/io/lettuce/core/protocol/CommandType.java @@ -103,6 +103,15 @@ public enum CommandType implements ProtocolKeyword { XACK, XADD, XAUTOCLAIM, XCLAIM, XDEL, XGROUP, XINFO, XLEN, XPENDING, XRANGE, XREVRANGE, XREAD, XREADGROUP, XTRIM, + // JSON + + JSON_ARRAPPEND("JSON.ARRAPPEND"), JSON_ARRINDEX("JSON.ARRINDEX"), JSON_ARRINSERT("JSON.ARRINSERT"), JSON_ARRLEN( + "JSON.ARRLEN"), JSON_ARRPOP("JSON.ARRPOP"), JSON_ARRTRIM("JSON.ARRTRIM"), JSON_CLEAR("JSON.CLEAR"), JSON_DEL( + "JSON.DEL"), JSON_GET("JSON.GET"), JSON_MERGE("JSON.MERGE"), JSON_MGET("JSON.MGET"), JSON_MSET( + "JSON.MSET"), JSON_NUMINCRBY("JSON.NUMINCRBY"), JSON_OBJKEYS("JSON.OBJKEYS"), JSON_OBJLEN( + "JSON.OBJLEN"), JSON_SET("JSON.SET"), JSON_STRAPPEND("JSON.STRAPPEND"), JSON_STRLEN( + "JSON.STRLEN"), JSON_TOGGLE("JSON.TOGGLE"), JSON_TYPE("JSON.TYPE"), + // Others TIME, WAIT, @@ -117,10 +126,34 @@ public enum CommandType implements ProtocolKeyword { public final byte[] bytes; + private final String command; + + /** + * Simple commands (comprised of only letters) use the name of the enum constant as command name. + */ CommandType() { + command = name(); bytes = name().getBytes(StandardCharsets.US_ASCII); } + /** + * Complex commands (comprised of other symbols besides letters) get the command name as a parameter. + * + * @param name the command name, must not be {@literal null}. + */ + CommandType(String name) { + command = name; + bytes = name.getBytes(StandardCharsets.US_ASCII); + } + + /** + * + * @return name of the command. + */ + public String toString() { + return command; + } + @Override public byte[] getBytes() { return bytes; diff --git a/src/main/java/io/lettuce/core/protocol/ProtocolKeyword.java b/src/main/java/io/lettuce/core/protocol/ProtocolKeyword.java index 88d432407a..79d96058dc 100644 --- a/src/main/java/io/lettuce/core/protocol/ProtocolKeyword.java +++ b/src/main/java/io/lettuce/core/protocol/ProtocolKeyword.java @@ -17,6 +17,6 @@ public interface ProtocolKeyword { * * @return name of the command. */ - String name(); + String toString(); } diff --git a/src/main/java/io/lettuce/core/pubsub/PubSubCommandHandler.java b/src/main/java/io/lettuce/core/pubsub/PubSubCommandHandler.java index 30d811c71f..bf7daf615a 100644 --- a/src/main/java/io/lettuce/core/pubsub/PubSubCommandHandler.java +++ b/src/main/java/io/lettuce/core/pubsub/PubSubCommandHandler.java @@ -213,7 +213,7 @@ protected void notifyPushListeners(PushMessage notification) { private boolean shouldCompleteCommand(PubSubOutput.Type type, RedisCommand command) { - String commandType = command.getType().name(); + String commandType = command.getType().toString(); switch (type) { case subscribe: return commandType.equalsIgnoreCase("SUBSCRIBE"); diff --git a/src/main/java/io/lettuce/core/pubsub/PubSubEndpoint.java b/src/main/java/io/lettuce/core/pubsub/PubSubEndpoint.java index 6da336c4d0..671b24aed3 100644 --- a/src/main/java/io/lettuce/core/pubsub/PubSubEndpoint.java +++ b/src/main/java/io/lettuce/core/pubsub/PubSubEndpoint.java @@ -27,7 +27,6 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; import io.lettuce.core.ClientOptions; import io.lettuce.core.ConnectionState; @@ -151,7 +150,7 @@ public RedisCommand write(RedisCommand command return command; } - if (!subscribeWritten && SUBSCRIBE_COMMANDS.contains(command.getType().name())) { + if (!subscribeWritten && SUBSCRIBE_COMMANDS.contains(command.getType().toString())) { subscribeWritten = true; } @@ -171,7 +170,7 @@ public RedisCommand write(RedisCommand command if (!subscribeWritten) { for (RedisCommand redisCommand : redisCommands) { - if (SUBSCRIBE_COMMANDS.contains(redisCommand.getType().name())) { + if (SUBSCRIBE_COMMANDS.contains(redisCommand.getType().toString())) { subscribeWritten = true; break; } @@ -184,14 +183,14 @@ public RedisCommand write(RedisCommand command protected void rejectCommand(RedisCommand command) { command.completeExceptionally( new RedisException(String.format("Command %s not allowed while subscribed. Allowed commands are: %s", - command.getType().name(), ALLOWED_COMMANDS_SUBSCRIBED))); + command.getType().toString(), ALLOWED_COMMANDS_SUBSCRIBED))); } protected void rejectCommands(Collection> redisCommands) { for (RedisCommand command : redisCommands) { command.completeExceptionally( new RedisException(String.format("Command %s not allowed while subscribed. Allowed commands are: %s", - command.getType().name(), ALLOWED_COMMANDS_SUBSCRIBED))); + command.getType().toString(), ALLOWED_COMMANDS_SUBSCRIBED))); } } @@ -215,7 +214,7 @@ private boolean isAllowed(RedisCommand command) { protocolVersion = getProtocolVersion(); } - return protocolVersion == ProtocolVersion.RESP3 || ALLOWED_COMMANDS_SUBSCRIBED.contains(command.getType().name()); + return protocolVersion == ProtocolVersion.RESP3 || ALLOWED_COMMANDS_SUBSCRIBED.contains(command.getType().toString()); } public boolean isSubscribed() { diff --git a/src/main/java/io/lettuce/core/pubsub/RedisPubSubAsyncCommandsImpl.java b/src/main/java/io/lettuce/core/pubsub/RedisPubSubAsyncCommandsImpl.java index 3e282b5410..44108e4942 100644 --- a/src/main/java/io/lettuce/core/pubsub/RedisPubSubAsyncCommandsImpl.java +++ b/src/main/java/io/lettuce/core/pubsub/RedisPubSubAsyncCommandsImpl.java @@ -47,7 +47,7 @@ public class RedisPubSubAsyncCommandsImpl extends RedisAsyncCommandsImpl connection, RedisCodec codec) { - super(connection, codec); + super(connection, codec, null); this.commandBuilder = new PubSubCommandBuilder<>(codec); } diff --git a/src/main/java/io/lettuce/core/pubsub/RedisPubSubReactiveCommandsImpl.java b/src/main/java/io/lettuce/core/pubsub/RedisPubSubReactiveCommandsImpl.java index e13bb3ce69..54774b7269 100644 --- a/src/main/java/io/lettuce/core/pubsub/RedisPubSubReactiveCommandsImpl.java +++ b/src/main/java/io/lettuce/core/pubsub/RedisPubSubReactiveCommandsImpl.java @@ -52,7 +52,7 @@ public class RedisPubSubReactiveCommandsImpl extends RedisReactiveCommands * @param codec Codec used to encode/decode keys and values. */ public RedisPubSubReactiveCommandsImpl(StatefulRedisPubSubConnection connection, RedisCodec codec) { - super(connection, codec); + super(connection, codec, null); this.commandBuilder = new PubSubCommandBuilder<>(codec); } diff --git a/src/main/java/io/lettuce/core/pubsub/StatefulRedisPubSubConnectionImpl.java b/src/main/java/io/lettuce/core/pubsub/StatefulRedisPubSubConnectionImpl.java index 33b1f1412e..09c802e80d 100644 --- a/src/main/java/io/lettuce/core/pubsub/StatefulRedisPubSubConnectionImpl.java +++ b/src/main/java/io/lettuce/core/pubsub/StatefulRedisPubSubConnectionImpl.java @@ -62,7 +62,7 @@ public class StatefulRedisPubSubConnectionImpl extends StatefulRedisConnec public StatefulRedisPubSubConnectionImpl(PubSubEndpoint endpoint, RedisChannelWriter writer, RedisCodec codec, Duration timeout) { - super(writer, endpoint, codec, timeout); + super(writer, endpoint, codec, timeout, null); this.endpoint = endpoint; endpoint.setConnectionState(getConnectionState()); } diff --git a/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java b/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java index d68142cefa..abd388827d 100644 --- a/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java +++ b/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java @@ -28,6 +28,7 @@ import io.lettuce.core.api.StatefulConnection; import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.json.JsonParser; import io.lettuce.core.output.CommandOutput; import io.lettuce.core.protocol.Command; import io.lettuce.core.protocol.CommandArgs; @@ -50,8 +51,8 @@ public class RedisSentinelReactiveCommandsImpl extends AbstractRedisReacti private final SentinelCommandBuilder commandBuilder; - public RedisSentinelReactiveCommandsImpl(StatefulConnection connection, RedisCodec codec) { - super(connection, codec); + public RedisSentinelReactiveCommandsImpl(StatefulConnection connection, RedisCodec codec, JsonParser parser) { + super(connection, codec, parser); commandBuilder = new SentinelCommandBuilder(codec); } diff --git a/src/main/java/io/lettuce/core/sentinel/StatefulRedisSentinelConnectionImpl.java b/src/main/java/io/lettuce/core/sentinel/StatefulRedisSentinelConnectionImpl.java index 0f729d9096..e8f1ec5ed9 100644 --- a/src/main/java/io/lettuce/core/sentinel/StatefulRedisSentinelConnectionImpl.java +++ b/src/main/java/io/lettuce/core/sentinel/StatefulRedisSentinelConnectionImpl.java @@ -27,6 +27,7 @@ import io.lettuce.core.RedisChannelWriter; import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.json.JsonParser; import io.lettuce.core.output.StatusOutput; import io.lettuce.core.protocol.*; import io.lettuce.core.sentinel.api.StatefulRedisSentinelConnection; @@ -50,14 +51,15 @@ public class StatefulRedisSentinelConnectionImpl extends RedisChannelHandl private final SentinelConnectionState connectionState = new SentinelConnectionState(); - public StatefulRedisSentinelConnectionImpl(RedisChannelWriter writer, RedisCodec codec, Duration timeout) { + public StatefulRedisSentinelConnectionImpl(RedisChannelWriter writer, RedisCodec codec, Duration timeout, + JsonParser parser) { super(writer, timeout); this.codec = codec; this.async = new RedisSentinelAsyncCommandsImpl<>(this, codec); this.sync = syncHandler(async, RedisSentinelCommands.class); - this.reactive = new RedisSentinelReactiveCommandsImpl<>(this, codec); + this.reactive = new RedisSentinelReactiveCommandsImpl<>(this, codec, parser); } @Override diff --git a/src/main/java/io/lettuce/core/tracing/BraveTracing.java b/src/main/java/io/lettuce/core/tracing/BraveTracing.java index 339db4178c..05b62d4388 100644 --- a/src/main/java/io/lettuce/core/tracing/BraveTracing.java +++ b/src/main/java/io/lettuce/core/tracing/BraveTracing.java @@ -341,7 +341,7 @@ static class BraveSpan extends Tracer.Span { @Override public BraveSpan start(RedisCommand command) { - span.name(command.getType().name()); + span.name(command.getType().toString()); if (includeCommandArgsInSpanTags && command.getArgs() != null) { span.tag("redis.args", command.getArgs().toCommandString()); diff --git a/src/main/java/io/lettuce/core/tracing/DefaultLettuceObservationConvention.java b/src/main/java/io/lettuce/core/tracing/DefaultLettuceObservationConvention.java index a79dd83a09..f75293d934 100644 --- a/src/main/java/io/lettuce/core/tracing/DefaultLettuceObservationConvention.java +++ b/src/main/java/io/lettuce/core/tracing/DefaultLettuceObservationConvention.java @@ -30,7 +30,7 @@ public KeyValues getLowCardinalityKeyValues(LettuceObservationContext context) { Tracing.Endpoint ep = context.getRequiredEndpoint(); KeyValues keyValues = KeyValues.of(LowCardinalityCommandKeyNames.DATABASE_SYSTEM.withValue("redis"), // - LowCardinalityCommandKeyNames.REDIS_COMMAND.withValue(context.getRequiredCommand().getType().name())); + LowCardinalityCommandKeyNames.REDIS_COMMAND.withValue(context.getRequiredCommand().getType().toString())); if (ep instanceof SocketAddressEndpoint) { @@ -62,7 +62,7 @@ public KeyValues getHighCardinalityKeyValues(LettuceObservationContext context) if (command.getArgs() != null) { return KeyValues.of(HighCardinalityCommandKeyNames.STATEMENT - .withValue(command.getType().name() + " " + command.getArgs().toCommandString())); + .withValue(command.getType().toString() + " " + command.getArgs().toCommandString())); } } @@ -71,7 +71,7 @@ public KeyValues getHighCardinalityKeyValues(LettuceObservationContext context) @Override public String getContextualName(LettuceObservationContext context) { - return context.getRequiredCommand().getType().name().toLowerCase(Locale.ROOT); + return context.getRequiredCommand().getType().toString().toLowerCase(Locale.ROOT); } public boolean includeCommandArgsInSpanTags() { diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommands.kt index 2732a4f348..f2372548fa 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommands.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommands.kt @@ -28,6 +28,7 @@ import io.lettuce.core.cluster.api.coroutines.RedisClusterCoroutinesCommands * @param Key type. * @param Value type. * @author Mikhael Sokolov + * @author Tihomir Mateev * @since 6.0 */ @ExperimentalLettuceCoroutinesApi @@ -47,7 +48,8 @@ interface RedisCoroutinesCommands : RedisStreamCoroutinesCommands, RedisStringCoroutinesCommands, RedisTransactionalCoroutinesCommands, - RedisClusterCoroutinesCommands { + RedisClusterCoroutinesCommands, + RedisJsonCoroutinesCommands{ /** * Authenticate to the server. diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommandsImpl.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommandsImpl.kt index 0dc6b5e196..a262d39435 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommandsImpl.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommandsImpl.kt @@ -31,9 +31,8 @@ import kotlinx.coroutines.reactive.awaitFirstOrNull * * @param Key type. * @param Value type. - * @author Mikhael Sokolov - * @author Mark Paluch - * @since 6.0 + * @author Tihomir Mateev + * @since 6.5 */ @ExperimentalLettuceCoroutinesApi open class RedisCoroutinesCommandsImpl( @@ -53,7 +52,8 @@ open class RedisCoroutinesCommandsImpl( RedisSortedSetCoroutinesCommands by RedisSortedSetCoroutinesCommandsImpl(ops), RedisStreamCoroutinesCommands by RedisStreamCoroutinesCommandsImpl(ops), RedisStringCoroutinesCommands by RedisStringCoroutinesCommandsImpl(ops), - RedisTransactionalCoroutinesCommands by RedisTransactionalCoroutinesCommandsImpl(ops) { + RedisTransactionalCoroutinesCommands by RedisTransactionalCoroutinesCommandsImpl(ops), + RedisJsonCoroutinesCommands by RedisJsonCoroutinesCommandsImpl(ops) { /** * Authenticate to the server. diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisJsonCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisJsonCoroutinesCommands.kt new file mode 100644 index 0000000000..589cad946c --- /dev/null +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisJsonCoroutinesCommands.kt @@ -0,0 +1,444 @@ +/* + * Copyright 2020-2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.api.coroutines + +import io.lettuce.core.ExperimentalLettuceCoroutinesApi +import kotlinx.coroutines.flow.Flow +import io.lettuce.core.json.JsonType +import io.lettuce.core.json.JsonValue +import io.lettuce.core.json.arguments.JsonGetArgs +import io.lettuce.core.json.arguments.JsonMsetArgs +import io.lettuce.core.json.JsonPath +import io.lettuce.core.json.arguments.JsonRangeArgs +import io.lettuce.core.json.arguments.JsonSetArgs + +/** + * Coroutine executed commands for JSON documents + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @see Redis JSON + * @since 6.5 + * @generated by io.lettuce.apigenerator.CreateKotlinCoroutinesApi + */ +@ExperimentalLettuceCoroutinesApi +interface RedisJsonCoroutinesCommands { + + /** + * Append the JSON values into the array at a given [JsonPath] after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the array inside the document. + * @param values one or more [JsonValue] to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonArrappend(key: K, jsonPath: JsonPath, vararg values: JsonValue): List + + /** + * Append the JSON values into the array at the {@link JsonPath#ROOT_PATH} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param values one or more [JsonValue] to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonArrappend(key: K, vararg values: JsonValue): List + + /** + * Search for the first occurrence of a [JsonValue] in an array at a given [JsonPath] and return its index. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the array inside the document. + * @param value the [JsonValue] to search for. + * @param range the [JsonRangeArgs] to search within. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + suspend fun jsonArrindex(key: K, jsonPath: JsonPath, value: JsonValue, range: JsonRangeArgs): List + + /** + * Search for the first occurrence of a [JsonValue] in an array at a given [JsonPath] and return its index. This + * method uses defaults for the start and end indexes, see {@link JsonRangeArgs#DEFAULT_START_INDEX} and + * {@link JsonRangeArgs#DEFAULT_END_INDEX}. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the array inside the document. + * @param value the [JsonValue] to search for. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + suspend fun jsonArrindex(key: K, jsonPath: JsonPath, value: JsonValue): List + + /** + * Insert the [JsonValue]s into the array at a given [JsonPath] before the provided index, shifting the existing + * elements to the right + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the array inside the document. + * @param index the index before which the new elements will be inserted. + * @param values one or more [JsonValue]s to be inserted. + * @return Long the resulting size of the arrays after the new data was inserted, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonArrinsert(key: K, jsonPath: JsonPath, index: Int, vararg values: JsonValue): List + + /** + * Report the length of the JSON array at a given [JsonPath] + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the array inside the document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonArrlen(key: K, jsonPath: JsonPath): List + + /** + * Report the length of the JSON array at a the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonArrlen(key: K): List + + /** + * Remove and return [JsonValue] at a given index in the array at a given [JsonPath] + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the array inside the document. + * @param index the index of the element to be removed. Default is -1, meaning the last element. Out-of-range indexes round + * to their respective array ends. Popping an empty array returns null. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + suspend fun jsonArrpop(key: K, jsonPath: JsonPath, index: Int): List + + /** + * Remove and return [JsonValue] at index -1 (last element) in the array at a given [JsonPath] + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the array inside the document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + suspend fun jsonArrpop(key: K, jsonPath: JsonPath): List + + /** + * Remove and return [JsonValue] at index -1 (last element) in the array at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + suspend fun jsonArrpop(key: K): List + + /** + * Trim an array at a given [JsonPath] so that it contains only the specified inclusive range of elements. All + * elements with indexes smaller than the start range and all elements with indexes bigger than the end range are trimmed. + *

+ * Behavior as of RedisJSON v2.0: + *

    + *
  • If start is larger than the array's size or start > stop, returns 0 and an empty array.
  • + *
  • If start is < 0, then start from the end of the array.
  • + *
  • If stop is larger than the end of the array, it is treated like the last element.
  • + *
+ * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the array inside the document. + * @param range the [JsonRangeArgs] to trim by. + * @return Long the resulting size of the arrays after the trimming, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonArrtrim(key: K, jsonPath: JsonPath, range: JsonRangeArgs): List + + /** + * Clear container values (arrays/objects) and set numeric values to 0 + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value to clear. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + suspend fun jsonClear(key: K, jsonPath: JsonPath): Long? + + /** + * Clear container values (arrays/objects) and set numeric values to 0 at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + suspend fun jsonClear(key: K): Long? + + /** + * Deletes a value inside the JSON document at a given [JsonPath] + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value to clear. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + suspend fun jsonDel(key: K, jsonPath: JsonPath): Long? + + /** + * Deletes a value inside the JSON document at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + suspend fun jsonDel(key: K): Long? + + /** + * Return the value at the specified path in JSON serialized form. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param options the [JsonGetArgs] to use. + * @param jsonPaths the [JsonPath]s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonGet(key: K, options: JsonGetArgs, vararg jsonPaths: JsonPath): List + + /** + * Return the value at the specified path in JSON serialized form. Uses defaults for the [JsonGetArgs]. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param jsonPaths the [JsonPath]s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonGet(key: K, vararg jsonPaths: JsonPath): List + + /** + * Merge a given [JsonValue] with the value matching [JsonPath]. Consequently, JSON values at matching paths are + * updated, deleted, or expanded with new children. + *

+ * Merging is done according to the following rules per JSON value in the value argument while considering the corresponding + * original value if it exists: + *

    + *
  • merging an existing object key with a null value deletes the key
  • + *
  • merging an existing object key with non-null value updates the value
  • + *
  • merging a non-existing object key adds the key and value
  • + *
  • merging an existing array with any merged value, replaces the entire array with the value
  • + *
+ *

+ * This command complies with RFC7396 "Json Merge Patch" + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value to merge. + * @param value the [JsonValue] to merge. + * @return String "OK" if the set was successful, error if the operation failed. + * @since 6.5 + * @see RFC7396 + */ + suspend fun jsonMerge(key: K, jsonPath: JsonPath, value: JsonValue): String? + + /** + * Return the values at the specified path from multiple key arguments. + * + * @param jsonPath the [JsonPath] pointing to the value to fetch. + * @param keys the keys holding the [JsonValue]s to fetch. + * @return List the values at path, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonMGet(jsonPath: JsonPath, vararg keys: K): List + + /** + * Set or update one or more JSON values according to the specified [JsonMsetArgs] + *

+ * JSON.MSET is atomic, hence, all given additions or updates are either applied or not. It is not possible for clients to + * see that some keys were updated while others are unchanged. + *

+ * A JSON value is a hierarchical structure. If you change a value in a specific path - nested values are affected. + * + * @param arguments the [JsonMsetArgs] specifying the values to change. + * @return "OK" if the operation was successful, error otherwise + * @since 6.5 + */ + suspend fun jsonMSet(arguments: List>): String? + + /** + * Increment the number value stored at the specified [JsonPath] in the JSON document by the provided increment. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value to increment. + * @param number the increment value. + * @return a [List] of the new values after the increment. + * @since 6.5 + */ + suspend fun jsonNumincrby(key: K, jsonPath: JsonPath, number: Number): List + + /** + * Return the keys in the JSON document that are referenced by the given [JsonPath] + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value(s) whose key(s) we want. + * @return List the keys in the JSON document that are referenced by the given [JsonPath]. + * @since 6.5 + */ + suspend fun jsonObjkeys(key: K, jsonPath: JsonPath): List + + /** + * Return the keys in the JSON document that are referenced by the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the keys in the JSON document that are referenced by the given [JsonPath]. + * @since 6.5 + */ + suspend fun jsonObjkeys(key: K): List + + /** + * Report the number of keys in the JSON object at the specified [JsonPath] and for the provided key + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value(s) whose key(s) we want to count + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonObjlen(key: K, jsonPath: JsonPath): List + + /** + * Report the number of keys in the JSON object at the {@link JsonPath#ROOT_PATH} and for the provided key + * + * @param key the key holding the JSON document. + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonObjlen(key: K): List + + /** + * Sets the JSON value at a given [JsonPath] in the JSON document. + *

+ * For new Redis keys, the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Any (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Any keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value(s) where we want to set the value. + * @param value the [JsonValue] to set. + * @param options the [JsonSetArgs] the options for setting the value. + * @return String "OK" if the set was successful, null if the [JsonSetArgs] conditions are not met. + * @since 6.5 + */ + suspend fun jsonSet(key: K, jsonPath: JsonPath, value: JsonValue, options: JsonSetArgs): String? + + /** + * Sets the JSON value at a given [JsonPath] in the JSON document using defaults for the [JsonSetArgs]. + *

+ * For new Redis keys the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Any (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Any keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value(s) where we want to set the value. + * @param value the [JsonValue] to set. + * @return String "OK" if the set was successful, null if the [JsonSetArgs] conditions are not met. + * @since 6.5 + */ + suspend fun jsonSet(key: K, jsonPath: JsonPath, value: JsonValue): String? + + /** + * Append the json-string values to the string at the provided [JsonPath] in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value(s) where we want to append the value. + * @param value the [JsonValue] to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + suspend fun jsonStrappend(key: K, jsonPath: JsonPath, value: JsonValue): List + + /** + * Append the json-string values to the string at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @param value the [JsonValue] to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + suspend fun jsonStrappend(key: K, value: JsonValue): List + + /** + * Report the length of the JSON String at the provided [JsonPath] in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value(s). + * @return Long (in recursive descent) the length of the JSON String at the provided [JsonPath], or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + suspend fun jsonStrlen(key: K, jsonPath: JsonPath): List + + /** + * Report the length of the JSON String at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return Long (in recursive descent) the length of the JSON String at the provided [JsonPath], or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + suspend fun jsonStrlen(key: K): List + + /** + * Toggle a Boolean value stored at the provided [JsonPath] in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value(s). + * @return List the new value after the toggle, 0 for false, 1 for true or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonToggle(key: K, jsonPath: JsonPath): List + + /** + * Report the type of JSON value at the provided [JsonPath] in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value(s). + * @return List the type of JSON value at the provided [JsonPath] + * @since 6.5 + */ + suspend fun jsonType(key: K, jsonPath: JsonPath): List + + /** + * Report the type of JSON value at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return List the type of JSON value at the provided [JsonPath] + * @since 6.5 + */ + suspend fun jsonType(key: K): List + +} + diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisJsonCoroutinesCommandsImpl.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisJsonCoroutinesCommandsImpl.kt new file mode 100644 index 0000000000..8941f21106 --- /dev/null +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisJsonCoroutinesCommandsImpl.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2020-Present, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.api.coroutines + +import io.lettuce.core.* +import io.lettuce.core.api.reactive.RedisJsonReactiveCommands +import io.lettuce.core.json.JsonPath +import io.lettuce.core.json.JsonType +import io.lettuce.core.json.JsonValue +import io.lettuce.core.json.arguments.JsonGetArgs +import io.lettuce.core.json.arguments.JsonMsetArgs +import io.lettuce.core.json.arguments.JsonRangeArgs +import io.lettuce.core.json.arguments.JsonSetArgs +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactive.awaitFirstOrNull + +/** + * Coroutine executed commands (based on reactive commands) for Keys (Key manipulation/querying). + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @since 6.5 + */ +@ExperimentalLettuceCoroutinesApi +internal class RedisJsonCoroutinesCommandsImpl(internal val ops: RedisJsonReactiveCommands) : + RedisJsonCoroutinesCommands { + override suspend fun jsonArrappend(key: K, jsonPath: JsonPath, vararg values: JsonValue): List = + ops.jsonArrappend(key, jsonPath, *values).asFlow().toList() + + override suspend fun jsonArrappend(key: K, vararg values: JsonValue): List = + ops.jsonArrappend(key, *values).asFlow().toList() + + override suspend fun jsonArrindex( + key: K, + jsonPath: JsonPath, + value: JsonValue, + range: JsonRangeArgs + ): List = ops.jsonArrindex(key, jsonPath, value, range).asFlow().toList() + + override suspend fun jsonArrindex(key: K, jsonPath: JsonPath, value: JsonValue): List = + ops.jsonArrindex(key, jsonPath, value).asFlow().toList() + + override suspend fun jsonArrinsert( + key: K, + jsonPath: JsonPath, + index: Int, + vararg values: JsonValue + ): List = ops.jsonArrinsert(key, jsonPath, index, *values).asFlow().toList() + + override suspend fun jsonArrlen(key: K, jsonPath: JsonPath): List = + ops.jsonArrlen(key, jsonPath).asFlow().toList() + + override suspend fun jsonArrlen(key: K): List = ops.jsonArrlen(key).asFlow().toList() + + override suspend fun jsonArrpop(key: K, jsonPath: JsonPath, index: Int): List = + ops.jsonArrpop(key, jsonPath, index).asFlow().toList() + + override suspend fun jsonArrpop(key: K, jsonPath: JsonPath): List = + ops.jsonArrpop(key, jsonPath).asFlow().toList() + + override suspend fun jsonArrpop(key: K): List = ops.jsonArrpop(key).asFlow().toList() + + override suspend fun jsonArrtrim(key: K, jsonPath: JsonPath, range: JsonRangeArgs): List = + ops.jsonArrtrim(key, jsonPath, range).asFlow().toList() + + override suspend fun jsonClear(key: K, jsonPath: JsonPath): Long? = + ops.jsonClear(key, jsonPath).awaitFirstOrNull() + + override suspend fun jsonClear(key: K): Long? = ops.jsonClear(key).awaitFirstOrNull() + + override suspend fun jsonDel(key: K, jsonPath: JsonPath): Long? = + ops.jsonDel(key, jsonPath).awaitFirstOrNull() + + override suspend fun jsonDel(key: K): Long? = ops.jsonDel(key).awaitFirstOrNull() + + override suspend fun jsonGet(key: K, options: JsonGetArgs, vararg jsonPaths: JsonPath): List = + ops.jsonGet(key, options, *jsonPaths).asFlow().toList() + + override suspend fun jsonGet(key: K, vararg jsonPaths: JsonPath): List = + ops.jsonGet(key, *jsonPaths).asFlow().toList() + + override suspend fun jsonMerge(key: K, jsonPath: JsonPath, value: JsonValue): String? = + ops.jsonMerge(key, jsonPath, value).awaitFirstOrNull() + + override suspend fun jsonMGet(jsonPath: JsonPath, vararg keys: K): List = + ops.jsonMGet(jsonPath, *keys).asFlow().toList() + + override suspend fun jsonMSet(arguments: List>): String? = + ops.jsonMSet(arguments).awaitFirstOrNull() + + override suspend fun jsonType(key: K, jsonPath: JsonPath): List = + ops.jsonType(key, jsonPath).asFlow().toList() + + override suspend fun jsonType(key: K): List = ops.jsonType(key).asFlow().toList() + + override suspend fun jsonToggle(key: K, jsonPath: JsonPath): List = + ops.jsonToggle(key, jsonPath).asFlow().toList() + + override suspend fun jsonStrlen(key: K, jsonPath: JsonPath): List = + ops.jsonStrlen(key, jsonPath).asFlow().toList() + + override suspend fun jsonStrlen(key: K): List = ops.jsonStrlen(key).asFlow().toList() + + override suspend fun jsonStrappend(key: K, jsonPath: JsonPath, value: JsonValue): List = + ops.jsonStrappend(key, jsonPath, value).asFlow().toList() + + override suspend fun jsonStrappend(key: K, value: JsonValue): List = + ops.jsonStrappend(key, value).asFlow().toList() + + override suspend fun jsonSet(key: K, jsonPath: JsonPath, value: JsonValue, options: JsonSetArgs): String? = + ops.jsonSet(key, jsonPath, value, options).awaitFirstOrNull() + + override suspend fun jsonSet(key: K, jsonPath: JsonPath, value: JsonValue): String? = + ops.jsonSet(key, jsonPath, value).awaitFirstOrNull() + + override suspend fun jsonObjlen(key: K, jsonPath: JsonPath): List = + ops.jsonObjlen(key, jsonPath).asFlow().toList() + + override suspend fun jsonObjlen(key: K): List = ops.jsonObjlen(key).asFlow().toList() + + override suspend fun jsonObjkeys(key: K, jsonPath: JsonPath): List = + ops.jsonObjkeys(key, jsonPath).asFlow().toList() + + override suspend fun jsonObjkeys(key: K): List = ops.jsonObjkeys(key).asFlow().toList() + + override suspend fun jsonNumincrby(key: K, jsonPath: JsonPath, number: Number): List = + ops.jsonNumincrby(key, jsonPath, number).asFlow().toList() +} + diff --git a/src/main/templates/io/lettuce/core/api/RedisJsonCommands.java b/src/main/templates/io/lettuce/core/api/RedisJsonCommands.java new file mode 100644 index 0000000000..fdc9dc2040 --- /dev/null +++ b/src/main/templates/io/lettuce/core/api/RedisJsonCommands.java @@ -0,0 +1,440 @@ +/* + * Copyright 2017-2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.api; + +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; + +import java.util.List; + +/** + * ${intent} for JSON documents + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @see Redis JSON + * @since 6.5 + */ +public interface RedisJsonCommands { + + /** + * Append the JSON values into the array at a given {@link JsonPath} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrappend(K key, JsonPath jsonPath, JsonValue... values); + + /** + * Append the JSON values into the array at the {@link JsonPath#ROOT_PATH} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrappend(K key, JsonValue... values); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @param range the {@link JsonRangeArgs} to search within. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. This + * method uses defaults for the start and end indexes, see {@link JsonRangeArgs#DEFAULT_START_INDEX} and + * {@link JsonRangeArgs#DEFAULT_END_INDEX}. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrindex(K key, JsonPath jsonPath, JsonValue value); + + /** + * Insert the {@link JsonValue}s into the array at a given {@link JsonPath} before the provided index, shifting the existing + * elements to the right + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index before which the new elements will be inserted. + * @param values one or more {@link JsonValue}s to be inserted. + * @return Long the resulting size of the arrays after the new data was inserted, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values); + + /** + * Report the length of the JSON array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON array at a the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrlen(K key); + + /** + * Remove and return {@link JsonValue} at a given index in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index of the element to be removed. Default is -1, meaning the last element. Out-of-range indexes round + * to their respective array ends. Popping an empty array returns null. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrpop(K key, JsonPath jsonPath, int index); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrpop(K key, JsonPath jsonPath); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrpop(K key); + + /** + * Trim an array at a given {@link JsonPath} so that it contains only the specified inclusive range of elements. All + * elements with indexes smaller than the start range and all elements with indexes bigger than the end range are trimmed. + *

+ * Behavior as of RedisJSON v2.0: + *

    + *
  • If start is larger than the array's size or start > stop, returns 0 and an empty array.
  • + *
  • If start is < 0, then start from the end of the array.
  • + *
  • If stop is larger than the end of the array, it is treated like the last element.
  • + *
+ * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param range the {@link JsonRangeArgs} to trim by. + * @return Long the resulting size of the arrays after the trimming, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + Long jsonClear(K key, JsonPath jsonPath); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + Long jsonClear(K key); + + /** + * Deletes a value inside the JSON document at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + Long jsonDel(K key, JsonPath jsonPath); + + /** + * Deletes a value inside the JSON document at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + Long jsonDel(K key); + + /** + * Return the value at the specified path in JSON serialized form. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param options the {@link JsonGetArgs} to use. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + List jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths); + + /** + * Return the value at the specified path in JSON serialized form. Uses defaults for the {@link JsonGetArgs}. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + List jsonGet(K key, JsonPath... jsonPaths); + + /** + * Merge a given {@link JsonValue} with the value matching {@link JsonPath}. Consequently, JSON values at matching paths are + * updated, deleted, or expanded with new children. + *

+ * Merging is done according to the following rules per JSON value in the value argument while considering the corresponding + * original value if it exists: + *

    + *
  • merging an existing object key with a null value deletes the key
  • + *
  • merging an existing object key with non-null value updates the value
  • + *
  • merging a non-existing object key adds the key and value
  • + *
  • merging an existing array with any merged value, replaces the entire array with the value
  • + *
+ *

+ * This command complies with RFC7396 "Json Merge Patch" + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to merge. + * @param value the {@link JsonValue} to merge. + * @return String "OK" if the set was successful, error if the operation failed. + * @since 6.5 + * @see RFC7396 + */ + String jsonMerge(K key, JsonPath jsonPath, JsonValue value); + + /** + * Return the values at the specified path from multiple key arguments. + * + * @param jsonPath the {@link JsonPath} pointing to the value to fetch. + * @param keys the keys holding the {@link JsonValue}s to fetch. + * @return List the values at path, or null if the path does not exist. + * @since 6.5 + */ + List jsonMGet(JsonPath jsonPath, K... keys); + + /** + * Set or update one or more JSON values according to the specified {@link JsonMsetArgs} + *

+ * JSON.MSET is atomic, hence, all given additions or updates are either applied or not. It is not possible for clients to + * see that some keys were updated while others are unchanged. + *

+ * A JSON value is a hierarchical structure. If you change a value in a specific path - nested values are affected. + * + * @param arguments the {@link JsonMsetArgs} specifying the values to change. + * @return "OK" if the operation was successful, error otherwise + * @since 6.5 + */ + String jsonMSet(List> arguments); + + /** + * Increment the number value stored at the specified {@link JsonPath} in the JSON document by the provided increment. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to increment. + * @param number the increment value. + * @return a {@link List} of the new values after the increment. + * @since 6.5 + */ + List jsonNumincrby(K key, JsonPath jsonPath, Number number); + + /** + * Return the keys in the JSON document that are referenced by the given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + List jsonObjkeys(K key, JsonPath jsonPath); + + /** + * Return the keys in the JSON document that are referenced by the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + List jsonObjkeys(K key); + + /** + * Report the number of keys in the JSON object at the specified {@link JsonPath} and for the provided key + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want to count + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + List jsonObjlen(K key, JsonPath jsonPath); + + /** + * Report the number of keys in the JSON object at the {@link JsonPath#ROOT_PATH} and for the provided key + * + * @param key the key holding the JSON document. + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + List jsonObjlen(K key); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document. + *

+ * For new Redis keys, the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @param options the {@link JsonSetArgs} the options for setting the value. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + String jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document using defaults for the {@link JsonSetArgs}. + *

+ * For new Redis keys the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + String jsonSet(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to append the value. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + List jsonStrappend(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + List jsonStrappend(K key, JsonValue value); + + /** + * Report the length of the JSON String at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + List jsonStrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON String at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + List jsonStrlen(K key); + + /** + * Toggle a Boolean value stored at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the new value after the toggle, 0 for false, 1 for true or null if the path does not exist. + * @since 6.5 + */ + List jsonToggle(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + List jsonType(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + List jsonType(K key); + +} diff --git a/src/test/java/biz/paluch/redis/extensibility/MyExtendedRedisClusterClient.java b/src/test/java/biz/paluch/redis/extensibility/MyExtendedRedisClusterClient.java index e97e5d823e..e87f278de3 100644 --- a/src/test/java/biz/paluch/redis/extensibility/MyExtendedRedisClusterClient.java +++ b/src/test/java/biz/paluch/redis/extensibility/MyExtendedRedisClusterClient.java @@ -29,6 +29,7 @@ import io.lettuce.core.cluster.RedisClusterClient; import io.lettuce.core.cluster.StatefulRedisClusterConnectionImpl; import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonParser; import io.lettuce.core.resource.ClientResources; /** @@ -48,8 +49,9 @@ public MyExtendedRedisClusterClient() { @Override protected StatefulRedisClusterConnectionImpl newStatefulRedisClusterConnection( - RedisChannelWriter channelWriter, ClusterPushHandler pushHandler, RedisCodec codec, Duration timeout) { - return new MyRedisClusterConnection<>(channelWriter, pushHandler, codec, timeout); + RedisChannelWriter channelWriter, ClusterPushHandler pushHandler, RedisCodec codec, Duration timeout, + JsonParser parser) { + return new MyRedisClusterConnection<>(channelWriter, pushHandler, codec, timeout, parser); } } diff --git a/src/test/java/biz/paluch/redis/extensibility/MyRedisClusterConnection.java b/src/test/java/biz/paluch/redis/extensibility/MyRedisClusterConnection.java index 18e8feacf8..088c6831fc 100644 --- a/src/test/java/biz/paluch/redis/extensibility/MyRedisClusterConnection.java +++ b/src/test/java/biz/paluch/redis/extensibility/MyRedisClusterConnection.java @@ -25,6 +25,7 @@ import io.lettuce.core.cluster.ClusterPushHandler; import io.lettuce.core.cluster.StatefulRedisClusterConnectionImpl; import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonParser; /** * Demo code for extending a @{@link StatefulRedisClusterConnectionImpl} @@ -35,8 +36,8 @@ class MyRedisClusterConnection extends StatefulRedisClusterConnectionImpl { public MyRedisClusterConnection(RedisChannelWriter writer, ClusterPushHandler pushHandler, RedisCodec codec, - Duration timeout) { - super(writer, pushHandler, codec, timeout); + Duration timeout, JsonParser parser) { + super(writer, pushHandler, codec, timeout, parser); } } diff --git a/src/test/java/io/lettuce/apigenerator/Constants.java b/src/test/java/io/lettuce/apigenerator/Constants.java index 2779ed17aa..896b939951 100644 --- a/src/test/java/io/lettuce/apigenerator/Constants.java +++ b/src/test/java/io/lettuce/apigenerator/Constants.java @@ -29,7 +29,8 @@ class Constants { public static final String[] TEMPLATE_NAMES = { "BaseRedisCommands", "RedisAclCommands", "RedisFunctionCommands", "RedisGeoCommands", "RedisHashCommands", "RedisHLLCommands", "RedisKeyCommands", "RedisListCommands", "RedisScriptingCommands", "RedisSentinelCommands", "RedisServerCommands", "RedisSetCommands", - "RedisSortedSetCommands", "RedisStreamCommands", "RedisStringCommands", "RedisTransactionalCommands" }; + "RedisSortedSetCommands", "RedisStreamCommands", "RedisStringCommands", "RedisTransactionalCommands", + "RedisJsonCommands" }; public static final File TEMPLATES = new File("src/main/templates"); diff --git a/src/test/java/io/lettuce/core/ClientOptionsUnitTests.java b/src/test/java/io/lettuce/core/ClientOptionsUnitTests.java index e7a29c6bb9..9b86ee189e 100644 --- a/src/test/java/io/lettuce/core/ClientOptionsUnitTests.java +++ b/src/test/java/io/lettuce/core/ClientOptionsUnitTests.java @@ -2,8 +2,15 @@ import static org.assertj.core.api.Assertions.*; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import io.lettuce.core.json.DefaultJsonParser; +import io.lettuce.core.json.JsonArray; +import io.lettuce.core.json.JsonObject; +import io.lettuce.core.json.JsonParser; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.JsonValue; import org.junit.jupiter.api.Test; import io.lettuce.core.protocol.Command; @@ -30,6 +37,7 @@ void testDefault() { assertThat(options.getReadOnlyCommands().isReadOnly(new Command<>(CommandType.SET, null))).isFalse(); assertThat(options.getReadOnlyCommands().isReadOnly(new Command<>(CommandType.PUBLISH, null))).isFalse(); assertThat(options.getReadOnlyCommands().isReadOnly(new Command<>(CommandType.GET, null))).isTrue(); + assertThat(options.getJsonParser()).isInstanceOf(DefaultJsonParser.class); } @Test @@ -52,6 +60,47 @@ void testCopy() { assertThat(original.mutate()).isNotSameAs(copy.mutate()); } + @Test + void jsonParser() { + JsonParser parser = new CustomJsonParser(); + ClientOptions options = ClientOptions.builder().jsonParser(parser).build(); + assertThat(options.getJsonParser()).isInstanceOf(CustomJsonParser.class); + } + + static class CustomJsonParser implements JsonParser { + + @Override + public JsonValue loadJsonValue(ByteBuffer buffer) { + return null; + } + + @Override + public JsonValue createJsonValue(ByteBuffer bytes) { + return null; + } + + @Override + public JsonValue createJsonValue(String value) { + return null; + } + + @Override + public JsonObject createJsonObject() { + return null; + } + + @Override + public JsonArray createJsonArray() { + return null; + } + + @Override + public JsonValue fromObject(Object object) { + return null; + } + + } + void checkAssertions(ClientOptions sut) { assertThat(sut.isAutoReconnect()).isTrue(); assertThat(sut.isCancelCommandsOnReconnectFailure()).isFalse(); diff --git a/src/test/java/io/lettuce/core/RedisContainerIntegrationTests.java b/src/test/java/io/lettuce/core/RedisContainerIntegrationTests.java new file mode 100644 index 0000000000..10d939f5a5 --- /dev/null +++ b/src/test/java/io/lettuce/core/RedisContainerIntegrationTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core; + +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.containers.ComposeContainer; +import org.testcontainers.containers.output.BaseConsumer; +import org.testcontainers.containers.output.OutputFrame; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.io.File; +import java.io.IOException; + +@Testcontainers +public class RedisContainerIntegrationTests { + + public static ComposeContainer CLUSTERED_STACK = new ComposeContainer( + new File("src/test/resources/docker/docker-compose.yml")).withExposedService("clustered-stack", 36379) + .withExposedService("clustered-stack", 36380).withExposedService("clustered-stack", 36381) + .withExposedService("clustered-stack", 36382).withExposedService("clustered-stack", 36383) + .withExposedService("clustered-stack", 36384).withExposedService("standalone-stack", 6379); + + @BeforeAll + public static void setup() throws IOException, InterruptedException { + // In case you need to debug the container uncomment these lines to redirect the output + CLUSTERED_STACK.withLogConsumer("clustered-stack", new SystemOutputConsumer("clustered")); + CLUSTERED_STACK.withLogConsumer("standalone-stack", new SystemOutputConsumer("standalone")); + + CLUSTERED_STACK.waitingFor("clustered-stack", + Wait.forLogMessage(".*Background RDB transfer terminated with success.*", 1)); + CLUSTERED_STACK.start(); + + } + + static public class SystemOutputConsumer extends BaseConsumer { + + String prefix; + + public SystemOutputConsumer(String prefix) { + this.prefix = prefix; + } + + @Override + public void accept(OutputFrame outputFrame) { + String output = String.format("%15s: %s", prefix, outputFrame.getUtf8String()); + System.out.print(output); + } + + } + +} diff --git a/src/test/java/io/lettuce/core/RedisJsonCommandBuilderUnitTests.java b/src/test/java/io/lettuce/core/RedisJsonCommandBuilderUnitTests.java new file mode 100644 index 0000000000..3c658ec509 --- /dev/null +++ b/src/test/java/io/lettuce/core/RedisJsonCommandBuilderUnitTests.java @@ -0,0 +1,270 @@ +package io.lettuce.core; + +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.json.DefaultJsonParser; +import io.lettuce.core.json.JsonParser; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; +import io.lettuce.core.protocol.Command; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link RedisJsonCommandBuilder}. + * + * @author Tihomir Mateev + */ +class RedisJsonCommandBuilderUnitTests { + + public static final String MY_KEY = "bikes:inventory"; + + public static final String MY_KEY2 = "bikes:repairLog"; + + public static final String ID_BIKE_6 = "{\"id\":\"bike6\"}"; + + public static final JsonParser PARSER = DefaultJsonParser.INSTANCE; + + public static final JsonValue ELEMENT = PARSER.createJsonValue(ID_BIKE_6); + + public static final JsonPath MY_PATH = JsonPath.of("$..commuter_bikes"); + + RedisJsonCommandBuilder builder = new RedisJsonCommandBuilder<>(StringCodec.UTF8, PARSER); + + @Test + void shouldCorrectlyConstructJsonArrappend() { + Command> command = builder.jsonArrappend(MY_KEY, MY_PATH, ELEMENT); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$14\r\n" + "JSON.ARRAPPEND\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonArrindex() { + JsonRangeArgs range = JsonRangeArgs.Builder.start(0).stop(1); + Command> command = builder.jsonArrindex(MY_KEY, MY_PATH, ELEMENT, range); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*6\r\n" + "$13\r\n" + "JSON.ARRINDEX\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n" + "$1" + "\r\n" + + "0" + "\r\n" + "$1" + "\r\n" + "1" + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonArrinsert() { + Command> command = builder.jsonArrinsert(MY_KEY, MY_PATH, 1, ELEMENT); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)) + .isEqualTo("*5\r\n" + "$14\r\n" + "JSON.ARRINSERT\r\n" + "$15\r\n" + "bikes:inventory\r\n" + "$17\r\n" + + "$..commuter_bikes\r\n" + "$1" + "\r\n" + "1" + "\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonArrlen() { + Command> command = builder.jsonArrlen(MY_KEY, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*3\r\n" + "$11\r\n" + "JSON.ARRLEN\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonArrpop() { + Command> command = builder.jsonArrpop(MY_KEY, MY_PATH, 3); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$11\r\n" + "JSON.ARRPOP\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$1" + "\r\n" + "3" + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonArrtrim() { + JsonRangeArgs range = JsonRangeArgs.Builder.start(0).stop(1); + Command> command = builder.jsonArrtrim(MY_KEY, MY_PATH, range); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)) + .isEqualTo("*5\r\n" + "$12\r\n" + "JSON.ARRTRIM\r\n" + "$15\r\n" + "bikes:inventory\r\n" + "$17\r\n" + + "$..commuter_bikes\r\n" + "$1" + "\r\n" + "0" + "\r\n" + "$1" + "\r\n" + "1" + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonClear() { + Command command = builder.jsonClear(MY_KEY, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*3\r\n" + "$10\r\n" + "JSON.CLEAR\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonGet() { + JsonGetArgs args = JsonGetArgs.Builder.indent(" ").newline("\n").space("/"); + Command> command = builder.jsonGet(MY_KEY, args, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*9\r\n" + "$8\r\n" + "JSON.GET\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$6\r\n" + "INDENT\r\n" + "$3\r\n" + " \r\n" + "$7\r\n" + "NEWLINE\r\n" + "$1\r\n" + + "\n\r\n" + "$5\r\n" + "SPACE\r\n" + "$1\r\n" + "/\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonMerge() { + Command command = builder.jsonMerge(MY_KEY, MY_PATH, ELEMENT); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$10\r\n" + "JSON.MERGE\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonMget() { + Command> command = builder.jsonMGet(MY_PATH, MY_KEY, MY_KEY2); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$9\r\n" + "JSON.MGET\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$15\r\n" + "bikes:repairLog\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonMset() { + JsonMsetArgs args1 = new JsonMsetArgs<>(MY_KEY, MY_PATH, ELEMENT); + Command command = builder.jsonMSet(Collections.singletonList(args1)); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$9\r\n" + "JSON.MSET\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonNumincrby() { + Command> command = builder.jsonNumincrby(MY_KEY, MY_PATH, 3); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$14\r\n" + "JSON.NUMINCRBY\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$1" + "\r\n" + "3" + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonObjkeys() { + Command> command = builder.jsonObjkeys(MY_KEY, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*3\r\n" + "$12\r\n" + "JSON.OBJKEYS\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonObjlen() { + Command> command = builder.jsonObjlen(MY_KEY, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*3\r\n" + "$11\r\n" + "JSON.OBJLEN\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonSet() { + JsonSetArgs args = JsonSetArgs.Builder.nx(); + Command command = builder.jsonSet(MY_KEY, MY_PATH, ELEMENT, args); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)) + .isEqualTo("*5\r\n" + "$8\r\n" + "JSON.SET\r\n" + "$15\r\n" + "bikes:inventory\r\n" + "$17\r\n" + + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n" + "$2\r\n" + "NX\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonStrappend() { + Command> command = builder.jsonStrappend(MY_KEY, MY_PATH, ELEMENT); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$14\r\n" + "JSON.STRAPPEND\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonStrlen() { + Command> command = builder.jsonStrlen(MY_KEY, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*3\r\n" + "$11\r\n" + "JSON.STRLEN\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonToggle() { + Command> command = builder.jsonToggle(MY_KEY, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*3\r\n" + "$11\r\n" + "JSON.TOGGLE\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonDel() { + Command command = builder.jsonDel(MY_KEY, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo( + "*3\r\n" + "$8\r\n" + "JSON.DEL\r\n" + "$15\r\n" + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonType() { + Command command = builder.jsonType(MY_KEY, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*3\r\n" + "$9\r\n" + "JSON.TYPE\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonTypeRootPath() { + Command command = builder.jsonType(MY_KEY, JsonPath.ROOT_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)) + .isEqualTo("*2\r\n" + "$9\r\n" + "JSON.TYPE\r\n" + "$15\r\n" + "bikes:inventory\r\n"); + } + +} diff --git a/src/test/java/io/lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java b/src/test/java/io/lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java index 502198b7ae..633950693b 100644 --- a/src/test/java/io/lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java @@ -23,7 +23,7 @@ void testCount() { void testResolvableCommandNames() { for (ProtocolKeyword readOnlyCommand : ClusterReadOnlyCommands.getReadOnlyCommands()) { - assertThat(readOnlyCommand.name()).isEqualTo(CommandType.valueOf(readOnlyCommand.name()).name()); + assertThat(readOnlyCommand.toString()).isEqualTo(CommandType.valueOf(readOnlyCommand.toString()).name()); } } diff --git a/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java index 3fed145c32..0952d592af 100644 --- a/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java @@ -635,7 +635,7 @@ void clientSetinfo() { @Test void testReadOnlyCommands() { for (ProtocolKeyword readOnlyCommand : ClusterReadOnlyCommands.getReadOnlyCommands()) { - assertThat(isCommandReadOnly(readOnlyCommand.name())).isTrue(); + assertThat(isCommandReadOnly(readOnlyCommand.toString())).isTrue(); } } diff --git a/src/test/java/io/lettuce/core/dynamic/CommandSegmentCommandFactoryUnitTests.java b/src/test/java/io/lettuce/core/dynamic/CommandSegmentCommandFactoryUnitTests.java index 1b962d1f7b..99e3bb5a3c 100644 --- a/src/test/java/io/lettuce/core/dynamic/CommandSegmentCommandFactoryUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/CommandSegmentCommandFactoryUnitTests.java @@ -155,7 +155,7 @@ private String toString(RedisCommand command) { StringBuilder builder = new StringBuilder(); - builder.append(command.getType().name()); + builder.append(command.getType().toString()); String commandString = command.getArgs().toCommandString(); diff --git a/src/test/java/io/lettuce/core/dynamic/segment/AnnotationCommandSegmentFactoryUnitTests.java b/src/test/java/io/lettuce/core/dynamic/segment/AnnotationCommandSegmentFactoryUnitTests.java index 7363bb45b3..dc5ba7f910 100644 --- a/src/test/java/io/lettuce/core/dynamic/segment/AnnotationCommandSegmentFactoryUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/segment/AnnotationCommandSegmentFactoryUnitTests.java @@ -28,7 +28,7 @@ void notAnnotatedDotAsIs() { CommandSegments commandSegments = factory.createCommandSegments(commandMethod); assertThat(commandSegments).isEmpty(); - assertThat(commandSegments.getCommandType().name()).isEqualTo("not.Annotated"); + assertThat(commandSegments.getCommandType().toString()).isEqualTo("not.Annotated"); } @Test @@ -40,7 +40,7 @@ void uppercaseDot() { CommandSegments commandSegments = factory.createCommandSegments(commandMethod); assertThat(commandSegments).isEmpty(); - assertThat(commandSegments.getCommandType().name()).isEqualTo("UPPER.CASE"); + assertThat(commandSegments.getCommandType().toString()).isEqualTo("UPPER.CASE"); } @Test @@ -52,7 +52,7 @@ void methodNameAsIs() { CommandSegments commandSegments = factory.createCommandSegments(commandMethod); assertThat(commandSegments).isEmpty(); - assertThat(commandSegments.getCommandType().name()).isEqualTo("methodName"); + assertThat(commandSegments.getCommandType().toString()).isEqualTo("methodName"); } @Test @@ -64,7 +64,7 @@ void splitAsIs() { CommandSegments commandSegments = factory.createCommandSegments(commandMethod); assertThat(commandSegments).hasSize(1).extracting(CommandSegment::asString).contains("Setname"); - assertThat(commandSegments.getCommandType().name()).isEqualTo("client"); + assertThat(commandSegments.getCommandType().toString()).isEqualTo("client"); } @Test @@ -76,7 +76,7 @@ void commandAnnotation() { CommandSegments commandSegments = factory.createCommandSegments(commandMethod); assertThat(commandSegments).hasSize(1).extracting(CommandSegment::asString).contains("WORLD"); - assertThat(commandSegments.getCommandType().name()).isEqualTo("HELLO"); + assertThat(commandSegments.getCommandType().toString()).isEqualTo("HELLO"); } @Test @@ -88,7 +88,7 @@ void splitDefault() { CommandSegments commandSegments = factory.createCommandSegments(commandMethod); assertThat(commandSegments).hasSize(1).extracting(CommandSegment::asString).contains("SETNAME"); - assertThat(commandSegments.getCommandType().name()).isEqualTo("CLIENT"); + assertThat(commandSegments.getCommandType().toString()).isEqualTo("CLIENT"); } @CommandNaming(strategy = Strategy.DOT, letterCase = LetterCase.AS_IS) diff --git a/src/test/java/io/lettuce/core/json/DefaultJsonParserUnitTests.java b/src/test/java/io/lettuce/core/json/DefaultJsonParserUnitTests.java new file mode 100644 index 0000000000..61efa3d25b --- /dev/null +++ b/src/test/java/io/lettuce/core/json/DefaultJsonParserUnitTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * Unit tests for {@link DefaultJsonParser}. + */ +class DefaultJsonParserUnitTests { + + @Test + void loadJsonValue() { + final String unprocessed = "{\"a\":1,\"b\":2}"; + + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + JsonValue jsonValue = parser.loadJsonValue(ByteBuffer.wrap(unprocessed.getBytes())); + + assertThat(jsonValue).isNotNull(); + assertThat(jsonValue).isInstanceOf(UnproccessedJsonValue.class); + assertThat(((UnproccessedJsonValue) jsonValue).isDeserialized()).isFalse(); + } + + @Test + void createJsonValue() { + final String unprocessed = "\"someValue\""; + + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + JsonValue jsonValue = parser.createJsonValue(ByteBuffer.wrap(unprocessed.getBytes())); + + assertThat(jsonValue).isNotNull(); + assertThat(jsonValue.isString()).isTrue(); + assertThat(jsonValue.asString()).isEqualTo("someValue"); + } + + @Test + void createJsonObject() { + final String unprocessed = "{\"a\":1,\"b\":2}"; + + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + JsonValue jsonValue = parser.createJsonObject(); + + assertThat(jsonValue).isNotNull(); + assertThat(jsonValue.isJsonObject()).isTrue(); + assertThat(jsonValue.asJsonObject().size()).isZero(); + + parser = DefaultJsonParser.INSTANCE; + jsonValue = parser.createJsonValue(ByteBuffer.wrap(unprocessed.getBytes())); + + assertThat(jsonValue).isNotNull(); + assertThat(jsonValue.isJsonObject()).isTrue(); + assertThat(jsonValue.asJsonObject().get("a").asNumber()).isEqualTo(1); + assertThat(jsonValue.asJsonObject().get("b").asNumber()).isEqualTo(2); + + jsonValue = parser.createJsonValue(unprocessed); + + assertThat(jsonValue).isNotNull(); + assertThat(jsonValue.isJsonObject()).isTrue(); + assertThat(jsonValue.asJsonObject().get("a").asNumber()).isEqualTo(1); + assertThat(jsonValue.asJsonObject().get("b").asNumber()).isEqualTo(2); + } + + @Test + void createJsonArray() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + JsonValue jsonValue = parser.createJsonArray(); + + assertThat(jsonValue).isNotNull(); + assertThat(jsonValue.isJsonArray()).isTrue(); + assertThat(jsonValue.asJsonArray().size()).isZero(); + + final String unprocessed = "[1,2]"; + + jsonValue = parser.createJsonValue(ByteBuffer.wrap(unprocessed.getBytes())); + + assertThat(jsonValue).isNotNull(); + assertThat(jsonValue.isJsonArray()).isTrue(); + assertThat(jsonValue.asJsonArray().get(0).asNumber()).isEqualTo(1); + assertThat(jsonValue.asJsonArray().get(1).asNumber()).isEqualTo(2); + } + + @Test + void parsingIssues() { + final String unprocessed = "{a\":1,\"b\":2}"; + + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + + assertThatThrownBy(() -> parser.createJsonValue(unprocessed)).isInstanceOf(RedisJsonException.class); + assertThatThrownBy(() -> parser.createJsonValue(ByteBuffer.wrap(unprocessed.getBytes()))) + .isInstanceOf(RedisJsonException.class); + + } + +} diff --git a/src/test/java/io/lettuce/core/json/DelegateJsonArrayUnitTests.java b/src/test/java/io/lettuce/core/json/DelegateJsonArrayUnitTests.java new file mode 100644 index 0000000000..f08cc63d90 --- /dev/null +++ b/src/test/java/io/lettuce/core/json/DelegateJsonArrayUnitTests.java @@ -0,0 +1,173 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import org.junit.jupiter.api.Test; + +import java.util.Iterator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Unit tests for {@link DelegateJsonArray}. + */ +class DelegateJsonArrayUnitTests { + + @Test + void add() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.add(parser.createJsonValue("\"test\"")).add(parser.createJsonValue("\"test2\"")) + .add(parser.createJsonValue("\"test3\"")); + + assertThat(underTest.size()).isEqualTo(3); + assertThat(underTest.get(0).isString()).isTrue(); + assertThat(underTest.get(0).asString()).isEqualTo("test"); + assertThat(underTest.get(1).isString()).isTrue(); + assertThat(underTest.get(1).asString()).isEqualTo("test2"); + assertThat(underTest.get(2).isString()).isTrue(); + assertThat(underTest.get(2).asString()).isEqualTo("test3"); + } + + @Test + void addCornerCases() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.add(null).add(parser.createJsonValue("null")).add(parser.createJsonValue("\"test3\"")); + + assertThatThrownBy(() -> underTest.addAll(null)).isInstanceOf(IllegalArgumentException.class); + + assertThat(underTest.size()).isEqualTo(3); + assertThat(underTest.get(0).isNull()).isTrue(); + assertThat(underTest.get(1).isNull()).isTrue(); + assertThat(underTest.get(2).isString()).isTrue(); + assertThat(underTest.get(2).asString()).isEqualTo("test3"); + } + + @Test + void getCornerCases() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.add(parser.createJsonValue("\"test\"")).add(parser.createJsonValue("\"test2\"")) + .add(parser.createJsonValue("\"test3\"")); + + assertThat(underTest.get(3)).isNull(); + assertThat(underTest.get(-1)).isNull(); + } + + @Test + void addAll() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + DelegateJsonArray array = new DelegateJsonArray(); + array.add(parser.createJsonValue("\"test\"")).add(parser.createJsonValue("\"test2\"")) + .add(parser.createJsonValue("\"test3\"")); + + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.addAll(array); + array.remove(1); // verify source array modifications not propagated + + assertThat(underTest.size()).isEqualTo(3); + assertThat(underTest.get(0).isString()).isTrue(); + assertThat(underTest.get(0).asString()).isEqualTo("test"); + assertThat(underTest.get(1).isString()).isTrue(); + assertThat(underTest.get(1).asString()).isEqualTo("test2"); + assertThat(underTest.get(2).isString()).isTrue(); + assertThat(underTest.get(2).asString()).isEqualTo("test3"); + } + + @Test + void asList() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.add(parser.createJsonValue("1")).add(parser.createJsonValue("2")).add(parser.createJsonValue("3")); + + assertThat(underTest.size()).isEqualTo(3); + assertThat(underTest.asList()).hasSize(3); + assertThat(underTest.asList().get(0).isNumber()).isTrue(); + assertThat(underTest.asList().get(0).asNumber()).isEqualTo(1); + } + + @Test + void getFirst() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.add(parser.createJsonValue("\"test\"")).add(parser.createJsonValue("\"test2\"")) + .add(parser.createJsonValue("\"test3\"")); + + assertThat(underTest.size()).isEqualTo(3); + assertThat(underTest.getFirst().isString()).isTrue(); + assertThat(underTest.getFirst().asString()).isEqualTo("test"); + } + + @Test + void iterator() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.add(parser.createJsonValue("1")).add(parser.createJsonValue("2")).add(parser.createJsonValue("3")); + + Iterator iterator = underTest.iterator(); + assertThat(iterator.hasNext()).isTrue(); + while (iterator.hasNext()) { + assertThat(iterator.next().isNumber()).isTrue(); + } + } + + @Test + void remove() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.add(parser.createJsonValue("1")).add(parser.createJsonValue("2")).add(parser.createJsonValue("3")); + + assertThat(underTest.remove(1).asNumber()).isEqualTo(2); + assertThat(underTest.size()).isEqualTo(2); + assertThat(underTest.get(0).asNumber()).isEqualTo(1); + assertThat(underTest.get(1).asNumber()).isEqualTo(3); + } + + @Test + void replace() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.add(parser.createJsonValue("1")).add(parser.createJsonValue("2")).add(parser.createJsonValue("3")); + underTest.replace(1, parser.createJsonValue("4")); + + assertThat(underTest.size()).isEqualTo(3); + assertThat(underTest.get(0).asNumber()).isEqualTo(1); + assertThat(underTest.get(1).asNumber()).isEqualTo(4); + assertThat(underTest.get(2).asNumber()).isEqualTo(3); + } + + @Test + void isJsonArray() { + DelegateJsonArray underTest = new DelegateJsonArray(); + assertThat(underTest.isJsonArray()).isTrue(); + + assertThat(underTest.isJsonObject()).isFalse(); + assertThat(underTest.isNull()).isFalse(); + assertThat(underTest.isNumber()).isFalse(); + assertThat(underTest.isString()).isFalse(); + } + + @Test + void asJsonArray() { + DelegateJsonArray underTest = new DelegateJsonArray(); + assertThat(underTest.asJsonArray()).isSameAs(underTest); + } + + @Test + void asAnythingElse() { + DelegateJsonArray underTest = new DelegateJsonArray(); + + assertThat(underTest.asBoolean()).isNull(); + assertThat(underTest.asJsonObject()).isNull(); + assertThat(underTest.asString()).isNull(); + assertThat(underTest.asNumber()).isNull(); + } + +} diff --git a/src/test/java/io/lettuce/core/json/DelegateJsonObjectUnitTests.java b/src/test/java/io/lettuce/core/json/DelegateJsonObjectUnitTests.java new file mode 100644 index 0000000000..c542a41480 --- /dev/null +++ b/src/test/java/io/lettuce/core/json/DelegateJsonObjectUnitTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link DelegateJsonObject}. + */ +class DelegateJsonObjectUnitTests { + + @Test + void put() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + DelegateJsonObject underTest = new DelegateJsonObject(); + + underTest.put("test", parser.createJsonValue("\"test\"")).put("test2", parser.createJsonValue("1")).put("test2", + parser.createJsonValue("true")); + + assertThat(underTest.size()).isEqualTo(2); + assertThat(underTest.get("test").asString()).isEqualTo("test"); + assertThat(underTest.get("test2").asBoolean()).isTrue(); + } + + @Test + void remove() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + DelegateJsonObject underTest = new DelegateJsonObject(); + + underTest.put("test", parser.createJsonValue("\"test\"")).put("test2", parser.createJsonValue("1")).remove("test"); + + assertThat(underTest.size()).isEqualTo(1); + assertThat(underTest.get("test")).isNull(); + assertThat(underTest.get("test2").asNumber()).isEqualTo(1); + } + + @Test + void isAnythingElse() { + DelegateJsonObject underTest = new DelegateJsonObject(); + + assertThat(underTest.isJsonObject()).isTrue(); + + assertThat(underTest.isNull()).isFalse(); + assertThat(underTest.isBoolean()).isFalse(); + assertThat(underTest.isNumber()).isFalse(); + assertThat(underTest.isString()).isFalse(); + assertThat(underTest.isJsonArray()).isFalse(); + } + + @Test + void asAnythingElse() { + DelegateJsonObject underTest = new DelegateJsonObject(); + + assertThat(underTest.asJsonObject()).isNotNull(); + + assertThat(underTest.asBoolean()).isNull(); + assertThat(underTest.asJsonArray()).isNull(); + assertThat(underTest.asString()).isNull(); + assertThat(underTest.asNumber()).isNull(); + } + +} diff --git a/src/test/java/io/lettuce/core/json/DelegateJsonValueUnitTests.java b/src/test/java/io/lettuce/core/json/DelegateJsonValueUnitTests.java new file mode 100644 index 0000000000..5fab965057 --- /dev/null +++ b/src/test/java/io/lettuce/core/json/DelegateJsonValueUnitTests.java @@ -0,0 +1,145 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link DelegateJsonValue}. + */ +class DelegateJsonValueUnitTests { + + @Test + void testString() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + JsonValue underTest = parser.createJsonValue("\"test\""); + + assertThat(underTest.toString()).isEqualTo("\"test\""); + assertThat(underTest.asByteBuffer().array()).isEqualTo("\"test\"".getBytes()); + + assertThat(underTest.isJsonArray()).isFalse(); + assertThat(underTest.asJsonArray()).isNull(); + + assertThat(underTest.isJsonObject()).isFalse(); + assertThat(underTest.asJsonObject()).isNull(); + + assertThat(underTest.isString()).isTrue(); + assertThat(underTest.asString()).isEqualTo("test"); + + assertThat(underTest.isNumber()).isFalse(); + assertThat(underTest.asNumber()).isNull(); + + assertThat(underTest.isBoolean()).isFalse(); + assertThat(underTest.asBoolean()).isNull(); + + assertThat(underTest.isNull()).isFalse(); + } + + @Test + void testNumber() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + JsonValue underTest = parser.createJsonValue("1"); + + assertThat(underTest.toString()).isEqualTo("1"); + assertThat(underTest.asByteBuffer().array()).isEqualTo("1".getBytes()); + + assertThat(underTest.isJsonArray()).isFalse(); + assertThat(underTest.asJsonArray()).isNull(); + + assertThat(underTest.isJsonObject()).isFalse(); + assertThat(underTest.asJsonObject()).isNull(); + + assertThat(underTest.isNumber()).isTrue(); + assertThat(underTest.asNumber()).isEqualTo(1); + + assertThat(underTest.isString()).isFalse(); + assertThat(underTest.asString()).isNull(); + + assertThat(underTest.isBoolean()).isFalse(); + assertThat(underTest.asBoolean()).isNull(); + + assertThat(underTest.isNull()).isFalse(); + } + + @Test + void testNumberExtended() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + JsonValue underTest = parser.createJsonValue("1"); + + assertThat(underTest.isNumber()).isTrue(); + assertThat(underTest.asNumber()).isEqualTo(1); + assertThat(underTest.asNumber()).isInstanceOf(Integer.class); + + underTest = parser.createJsonValue(String.valueOf(Long.MAX_VALUE)); + + assertThat(underTest.isNumber()).isTrue(); + assertThat(underTest.asNumber()).isEqualTo(Long.MAX_VALUE); + assertThat(underTest.asNumber()).isInstanceOf(Long.class); + + underTest = parser.createJsonValue(String.valueOf(Double.MAX_VALUE)); + + assertThat(underTest.isNumber()).isTrue(); + assertThat(underTest.asNumber()).isEqualTo(Double.MAX_VALUE); + assertThat(underTest.asNumber()).isInstanceOf(Double.class); + } + + @Test + void testBoolean() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + JsonValue underTest = parser.createJsonValue("true"); + + assertThat(underTest.toString()).isEqualTo("true"); + assertThat(underTest.asByteBuffer().array()).isEqualTo("true".getBytes()); + + assertThat(underTest.isJsonArray()).isFalse(); + assertThat(underTest.asJsonArray()).isNull(); + + assertThat(underTest.isJsonObject()).isFalse(); + assertThat(underTest.asJsonObject()).isNull(); + + assertThat(underTest.isBoolean()).isTrue(); + assertThat(underTest.asBoolean()).isTrue(); + + assertThat(underTest.isString()).isFalse(); + assertThat(underTest.asString()).isNull(); + + assertThat(underTest.isNumber()).isFalse(); + assertThat(underTest.asNumber()).isNull(); + + assertThat(underTest.isNull()).isFalse(); + } + + @Test + void testNull() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + JsonValue underTest = parser.createJsonValue("null"); + + assertThat(underTest.toString()).isEqualTo("null"); + assertThat(underTest.asByteBuffer().array()).isEqualTo("null".getBytes()); + + assertThat(underTest.isJsonArray()).isFalse(); + assertThat(underTest.asJsonArray()).isNull(); + + assertThat(underTest.isJsonObject()).isFalse(); + assertThat(underTest.asJsonObject()).isNull(); + + assertThat(underTest.isNumber()).isFalse(); + assertThat(underTest.asNumber()).isNull(); + + assertThat(underTest.isString()).isFalse(); + assertThat(underTest.asString()).isNull(); + + assertThat(underTest.isBoolean()).isFalse(); + assertThat(underTest.asBoolean()).isNull(); + + assertThat(underTest.isNull()).isTrue(); + } + +} diff --git a/src/test/java/io/lettuce/core/json/RedisJsonClusterIntegrationTests.java b/src/test/java/io/lettuce/core/json/RedisJsonClusterIntegrationTests.java new file mode 100644 index 0000000000..7c2746d4c0 --- /dev/null +++ b/src/test/java/io/lettuce/core/json/RedisJsonClusterIntegrationTests.java @@ -0,0 +1,433 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import io.lettuce.core.RedisContainerIntegrationTests; +import io.lettuce.core.RedisURI; +import io.lettuce.core.cluster.RedisClusterClient; +import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RedisJsonClusterIntegrationTests extends RedisContainerIntegrationTests { + + protected static RedisClusterClient client; + + protected static RedisAdvancedClusterCommands redis; + + public RedisJsonClusterIntegrationTests() { + RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(36379).build(); + + client = RedisClusterClient.create(redisURI); + redis = client.connect().sync(); + } + + private static final String BIKES_INVENTORY = "bikes:inventory"; + + private static final String BIKE_COLORS_V1 = "..mountain_bikes[1].colors"; + + private static final String BIKE_COLORS_V2 = "$..mountain_bikes[1].colors"; + + private static final String MOUNTAIN_BIKES_V1 = "..mountain_bikes"; + + private static final String MOUNTAIN_BIKES_V2 = "$..mountain_bikes"; + + @BeforeEach + public void prepare() throws IOException { + redis.flushall(); + + Path path = Paths.get("src/test/resources/bike-inventory.json"); + String read = String.join("", Files.readAllLines(path)); + JsonValue value = redis.getJsonParser().createJsonValue(read); + + redis.jsonSet("bikes:inventory", JsonPath.ROOT_PATH, value, JsonSetArgs.Builder.defaults()); + } + + @AfterAll + static void teardown() { + if (client != null) { + client.shutdown(); + } + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonArrappend(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + + JsonValue element = parser.createJsonValue("\"{id:bike6}\""); + List appendedElements = redis.jsonArrappend(BIKES_INVENTORY, myPath, element); + assertThat(appendedElements).hasSize(1); + assertThat(appendedElements.get(0)).isEqualTo(4); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 }) + void jsonArrindex(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + JsonValue element = parser.createJsonValue("\"white\""); + + List arrayIndex = redis.jsonArrindex(BIKES_INVENTORY, myPath, element, null); + assertThat(arrayIndex).isNotNull(); + assertThat(arrayIndex).hasSize(1); + assertThat(arrayIndex.get(0).longValue()).isEqualTo(1L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 }) + void jsonArrinsert(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + JsonValue element = parser.createJsonValue("\"ultramarine\""); + + List arrayIndex = redis.jsonArrinsert(BIKES_INVENTORY, myPath, 1, element); + assertThat(arrayIndex).isNotNull(); + assertThat(arrayIndex).hasSize(1); + assertThat(arrayIndex.get(0).longValue()).isEqualTo(3L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonArrlen(String path) { + JsonPath myPath = JsonPath.of(path); + + List poppedJson = redis.jsonArrlen(BIKES_INVENTORY, myPath); + assertThat(poppedJson).hasSize(1); + assertThat(poppedJson.get(0).longValue()).isEqualTo(3); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonArrpop(String path) { + JsonPath myPath = JsonPath.of(path); + + List poppedJson = redis.jsonArrpop(BIKES_INVENTORY, myPath, -1); + assertThat(poppedJson).hasSize(1); + assertThat(poppedJson.get(0).toString()).contains( + "{\"id\":\"bike:3\",\"model\":\"Weywot\",\"description\":\"This bike gives kids aged six years and old"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 }) + void jsonArrtrim(String path) { + JsonPath myPath = JsonPath.of(path); + JsonRangeArgs range = JsonRangeArgs.Builder.start(1).stop(2); + + List arrayIndex = redis.jsonArrtrim(BIKES_INVENTORY, myPath, range); + assertThat(arrayIndex).isNotNull(); + assertThat(arrayIndex).hasSize(1); + assertThat(arrayIndex.get(0).longValue()).isEqualTo(1L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 }) + void jsonClear(String path) { + JsonPath myPath = JsonPath.of(path); + + Long result = redis.jsonClear(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).isEqualTo(1L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "..mountain_bikes[0:2].model", "$..mountain_bikes[0:2].model" }) + void jsonGet(String path) { + JsonPath myPath = JsonPath.of(path); + + // Verify codec parsing + List value = redis.jsonGet(BIKES_INVENTORY, JsonGetArgs.Builder.defaults(), myPath); + assertThat(value).hasSize(1); + + if (path.startsWith("$")) { + assertThat(value.get(0).toString()).isEqualTo("[\"Phoebe\",\"Quaoar\"]"); + + // Verify array parsing + assertThat(value.get(0).isJsonArray()).isTrue(); + assertThat(value.get(0).asJsonArray().size()).isEqualTo(2); + assertThat(value.get(0).asJsonArray().asList().get(0).toString()).isEqualTo("\"Phoebe\""); + assertThat(value.get(0).asJsonArray().asList().get(1).toString()).isEqualTo("\"Quaoar\""); + + // Verify String parsing + assertThat(value.get(0).asJsonArray().asList().get(0).isString()).isTrue(); + assertThat(value.get(0).asJsonArray().asList().get(0).asString()).isEqualTo("Phoebe"); + assertThat(value.get(0).asJsonArray().asList().get(1).isString()).isTrue(); + assertThat(value.get(0).asJsonArray().asList().get(1).asString()).isEqualTo("Quaoar"); + } else { + assertThat(value.get(0).toString()).isEqualTo("\"Phoebe\""); + + // Verify array parsing + assertThat(value.get(0).isString()).isTrue(); + assertThat(value.get(0).asString()).isEqualTo("Phoebe"); + } + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) + void jsonMerge(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + JsonValue element = parser.createJsonValue("\"ultramarine\""); + + String result = redis.jsonMerge(BIKES_INVENTORY, myPath, element); + assertThat(result).isNotNull(); + assertThat(result).isEqualTo("OK"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "..model", "$..model" }) + void jsonMGet(String path) { + JsonPath myPath = JsonPath.of(path); + + List value = redis.jsonMGet(myPath, BIKES_INVENTORY); + assertThat(value).hasSize(1); + if (path.startsWith("$")) { + assertThat(value.get(0).toString()).isEqualTo("[\"Phoebe\",\"Quaoar\",\"Weywot\"]"); + } else { + assertThat(value.get(0).toString()).isEqualTo("\"Phoebe\""); + } + } + + @Test + void jsonCrossSlot() { + JsonParser parser = redis.getJsonParser(); + + JsonObject bikeRecord = parser.createJsonObject(); + JsonObject bikeSpecs = parser.createJsonObject(); + JsonArray bikeColors = parser.createJsonArray(); + + bikeSpecs.put("material", parser.createJsonValue("\"composite\"")); + bikeSpecs.put("weight", parser.createJsonValue("11")); + + bikeColors.add(parser.createJsonValue("\"yellow\"")); + bikeColors.add(parser.createJsonValue("\"orange\"")); + + bikeRecord.put("id", parser.createJsonValue("\"bike:43\"")); + bikeRecord.put("model", parser.createJsonValue("\"DesertFox\"")); + bikeRecord.put("description", parser.createJsonValue("\"The DesertFox is a versatile bike for all terrains\"")); + bikeRecord.put("price", parser.createJsonValue("\"1299\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + JsonObject bikeServiceRecord = parser.createJsonObject(); + String today = "\"" + DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDateTime.now()) + "\""; + String lastWeek = "\"" + DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDateTime.now().minusDays(7)) + "\""; + + JsonArray serviceHistory = parser.createJsonArray(); + + serviceHistory.add(parser.createJsonValue(today)); + serviceHistory.add(parser.createJsonValue(lastWeek)); + + bikeServiceRecord.put("id", parser.createJsonValue("\"bike:43\"")); + bikeServiceRecord.put("serviceHistory", serviceHistory); + bikeServiceRecord.put("purchaseDate", parser.createJsonValue(lastWeek)); + bikeServiceRecord.put("guarantee", parser.createJsonValue("\"2 years\"")); + + // set value on a different slot + + JsonMsetArgs args1 = new JsonMsetArgs<>(BIKES_INVENTORY, JsonPath.ROOT_PATH, bikeRecord); + JsonMsetArgs args2 = new JsonMsetArgs<>("bikes:service", JsonPath.ROOT_PATH, bikeServiceRecord); + String result = redis.jsonMSet(Arrays.asList(args1, args2)); + assertThat(result).isEqualTo("OK"); + + // get values from two different slots + List value = redis.jsonMGet(JsonPath.ROOT_PATH, BIKES_INVENTORY, "bikes:service"); + assertThat(value).hasSize(2); + JsonValue slot1 = value.get(0); + JsonValue slot2 = value.get(1); + assertThat(slot1.toString()).contains("bike:43"); + assertThat(slot2.toString()).contains("bike:43"); + assertThat(slot1.isJsonArray()).isTrue(); + assertThat(slot2.isJsonArray()).isTrue(); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) + void jsonMset(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + + JsonObject bikeRecord = parser.createJsonObject(); + JsonObject bikeSpecs = parser.createJsonObject(); + JsonArray bikeColors = parser.createJsonArray(); + bikeSpecs.put("material", parser.createJsonValue("\"composite\"")); + bikeSpecs.put("weight", parser.createJsonValue("11")); + bikeColors.add(parser.createJsonValue("\"yellow\"")); + bikeColors.add(parser.createJsonValue("\"orange\"")); + bikeRecord.put("id", parser.createJsonValue("\"bike:43\"")); + bikeRecord.put("model", parser.createJsonValue("\"DesertFox\"")); + bikeRecord.put("description", parser.createJsonValue("\"The DesertFox is a versatile bike for all terrains\"")); + bikeRecord.put("price", parser.createJsonValue("\"1299\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + JsonMsetArgs args1 = new JsonMsetArgs<>(BIKES_INVENTORY, myPath, bikeRecord); + + bikeRecord = parser.createJsonObject(); + bikeSpecs = parser.createJsonObject(); + bikeColors = parser.createJsonArray(); + bikeSpecs.put("material", parser.createJsonValue("\"wood\"")); + bikeSpecs.put("weight", parser.createJsonValue("19")); + bikeColors.add(parser.createJsonValue("\"walnut\"")); + bikeColors.add(parser.createJsonValue("\"chestnut\"")); + bikeRecord.put("id", parser.createJsonValue("\"bike:13\"")); + bikeRecord.put("model", parser.createJsonValue("\"Woody\"")); + bikeRecord.put("description", parser.createJsonValue("\"The Woody is an environmentally-friendly wooden bike\"")); + bikeRecord.put("price", parser.createJsonValue("\"1112\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + JsonMsetArgs args2 = new JsonMsetArgs<>(BIKES_INVENTORY, myPath, bikeRecord); + + List> args = Arrays.asList(args1, args2); + String result = redis.jsonMSet(args); + + assertThat(result).isNotNull(); + assertThat(result).isEqualTo("OK"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "$..mountain_bikes[0:1].price", "..mountain_bikes[0:1].price" }) + void jsonNumincrby(String path) { + JsonPath myPath = JsonPath.of(path); + + List value = redis.jsonNumincrby(BIKES_INVENTORY, myPath, 5L); + assertThat(value).hasSize(1); + assertThat(value.get(0).longValue()).isEqualTo(1933L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) + void jsonObjkeys(String path) { + JsonPath myPath = JsonPath.of(path); + + List result = redis.jsonObjkeys(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).hasSize(6); + assertThat(result).contains("id", "model", "description", "price", "specs", "colors"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) + void jsonObjlen(String path) { + JsonPath myPath = JsonPath.of(path); + + List result = redis.jsonObjlen(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEqualTo(6L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonSet(String path) { + JsonPath myPath = JsonPath.of(path); + + JsonParser parser = redis.getJsonParser(); + JsonObject bikeRecord = parser.createJsonObject(); + JsonObject bikeSpecs = parser.createJsonObject(); + JsonArray bikeColors = parser.createJsonArray(); + + bikeSpecs.put("material", parser.createJsonValue("\"composite\"")); + bikeSpecs.put("weight", parser.createJsonValue("11")); + + bikeColors.add(parser.createJsonValue("\"yellow\"")); + bikeColors.add(parser.createJsonValue("\"orange\"")); + + bikeRecord.put("id", parser.createJsonValue("\"bike:43\"")); + bikeRecord.put("model", parser.createJsonValue("\"DesertFox\"")); + bikeRecord.put("description", parser.createJsonValue("\"The DesertFox is a versatile bike for all terrains\"")); + bikeRecord.put("price", parser.createJsonValue("\"1299\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + JsonSetArgs args = JsonSetArgs.Builder.defaults(); + + String result = redis.jsonSet(BIKES_INVENTORY, myPath, bikeRecord, args); + assertThat(result).isEqualTo("OK"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "..mountain_bikes[1].colors[1]", "$..mountain_bikes[1].colors[1]" }) + void jsonStrappend(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + JsonValue element = parser.createJsonValue("\"-light\""); + + List result = redis.jsonStrappend(BIKES_INVENTORY, myPath, element); + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEqualTo(11L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1 + "[1]", BIKE_COLORS_V2 + "[1]" }) + void jsonStrlen(String path) { + JsonPath myPath = JsonPath.of(path); + + List result = redis.jsonStrlen(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEqualTo(5L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "$..complete", "..complete" }) + void jsonToggle(String path) { + JsonPath myPath = JsonPath.of(path); + + List result = redis.jsonToggle(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + if (path.startsWith("$")) { + assertThat(result.get(0)).isEqualTo(1L); + } else { + // seems that for JSON.TOGGLE when we use a V1 path the resulting value is a list of string values and not a + // list of integer values as per the documentation + assertThat(result).isNotEmpty(); + } + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[2:3]", MOUNTAIN_BIKES_V2 + "[2:3]" }) + void jsonDel(String path) { + JsonPath myPath = JsonPath.of(path); + + Long value = redis.jsonDel(BIKES_INVENTORY, myPath); + assertThat(value).isEqualTo(1); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonType(String path) { + JsonPath myPath = JsonPath.of(path); + + JsonType jsonType = redis.jsonType(BIKES_INVENTORY, myPath).get(0); + assertThat(jsonType).isEqualTo(JsonType.ARRAY); + } + +} diff --git a/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java b/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java new file mode 100644 index 0000000000..25ea348c76 --- /dev/null +++ b/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java @@ -0,0 +1,623 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import io.lettuce.core.ClientOptions; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisContainerIntegrationTests; +import io.lettuce.core.RedisFuture; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.async.RedisAsyncCommands; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.codec.ByteArrayCodec; +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RedisJsonIntegrationTests extends RedisContainerIntegrationTests { + + private static final String BIKES_INVENTORY = "bikes:inventory"; + + private static final String BIKE_COLORS_V1 = "..mountain_bikes[1].colors"; + + private static final String BIKE_COLORS_V2 = "$..mountain_bikes[1].colors"; + + private static final String MOUNTAIN_BIKES_V1 = "..mountain_bikes"; + + private static final String MOUNTAIN_BIKES_V2 = "$..mountain_bikes"; + + protected static RedisClient client; + + protected static RedisCommands redis; + + public RedisJsonIntegrationTests() { + RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build(); + + client = RedisClient.create(redisURI); + redis = client.connect().sync(); + } + + @BeforeEach + public void prepare() throws IOException { + redis.flushall(); + + Path path = Paths.get("src/test/resources/bike-inventory.json"); + String read = String.join("", Files.readAllLines(path)); + JsonValue value = redis.getJsonParser().createJsonValue(read); + + redis.jsonSet("bikes:inventory", JsonPath.ROOT_PATH, value); + } + + @AfterAll + static void teardown() { + if (client != null) { + client.shutdown(); + } + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonArrappend(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + + JsonValue element = parser.createJsonValue("\"{id:bike6}\""); + List appendedElements = redis.jsonArrappend(BIKES_INVENTORY, myPath, element); + assertThat(appendedElements).hasSize(1); + assertThat(appendedElements.get(0)).isEqualTo(4); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 }) + void jsonArrindex(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + JsonValue element = parser.createJsonValue("\"white\""); + + List arrayIndex = redis.jsonArrindex(BIKES_INVENTORY, myPath, element); + assertThat(arrayIndex).isNotNull(); + assertThat(arrayIndex).hasSize(1); + assertThat(arrayIndex.get(0).longValue()).isEqualTo(1L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 }) + void jsonArrinsert(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + JsonValue element = parser.createJsonValue("\"ultramarine\""); + + List arrayIndex = redis.jsonArrinsert(BIKES_INVENTORY, myPath, 1, element); + assertThat(arrayIndex).isNotNull(); + assertThat(arrayIndex).hasSize(1); + assertThat(arrayIndex.get(0).longValue()).isEqualTo(3L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonArrlen(String path) { + JsonPath myPath = JsonPath.of(path); + + List poppedJson = redis.jsonArrlen(BIKES_INVENTORY, myPath); + assertThat(poppedJson).hasSize(1); + assertThat(poppedJson.get(0).longValue()).isEqualTo(3); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonArrpop(String path) { + JsonPath myPath = JsonPath.of(path); + + List poppedJson = redis.jsonArrpop(BIKES_INVENTORY, myPath); + assertThat(poppedJson).hasSize(1); + assertThat(poppedJson.get(0).toString()).contains( + "{\"id\":\"bike:3\",\"model\":\"Weywot\",\"description\":\"This bike gives kids aged six years and old"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 }) + void jsonArrtrim(String path) { + JsonPath myPath = JsonPath.of(path); + JsonRangeArgs range = JsonRangeArgs.Builder.start(1).stop(2); + + List arrayIndex = redis.jsonArrtrim(BIKES_INVENTORY, myPath, range); + assertThat(arrayIndex).isNotNull(); + assertThat(arrayIndex).hasSize(1); + assertThat(arrayIndex.get(0).longValue()).isEqualTo(1L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 }) + void jsonClear(String path) { + JsonPath myPath = JsonPath.of(path); + + Long result = redis.jsonClear(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).isEqualTo(1L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "..mountain_bikes[0:2].model", "$..mountain_bikes[0:2].model" }) + void jsonGet(String path) { + JsonPath myPath = JsonPath.of(path); + + // Verify codec parsing + List value = redis.jsonGet(BIKES_INVENTORY, myPath); + assertThat(value).hasSize(1); + + if (path.startsWith("$")) { + assertThat(value.get(0).toString()).isEqualTo("[\"Phoebe\",\"Quaoar\"]"); + + // Verify array parsing + assertThat(value.get(0).isJsonArray()).isTrue(); + assertThat(value.get(0).asJsonArray().size()).isEqualTo(2); + assertThat(value.get(0).asJsonArray().asList().get(0).toString()).isEqualTo("\"Phoebe\""); + assertThat(value.get(0).asJsonArray().asList().get(1).toString()).isEqualTo("\"Quaoar\""); + + // Verify String parsing + assertThat(value.get(0).asJsonArray().asList().get(0).isString()).isTrue(); + assertThat(value.get(0).asJsonArray().asList().get(0).asString()).isEqualTo("Phoebe"); + assertThat(value.get(0).asJsonArray().asList().get(1).isString()).isTrue(); + assertThat(value.get(0).asJsonArray().asList().get(1).isNull()).isFalse(); + assertThat(value.get(0).asJsonArray().asList().get(1).asString()).isEqualTo("Quaoar"); + } else { + assertThat(value.get(0).toString()).isEqualTo("\"Phoebe\""); + + // Verify array parsing + assertThat(value.get(0).isString()).isTrue(); + assertThat(value.get(0).asString()).isEqualTo("Phoebe"); + } + } + + @Test + void jsonGetNull() { + JsonPath myPath = JsonPath.of("$..inventory.owner"); + + // Verify codec parsing + List value = redis.jsonGet(BIKES_INVENTORY, myPath); + assertThat(value).hasSize(1); + + assertThat(value.get(0).toString()).isEqualTo("[null]"); + + // Verify array parsing + assertThat(value.get(0).isJsonArray()).isTrue(); + assertThat(value.get(0).asJsonArray().size()).isEqualTo(1); + assertThat(value.get(0).asJsonArray().asList().get(0).toString()).isEqualTo("null"); + assertThat(value.get(0).asJsonArray().asList().get(0).isNull()).isTrue(); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) + void jsonMerge(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + JsonValue element = parser.createJsonValue("\"ultramarine\""); + + String result = redis.jsonMerge(BIKES_INVENTORY, myPath, element); + assertThat(result).isNotNull(); + assertThat(result).isEqualTo("OK"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "..model", "$..model" }) + void jsonMGet(String path) { + JsonPath myPath = JsonPath.of(path); + + List value = redis.jsonMGet(myPath, BIKES_INVENTORY); + assertThat(value).hasSize(1); + if (path.startsWith("$")) { + assertThat(value.get(0).toString()).isEqualTo("[\"Phoebe\",\"Quaoar\",\"Weywot\"]"); + } else { + assertThat(value.get(0).toString()).isEqualTo("\"Phoebe\""); + } + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) + void jsonMset(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + + JsonObject bikeRecord = parser.createJsonObject(); + JsonObject bikeSpecs = parser.createJsonObject(); + JsonArray bikeColors = parser.createJsonArray(); + bikeSpecs.put("material", parser.createJsonValue("\"composite\"")); + bikeSpecs.put("weight", parser.createJsonValue("11")); + bikeColors.add(parser.createJsonValue("\"yellow\"")); + bikeColors.add(parser.createJsonValue("\"orange\"")); + bikeRecord.put("id", parser.createJsonValue("\"bike:43\"")); + bikeRecord.put("model", parser.createJsonValue("\"DesertFox\"")); + bikeRecord.put("description", parser.createJsonValue("\"The DesertFox is a versatile bike for all terrains\"")); + bikeRecord.put("price", parser.createJsonValue("\"1299\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + JsonMsetArgs args1 = new JsonMsetArgs<>(BIKES_INVENTORY, myPath, bikeRecord); + + bikeRecord = parser.createJsonObject(); + bikeSpecs = parser.createJsonObject(); + bikeColors = parser.createJsonArray(); + bikeSpecs.put("material", parser.createJsonValue("\"wood\"")); + bikeSpecs.put("weight", parser.createJsonValue("19")); + bikeColors.add(parser.createJsonValue("\"walnut\"")); + bikeColors.add(parser.createJsonValue("\"chestnut\"")); + bikeRecord.put("id", parser.createJsonValue("\"bike:13\"")); + bikeRecord.put("model", parser.createJsonValue("\"Woody\"")); + bikeRecord.put("description", parser.createJsonValue("\"The Woody is an environmentally-friendly wooden bike\"")); + bikeRecord.put("price", parser.createJsonValue("\"1112\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + JsonMsetArgs args2 = new JsonMsetArgs<>(BIKES_INVENTORY, myPath, bikeRecord); + + List> args = Arrays.asList(args1, args2); + String result = redis.jsonMSet(args); + + assertThat(result).isNotNull(); + assertThat(result).isEqualTo("OK"); + + JsonValue value = redis.jsonGet(BIKES_INVENTORY, JsonPath.ROOT_PATH).get(0); + assertThat(value).isNotNull(); + assertThat(value.isJsonArray()).isTrue(); + assertThat(value.asJsonArray().size()).isEqualTo(1); + assertThat(value.asJsonArray().asList().get(0).toString()).contains( + "{\"id\":\"bike:13\",\"model\":\"Woody\",\"description\":\"The Woody is an environmentally-friendly wooden bike\""); + } + + @Test + void jsonMsetCrossslot() { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(BIKES_INVENTORY); + + JsonObject bikeRecord = parser.createJsonObject(); + JsonObject bikeSpecs = parser.createJsonObject(); + JsonArray bikeColors = parser.createJsonArray(); + bikeSpecs.put("material", parser.createJsonValue("\"composite\"")); + bikeSpecs.put("weight", parser.createJsonValue("11")); + bikeColors.add(parser.createJsonValue("\"yellow\"")); + bikeColors.add(parser.createJsonValue("\"orange\"")); + bikeRecord.put("id", parser.createJsonValue("\"bike:43\"")); + bikeRecord.put("model", parser.createJsonValue("\"DesertFox\"")); + bikeRecord.put("description", parser.createJsonValue("\"The DesertFox is a versatile bike for all terrains\"")); + bikeRecord.put("price", parser.createJsonValue("\"1299\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + JsonMsetArgs args1 = new JsonMsetArgs<>(BIKES_INVENTORY, myPath, bikeRecord); + + bikeRecord = parser.createJsonObject(); + bikeSpecs = parser.createJsonObject(); + bikeColors = parser.createJsonArray(); + bikeSpecs.put("material", parser.createJsonValue("\"wood\"")); + bikeSpecs.put("weight", parser.createJsonValue("19")); + bikeColors.add(parser.createJsonValue("\"walnut\"")); + bikeColors.add(parser.createJsonValue("\"chestnut\"")); + bikeRecord.put("id", parser.createJsonValue("\"bike:13\"")); + bikeRecord.put("model", parser.createJsonValue("\"Woody\"")); + bikeRecord.put("description", parser.createJsonValue("\"The Woody is an environmentally-friendly wooden bike\"")); + bikeRecord.put("price", parser.createJsonValue("\"1112\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + JsonMsetArgs args2 = new JsonMsetArgs<>("bikes:service", JsonPath.ROOT_PATH, bikeRecord); + + List> args = Arrays.asList(args1, args2); + String result = redis.jsonMSet(args); + + assertThat(result).isNotNull(); + assertThat(result).isEqualTo("OK"); + + JsonValue value = redis.jsonGet("bikes:service", JsonPath.ROOT_PATH).get(0); + assertThat(value).isNotNull(); + assertThat(value.isJsonArray()).isTrue(); + assertThat(value.asJsonArray().size()).isEqualTo(1); + assertThat(value.asJsonArray().asList().get(0).toString()).contains( + "{\"id\":\"bike:13\",\"model\":\"Woody\",\"description\":\"The Woody is an environmentally-friendly wooden bike\""); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "$..mountain_bikes[0:1].price", "..mountain_bikes[0:1].price" }) + void jsonNumincrby(String path) { + JsonPath myPath = JsonPath.of(path); + + List value = redis.jsonNumincrby(BIKES_INVENTORY, myPath, 5L); + assertThat(value).hasSize(1); + assertThat(value.get(0).longValue()).isEqualTo(1933L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) + void jsonObjkeys(String path) { + JsonPath myPath = JsonPath.of(path); + + List result = redis.jsonObjkeys(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).hasSize(6); + assertThat(result).contains("id", "model", "description", "price", "specs", "colors"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) + void jsonObjlen(String path) { + JsonPath myPath = JsonPath.of(path); + + List result = redis.jsonObjlen(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEqualTo(6L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonSet(String path) { + JsonPath myPath = JsonPath.of(path); + + JsonParser parser = redis.getJsonParser(); + JsonObject bikeRecord = parser.createJsonObject(); + JsonObject bikeSpecs = parser.createJsonObject(); + JsonArray bikeColors = parser.createJsonArray(); + + bikeSpecs.put("material", parser.createJsonValue("null")); + bikeSpecs.put("weight", parser.createJsonValue("11")); + + bikeColors.add(parser.createJsonValue("\"yellow\"")); + bikeColors.add(parser.createJsonValue("\"orange\"")); + + bikeRecord.put("id", parser.createJsonValue("\"bike:43\"")); + bikeRecord.put("model", parser.createJsonValue("\"DesertFox\"")); + bikeRecord.put("description", parser.createJsonValue("\"The DesertFox is a versatile bike for all terrains\"")); + bikeRecord.put("price", parser.createJsonValue("\"1299\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + String result = redis.jsonSet(BIKES_INVENTORY, myPath, bikeRecord); + assertThat(result).isEqualTo("OK"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "..mountain_bikes[1].colors[1]", "$..mountain_bikes[1].colors[1]" }) + void jsonStrappend(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + JsonValue element = parser.createJsonValue("\"-light\""); + + List result = redis.jsonStrappend(BIKES_INVENTORY, myPath, element); + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEqualTo(11L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1 + "[1]", BIKE_COLORS_V2 + "[1]" }) + void jsonStrlen(String path) { + JsonPath myPath = JsonPath.of(path); + + List result = redis.jsonStrlen(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEqualTo(5L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "$..complete", "..complete" }) + void jsonToggle(String path) { + JsonPath myPath = JsonPath.of(path); + + List result = redis.jsonToggle(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + if (path.startsWith("$")) { + assertThat(result.get(0)).isEqualTo(1L); + } else { + // seems that for JSON.TOGGLE when we use a V1 path the resulting value is a list of string values and not a + // list of integer values as per the documentation + assertThat(result).isNotEmpty(); + } + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[2:3]", MOUNTAIN_BIKES_V2 + "[2:3]" }) + void jsonDel(String path) { + JsonPath myPath = JsonPath.of(path); + + Long value = redis.jsonDel(BIKES_INVENTORY, myPath); + assertThat(value).isEqualTo(1); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonType(String path) { + JsonPath myPath = JsonPath.of(path); + + JsonType jsonType = redis.jsonType(BIKES_INVENTORY, myPath).get(0); + assertThat(jsonType).isEqualTo(JsonType.ARRAY); + } + + @Test + void jsonAllTypes() { + JsonPath myPath = JsonPath.of("$..mountain_bikes[1]"); + + JsonType jsonType = redis.jsonType(BIKES_INVENTORY, myPath).get(0); + assertThat(jsonType).isEqualTo(JsonType.OBJECT); + + myPath = JsonPath.of("$..mountain_bikes[0:1].price"); + jsonType = redis.jsonType(BIKES_INVENTORY, myPath).get(0); + assertThat(jsonType).isEqualTo(JsonType.INTEGER); + + myPath = JsonPath.of("$..weight"); + jsonType = redis.jsonType(BIKES_INVENTORY, myPath).get(0); + assertThat(jsonType).isEqualTo(JsonType.NUMBER); + + myPath = JsonPath.of("$..complete"); + jsonType = redis.jsonType(BIKES_INVENTORY, myPath).get(0); + assertThat(jsonType).isEqualTo(JsonType.BOOLEAN); + + myPath = JsonPath.of("$..inventory.owner"); + jsonType = redis.jsonType(BIKES_INVENTORY, myPath).get(0); + assertThat(jsonType).isEqualTo(JsonType.UNKNOWN); + } + + @Test + void jsonGetToObject() { + JsonPath myPath = JsonPath.of("$..mountain_bikes[1]"); + JsonValue value = redis.jsonGet(BIKES_INVENTORY, myPath).get(0); + assertThat(value).isNotNull(); + assertThat(value.isNull()).isFalse(); + assertThat(value.asJsonArray().get(0).isJsonObject()).isTrue(); + + MountainBike bike = value.asJsonArray().get(0).asJsonObject().toObject(MountainBike.class); + + assertThat(bike).isNotNull(); + assertThat(bike).isInstanceOf(MountainBike.class); + + assertThat(bike.id).isEqualTo("bike:2"); + assertThat(bike.model).isEqualTo("Quaoar"); + assertThat(bike.description).contains("Redesigned for the 2020 model year, this bike impressed"); + } + + static class MountainBike { + + public String id; + + public String model; + + public String description; + + public String price; + + public Specs specs; + + public List colors; + + } + + static class Specs { + + public String material; + + public String weight; + + } + + @Test + void jsonSetFromObject() { + JsonPath myPath = JsonPath.of("$..mountain_bikes[1]"); + JsonValue value = redis.jsonGet(BIKES_INVENTORY, myPath).get(0); + JsonParser parser = redis.getJsonParser(); + + MountainBike desertFox = new MountainBike(); + desertFox.specs = new Specs(); + desertFox.id = "bike:43"; + desertFox.model = "DesertFox"; + desertFox.description = "The DesertFox is a versatile bike for all terrains"; + desertFox.price = "1299"; + desertFox.specs.material = "composite"; + desertFox.specs.weight = "11"; + desertFox.colors = Arrays.asList("yellow", "orange"); + + JsonValue newValue = parser.fromObject(desertFox); + + assertThat(newValue).isNotNull(); + assertThat(newValue.isNull()).isFalse(); + assertThat(newValue.isJsonObject()).isTrue(); + assertThat(newValue.asJsonObject().size()).isEqualTo(6); + assertThat(newValue.asJsonObject().get("id").toString()).isEqualTo("\"bike:43\""); + assertThat(newValue.asJsonObject().get("model").toString()).isEqualTo("\"DesertFox\""); + assertThat(newValue.asJsonObject().get("description").toString()) + .isEqualTo("\"The DesertFox is a versatile bike for all terrains\""); + assertThat(newValue.asJsonObject().get("price").toString()).isEqualTo("\"1299\""); + assertThat(newValue.asJsonObject().get("specs").toString()).isEqualTo("{\"material\":\"composite\",\"weight\":\"11\"}"); + assertThat(newValue.asJsonObject().get("colors").toString()).isEqualTo("[\"yellow\",\"orange\"]"); + + String result = redis.jsonSet(BIKES_INVENTORY, myPath, newValue); + + assertThat(result).isEqualTo("OK"); + } + + @Test + void byteArrayCodec() throws ExecutionException, InterruptedException { + JsonPath myPath = JsonPath.of("$..mountain_bikes"); + byte[] myMountainBikesKey = BIKES_INVENTORY.getBytes(); + byte[] myServiceBikesKey = "service_bikes".getBytes(); + + RedisAsyncCommands redis = client.connect(ByteArrayCodec.INSTANCE).async(); + RedisFuture> bikes = redis.jsonGet(myMountainBikesKey, myPath); + + CompletionStage> stage = bikes + .thenApply(fetchedBikes -> redis.jsonSet(myServiceBikesKey, JsonPath.ROOT_PATH, fetchedBikes.get(0))); + + String result = stage.toCompletableFuture().get().get(); + + assertThat(result).isEqualTo("OK"); + } + + @Test + void withCustomParser() { + RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build(); + + try (RedisClient client = RedisClient.create(redisURI)) { + client.setOptions(ClientOptions.builder().jsonParser(new CustomParser()).build()); + StatefulRedisConnection connection = client.connect(StringCodec.UTF8); + RedisCommands redis = connection.sync(); + assertThat(redis.getJsonParser()).isInstanceOf(CustomParser.class); + } + } + + static class CustomParser implements JsonParser { + + @Override + public JsonValue loadJsonValue(ByteBuffer bytes) { + return null; + } + + @Override + public JsonValue createJsonValue(ByteBuffer bytes) { + return null; + } + + @Override + public JsonValue createJsonValue(String value) { + return null; + } + + @Override + public JsonObject createJsonObject() { + return null; + } + + @Override + public JsonArray createJsonArray() { + return null; + } + + @Override + public JsonValue fromObject(Object object) { + return null; + } + + } + +} diff --git a/src/test/java/io/lettuce/core/json/UnproccessedJsonValueUnitTests.java b/src/test/java/io/lettuce/core/json/UnproccessedJsonValueUnitTests.java new file mode 100644 index 0000000000..369bae1689 --- /dev/null +++ b/src/test/java/io/lettuce/core/json/UnproccessedJsonValueUnitTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + * + * This file contains contributions from third-party contributors + * licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.lettuce.core.json; + +import io.lettuce.core.codec.StringCodec; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * Unit tests for {@link UnproccessedJsonValue}. + */ +class UnproccessedJsonValueUnitTests { + + @Test + void asString() { + final String unprocessed = "{\"a\":1,\"b\":2}"; + final String modified = "{\"a\":1}"; + + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + ByteBuffer buffer = ByteBuffer.wrap(unprocessed.getBytes()); + UnproccessedJsonValue underTest = new UnproccessedJsonValue(buffer, parser); + + String value = StringCodec.UTF8.decodeValue(buffer); + assertThat(underTest.toString()).isEqualTo(value); + assertThat(underTest.asByteBuffer()).isEqualTo(ByteBuffer.wrap(unprocessed.getBytes())); + + assertThat(underTest.isJsonObject()).isTrue(); + assertThat(underTest.asJsonObject().remove("b")).isNotNull(); + + assertThat(underTest.toString()).isEqualTo(modified); + assertThat(underTest.asByteBuffer()).isEqualTo(ByteBuffer.wrap(modified.getBytes())); + } + + @Test + void asTextual() { + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + ByteBuffer buffer = ByteBuffer.wrap("\"textual\"".getBytes()); + UnproccessedJsonValue underTest = new UnproccessedJsonValue(buffer, parser); + + assertThat(underTest.isString()).isTrue(); + assertThat(underTest.asString()).isEqualTo("textual"); + + Assertions.assertThat(underTest.isBoolean()).isFalse(); + Assertions.assertThat(underTest.isNull()).isFalse(); + Assertions.assertThat(underTest.isNumber()).isFalse(); + Assertions.assertThat(underTest.isJsonObject()).isFalse(); + Assertions.assertThat(underTest.isJsonArray()).isFalse(); + } + + @Test + void asNull() { + + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + ByteBuffer buffer = ByteBuffer.wrap("null".getBytes()); + UnproccessedJsonValue underTest = new UnproccessedJsonValue(buffer, parser); + + assertThat(underTest.isNull()).isTrue(); + + Assertions.assertThat(underTest.isBoolean()).isFalse(); + Assertions.assertThat(underTest.isString()).isFalse(); + Assertions.assertThat(underTest.isNumber()).isFalse(); + Assertions.assertThat(underTest.isJsonObject()).isFalse(); + Assertions.assertThat(underTest.isJsonArray()).isFalse(); + } + + @Test + void asNumber() { + + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + ByteBuffer buffer = ByteBuffer.wrap("1".getBytes()); + UnproccessedJsonValue underTest = new UnproccessedJsonValue(buffer, parser); + + assertThat(underTest.isNumber()).isTrue(); + assertThat(underTest.asNumber()).isEqualTo(1); + + Assertions.assertThat(underTest.isBoolean()).isFalse(); + Assertions.assertThat(underTest.isString()).isFalse(); + Assertions.assertThat(underTest.isNull()).isFalse(); + Assertions.assertThat(underTest.isJsonObject()).isFalse(); + Assertions.assertThat(underTest.isJsonArray()).isFalse(); + } + + @Test + void asBoolean() { + + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + ByteBuffer buffer = ByteBuffer.wrap("true".getBytes()); + UnproccessedJsonValue underTest = new UnproccessedJsonValue(buffer, parser); + + assertThat(underTest.isBoolean()).isTrue(); + assertThat(underTest.asBoolean()).isEqualTo(true); + + Assertions.assertThat(underTest.isNumber()).isFalse(); + Assertions.assertThat(underTest.isString()).isFalse(); + Assertions.assertThat(underTest.isNull()).isFalse(); + Assertions.assertThat(underTest.isJsonObject()).isFalse(); + Assertions.assertThat(underTest.isJsonArray()).isFalse(); + } + + @Test + void asArray() { + + DefaultJsonParser parser = DefaultJsonParser.INSTANCE; + ByteBuffer buffer = ByteBuffer.wrap("[1,2,3,4]".getBytes()); + UnproccessedJsonValue underTest = new UnproccessedJsonValue(buffer, parser); + + assertThat(underTest.isJsonArray()).isTrue(); + assertThat(underTest.asJsonArray().toString()).isEqualTo("[1,2,3,4]"); + + Assertions.assertThat(underTest.isNumber()).isFalse(); + Assertions.assertThat(underTest.isString()).isFalse(); + Assertions.assertThat(underTest.isNull()).isFalse(); + Assertions.assertThat(underTest.isJsonObject()).isFalse(); + Assertions.assertThat(underTest.isBoolean()).isFalse(); + } + +} diff --git a/src/test/java/io/lettuce/core/metrics/CommandLatencyIdUnitTests.java b/src/test/java/io/lettuce/core/metrics/CommandLatencyIdUnitTests.java index f6f32247b3..d2556570ef 100644 --- a/src/test/java/io/lettuce/core/metrics/CommandLatencyIdUnitTests.java +++ b/src/test/java/io/lettuce/core/metrics/CommandLatencyIdUnitTests.java @@ -54,7 +54,7 @@ public byte[] getBytes() { } @Override - public String name() { + public String toString() { return name; } diff --git a/src/test/java/io/lettuce/core/output/JsonTypeListOutputUnitTests.java b/src/test/java/io/lettuce/core/output/JsonTypeListOutputUnitTests.java new file mode 100644 index 0000000000..99607040b6 --- /dev/null +++ b/src/test/java/io/lettuce/core/output/JsonTypeListOutputUnitTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.output; + +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.json.DefaultJsonParser; +import io.lettuce.core.json.JsonType; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link JsonTypeListOutput}. + */ +class JsonTypeListOutputUnitTests { + + @Test + void set() { + JsonTypeListOutput sut = new JsonTypeListOutput<>(StringCodec.UTF8); + sut.multi(7); + sut.set(ByteBuffer.wrap("object".getBytes())); + sut.set(ByteBuffer.wrap("array".getBytes())); + sut.set(ByteBuffer.wrap("string".getBytes())); + sut.set(ByteBuffer.wrap("integer".getBytes())); + sut.set(ByteBuffer.wrap("number".getBytes())); + sut.set(ByteBuffer.wrap("boolean".getBytes())); + sut.set(ByteBuffer.wrap("null".getBytes())); + + assertThat(sut.get().isEmpty()).isFalse(); + assertThat(sut.get().size()).isEqualTo(7); + assertThat(sut.get().get(0)).isEqualTo(JsonType.OBJECT); + assertThat(sut.get().get(1)).isEqualTo(JsonType.ARRAY); + assertThat(sut.get().get(2)).isEqualTo(JsonType.STRING); + assertThat(sut.get().get(3)).isEqualTo(JsonType.INTEGER); + assertThat(sut.get().get(4)).isEqualTo(JsonType.NUMBER); + assertThat(sut.get().get(5)).isEqualTo(JsonType.BOOLEAN); + assertThat(sut.get().get(6)).isEqualTo(JsonType.UNKNOWN); + } + +} diff --git a/src/test/java/io/lettuce/core/output/JsonValueListOutputUnitTests.java b/src/test/java/io/lettuce/core/output/JsonValueListOutputUnitTests.java new file mode 100644 index 0000000000..0406133881 --- /dev/null +++ b/src/test/java/io/lettuce/core/output/JsonValueListOutputUnitTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.output; + +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.json.DefaultJsonParser; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link JsonValueListOutput}. + */ +class JsonValueListOutputUnitTests { + + @Test + void set() { + JsonValueListOutput sut = new JsonValueListOutput<>(StringCodec.UTF8, DefaultJsonParser.INSTANCE); + sut.multi(2); + sut.set(ByteBuffer.wrap("[1,2,3]".getBytes())); + sut.set(ByteBuffer.wrap("world".getBytes())); + + assertThat(sut.get().isEmpty()).isFalse(); + assertThat(sut.get().size()).isEqualTo(2); + assertThat(sut.get().get(0).toString()).isEqualTo("[1,2,3]"); + assertThat(sut.get().get(1).toString()).isEqualTo("world"); + } + +} diff --git a/src/test/java/io/lettuce/core/output/NumberListOutputUnitTests.java b/src/test/java/io/lettuce/core/output/NumberListOutputUnitTests.java new file mode 100644 index 0000000000..50d585f103 --- /dev/null +++ b/src/test/java/io/lettuce/core/output/NumberListOutputUnitTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.output; + +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.json.JsonType; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link NumberListOutput}. + */ +class NumberListOutputUnitTests { + + @Test + void set() { + NumberListOutput sut = new NumberListOutput<>(StringCodec.UTF8); + sut.multi(4); + sut.set(ByteBuffer.wrap((String.valueOf(Double.MAX_VALUE)).getBytes())); + sut.set(1.2); + sut.set(1L); + sut.setBigNumber(ByteBuffer.wrap(String.valueOf(Double.MAX_VALUE).getBytes())); + + assertThat(sut.get().isEmpty()).isFalse(); + assertThat(sut.get().size()).isEqualTo(4); + assertThat(sut.get().get(0)).isEqualTo(Double.MAX_VALUE); + assertThat(sut.get().get(1)).isEqualTo(1.2); + assertThat(sut.get().get(2)).isEqualTo(1L); + assertThat(sut.get().get(3)).isEqualTo(Double.MAX_VALUE); + } + + @Test + void setNegative() { + NumberListOutput sut = new NumberListOutput<>(StringCodec.UTF8); + sut.multi(1); + sut.set(ByteBuffer.wrap("Not a number".getBytes())); + + assertThat(sut.get().isEmpty()).isFalse(); + assertThat(sut.get().size()).isEqualTo(1); + assertThat(sut.get().get(0)).isEqualTo(0); + } + +} diff --git a/src/test/java/io/lettuce/core/tracing/BraveTracingUnitTests.java b/src/test/java/io/lettuce/core/tracing/BraveTracingUnitTests.java index 5940314e51..dcd9b4c5a9 100644 --- a/src/test/java/io/lettuce/core/tracing/BraveTracingUnitTests.java +++ b/src/test/java/io/lettuce/core/tracing/BraveTracingUnitTests.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.util.List; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; @@ -14,7 +13,6 @@ import io.lettuce.core.protocol.AsyncCommand; import zipkin2.Span; -import brave.Tag; import brave.Tracer; import brave.Tracing; import brave.handler.MutableSpan; @@ -102,7 +100,7 @@ void shouldCustomizeEndpoint() { void shouldCustomizeSpan() { BraveTracing tracing = BraveTracing.builder().tracing(clientTracing) - .spanCustomizer((command, span) -> span.tag("cmd", command.getType().name())).build(); + .spanCustomizer((command, span) -> span.tag("cmd", command.getType().toString())).build(); BraveTracing.BraveSpan span = (BraveTracing.BraveSpan) tracing.getTracerProvider().getTracer().nextSpan(); span.start(new AsyncCommand<>(new Command<>(CommandType.AUTH, null))); diff --git a/src/test/java/io/lettuce/test/CliParser.java b/src/test/java/io/lettuce/test/CliParser.java index 0beac0e3b4..e12d1586a6 100644 --- a/src/test/java/io/lettuce/test/CliParser.java +++ b/src/test/java/io/lettuce/test/CliParser.java @@ -54,11 +54,11 @@ public static Command> parse(String command) { @Override public byte[] getBytes() { - return name().getBytes(StandardCharsets.UTF_8); + return this.toString().getBytes(StandardCharsets.UTF_8); } @Override - public String name() { + public String toString() { return typeName; } diff --git a/src/test/jmh/io/lettuce/core/dynamic/RedisCommandFactoryBenchmark.java b/src/test/jmh/io/lettuce/core/dynamic/RedisCommandFactoryBenchmark.java index 9147df0d0c..04085e1d22 100644 --- a/src/test/jmh/io/lettuce/core/dynamic/RedisCommandFactoryBenchmark.java +++ b/src/test/jmh/io/lettuce/core/dynamic/RedisCommandFactoryBenchmark.java @@ -1,5 +1,6 @@ package io.lettuce.core.dynamic; +import io.lettuce.core.json.DefaultJsonParser; import org.mockito.Mockito; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; @@ -30,7 +31,7 @@ public void setup() { redisCommandFactory = new RedisCommandFactory(new MockStatefulConnection(EmptyRedisChannelWriter.INSTANCE)); regularCommands = redisCommandFactory.getCommands(RegularCommands.class); - asyncCommands = new RedisAsyncCommandsImpl<>(EmptyStatefulRedisConnection.INSTANCE, StringCodec.UTF8); + asyncCommands = new RedisAsyncCommandsImpl<>(EmptyStatefulRedisConnection.INSTANCE, StringCodec.UTF8, DefaultJsonParser.INSTANCE); } @Benchmark diff --git a/src/test/resources/bike-inventory.json b/src/test/resources/bike-inventory.json new file mode 100644 index 0000000000..a69a073ee3 --- /dev/null +++ b/src/test/resources/bike-inventory.json @@ -0,0 +1,46 @@ +{ + "inventory": { + "complete": false, + "owner": null, + "mountain_bikes": [ + { + "id": "bike:1", + "model": "Phoebe", + "description": "This is a mid-travel trail slayer that is a fantastic daily driver or one bike quiver. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there's room for mudguards and a rack too. This is the bike for the rider who wants trail manners with low fuss ownership.", + "price": 1928, + "specs": { + "material": "carbon", + "weight": 13.1 + }, + "colors": [ + "black", + "silver" + ] + }, + { + "id": "bike:2", + "model": "Quaoar", + "description": "Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. All in all it's an impressive package for the price, making it very competitive.", + "price": 2072, + "specs": { + "material": "aluminium", + "weight": 7.9 + }, + "colors": [ + "black", + "white" + ] + }, + { + "id": "bike:3", + "model": "Weywot", + "description": "This bike gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. If you're after a budget option, this is one of the best bikes you could get.", + "price": 3264, + "specs": { + "material": "alloy", + "weight": 13.8 + } + } + ] + } +} diff --git a/src/test/resources/docker/Dockerfile b/src/test/resources/docker/Dockerfile new file mode 100644 index 0000000000..fb85a2d529 --- /dev/null +++ b/src/test/resources/docker/Dockerfile @@ -0,0 +1,23 @@ +FROM redis/redis-stack:latest + +RUN mkdir -p /nodes/36379 /nodes/36380 /nodes/36381 /nodes/36382 /nodes/36383 /nodes/36384 + +COPY cluster-nodes/nodes-36379.conf /nodes/36379/nodes.conf +COPY cluster-nodes/nodes-36380.conf /nodes/36380/nodes.conf +COPY cluster-nodes/nodes-36381.conf /nodes/36381/nodes.conf +COPY cluster-nodes/nodes-36382.conf /nodes/36382/nodes.conf +COPY cluster-nodes/nodes-36383.conf /nodes/36383/nodes.conf +COPY cluster-nodes/nodes-36384.conf /nodes/36384/nodes.conf + +COPY cluster-nodes/redis-36379.conf /nodes/36379/redis.conf +COPY cluster-nodes/redis-36380.conf /nodes/36380/redis.conf +COPY cluster-nodes/redis-36381.conf /nodes/36381/redis.conf +COPY cluster-nodes/redis-36382.conf /nodes/36382/redis.conf +COPY cluster-nodes/redis-36383.conf /nodes/36383/redis.conf +COPY cluster-nodes/redis-36384.conf /nodes/36384/redis.conf + +COPY start_cluster.sh /start_cluster.sh + +RUN chmod a+x /start_cluster.sh + +ENTRYPOINT [ "/start_cluster.sh"] \ No newline at end of file diff --git a/src/test/resources/docker/cluster-nodes/nodes-36379.conf b/src/test/resources/docker/cluster-nodes/nodes-36379.conf new file mode 100644 index 0000000000..f895cdc702 --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/nodes-36379.conf @@ -0,0 +1,7 @@ +2ece6db60f8e5665c140c89720d3d259c024d179 127.0.0.1:36381@46381,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee master - 0 1724417170931 3 connected 10923-16383 +916a157a7772af418664b6b2c7feeede5e1851dd 127.0.0.1:36383@46383,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 slave 3c5ae3144eb30caa272796173d3d8b314d881b51 1724417171949 1724417168901 1 connected +024a891ec46aeded14511b7c5899460198306353 127.0.0.1:36384@46384,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 slave 72d77afa00d3572b7fcb0215181bb46b3e869c42 0 1724417170000 2 connected +72d77afa00d3572b7fcb0215181bb46b3e869c42 127.0.0.1:36380@46380,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 master - 0 1724417170000 2 connected 5461-10922 +a7ff9d3ed43a7ff58485c4e780f86c4202b8224f 127.0.0.1:36382@46382,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee slave 2ece6db60f8e5665c140c89720d3d259c024d179 0 1724417169000 3 connected +3c5ae3144eb30caa272796173d3d8b314d881b51 127.0.0.1:36379@46379,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 myself,master - 0 1724417168000 1 connected 0-5460 +vars currentEpoch 6 lastVoteEpoch 0 \ No newline at end of file diff --git a/src/test/resources/docker/cluster-nodes/nodes-36380.conf b/src/test/resources/docker/cluster-nodes/nodes-36380.conf new file mode 100644 index 0000000000..a39749dac8 --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/nodes-36380.conf @@ -0,0 +1,7 @@ +72d77afa00d3572b7fcb0215181bb46b3e869c42 127.0.0.1:36380@46380,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 myself,master - 0 1724417169000 2 connected 5461-10922 +3c5ae3144eb30caa272796173d3d8b314d881b51 127.0.0.1:36379@46379,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 master - 0 1724417169000 1 connected 0-5460 +024a891ec46aeded14511b7c5899460198306353 127.0.0.1:36384@46384,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 slave 72d77afa00d3572b7fcb0215181bb46b3e869c42 0 1724417167171 2 connected +2ece6db60f8e5665c140c89720d3d259c024d179 127.0.0.1:36381@46381,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee master - 0 1724417170930 3 connected 10923-16383 +a7ff9d3ed43a7ff58485c4e780f86c4202b8224f 127.0.0.1:36382@46382,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee slave 2ece6db60f8e5665c140c89720d3d259c024d179 0 1724417169917 3 connected +916a157a7772af418664b6b2c7feeede5e1851dd 127.0.0.1:36383@46383,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 slave 3c5ae3144eb30caa272796173d3d8b314d881b51 0 1724417168901 1 connected +vars currentEpoch 6 lastVoteEpoch 0 \ No newline at end of file diff --git a/src/test/resources/docker/cluster-nodes/nodes-36381.conf b/src/test/resources/docker/cluster-nodes/nodes-36381.conf new file mode 100644 index 0000000000..b69b042a7c --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/nodes-36381.conf @@ -0,0 +1,7 @@ +024a891ec46aeded14511b7c5899460198306353 127.0.0.1:36384@46384,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 slave 72d77afa00d3572b7fcb0215181bb46b3e869c42 0 1724417167171 2 connected +2ece6db60f8e5665c140c89720d3d259c024d179 127.0.0.1:36381@46381,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee myself,master - 0 1724417166000 3 connected 10923-16383 +3c5ae3144eb30caa272796173d3d8b314d881b51 127.0.0.1:36379@46379,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 master - 0 1724417168901 1 connected 0-5460 +72d77afa00d3572b7fcb0215181bb46b3e869c42 127.0.0.1:36380@46380,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 master - 0 1724417169916 2 connected 5461-10922 +a7ff9d3ed43a7ff58485c4e780f86c4202b8224f 127.0.0.1:36382@46382,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee slave 2ece6db60f8e5665c140c89720d3d259c024d179 0 1724417167000 3 connected +916a157a7772af418664b6b2c7feeede5e1851dd 127.0.0.1:36383@46383,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 slave 3c5ae3144eb30caa272796173d3d8b314d881b51 0 1724417168000 1 connected +vars currentEpoch 6 lastVoteEpoch 0 \ No newline at end of file diff --git a/src/test/resources/docker/cluster-nodes/nodes-36382.conf b/src/test/resources/docker/cluster-nodes/nodes-36382.conf new file mode 100644 index 0000000000..415345dbfe --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/nodes-36382.conf @@ -0,0 +1,7 @@ +024a891ec46aeded14511b7c5899460198306353 127.0.0.1:36384@46384,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 slave 72d77afa00d3572b7fcb0215181bb46b3e869c42 0 1724417171948 2 connected +2ece6db60f8e5665c140c89720d3d259c024d179 127.0.0.1:36381@46381,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee master - 0 1724417171000 3 connected 10923-16383 +a7ff9d3ed43a7ff58485c4e780f86c4202b8224f 127.0.0.1:36382@46382,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee myself,slave 2ece6db60f8e5665c140c89720d3d259c024d179 0 1724417169000 3 connected +3c5ae3144eb30caa272796173d3d8b314d881b51 127.0.0.1:36379@46379,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 master - 0 1724417170931 1 connected 0-5460 +72d77afa00d3572b7fcb0215181bb46b3e869c42 127.0.0.1:36380@46380,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 master - 1724417172965 1724417168902 2 connected 5461-10922 +916a157a7772af418664b6b2c7feeede5e1851dd 127.0.0.1:36383@46383,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 slave 3c5ae3144eb30caa272796173d3d8b314d881b51 0 1724417168000 1 connected +vars currentEpoch 6 lastVoteEpoch 0 \ No newline at end of file diff --git a/src/test/resources/docker/cluster-nodes/nodes-36383.conf b/src/test/resources/docker/cluster-nodes/nodes-36383.conf new file mode 100644 index 0000000000..0cbe1550b9 --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/nodes-36383.conf @@ -0,0 +1,7 @@ +72d77afa00d3572b7fcb0215181bb46b3e869c42 127.0.0.1:36380@46380,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 master - 0 1724417170000 2 connected 5461-10922 +3c5ae3144eb30caa272796173d3d8b314d881b51 127.0.0.1:36379@46379,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 master - 0 1724417171000 1 connected 0-5460 +024a891ec46aeded14511b7c5899460198306353 127.0.0.1:36384@46384,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 slave 72d77afa00d3572b7fcb0215181bb46b3e869c42 0 1724417171000 2 connected +2ece6db60f8e5665c140c89720d3d259c024d179 127.0.0.1:36381@46381,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee master - 0 1724417171949 3 connected 10923-16383 +916a157a7772af418664b6b2c7feeede5e1851dd 127.0.0.1:36383@46383,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 myself,slave 3c5ae3144eb30caa272796173d3d8b314d881b51 0 1724417168000 1 connected +a7ff9d3ed43a7ff58485c4e780f86c4202b8224f 127.0.0.1:36382@46382,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee slave 2ece6db60f8e5665c140c89720d3d259c024d179 0 1724417172966 3 connected +vars currentEpoch 6 lastVoteEpoch 0 \ No newline at end of file diff --git a/src/test/resources/docker/cluster-nodes/nodes-36384.conf b/src/test/resources/docker/cluster-nodes/nodes-36384.conf new file mode 100644 index 0000000000..313b0c30eb --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/nodes-36384.conf @@ -0,0 +1,7 @@ +3c5ae3144eb30caa272796173d3d8b314d881b51 127.0.0.1:36379@46379,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 master - 0 1724417169000 1 connected 0-5460 +916a157a7772af418664b6b2c7feeede5e1851dd 127.0.0.1:36383@46383,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 slave 3c5ae3144eb30caa272796173d3d8b314d881b51 0 1724417166981 1 connected +2ece6db60f8e5665c140c89720d3d259c024d179 127.0.0.1:36381@46381,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee master - 0 1724417168903 3 connected 10923-16383 +024a891ec46aeded14511b7c5899460198306353 127.0.0.1:36384@46384,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 myself,slave 72d77afa00d3572b7fcb0215181bb46b3e869c42 0 1724417167000 2 connected +a7ff9d3ed43a7ff58485c4e780f86c4202b8224f 127.0.0.1:36382@46382,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee slave 2ece6db60f8e5665c140c89720d3d259c024d179 0 1724417169917 3 connected +72d77afa00d3572b7fcb0215181bb46b3e869c42 127.0.0.1:36380@46380,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 master - 1724417170930 1724417167000 2 connected 5461-10922 +vars currentEpoch 6 lastVoteEpoch 0 \ No newline at end of file diff --git a/src/test/resources/docker/cluster-nodes/redis-36379.conf b/src/test/resources/docker/cluster-nodes/redis-36379.conf new file mode 100644 index 0000000000..710b18d611 --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/redis-36379.conf @@ -0,0 +1,13 @@ +dir /nodes/36379 +port 36379 +logfile /redis.log +daemonize yes +protected-mode no +cluster-enabled yes +enable-debug-command yes +loadmodule /opt/redis-stack/lib/redisearch.so +# loadmodule /opt/redis-stack/lib/redisgraph.so +loadmodule /opt/redis-stack/lib/redistimeseries.so +loadmodule /opt/redis-stack/lib/rejson.so +loadmodule /opt/redis-stack/lib/redisbloom.so +loadmodule /opt/redis-stack/lib/redisgears.so v8-plugin-path /opt/redis-stack/lib/libredisgears_v8_plugin.so diff --git a/src/test/resources/docker/cluster-nodes/redis-36380.conf b/src/test/resources/docker/cluster-nodes/redis-36380.conf new file mode 100644 index 0000000000..5aab7dfa55 --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/redis-36380.conf @@ -0,0 +1,13 @@ +dir /nodes/36380 +port 36380 +logfile /redis.log +daemonize yes +protected-mode no +cluster-enabled yes +enable-debug-command yes +loadmodule /opt/redis-stack/lib/redisearch.so +# loadmodule /opt/redis-stack/lib/redisgraph.so +loadmodule /opt/redis-stack/lib/redistimeseries.so +loadmodule /opt/redis-stack/lib/rejson.so +loadmodule /opt/redis-stack/lib/redisbloom.so +loadmodule /opt/redis-stack/lib/redisgears.so v8-plugin-path /opt/redis-stack/lib/libredisgears_v8_plugin.so diff --git a/src/test/resources/docker/cluster-nodes/redis-36381.conf b/src/test/resources/docker/cluster-nodes/redis-36381.conf new file mode 100644 index 0000000000..91210574d4 --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/redis-36381.conf @@ -0,0 +1,13 @@ +dir /nodes/36381 +port 36381 +logfile /redis.log +daemonize yes +protected-mode no +cluster-enabled yes +enable-debug-command yes +loadmodule /opt/redis-stack/lib/redisearch.so +# loadmodule /opt/redis-stack/lib/redisgraph.so +loadmodule /opt/redis-stack/lib/redistimeseries.so +loadmodule /opt/redis-stack/lib/rejson.so +loadmodule /opt/redis-stack/lib/redisbloom.so +loadmodule /opt/redis-stack/lib/redisgears.so v8-plugin-path /opt/redis-stack/lib/libredisgears_v8_plugin.so diff --git a/src/test/resources/docker/cluster-nodes/redis-36382.conf b/src/test/resources/docker/cluster-nodes/redis-36382.conf new file mode 100644 index 0000000000..fcf2c7ecd7 --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/redis-36382.conf @@ -0,0 +1,13 @@ +dir /nodes/36382 +port 36382 +logfile /redis.log +daemonize yes +protected-mode no +cluster-enabled yes +enable-debug-command yes +loadmodule /opt/redis-stack/lib/redisearch.so +# loadmodule /opt/redis-stack/lib/redisgraph.so +loadmodule /opt/redis-stack/lib/redistimeseries.so +loadmodule /opt/redis-stack/lib/rejson.so +loadmodule /opt/redis-stack/lib/redisbloom.so +loadmodule /opt/redis-stack/lib/redisgears.so v8-plugin-path /opt/redis-stack/lib/libredisgears_v8_plugin.so diff --git a/src/test/resources/docker/cluster-nodes/redis-36383.conf b/src/test/resources/docker/cluster-nodes/redis-36383.conf new file mode 100644 index 0000000000..a3d4772714 --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/redis-36383.conf @@ -0,0 +1,13 @@ +dir /nodes/36383 +port 36383 +logfile /redis.log +daemonize yes +protected-mode no +cluster-enabled yes +enable-debug-command yes +loadmodule /opt/redis-stack/lib/redisearch.so +# loadmodule /opt/redis-stack/lib/redisgraph.so +loadmodule /opt/redis-stack/lib/redistimeseries.so +loadmodule /opt/redis-stack/lib/rejson.so +loadmodule /opt/redis-stack/lib/redisbloom.so +loadmodule /opt/redis-stack/lib/redisgears.so v8-plugin-path /opt/redis-stack/lib/libredisgears_v8_plugin.so diff --git a/src/test/resources/docker/cluster-nodes/redis-36384.conf b/src/test/resources/docker/cluster-nodes/redis-36384.conf new file mode 100644 index 0000000000..70c5428e4d --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/redis-36384.conf @@ -0,0 +1,13 @@ +dir /nodes/36384 +port 36384 +logfile /redis.log +daemonize yes +protected-mode no +cluster-enabled yes +enable-debug-command yes +loadmodule /opt/redis-stack/lib/redisearch.so +# loadmodule /opt/redis-stack/lib/redisgraph.so +loadmodule /opt/redis-stack/lib/redistimeseries.so +loadmodule /opt/redis-stack/lib/rejson.so +loadmodule /opt/redis-stack/lib/redisbloom.so +loadmodule /opt/redis-stack/lib/redisgears.so v8-plugin-path /opt/redis-stack/lib/libredisgears_v8_plugin.so diff --git a/src/test/resources/docker/docker-compose.yml b/src/test/resources/docker/docker-compose.yml new file mode 100644 index 0000000000..aede734651 --- /dev/null +++ b/src/test/resources/docker/docker-compose.yml @@ -0,0 +1,17 @@ +--- +services: + + standalone-stack: + image: redis/redis-stack:latest + ports: + - "16379:6379" + + clustered-stack: + image: tihomirmateev339/cae-infra:latest + ports: + - "36379:36379" + - "36380:36380" + - "36381:36381" + - "36382:36382" + - "36383:36383" + - "36384:36384" \ No newline at end of file diff --git a/src/test/resources/docker/start_cluster.sh b/src/test/resources/docker/start_cluster.sh new file mode 100644 index 0000000000..85e3ac94ec --- /dev/null +++ b/src/test/resources/docker/start_cluster.sh @@ -0,0 +1,31 @@ +#! /bin/bash + +# +# Copyright 2024, Redis Ltd. and Contributors +# All rights reserved. +# +# Licensed under the MIT License. +# + +if [ -z ${START_PORT} ]; then + START_PORT=36379 +fi +if [ -z ${END_PORT} ]; then + END_PORT=36384 +fi +if [ ! -z "$3" ]; then + START_PORT=$2 + START_PORT=$3 +fi + +for PORT in `seq ${START_PORT} ${END_PORT}`; do + echo ">>> Starting Redis server at port ${PORT}" + /opt/redis-stack/bin/redis-server /nodes/$PORT/redis.conf > /nodes/$PORT/console.log + if [ $? -ne 0 ]; then + echo "Redis failed to start, exiting." + continue + fi + echo 127.0.0.1:$PORT >> /nodes/nodemap +done + +tail -f /redis.log \ No newline at end of file