Skip to content

Commit 25d3e35

Browse files
committed
feat: proxy infrastructure for Lettuce command adapters
- LettuceResult: CompletionStage → Uni (lazy) and → blocking adapters - QuarkusRedisCodec: bridges Quarkus Codec to Lettuce RedisCodec - LettuceConverterRegistry: skeleton for arg/result converters Unit tests for each class, plus an integration test against real Redis.
1 parent 8f15dcb commit 25d3e35

7 files changed

Lines changed: 680 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package io.quarkus.redis.runtime.client.lettuce;
2+
3+
import java.util.Map;
4+
import java.util.concurrent.ConcurrentHashMap;
5+
import java.util.function.Function;
6+
7+
/**
8+
* Central registry for type converters between Quarkus Redis types and Lettuce types.
9+
* <p>
10+
* Converters are registered by source type and looked up at command execution time.
11+
* Each command story (Value Commands, Key Commands, etc.) registers its own converters
12+
* when loaded.
13+
* <p>
14+
* Converters are stateless functions: {@code Function<QuarkusType, LettuceType>}.
15+
*
16+
* <h3>Usage</h3>
17+
*
18+
* <pre>{@code
19+
* // Register (typically at startup)
20+
* LettuceConverterRegistry.registerArgConverter(
21+
* io.quarkus.redis.datasource.value.SetArgs.class,
22+
* quarkusSetArgs -> { ... return lettuceSetArgs; }
23+
* );
24+
*
25+
* // Lookup
26+
* Function<Object, Object> converter = LettuceConverterRegistry.getArgConverter(
27+
* io.quarkus.redis.datasource.value.SetArgs.class
28+
* );
29+
* }</pre>
30+
*/
31+
public final class LettuceConverterRegistry {
32+
33+
/**
34+
* Converters for command arguments: Quarkus arg type → Lettuce arg type.
35+
*/
36+
private static final Map<Class<?>, Function<Object, Object>> ARG_CONVERTERS = new ConcurrentHashMap<>();
37+
38+
/**
39+
* Converters for command results: Lettuce result type → Quarkus result type.
40+
*/
41+
private static final Map<Class<?>, Function<Object, Object>> RESULT_CONVERTERS = new ConcurrentHashMap<>();
42+
43+
private LettuceConverterRegistry() {
44+
// Utility class
45+
}
46+
47+
/**
48+
* Register a converter for a Quarkus command argument type.
49+
*
50+
* @param <S> the Quarkus source type
51+
* @param <T> the Lettuce target type
52+
* @param sourceType the Quarkus argument class
53+
* @param converter the conversion function
54+
*/
55+
@SuppressWarnings("unchecked")
56+
public static <S, T> void registerArgConverter(Class<S> sourceType, Function<S, T> converter) {
57+
ARG_CONVERTERS.put(sourceType, (Function<Object, Object>) converter);
58+
}
59+
60+
/**
61+
* Register a converter for a Lettuce result type.
62+
*
63+
* @param <S> the Lettuce source type
64+
* @param <T> the Quarkus target type
65+
* @param sourceType the Lettuce result class
66+
* @param converter the conversion function
67+
*/
68+
@SuppressWarnings("unchecked")
69+
public static <S, T> void registerResultConverter(Class<S> sourceType, Function<S, T> converter) {
70+
RESULT_CONVERTERS.put(sourceType, (Function<Object, Object>) converter);
71+
}
72+
73+
/**
74+
* Look up an argument converter by Quarkus source type.
75+
*
76+
* @param sourceType the Quarkus argument class
77+
* @return the converter, or {@code null} if none is registered
78+
*/
79+
public static Function<Object, Object> getArgConverter(Class<?> sourceType) {
80+
return ARG_CONVERTERS.get(sourceType);
81+
}
82+
83+
/**
84+
* Look up a result converter by Lettuce source type.
85+
*
86+
* @param sourceType the Lettuce result class
87+
* @return the converter, or {@code null} if none is registered
88+
*/
89+
public static Function<Object, Object> getResultConverter(Class<?> sourceType) {
90+
return RESULT_CONVERTERS.get(sourceType);
91+
}
92+
93+
/**
94+
* Convert a Quarkus argument to its Lettuce equivalent.
95+
*
96+
* @param <T> the expected Lettuce type
97+
* @param arg the Quarkus argument
98+
* @return the converted Lettuce argument
99+
* @throws IllegalArgumentException if no converter is registered for the type
100+
*/
101+
@SuppressWarnings("unchecked")
102+
public static <T> T convertArg(Object arg) {
103+
if (arg == null) {
104+
return null;
105+
}
106+
Function<Object, Object> converter = ARG_CONVERTERS.get(arg.getClass());
107+
if (converter == null) {
108+
throw new IllegalArgumentException(
109+
"No Lettuce converter registered for argument type: " + arg.getClass().getName());
110+
}
111+
return (T) converter.apply(arg);
112+
}
113+
114+
/**
115+
* Convert a Lettuce result to its Quarkus equivalent.
116+
*
117+
* @param <T> the expected Quarkus type
118+
* @param result the Lettuce result
119+
* @return the converted Quarkus result
120+
* @throws IllegalArgumentException if no converter is registered for the type
121+
*/
122+
@SuppressWarnings("unchecked")
123+
public static <T> T convertResult(Object result) {
124+
if (result == null) {
125+
return null;
126+
}
127+
Function<Object, Object> converter = RESULT_CONVERTERS.get(result.getClass());
128+
if (converter == null) {
129+
throw new IllegalArgumentException(
130+
"No Lettuce converter registered for result type: " + result.getClass().getName());
131+
}
132+
return (T) converter.apply(result);
133+
}
134+
135+
/**
136+
* Clear all registered converters. Intended for testing only.
137+
*/
138+
public static void clear() {
139+
ARG_CONVERTERS.clear();
140+
RESULT_CONVERTERS.clear();
141+
}
142+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.quarkus.redis.runtime.client.lettuce;
2+
3+
import java.time.Duration;
4+
import java.util.concurrent.CompletionStage;
5+
6+
import io.smallrye.mutiny.Uni;
7+
8+
/**
9+
* Utility for adapting Lettuce {@link CompletionStage} results to Mutiny {@link Uni}
10+
* and to blocking calls.
11+
* <p>
12+
* Lettuce async commands return {@link io.lettuce.core.RedisFuture} which extends
13+
* {@link CompletionStage}. This class provides the bridge to Quarkus APIs without
14+
* exposing any Reactor types.
15+
*/
16+
public final class LettuceResult {
17+
18+
private LettuceResult() {
19+
// Utility class
20+
}
21+
22+
/**
23+
* Converts a {@link CompletionStage} to a Mutiny {@link Uni}.
24+
* <p>
25+
* The returned {@code Uni} subscribes lazily — the {@code CompletionStage} supplier
26+
* is invoked only when a subscriber requests it.
27+
*
28+
* @param <T> the result type
29+
* @param supplier a supplier of the {@link CompletionStage} (typically a Lettuce async command call)
30+
* @return a {@link Uni} that completes with the result of the {@link CompletionStage}
31+
*/
32+
public static <T> Uni<T> toUni(java.util.function.Supplier<CompletionStage<T>> supplier) {
33+
return Uni.createFrom().completionStage(supplier);
34+
}
35+
36+
/**
37+
* Blocks on a {@link CompletionStage} and returns the result.
38+
* <p>
39+
* This must only be called from a worker thread, never from an event loop thread.
40+
*
41+
* @param <T> the result type
42+
* @param stage the {@link CompletionStage} to block on
43+
* @param timeout the maximum time to wait
44+
* @return the result
45+
* @throws java.util.concurrent.CompletionException if the computation threw an exception
46+
*/
47+
public static <T> T toBlocking(CompletionStage<T> stage, Duration timeout) {
48+
return Uni.createFrom().completionStage(stage)
49+
.await().atMost(timeout);
50+
}
51+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package io.quarkus.redis.runtime.client.lettuce;
2+
3+
import java.lang.reflect.Type;
4+
import java.nio.ByteBuffer;
5+
6+
import io.lettuce.core.codec.RedisCodec;
7+
import io.quarkus.redis.datasource.codecs.Codec;
8+
import io.quarkus.redis.datasource.codecs.Codecs;
9+
10+
/**
11+
* Adapts Quarkus {@link Codec} (byte[]-based) to Lettuce {@link RedisCodec} (ByteBuffer-based).
12+
* <p>
13+
* This allows Lettuce commands to use the same serialization logic as the existing Quarkus
14+
* Redis extension, including custom user-provided codecs registered via CDI.
15+
*
16+
* @param <K> the key type
17+
* @param <V> the value type
18+
*/
19+
public class QuarkusRedisCodec<K, V> implements RedisCodec<K, V> {
20+
21+
private final Codec keyCodec;
22+
private final Codec valueCodec;
23+
private final Type keyType;
24+
private final Type valueType;
25+
26+
public QuarkusRedisCodec(Type keyType, Type valueType) {
27+
this.keyType = keyType;
28+
this.valueType = valueType;
29+
this.keyCodec = Codecs.getDefaultCodecFor(keyType);
30+
this.valueCodec = Codecs.getDefaultCodecFor(valueType);
31+
}
32+
33+
@Override
34+
@SuppressWarnings("unchecked")
35+
public K decodeKey(ByteBuffer bytes) {
36+
if (bytes == null || !bytes.hasRemaining()) {
37+
return null;
38+
}
39+
return (K) keyCodec.decode(toBytes(bytes));
40+
}
41+
42+
@Override
43+
@SuppressWarnings("unchecked")
44+
public V decodeValue(ByteBuffer bytes) {
45+
if (bytes == null || !bytes.hasRemaining()) {
46+
return null;
47+
}
48+
return (V) valueCodec.decode(toBytes(bytes));
49+
}
50+
51+
@Override
52+
public ByteBuffer encodeKey(K key) {
53+
if (key == null) {
54+
return ByteBuffer.allocate(0);
55+
}
56+
return ByteBuffer.wrap(keyCodec.encode(key));
57+
}
58+
59+
@Override
60+
public ByteBuffer encodeValue(V value) {
61+
if (value == null) {
62+
return ByteBuffer.allocate(0);
63+
}
64+
return ByteBuffer.wrap(valueCodec.encode(value));
65+
}
66+
67+
/**
68+
* @return the key type this codec handles
69+
*/
70+
public Type keyType() {
71+
return keyType;
72+
}
73+
74+
/**
75+
* @return the value type this codec handles
76+
*/
77+
public Type valueType() {
78+
return valueType;
79+
}
80+
81+
private static byte[] toBytes(ByteBuffer buffer) {
82+
byte[] bytes = new byte[buffer.remaining()];
83+
buffer.get(bytes);
84+
return bytes;
85+
}
86+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package io.quarkus.redis.runtime.client.lettuce;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5+
6+
import java.util.function.Function;
7+
8+
import org.junit.jupiter.api.AfterEach;
9+
import org.junit.jupiter.api.Test;
10+
11+
/**
12+
* Unit tests for {@link LettuceConverterRegistry}.
13+
*/
14+
class LettuceConverterRegistryTest {
15+
16+
@AfterEach
17+
void cleanup() {
18+
LettuceConverterRegistry.clear();
19+
}
20+
21+
@Test
22+
void registerAndLookupArgConverter() {
23+
LettuceConverterRegistry.registerArgConverter(String.class, s -> s.toUpperCase());
24+
Function<Object, Object> converter = LettuceConverterRegistry.getArgConverter(String.class);
25+
assertThat(converter).isNotNull();
26+
assertThat(converter.apply("hello")).isEqualTo("HELLO");
27+
}
28+
29+
@Test
30+
void registerAndLookupResultConverter() {
31+
LettuceConverterRegistry.registerResultConverter(Integer.class, i -> i * 2);
32+
Function<Object, Object> converter = LettuceConverterRegistry.getResultConverter(Integer.class);
33+
assertThat(converter).isNotNull();
34+
assertThat(converter.apply(5)).isEqualTo(10);
35+
}
36+
37+
@Test
38+
void lookupReturnsNullForUnregistered() {
39+
assertThat(LettuceConverterRegistry.getArgConverter(Double.class)).isNull();
40+
assertThat(LettuceConverterRegistry.getResultConverter(Double.class)).isNull();
41+
}
42+
43+
@Test
44+
void convertArgShouldApplyConverter() {
45+
LettuceConverterRegistry.registerArgConverter(String.class, s -> s.length());
46+
int result = LettuceConverterRegistry.convertArg("hello");
47+
assertThat(result).isEqualTo(5);
48+
}
49+
50+
@Test
51+
void convertArgShouldReturnNullForNullInput() {
52+
String result = LettuceConverterRegistry.convertArg(null);
53+
assertThat(result).isNull();
54+
}
55+
56+
@Test
57+
void convertArgShouldThrowForUnregistered() {
58+
assertThatThrownBy(() -> LettuceConverterRegistry.convertArg("no-converter"))
59+
.isInstanceOf(IllegalArgumentException.class)
60+
.hasMessageContaining("No Lettuce converter registered for argument type");
61+
}
62+
63+
@Test
64+
void convertResultShouldApplyConverter() {
65+
LettuceConverterRegistry.registerResultConverter(Long.class, l -> l.toString());
66+
String result = LettuceConverterRegistry.convertResult(42L);
67+
assertThat(result).isEqualTo("42");
68+
}
69+
70+
@Test
71+
void convertResultShouldThrowForUnregistered() {
72+
assertThatThrownBy(() -> LettuceConverterRegistry.convertResult(3.14))
73+
.isInstanceOf(IllegalArgumentException.class)
74+
.hasMessageContaining("No Lettuce converter registered for result type");
75+
}
76+
77+
@Test
78+
void clearShouldRemoveAllConverters() {
79+
LettuceConverterRegistry.registerArgConverter(String.class, s -> s);
80+
LettuceConverterRegistry.registerResultConverter(Integer.class, i -> i);
81+
LettuceConverterRegistry.clear();
82+
assertThat(LettuceConverterRegistry.getArgConverter(String.class)).isNull();
83+
assertThat(LettuceConverterRegistry.getResultConverter(Integer.class)).isNull();
84+
}
85+
}

0 commit comments

Comments
 (0)