Skip to content

Commit 6c201e6

Browse files
committed
Add support for creating regex and subnet-based ReadFrom instances from a single string #3013
Before this commit, it was not possible to use ReadFrom.valueOf for subnet and regex types. This commit introduces support for these types, as well as the use of names in kebab-case format
1 parent d255b1a commit 6c201e6

File tree

2 files changed

+147
-38
lines changed

2 files changed

+147
-38
lines changed

src/main/java/io/lettuce/core/ReadFrom.java

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import java.util.List;
2323
import java.util.regex.Pattern;
24+
import java.util.regex.PatternSyntaxException;
2425

2526
import io.lettuce.core.internal.LettuceStrings;
2627
import io.lettuce.core.models.role.RedisNodeDescription;
@@ -181,9 +182,10 @@ protected boolean isOrderSensitive() {
181182
}
182183

183184
/**
184-
* Retrieve the {@link ReadFrom} preset by name.
185+
* Retrieve the {@link ReadFrom} preset by name. For complex types like {@code subnet} or {@code regex}, the following
186+
* syntax could be used {@code subnet:192.168.0.0/16,2001:db8:abcd:0000::/52} and {@code regex:.*region-1.*} respectively.
185187
*
186-
* @param name the name of the read from setting
188+
* @param name the case-insensitive name either in {@code kebab-case} or {@code camelCase} format of the read from setting
187189
* @return the {@link ReadFrom} preset
188190
* @throws IllegalArgumentException if {@code name} is empty, {@code null} or the {@link ReadFrom} preset is unknown.
189191
*/
@@ -193,50 +195,64 @@ public static ReadFrom valueOf(String name) {
193195
throw new IllegalArgumentException("Name must not be empty");
194196
}
195197

