Skip to content

Commit cfa5e3e

Browse files
Expose method to add upstream driver libraries to CLIENT SETINFO payload
1 parent a1bb28d commit cfa5e3e

File tree

4 files changed

+338
-7
lines changed

4 files changed

+338
-7
lines changed

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

Lines changed: 150 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,24 @@ public class RedisURI implements Serializable, ConnectionPoint {
225225

226226
public static final Duration DEFAULT_TIMEOUT_DURATION = Duration.ofSeconds(DEFAULT_TIMEOUT);
227227

228+
/**
229+
* Regex pattern for driver name validation. The name must start with a lowercase letter and contain only lowercase letters,
230+
* digits, hyphens, and underscores. Mostly follows Maven artifactId naming conventions but also allows underscores.
231+
*
232+
* @see <a href="https://maven.apache.org/guides/mini/guide-naming-conventions.html">Maven Naming Conventions</a>
233+
*/
234+
private static final String DRIVER_NAME_PATTERN = "^[a-z][a-z0-9_-]*$";
235+
236+
/**
237+
* Official semver.org regex pattern for semantic versioning validation.
238+
*
239+
* @see <a href="https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string">semver.org
240+
* regex</a>
241+
*/
242+
private static final String SEMVER_PATTERN = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)"
243+
+ "(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
244+
+ "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$";
245+
228246
private String host;
229247

