Skip to content

Commit

Permalink
Circle back to using domain objects
Browse files Browse the repository at this point in the history
  • Loading branch information
tishun committed Aug 7, 2024
1 parent 3bd50c1 commit ece67c0
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
* command.
*
* @author Tihomir Mateev
* @since 7.0
* @since 6.5
*/
public class TrackingInfo {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* limitations under the License.
*/

package io.lettuce.core.output.data;
package io.lettuce.core.output;

import java.util.ArrayList;
import java.util.Collections;
Expand All @@ -41,9 +41,9 @@
*
* @see DynamicAggregateData
* @author Tihomir Mateev
* @since 7.0
* @since 6.5
*/
public class ArrayAggregateData extends DynamicAggregateData {
class ArrayAggregateData extends DynamicAggregateData {

private final List<Object> data;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* limitations under the License.
*/

package io.lettuce.core.output.data;
package io.lettuce.core.output;

import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -57,7 +57,7 @@
* @see SetAggregateData
* @see MapAggregateData
* @author Tihomir Mateev
* @since 7.0
* @since 6.5
*/
public abstract class DynamicAggregateData {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,27 @@
* limitations under the License.
*/

package io.lettuce.core.output;

/**
* Model and parser for the {@code CLIENT TRACKINGINFO} output.
* Any usage of the {@link DynamicAggregateOutput} comes hand in hand with a respective {@link DynamicAggregateDataParser} that
* is able to parse the data extracted from the server to a meaningful Java object.
*
* @param <T> the type of the parsed object
* @author Tihomir Mateev
* @see DynamicAggregateData
* @see DynamicAggregateOutput
* @since 6.5
*/
package io.lettuce.core.api.parsers.tracking;
public interface DynamicAggregateDataParser<T> {

/**
* Parse the data extracted from the server to a specific domain object.
*
* @param data the data to parse
* @return the parsed object
* @since 6.5
*/
T parse(DynamicAggregateData data);

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,118 +23,115 @@
import io.lettuce.core.codec.RedisCodec;
import io.lettuce.core.codec.StringCodec;
import io.lettuce.core.internal.LettuceFactories;
import io.lettuce.core.output.data.DynamicAggregateData;
import io.lettuce.core.output.data.ArrayAggregateData;
import io.lettuce.core.output.data.MapAggregateData;
import io.lettuce.core.output.data.SetAggregateData;

import java.nio.ByteBuffer;
import java.util.Deque;

/**
* An implementation of the {@link CommandOutput} that heuristically attempts to parse any response the Redis server could
* provide, leaving out the user to provide the knowledge of how this data is to be processed
* An implementation of the {@link CommandOutput} that is used in combination with a given {@link DynamicAggregateDataParser} to
* produce a domain object from the data extracted from the server. Since there already are implementations of the
* {@link CommandOutput} interface for most simple types, this implementation is better suited to parse complex, often nested,
* data structures, for example a map containing other maps, arrays or sets as values for one or more of its keys.
* <p>
* The Redis server could return, besides the simple types such as boolean, long, String, etc. also aggregations of the former,
* inside aggregate data structures such as arrays, maps and sets, defined in the specification for both the
* <a href="https://github.com/redis/redis-specifications/blob/master/protocol/RESP2.md">RESP2</a> and
* <a href="https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md">RESP3</a> protocol.
* <p>
* Commands typically result in simple types, however some of the commands could return complex nested structures. A simple
* solution to parse such a structure is to have a dynamic data type and leave the user to parse the result to a domain object.
* If there is some breach of contract then the code consuming the driver could simply stop using the provided parser and start
* parsing the new dynamic data itself, without having to change the version of the library. This allows a certain degree of
* stability against change. Consult the members of the {@link io.lettuce.core.api.parsers} package for details on how aggregate
* Objects could be parsed.
* <p>
* The {@link DynamicAggregateOutput} is supposed to be compatible with all Redis commands.
* The implementation of the {@link DynamicAggregateDataParser} is responsible for mapping the data from the result to
* meaningful properties that the user of the LEttuce driver could then use in a statically typed manner.
*
* @see DynamicAggregateData
* @see DynamicAggregateDataParser
* @author Tihomir Mateev
* @since 7.0
* @since 6.5
*/
public class DynamicAggregateOutput<K, V> extends CommandOutput<K, V, DynamicAggregateData> {
public class DynamicAggregateOutput<K, V, T> extends CommandOutput<K, V, T> {

private final Deque<DynamicAggregateData> dataStack;

private final DynamicAggregateDataParser<T> parser;

private DynamicAggregateData data;

/**
* Constructs a new instance of the {@link DynamicAggregateOutput}
*
* @param codec the {@link RedisCodec} to be applied
*/
public DynamicAggregateOutput(RedisCodec<K, V> codec) {
public DynamicAggregateOutput(RedisCodec<K, V> codec, DynamicAggregateDataParser<T> parser) {
super(codec, null);
dataStack = LettuceFactories.newSpScQueue();
this.parser = parser;
}

@Override
public T get() {
return parser.parse(data);
}

@Override
public void set(long integer) {
if (output == null) {
if (data == null) {
throw new RuntimeException("Invalid output received for dynamic aggregate output."
+ "Integer value should have been preceded by some sort of aggregation.");
}

output.store(integer);
data.store(integer);
}

@Override
public void set(double number) {
if (output == null) {
if (data == null) {
throw new RuntimeException("Invalid output received for dynamic aggregate output."
+ "Double value should have been preceded by some sort of aggregation.");
}

output.store(number);
data.store(number);
}

@Override
public void set(boolean value) {
if (output == null) {
if (data == null) {
throw new RuntimeException("Invalid output received for dynamic aggregate output."
+ "Double value should have been preceded by some sort of aggregation.");
}

output.store(value);
data.store(value);
}

@Override
public void set(ByteBuffer bytes) {
if (output == null) {
if (data == null) {
throw new RuntimeException("Invalid output received for dynamic aggregate output."
+ "ByteBuffer value should have been preceded by some sort of aggregation.");
}

output.storeObject(bytes == null ? null : codec.decodeValue(bytes));
data.storeObject(bytes == null ? null : codec.decodeValue(bytes));
}

@Override
public void setSingle(ByteBuffer bytes) {
if (output == null) {
if (data == null) {
throw new RuntimeException("Invalid output received for dynamic aggregate output."
+ "String value should have been preceded by some sort of aggregation.");
}

output.store(bytes == null ? null : StringCodec.UTF8.decodeValue(bytes));
data.store(bytes == null ? null : StringCodec.UTF8.decodeValue(bytes));
}

@Override
public void complete(int depth) {
if (!dataStack.isEmpty() && depth == dataStack.size()) {
output = dataStack.pop();
data = dataStack.pop();
}
}

private void multi(DynamicAggregateData data) {
private void multi(DynamicAggregateData newData) {
// if there is no data set, then we are at the root object
if (output == null) {
output = data;
if (data == null) {
data = newData;
return;
}

// otherwise we need to nest the provided structure
output.storeObject(data);
dataStack.push(output);
output = data;
data.storeObject(newData);
dataStack.push(data);
data = newData;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* limitations under the License.
*/

package io.lettuce.core.output.data;
package io.lettuce.core.output;

import java.util.Collections;
import java.util.HashMap;
Expand All @@ -31,9 +31,9 @@
*
* @see DynamicAggregateData
* @author Tihomir Mateev
* @since 7.0
* @since 6.5
*/
public class MapAggregateData extends DynamicAggregateData {
class MapAggregateData extends DynamicAggregateData {

private final Map<Object, Object> data;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* limitations under the License.
*/

package io.lettuce.core.output.data;
package io.lettuce.core.output;

import java.util.ArrayList;
import java.util.Collections;
Expand All @@ -33,7 +33,7 @@
*
* @see DynamicAggregateData
* @author Tihomir Mateev
* @since 7.0
* @since 6.5
*/
public class SetAggregateData extends DynamicAggregateData {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
* limitations under the License.
*/

package io.lettuce.core.api.parsers.tracking;
package io.lettuce.core.output;

import io.lettuce.core.output.data.DynamicAggregateData;
import io.lettuce.core.output.DynamicAggregateDataParser;
import io.lettuce.core.output.DynamicAggregateData;
import io.lettuce.core.protocol.CommandKeyword;

import java.util.ArrayList;
Expand All @@ -35,7 +36,9 @@
* @author Tihomir Mateev
* @since 7.0
*/
public class TrackingInfoParser {
public class TrackingInfoParser implements DynamicAggregateDataParser<TrackingInfo> {

public static final TrackingInfoParser INSTANCE = new TrackingInfoParser();

/**
* Utility constructor.
Expand All @@ -46,11 +49,11 @@ private TrackingInfoParser() {
/**
* Parse the output of the Redis CLIENT TRACKINGINFO command and convert it to a {@link TrackingInfo}
*
* @param trackinginfoOutput output of CLIENT TRACKINGINFO command
* @param dynamicData output of CLIENT TRACKINGINFO command
* @return an {@link TrackingInfo} instance
*/
public static TrackingInfo parse(DynamicAggregateData trackinginfoOutput) {
Map<Object, Object> data = verifyStructure(trackinginfoOutput);
public TrackingInfo parse(DynamicAggregateData dynamicData) {
Map<Object, Object> data = verifyStructure(dynamicData);
Set<?> flags = ((DynamicAggregateData) data.get(CommandKeyword.FLAGS.toString().toLowerCase())).getDynamicSet();
Long clientId = (Long) data.get(CommandKeyword.REDIRECT.toString().toLowerCase());
List<?> prefixes = ((DynamicAggregateData) data.get(CommandKeyword.PREFIXES.toString().toLowerCase())).getDynamicList();
Expand All @@ -70,7 +73,7 @@ public static TrackingInfo parse(DynamicAggregateData trackinginfoOutput) {
return new TrackingInfo(parsedFlags, clientId, parsedPrefixes);
}

private static Map<Object, Object> verifyStructure(DynamicAggregateData trackinginfoOutput) {
private Map<Object, Object> verifyStructure(DynamicAggregateData trackinginfoOutput) {

if (trackinginfoOutput == null) {
throw new IllegalArgumentException("Failed while parsing CLIENT TRACKINGINFO: trackinginfoOutput must not be null");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package io.lettuce.core.cluster.models.tracking;
package io.lettuce.core.output;

import io.lettuce.core.api.parsers.tracking.TrackingInfo;
import io.lettuce.core.api.parsers.tracking.TrackingInfoParser;
import io.lettuce.core.output.data.ArrayAggregateData;
import io.lettuce.core.output.data.DynamicAggregateData;
import io.lettuce.core.output.data.MapAggregateData;
import io.lettuce.core.output.data.SetAggregateData;
import io.lettuce.core.output.ArrayAggregateData;
import io.lettuce.core.output.DynamicAggregateData;
import io.lettuce.core.output.MapAggregateData;
import io.lettuce.core.output.SetAggregateData;
import io.lettuce.core.protocol.CommandKeyword;
import org.junit.jupiter.api.Test;

Expand Down

0 comments on commit ece67c0

Please sign in to comment.