196-
if (name.equalsIgnoreCase("master")) {
198+
int index = name.indexOf(':');
199+
200+
if (index != -1) {
201+
String type = name.substring(0, index);
202+
String value = name.substring(index + 1);
203+
if (LettuceStrings.isEmpty(value)) {
204+
throw new IllegalArgumentException("Value must not be empty for the type '" + type + "'");
205+
}
206+
if (type.equalsIgnoreCase("subnet")) {
207+
return subnet(value.split(","));
208+
}
209+
if (type.equalsIgnoreCase("regex")) {
210+
try {
211+
return regex(Pattern.compile(value));
212+
} catch (PatternSyntaxException ex) {
213+
throw new IllegalArgumentException("Value '" + value + "' is not a valid regular expression", ex);
214+
}
215+
}
216+
}
217+
218+
String type = name.replaceAll("-", "");
219+
220+
if (type.equalsIgnoreCase("master")) {
197221
return UPSTREAM;
198222
}
199223

200-
if (name.equalsIgnoreCase("masterPreferred")) {
224+
if (type.equalsIgnoreCase("masterpreferred")) {
201225
return UPSTREAM_PREFERRED;
202226
}
203227

204-
if (name.equalsIgnoreCase("upstream")) {
228+
if (type.equalsIgnoreCase("upstream")) {
205229
return UPSTREAM;
206230
}
207231

208-
if (name.equalsIgnoreCase("upstreamPreferred")) {
232+
if (type.equalsIgnoreCase("upstreampreferred")) {
209233
return UPSTREAM_PREFERRED;
210234
}
211235

212-
if (name.equalsIgnoreCase("slave") || name.equalsIgnoreCase("replica")) {
236+
if (type.equalsIgnoreCase("slave") || type.equalsIgnoreCase("replica")) {
213237
return REPLICA;
214238
}
215239

216-
if (name.equalsIgnoreCase("slavePreferred") || name.equalsIgnoreCase("replicaPreferred")) {
240+
if (type.equalsIgnoreCase("slavepreferred") || type.equalsIgnoreCase("replicapreferred")) {
217241
return REPLICA_PREFERRED;
218242
}
219243

220-
if (name.equalsIgnoreCase("nearest") || name.equalsIgnoreCase("lowestLatency")) {
244+
if (type.equalsIgnoreCase("nearest") || type.equalsIgnoreCase("lowestlatency")) {
221245
return LOWEST_LATENCY;
222246
}
223247

224-
if (name.equalsIgnoreCase("any")) {
248+
if (type.equalsIgnoreCase("any")) {
225249
return ANY;
226250
}
227251

228-
if (name.equalsIgnoreCase("anyReplica")) {
252+
if (type.equalsIgnoreCase("anyreplica")) {
229253
return ANY_REPLICA;
230254
}
231255

232-
if (name.equalsIgnoreCase("subnet")) {
233-
throw new IllegalArgumentException("subnet must be created via ReadFrom#subnet");
234-
}
235-
236-
if (name.equalsIgnoreCase("regex")) {
237-
throw new IllegalArgumentException("regex must be created via ReadFrom#regex");
238-
}
239-
240256
throw new IllegalArgumentException("ReadFrom " + name + " not supported");
241257
}
242258

src/test/java/io/lettuce/core/cluster/ReadFromUnitTests.java

Lines changed: 112 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131

3232
import org.junit.jupiter.api.BeforeEach;
3333
import org.junit.jupiter.api.Test;
34+
import org.junit.jupiter.params.ParameterizedTest;
35+
import org.junit.jupiter.params.provider.ValueSource;
3436

3537
import io.lettuce.core.ReadFrom;
3638
import io.lettuce.core.RedisURI;
@@ -220,44 +222,135 @@ void valueOfUnknown() {
220222
assertThatThrownBy(() -> ReadFrom.valueOf("unknown")).isInstanceOf(IllegalArgumentException.class);
221223
}
222224

223-
@Test
224-
void valueOfNearest() {
225-
assertThat(ReadFrom.valueOf("nearest")).isEqualTo(ReadFrom.NEAREST);
225+
@ParameterizedTest
226+
@ValueSource(strings = { "NEAREST", "nearest", "Nearest" })
227+
void valueOfNearest(String name) {
228+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.NEAREST);
226229
}
227230

228-
@Test
229-
void valueOfMaster() {
230-
assertThat(ReadFrom.valueOf("master")).isEqualTo(ReadFrom.UPSTREAM);
231+
@ParameterizedTest
232+
@ValueSource(strings = { "LOWEST-LATENCY", "lowest-latency", "lowestLatency", "lowestlatency", "LOWESTLATENCY" })
233+
void valueOfLowestLatency(String name) {
234+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.LOWEST_LATENCY);
231235
}
232236

233-
@Test
234-
void valueOfMasterPreferred() {
235-
assertThat(ReadFrom.valueOf("masterPreferred")).isEqualTo(ReadFrom.UPSTREAM_PREFERRED);
237+
@ParameterizedTest
238+
@ValueSource(strings = { "MASTER", "master", "Master" })
239+
void valueOfMaster(String name) {
240+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM);
241+
}
242+
243+
@ParameterizedTest
244+
@ValueSource(strings = { "MASTER-PREFERRED", "master-preferred", "masterPreferred", "masterpreferred", "MASTERPREFERRED" })
245+
void valueOfMasterPreferred(String name) {
246+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM_PREFERRED);
247+
}
248+
249+
@ParameterizedTest
250+
@ValueSource(strings = { "slave", "SLAVE", "Slave" })
251+
void valueOfSlave(String name) {
252+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA);
253+
}
254+
255+
@ParameterizedTest
256+
@ValueSource(strings = { "SLAVE-PREFERRED", "slave-preferred", "slavePreferred", "slavepreferred", "SLAVEPREFERRED" })
257+
void valueOfSlavePreferred(String name) {
258+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA_PREFERRED);
259+
}
260+
261+
@ParameterizedTest
262+
@ValueSource(strings = { "REPLICA-PREFERRED", "replica-preferred", "replicaPreferred", "replicapreferred",
263+
"REPLICAPREFERRED" })
264+
void valueOfReplicaPreferred(String name) {
265+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA_PREFERRED);
266+
}
267+
268+
@ParameterizedTest
269+
@ValueSource(strings = { "ANY-REPLICA", "any-replica", "anyReplica", "anyreplica", "ANYREPLICA" })
270+
void valueOfAnyReplica(String name) {
271+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.ANY_REPLICA);
236272
}
237273

238274
@Test
239-
void valueOfSlave() {
240-
assertThat(ReadFrom.valueOf("slave")).isEqualTo(ReadFrom.REPLICA);
275+
void valueOfSubnetWithEmptyCidrNotations() {
276+
assertThatThrownBy(() -> ReadFrom.valueOf("subnet")).isInstanceOf(IllegalArgumentException.class);
277+
}
278+
279+
@ParameterizedTest
280+
@ValueSource(strings = { "subnet:192.0.2.0/24,2001:db8:abcd:0000::/52", "SUBNET:192.0.2.0/24,2001:db8:abcd:0000::/52" })
281+
void valueOfSubnet(String name) {
282+
RedisClusterNode nodeInSubnetIpv4 = createNodeWithHost("192.0.2.1");
283+
RedisClusterNode nodeNotInSubnetIpv4 = createNodeWithHost("198.51.100.1");
284+
RedisClusterNode nodeInSubnetIpv6 = createNodeWithHost("2001:db8:abcd:0000::1");
285+
RedisClusterNode nodeNotInSubnetIpv6 = createNodeWithHost("2001:db8:abcd:1000::");
286+
ReadFrom sut = ReadFrom.valueOf(name);
287+
List<RedisNodeDescription> result = sut
288+
.select(getNodes(nodeInSubnetIpv4, nodeNotInSubnetIpv4, nodeInSubnetIpv6, nodeNotInSubnetIpv6));
289+
assertThat(result).hasSize(2).containsExactly(nodeInSubnetIpv4, nodeInSubnetIpv6);
241290
}
242291

243292
@Test
244-
void valueOfSlavePreferred() {
245-
assertThat(ReadFrom.valueOf("slavePreferred")).isEqualTo(ReadFrom.REPLICA_PREFERRED);
293+
void valueOfRegexWithEmptyRegexValue() {
294+
assertThatThrownBy(() -> ReadFrom.valueOf("regex")).isInstanceOf(IllegalArgumentException.class);
295+
}
296+
297+
@ParameterizedTest
298+
@ValueSource(strings = { "regex:.*region-1.*", "REGEX:.*region-1.*" })
299+
void valueOfRegex(String name) {
300+
ReadFrom sut = ReadFrom.valueOf(name);
301+
302+
RedisClusterNode node1 = createNodeWithHost("redis-node-1.region-1.example.com");
303+
RedisClusterNode node2 = createNodeWithHost("redis-node-2.region-1.example.com");
304+
RedisClusterNode node3 = createNodeWithHost("redis-node-1.region-2.example.com");
305+
RedisClusterNode node4 = createNodeWithHost("redis-node-2.region-2.example.com");
306+
307+
List<RedisNodeDescription> result = sut.select(getNodes(node1, node2, node3, node4));
308+
309+
assertThat(sut).hasFieldOrPropertyWithValue("orderSensitive", false);
310+
assertThat(result).hasSize(2).containsExactly(node1, node2);
311+
}
312+
313+
@ParameterizedTest
314+
@ValueSource(strings = { "REPLICA", "replica", "Replica" })
315+
void valueOfReplica(String name) {
316+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA);
317+
}
318+
319+
@ParameterizedTest
320+
@ValueSource(strings = { "UPSTREAM", "upstream", "Upstream" })
321+
void valueOfUpstream(String name) {
322+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM);
323+
}
324+
325+
@ParameterizedTest
326+
@ValueSource(strings = { "UPSTREAM-PREFERRED", "upstream-preferred", "upstreamPreferred", "UPSTREAMPREFERRED",
327+
"UpstreamPreferred" })
328+
void valueOfUpstreamPreferred(String name) {
329+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM_PREFERRED);
246330
}
247331

248332
@Test
249-
void valueOfAnyReplica() {
250-
assertThat(ReadFrom.valueOf("anyReplica")).isEqualTo(ReadFrom.ANY_REPLICA);
333+
void valueOfWhenNameIsPresentButValueIsAbsent() {
334+
assertThatThrownBy(() -> ReadFrom.valueOf("subnet:")).isInstanceOf(IllegalArgumentException.class)
335+
.hasMessageContaining("Value must not be empty for the type 'subnet'");
251336
}
252337

253338
@Test
254-
void valueOfSubnet() {
255-
assertThatThrownBy(() -> ReadFrom.valueOf("subnet")).isInstanceOf(IllegalArgumentException.class);
339+
void valueOfWhenNameIsEmptyButValueIsPresent() {
340+
assertThatThrownBy(() -> ReadFrom.valueOf(":192.0.2.0/24")).isInstanceOf(IllegalArgumentException.class)
341+
.hasMessageContaining("ReadFrom :192.0.2.0/24 not supported");
256342
}
257343

258344
@Test
259-
void valueOfRegex() {
260-
assertThatThrownBy(() -> ReadFrom.valueOf("regex")).isInstanceOf(IllegalArgumentException.class);
345+
void valueOfRegexWithInvalidPatternShouldThrownIllegalArgumentException() {
346+
assertThatThrownBy(() -> ReadFrom.valueOf("regex:\\")).isInstanceOf(IllegalArgumentException.class)
347+
.hasMessageContaining("is not a valid regular expression");
348+
}
349+
350+
@ParameterizedTest
351+
@ValueSource(strings = { "ANY", "any", "Any" })
352+
void valueOfAny(String name) {
353+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.ANY);
261354
}
262355

263356
private ReadFrom.Nodes getNodes() {

0 commit comments

Comments
 (0)