230248
private String socket;
@@ -239,9 +257,11 @@ public class RedisURI implements Serializable, ConnectionPoint {
239257

240258
private String libraryName = LettuceVersion.getName();
241259

260+
private String upstreamDrivers;
261+
242262
private String libraryVersion = LettuceVersion.getVersion();
243263

244-
private RedisCredentialsProvider credentialsProvider = new StaticCredentialsProvider(null, null);;
264+
private RedisCredentialsProvider credentialsProvider = new StaticCredentialsProvider(null, null);
245265

246266
private boolean ssl = false;
247267

@@ -606,13 +626,19 @@ public void setClientName(String clientName) {
606626
}
607627

608628
/**
609-
* Returns the library name.
629+
* Returns the library name to be sent via {@code CLIENT SETINFO}.
630+
* <p>
631+
* If upstream drivers have been added via {@link #addUpstreamDriver(String, String)}, the returned value will include them
632+
* in the format: {@code libraryName(driver1_v1.0.0;driver2_v2.0.0)}. Otherwise, returns just the library name.
610633
*
611-
* @return the library name.
634+
* @return the library name, potentially including upstream driver information.
612635
* @since 6.3
613636
*/
614637
public String getLibraryName() {
615-
return libraryName;
638+
if (upstreamDrivers == null) {
639+
return libraryName;
640+
}
641+
return libraryName + "(" + upstreamDrivers + ")";
616642
}
617643

618644
/**
@@ -628,6 +654,79 @@ public void setLibraryName(String libraryName) {
628654
this.libraryName = libraryName;
629655
}
630656

657+
/**
658+
* Adds an upstream driver to be appended to the library name when sent via {@code CLIENT SETINFO}.
659+
* <p>
660+
* This method allows upstream libraries (e.g., Spring Data Redis) that use Lettuce as their Redis driver to identify
661+
* themselves. Multiple upstream drivers can be added and will be formatted according to the Redis CLIENT SETINFO format:
662+
* {@code lettuce(driver1_v1.0.0;driver2_v2.0.0)}.
663+
* <p>
664+
* Each newly added upstream driver is prepended to the list, so the most recently added driver appears first. For example,
665+
* if you call {@code addUpstreamDriver("spring-data-redis", "3.2.0")} followed by
666+
* {@code addUpstreamDriver("spring-boot", "3.3.0")}, the resulting library name will be
667+
* {@code lettuce(spring-boot_v3.3.0;spring-data-redis_v3.2.0)}.
668+
* <p>
669+
* The driver name must follow <a href="https://maven.apache.org/guides/mini/guide-naming-conventions.html">Maven artifactId
670+
* naming conventions</a>: lowercase letters, digits, hyphens, and underscores only, starting with a lowercase letter (e.g.,
671+
* {@code spring-data-redis}, {@code lettuce-core}, {@code akka-redis_2.13}).
672+
* <p>
673+
* The driver version must follow <a href="https://semver.org/">semantic versioning</a> (e.g., {@code 1.0.0},
674+
* {@code 2.1.3-beta}, {@code 1.0.0-alpha.1}, {@code 1.0.0+build.123}).
675+
*
676+
* @param driverName the name of the upstream driver (e.g., "spring-data-redis"), must not be {@code null} and must follow
677+
* <a href="https://maven.apache.org/guides/mini/guide-naming-conventions.html">Maven artifactId naming
678+
* conventions</a>
679+
* @param driverVersion the version of the upstream driver (e.g., "3.2.0"), must not be {@code null} and must follow
680+
* <a href="https://semver.org/">semantic versioning</a>
681+
* @throws IllegalArgumentException if the driver name or version format is invalid
682+
* @since 6.5
683+
* @see <a href="https://redis.io/docs/latest/commands/client-setinfo/">CLIENT SETINFO</a>
684+
*/
685+
public void addUpstreamDriver(String driverName, String driverVersion) {
686+
687+
LettuceAssert.notNull(driverName, "Upstream driver name must not be null");
688+
LettuceAssert.notNull(driverVersion, "Upstream driver version must not be null");
689+
validateDriverName(driverName);
690+
validateDriverVersion(driverVersion);
691+
692+
String driver = driverName + "_v" + driverVersion;
693+
this.upstreamDrivers = this.upstreamDrivers == null ? driver : driver + ";" + this.upstreamDrivers;
694+
}
695+
696+
/**
697+
* Validates that the driver name follows Maven artifactId naming conventions: lowercase letters, digits, hyphens, and
698+
* underscores only, starting with a lowercase letter (e.g., {@code spring-data-redis}, {@code lettuce-core},
699+
* {@code akka-redis_2.13}).
700+
*
701+
* @param driverName the driver name to validate
702+
* @throws IllegalArgumentException if the driver name does not follow the expected naming conventions
703+
* @see <a href="https://maven.apache.org/guides/mini/guide-naming-conventions.html">Maven Naming Conventions</a>
704+
*/
705+
private static void validateDriverName(String driverName) {
706+
if (!driverName.matches(DRIVER_NAME_PATTERN)) {
707+
throw new IllegalArgumentException(
708+
"Upstream driver name must follow Maven artifactId naming conventions: lowercase letters, digits, hyphens, and underscores only (e.g., 'spring-data-redis', 'lettuce-core')");
709+
}
710+
}
711+
712+
/**
713+
* Validates that the driver version follows semantic versioning (semver.org). The version must be in the format
714+
* {@code MAJOR.MINOR.PATCH} with optional pre-release and build metadata suffixes.
715+
* <p>
716+
* Examples of valid versions: {@code 1.0.0}, {@code 2.1.3}, {@code 1.0.0-alpha}, {@code 1.0.0-alpha.1},
717+
* {@code 1.0.0-0.3.7}, {@code 1.0.0-x.7.z.92}, {@code 1.0.0+20130313144700}, {@code 1.0.0-beta+exp.sha.5114f85}
718+
*
719+
* @param driverVersion the driver version to validate
720+
* @throws IllegalArgumentException if the driver version does not follow semantic versioning
721+
* @see <a href="https://semver.org/">Semantic Versioning 2.0.0</a>
722+
*/
723+
private static void validateDriverVersion(String driverVersion) {
724+
if (!driverVersion.matches(SEMVER_PATTERN)) {
725+
throw new IllegalArgumentException(
726+
"Upstream driver version must follow semantic versioning (e.g., '1.0.0', '2.1.3-beta', '1.0.0+build.123')");
727+
}
728+
}
729+
631730
/**
632731
* Returns the library version.
633732
*
@@ -1295,6 +1394,8 @@ public static class Builder {
12951394

12961395
private String libraryVersion = LettuceVersion.getVersion();
12971396

1397+
private String upstreamDrivers;
1398+
12981399
private RedisCredentialsProvider credentialsProvider;
12991400

13001401
private boolean ssl = false;
@@ -1630,6 +1731,50 @@ public Builder withLibraryName(String libraryName) {
16301731
return this;
16311732
}
16321733

1734+
/**
1735+
* Adds an upstream driver to be appended to the library name when sent via {@code CLIENT SETINFO}.
1736+
* <p>
1737+
* This method allows upstream libraries (e.g., Spring Data Redis) that use Lettuce as their Redis driver to identify
1738+
* themselves. Multiple upstream drivers can be added and will be formatted according to the Redis CLIENT SETINFO
1739+
* format: {@code lettuce(driver1_v1.0.0;driver2_v2.0.0)}.
1740+
* <p>
1741+
* Each newly added upstream driver is prepended to the list, so the most recently added driver appears first. For
1742+
* example, if you call {@code addUpstreamDriver("spring-data-redis", "3.2.0")} followed by
1743+
* {@code addUpstreamDriver("spring-boot", "3.3.0")}, the resulting library name will be
1744+
* {@code lettuce(spring-boot_v3.3.0;spring-data-redis_v3.2.0)}.
1745+
* <p>
1746+
* The driver name must follow <a href="https://maven.apache.org/guides/mini/guide-naming-conventions.html">Maven
1747+
* artifactId naming conventions</a>: lowercase letters, digits, hyphens, and underscores only, starting with a
1748+
* lowercase letter (e.g., {@code spring-data-redis}, {@code lettuce-core}, {@code akka-redis_2.13}).
1749+
* <p>
1750+
* The driver version must follow <a href="https://semver.org/">semantic versioning</a> (e.g., {@code 1.0.0},
1751+
* {@code 2.1.3-beta}, {@code 1.0.0-alpha.1}, {@code 1.0.0+build.123}).
1752+
* <p>
1753+
* Also sets upstream driver for already configured Redis Sentinel nodes.
1754+
*
1755+
* @param driverName the name of the upstream driver (e.g., "spring-data-redis"), must not be {@code null} and must
1756+
* follow <a href="https://maven.apache.org/guides/mini/guide-naming-conventions.html">Maven artifactId naming
1757+
* conventions</a>
1758+
* @param driverVersion the version of the upstream driver (e.g., "3.2.0"), must not be {@code null} and must follow
1759+
* <a href="https://semver.org/">semantic versioning</a>
1760+
* @return the builder
1761+
* @throws IllegalArgumentException if the driver name or version format is invalid
1762+
* @since 6.5
1763+
* @see <a href="https://redis.io/docs/latest/commands/client-setinfo/">CLIENT SETINFO</a>
1764+
*/
1765+
public Builder addUpstreamDriver(String driverName, String driverVersion) {
1766+
1767+
LettuceAssert.notNull(driverName, "Upstream driver name must not be null");
1768+
LettuceAssert.notNull(driverVersion, "Upstream driver version must not be null");
1769+
validateDriverName(driverName);
1770+
validateDriverVersion(driverVersion);
1771+
1772+
String driver = driverName + "_v" + driverVersion;
1773+
this.upstreamDrivers = this.upstreamDrivers == null ? driver : driver + ";" + this.upstreamDrivers;
1774+
this.sentinels.forEach(it -> it.addUpstreamDriver(driverName, driverVersion));
1775+
return this;
1776+
}
1777+
16331778
/**
16341779
* Configures a library version. Sets library version also for already configured Redis Sentinel nodes.
16351780
*
@@ -1790,6 +1935,7 @@ public RedisURI build() {
17901935
redisURI.setClientName(clientName);
17911936
redisURI.setLibraryName(libraryName);
17921937
redisURI.setLibraryVersion(libraryVersion);
1938+
redisURI.upstreamDrivers = upstreamDrivers;
17931939

17941940
redisURI.setSentinelMasterId(sentinelMasterId);
17951941

src/test/java/io/lettuce/core/RedisURIBuilderUnitTests.java

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import org.junit.jupiter.api.Test;
3232
import org.junit.jupiter.api.condition.DisabledOnOs;
3333
import org.junit.jupiter.api.condition.OS;
34-
import reactor.core.publisher.Mono;
3534
import reactor.test.StepVerifier;
3635

3736
/**
@@ -102,6 +101,76 @@ void redisWithLibrary() {
102101
assertThat(result.getLibraryVersion()).isEqualTo("1.foo");
103102
}
104103

104+
@Test
105+
void redisWithSingleUpstreamDriver() {
106+
RedisURI result = RedisURI.Builder.redis("localhost").withLibraryName("lettuce")
107+
.addUpstreamDriver("spring-data-redis", "3.2.0").build();
108+
109+
assertThat(result.getLibraryName()).isEqualTo("lettuce(spring-data-redis_v3.2.0)");
110+
}
111+
112+
@Test
113+
void redisWithMultipleUpstreamDrivers() {
114+
RedisURI result = RedisURI.Builder.redis("localhost").withLibraryName("lettuce")
115+
.addUpstreamDriver("spring-data-redis", "3.2.0").addUpstreamDriver("spring-boot", "3.3.0").build();
116+
117+
// Most recently added driver should appear first (prepend behavior)
118+
assertThat(result.getLibraryName()).isEqualTo("lettuce(spring-boot_v3.3.0;spring-data-redis_v3.2.0)");
119+
}
120+
121+
@Test
122+
void redisWithUpstreamDriverNullNameShouldFail() {
123+
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver(null, "1.0.0"))
124+
.isInstanceOf(IllegalArgumentException.class);
125+
}
126+
127+
@Test
128+
void redisWithUpstreamDriverNullVersionShouldFail() {
129+
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("example-driver", null))
130+
.isInstanceOf(IllegalArgumentException.class);
131+
}
132+
133+
@Test
134+
void redisWithUpstreamDriverNameWithSpacesShouldFail() {
135+
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("spring data", "1.0.0"))
136+
.isInstanceOf(IllegalArgumentException.class);
137+
}
138+
139+
@Test
140+
void redisWithUpstreamDriverVersionWithSpacesShouldFail() {
141+
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("example-driver", "1.0 beta"))
142+
.isInstanceOf(IllegalArgumentException.class);
143+
}
144+
145+
@Test
146+
void redisWithUpstreamDriverInvalidNameFormatShouldFail() {
147+
// Name starting with a number
148+
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("123driver", "1.0.0"))
149+
.isInstanceOf(IllegalArgumentException.class);
150+
// Name with @ symbol
151+
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("driver@name", "1.0.0"))
152+
.isInstanceOf(IllegalArgumentException.class);
153+
// Name starting with hyphen
154+
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("-spring-data", "1.0.0"))
155+
.isInstanceOf(IllegalArgumentException.class);
156+
// Name with dots (not allowed in Maven artifactId)
157+
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("com.example.driver", "1.0.0"))
158+
.isInstanceOf(IllegalArgumentException.class);
159+
// Name with uppercase letters
160+
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("Spring-Data-Redis", "1.0.0"))
161+
.isInstanceOf(IllegalArgumentException.class);
162+
}
163+
164+
@Test
165+
void redisWithUpstreamDriverInvalidVersionFormatShouldFail() {
166+
// Version without patch number
167+
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("example-driver", "1.0"))
168+
.isInstanceOf(IllegalArgumentException.class);
169+
// Version with leading zeros
170+
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost").addUpstreamDriver("example-driver", "01.0.0"))
171+
.isInstanceOf(IllegalArgumentException.class);
172+
}
173+
105174
@Test
106175
void redisHostAndPortWithInvalidPort() {
107176
assertThatThrownBy(() -> RedisURI.Builder.redis("localhost", -1)).isInstanceOf(IllegalArgumentException.class);

0 commit comments

Comments
 (0)