Skip to content

Commit 613a92f

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 snake_case, kebab-case formats
1 parent d255b1a commit 613a92f

File tree

2 files changed

+152
-38
lines changed

2 files changed

+152
-38
lines changed

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

Lines changed: 36 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,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 case-insensitive name either in {@code camelCase}, {@code snake_case} or {@code kebab-case} format of the
189+
* read from setting
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,50 +196,64 @@ public static ReadFrom valueOf(String name) {
193196
throw new IllegalArgumentException("Name must not be empty");
194197
}
195198

196-
if (name.equalsIgnoreCase("master")) {
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+
219+
String type = name.replaceAll("[_-]", "");
220+
221+
if (type.equalsIgnoreCase("master")) {
197222
return UPSTREAM;
198223
}
199224

200-
if (name.equalsIgnoreCase("masterPreferred")) {
225+
if (type.equalsIgnoreCase("masterpreferred")) {
201226
return UPSTREAM_PREFERRED;
202227
}
203228

204-
if (name.equalsIgnoreCase("upstream")) {
229+
if (type.equalsIgnoreCase("upstream")) {
205230
return UPSTREAM;
206231
}
207232

208-
if (name.equalsIgnoreCase("upstreamPreferred")) {
233+
if (type.equalsIgnoreCase("upstreampreferred")) {
209234
return UPSTREAM_PREFERRED;
210235
}
211236

212-
if (name.equalsIgnoreCase("slave") || name.equalsIgnoreCase("replica")) {
237+
if (type.equalsIgnoreCase("slave") || type.equalsIgnoreCase("replica")) {
213238
return REPLICA;
214239
}
215240

216-
if (name.equalsIgnoreCase("slavePreferred") || name.equalsIgnoreCase("replicaPreferred")) {
241+
if (type.equalsIgnoreCase("slavepreferred") || type.equalsIgnoreCase("replicapreferred")) {
217242
return REPLICA_PREFERRED;
218243
}
219244

220-
if (name.equalsIgnoreCase("nearest") || name.equalsIgnoreCase("lowestLatency")) {
245+
if (type.equalsIgnoreCase("nearest") || type.equalsIgnoreCase("lowestlatency")) {
221246
return LOWEST_LATENCY;
222247
}
223248

224-
if (name.equalsIgnoreCase("any")) {
249+
if (type.equalsIgnoreCase("any")) {
225250
return ANY;
226251
}
227252

228-
if (name.equalsIgnoreCase("anyReplica")) {
253+
if (type.equalsIgnoreCase("anyreplica")) {
229254
return ANY_REPLICA;
230255
}
231256

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-
240257
throw new IllegalArgumentException("ReadFrom " + name + " not supported");
241258
}
242259

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

Lines changed: 116 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,139 @@ 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", "lowest_latency", "lowest-latency", "lowestLatency",
233+
"lowestlatency", "LOWESTLATENCY" })
234+
void valueOfLowestLatency(String name) {
235+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.LOWEST_LATENCY);
231236
}
232237

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

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

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

248336
@Test
249-
void valueOfAnyReplica() {
250-
assertThat(ReadFrom.valueOf("anyReplica")).isEqualTo(ReadFrom.ANY_REPLICA);
337+
void valueOfWhenNameIsPresentButValueIsAbsent() {
338+
assertThatThrownBy(() -> ReadFrom.valueOf("subnet:")).isInstanceOf(IllegalArgumentException.class)
339+
.hasMessageContaining("Value must not be empty for the type 'subnet'");
251340
}
252341

253342
@Test
254-
void valueOfSubnet() {
255-
assertThatThrownBy(() -> ReadFrom.valueOf("subnet")).isInstanceOf(IllegalArgumentException.class);
343+
void valueOfWhenNameIsEmptyButValueIsPresent() {
344+
assertThatThrownBy(() -> ReadFrom.valueOf(":192.0.2.0/24")).isInstanceOf(IllegalArgumentException.class)
345+
.hasMessageContaining("ReadFrom :192.0.2.0/24 not supported");
256346
}
257347

258348
@Test
259-
void valueOfRegex() {
260-
assertThatThrownBy(() -> ReadFrom.valueOf("regex")).isInstanceOf(IllegalArgumentException.class);
349+
void valueOfRegexWithInvalidPatternShouldThrownIllegalArgumentException() {
350+
assertThatThrownBy(() -> ReadFrom.valueOf("regex:\\")).isInstanceOf(IllegalArgumentException.class)
351+
.hasMessageContaining("is not a valid regular expression");
352+
}
353+
354+
@ParameterizedTest
355+
@ValueSource(strings = { "ANY", "any", "Any" })
356+
void valueOfAny(String name) {
357+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.ANY);
261358
}
262359

263360
private ReadFrom.Nodes getNodes() {

0 commit comments

Comments
 (0)