Skip to content

Commit 9a4c11b

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 underscore format
1 parent d255b1a commit 9a4c11b

File tree

2 files changed

+128
-29
lines changed

2 files changed

+128
-29
lines changed

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

Lines changed: 25 additions & 10 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,11 @@ 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 name of the read from setting (in different formats like {@code UPSTREAM_PREFERRED},
189+
* {@code upstreamPreferred}).
187190
* @return the {@link ReadFrom} preset
188191
* @throws IllegalArgumentException if {@code name} is empty, {@code null} or the {@link ReadFrom} preset is unknown.
189192
*/
@@ -193,6 +196,26 @@ public static ReadFrom valueOf(String name) {
193196
throw new IllegalArgumentException("Name must not be empty");
194197
}
195198

199+
int index = name.indexOf(':');
200+
201+
if (index != -1) {
202+
String type = name.substring(0, index);
203+
String value = name.substring(index + 1);
204+
if (LettuceStrings.isEmpty(value)) {
205+
throw new IllegalArgumentException("Value must not be empty for the type '" + type + "'");
206+
}
207+
if (type.equalsIgnoreCase("subnet")) {
208+
return subnet(value.split(","));
209+
}
210+
if (type.equalsIgnoreCase("regex")) {
211+
try {
212+
return regex(Pattern.compile(value));
213+
} catch (PatternSyntaxException ex) {
214+
throw new IllegalArgumentException("Value '" + value + "' is not a valid regular expression", ex);
215+
}
216+
}
217+
}
218+
name = name.replaceAll("_", "");
196219
if (name.equalsIgnoreCase("master")) {
197220
return UPSTREAM;
198221
}
@@ -229,14 +252,6 @@ public static ReadFrom valueOf(String name) {
229252
return ANY_REPLICA;
230253
}
231254

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-
240255
throw new IllegalArgumentException("ReadFrom " + name + " not supported");
241256
}
242257

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

Lines changed: 103 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,126 @@ 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 = { "lowestLatency", "LOWEST_LATENCY" })
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", "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", "slavePreferred", "slavepreferred" })
257+
void valueOfSlavePreferred(String name) {
258+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA_PREFERRED);
259+
}
260+
261+
@ParameterizedTest
262+
@ValueSource(strings = { "ANY_REPLICA", "anyReplica", "anyreplica" })
263+
void valueOfAnyReplica(String name) {
264+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.ANY_REPLICA);
236265
}
237266

238267
@Test
239-
void valueOfSlave() {
240-
assertThat(ReadFrom.valueOf("slave")).isEqualTo(ReadFrom.REPLICA);
268+
void valueOfSubnetWithEmptyCidrNotations() {
269+
assertThatThrownBy(() -> ReadFrom.valueOf("subnet")).isInstanceOf(IllegalArgumentException.class);
270+
}
271+
272+
@ParameterizedTest
273+
@ValueSource(strings = { "subnet:192.0.2.0/24,2001:db8:abcd:0000::/52", "SUBNET:192.0.2.0/24,2001:db8:abcd:0000::/52" })
274+
void valueOfSubnet(String name) {
275+
RedisClusterNode nodeInSubnetIpv4 = createNodeWithHost("192.0.2.1");
276+
RedisClusterNode nodeNotInSubnetIpv4 = createNodeWithHost("198.51.100.1");
277+
RedisClusterNode nodeInSubnetIpv6 = createNodeWithHost("2001:db8:abcd:0000::1");
278+
RedisClusterNode nodeNotInSubnetIpv6 = createNodeWithHost("2001:db8:abcd:1000::");
279+
ReadFrom sut = ReadFrom.valueOf(name);
280+
List<RedisNodeDescription> result = sut
281+
.select(getNodes(nodeInSubnetIpv4, nodeNotInSubnetIpv4, nodeInSubnetIpv6, nodeNotInSubnetIpv6));
282+
assertThat(result).hasSize(2).containsExactly(nodeInSubnetIpv4, nodeInSubnetIpv6);
241283
}
242284

243285
@Test
244-
void valueOfSlavePreferred() {
245-
assertThat(ReadFrom.valueOf("slavePreferred")).isEqualTo(ReadFrom.REPLICA_PREFERRED);
286+
void valueOfRegexWithEmptyRegexValue() {
287+
assertThatThrownBy(() -> ReadFrom.valueOf("regex")).isInstanceOf(IllegalArgumentException.class);
288+
}
289+
290+
@ParameterizedTest
291+
@ValueSource(strings = { "regex:.*region-1.*", "REGEX:.*region-1.*" })
292+
void valueOfRegex() {
293+
ReadFrom sut = ReadFrom.valueOf("regex:.*region-1.*");
294+
295+
RedisClusterNode node1 = createNodeWithHost("redis-node-1.region-1.example.com");
296+
RedisClusterNode node2 = createNodeWithHost("redis-node-2.region-1.example.com");
297+
RedisClusterNode node3 = createNodeWithHost("redis-node-1.region-2.example.com");
298+
RedisClusterNode node4 = createNodeWithHost("redis-node-2.region-2.example.com");
299+
300+
List<RedisNodeDescription> result = sut.select(getNodes(node1, node2, node3, node4));
301+
302+
assertThat(result).hasSize(2).containsExactly(node1, node2);
303+
}
304+
305+
@ParameterizedTest
306+
@ValueSource(strings = { "replica", "Replica", "REPLICA" })
307+
void valueOfReplica(String name) {
308+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA);
309+
}
310+
311+
@ParameterizedTest
312+
@ValueSource(strings = { "UPSTREAM", "Upstream", "upstream", "UpStream" })
313+
void valueOfUpstream(String name) {
314+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM);
315+
}
316+
317+
@ParameterizedTest
318+
@ValueSource(strings = { "UPSTREAM_PREFERRED", "upstreamPreferred", "UpStreamPreferred" })
319+
void valueOfUpstreamPreferred(String name) {
320+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM_PREFERRED);
246321
}
247322

248323
@Test
249-
void valueOfAnyReplica() {
250-
assertThat(ReadFrom.valueOf("anyReplica")).isEqualTo(ReadFrom.ANY_REPLICA);
324+
void valueOfWhenNameIsPresentButValueIsAbsent() {
325+
assertThatThrownBy(() -> ReadFrom.valueOf("subnet:")).isInstanceOf(IllegalArgumentException.class)
326+
.hasMessageContaining("Value must not be empty for the type 'subnet'");
251327
}
252328

253329
@Test
254-
void valueOfSubnet() {
255-
assertThatThrownBy(() -> ReadFrom.valueOf("subnet")).isInstanceOf(IllegalArgumentException.class);
330+
void valueOfWhenNameIsEmptyButValueIsPresent() {
331+
assertThatThrownBy(() -> ReadFrom.valueOf(":192.0.2.0/24")).isInstanceOf(IllegalArgumentException.class)
332+
.hasMessageContaining("ReadFrom :192.0.2.0/24 not supported");
256333
}
257334

258335
@Test
259-
void valueOfRegex() {
260-
assertThatThrownBy(() -> ReadFrom.valueOf("regex")).isInstanceOf(IllegalArgumentException.class);
336+
void valueOfRegexWithInvalidPatternShouldThrownIllegalArgumentException() {
337+
assertThatThrownBy(() -> ReadFrom.valueOf("regex:\\")).isInstanceOf(IllegalArgumentException.class)
338+
.hasMessageContaining("is not a valid regular expression");
339+
}
340+
341+
@ParameterizedTest
342+
@ValueSource(strings = { "ANY", "any", "Any" })
343+
void valueOfAny(String name) {
344+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.ANY);
261345
}
262346

263347
private ReadFrom.Nodes getNodes() {

0 commit comments

Comments
 (0)