1#![doc = include_str!("../README.md")]
2
3#[cfg(any(feature = "tls-native-tls", feature = "tls-rustls"))]
4use redis::TlsMode;
5use redis::{
6 Client, ErrorKind, RedisConnectionInfo, RedisError, RedisResult, cluster::ClusterClient,
7};
8
9#[derive(Clone)]
37pub enum UniversalClient {
38 Client(Client),
39 Cluster(ClusterClient),
40}
41
42impl UniversalClient {
43 pub async fn get_connection(&self) -> RedisResult<UniversalConnection> {
44 match self {
45 Self::Client(cli) => cli
46 .get_multiplexed_async_connection()
47 .await
48 .map(UniversalConnection::Client),
49 Self::Cluster(cli) => cli
50 .get_async_connection()
51 .await
52 .map(|c| UniversalConnection::Cluster(Box::new(c))),
53 }
54 }
55
56 pub fn open<T: redis::IntoConnectionInfo + Clone>(
63 addrs: Vec<T>,
64 ) -> RedisResult<UniversalClient> {
65 let mut addrs = addrs;
66
67 if addrs.is_empty() {
68 return Err(RedisError::from((
69 ErrorKind::InvalidClientConfig,
70 "No address specified",
71 )));
72 }
73
74 if addrs.len() == 1 {
75 Client::open(addrs.remove(0)).map(Self::Client)
76 } else {
77 ClusterClient::new(addrs).map(Self::Cluster)
78 }
79 }
80}
81
82pub struct UniversalBuilder<T> {
108 addrs: Vec<T>,
109 cluster: bool,
110 username: Option<String>,
111 password: Option<String>,
112 #[cfg(any(feature = "tls-native-tls", feature = "tls-rustls"))]
113 tls: Option<TlsMode>,
114}
115
116impl<T> UniversalBuilder<T> {
117 pub fn new(addrs: Vec<T>) -> UniversalBuilder<T> {
118 UniversalBuilder {
119 addrs,
120 cluster: false,
121 username: None,
122 password: None,
123 #[cfg(any(feature = "tls-native-tls", feature = "tls-rustls"))]
124 tls: None,
125 }
126 }
127
128 pub fn cluster(mut self, flag: bool) -> UniversalBuilder<T> {
129 self.cluster = flag;
130 self
131 }
132
133 pub fn username(mut self, username: impl Into<String>) -> UniversalBuilder<T> {
135 self.username = Some(username.into());
136 self
137 }
138
139 pub fn password(mut self, password: impl Into<String>) -> UniversalBuilder<T> {
141 self.password = Some(password.into());
142 self
143 }
144
145 #[cfg(any(feature = "tls-native-tls", feature = "tls-rustls"))]
150 pub fn tls(mut self, mode: TlsMode) -> UniversalBuilder<T> {
151 self.tls = Some(mode);
152 self
153 }
154
155 pub fn build(self) -> RedisResult<UniversalClient>
156 where
157 T: redis::IntoConnectionInfo + Clone,
158 {
159 let UniversalBuilder {
160 mut addrs,
161 cluster,
162 username,
163 password,
164 #[cfg(any(feature = "tls-native-tls", feature = "tls-rustls"))]
165 tls,
166 } = self;
167
168 if addrs.is_empty() {
169 return Err(RedisError::from((
170 ErrorKind::InvalidClientConfig,
171 "No address specified",
172 )));
173 }
174
175 if cluster {
176 let mut builder = ClusterClient::builder(addrs);
177 if let Some(u) = username {
178 builder = builder.username(u);
179 }
180 if let Some(p) = password {
181 builder = builder.password(p);
182 }
183 #[cfg(any(feature = "tls-native-tls", feature = "tls-rustls"))]
184 if let Some(mode) = tls {
185 builder = builder.tls(mode);
186 }
187 builder.build().map(UniversalClient::Cluster)
188 } else if username.is_some() || password.is_some() || {
189 #[cfg(any(feature = "tls-native-tls", feature = "tls-rustls"))]
190 {
191 tls.is_some()
192 }
193 #[cfg(not(any(feature = "tls-native-tls", feature = "tls-rustls")))]
194 {
195 false
196 }
197 } {
198 let conn_info = addrs.remove(0).into_connection_info()?;
199 let orig = conn_info.redis_settings();
200 let mut redis_info = RedisConnectionInfo::default()
201 .set_db(orig.db())
202 .set_protocol(orig.protocol());
203 if let Some(u) = username {
204 redis_info = redis_info.set_username(u);
205 }
206 if let Some(p) = password {
207 redis_info = redis_info.set_password(p);
208 }
209 let conn_info = conn_info.set_redis_settings(redis_info);
210 #[cfg(any(feature = "tls-native-tls", feature = "tls-rustls"))]
211 let conn_info = if let Some(mode) = tls {
212 apply_tls_to_conn_info(conn_info, mode)?
213 } else {
214 conn_info
215 };
216 Client::open(conn_info).map(UniversalClient::Client)
217 } else {
218 Client::open(addrs.remove(0)).map(UniversalClient::Client)
219 }
220 }
221}
222
223#[cfg(any(feature = "tls-native-tls", feature = "tls-rustls"))]
228fn apply_tls_to_conn_info(
229 conn_info: redis::ConnectionInfo,
230 mode: TlsMode,
231) -> RedisResult<redis::ConnectionInfo> {
232 let insecure = mode == TlsMode::Insecure;
233 let new_addr = match conn_info.addr() {
234 redis::ConnectionAddr::Tcp(host, port) => redis::ConnectionAddr::TcpTls {
235 host: host.clone(),
236 port: *port,
237 insecure,
238 tls_params: None,
239 },
240 other => other.clone(),
242 };
243 Ok(conn_info.set_addr(new_addr))
244}
245
246#[derive(Clone)]
254pub enum UniversalConnection {
255 Client(redis::aio::MultiplexedConnection),
256 Cluster(Box<redis::cluster_async::ClusterConnection>),
257}
258
259#[cfg(test)]
260impl UniversalClient {
261 fn is_client(&self) -> bool {
262 matches!(self, Self::Client(_))
263 }
264
265 fn is_cluster(&self) -> bool {
266 matches!(self, Self::Cluster(_))
267 }
268}
269
270impl redis::aio::ConnectionLike for UniversalConnection {
271 fn req_packed_command<'a>(
272 &'a mut self,
273 cmd: &'a redis::Cmd,
274 ) -> redis::RedisFuture<'a, redis::Value> {
275 match self {
276 Self::Client(conn) => conn.req_packed_command(cmd),
277 Self::Cluster(conn) => conn.req_packed_command(cmd),
278 }
279 }
280
281 fn req_packed_commands<'a>(
282 &'a mut self,
283 cmd: &'a redis::Pipeline,
284 offset: usize,
285 count: usize,
286 ) -> redis::RedisFuture<'a, Vec<redis::Value>> {
287 match self {
288 Self::Client(conn) => conn.req_packed_commands(cmd, offset, count),
289 Self::Cluster(conn) => conn.req_packed_commands(cmd, offset, count),
290 }
291 }
292
293 fn get_db(&self) -> i64 {
294 match self {
295 Self::Client(conn) => conn.get_db(),
296 Self::Cluster(conn) => conn.get_db(),
297 }
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn open_empty_addrs_error() {
307 let result = UniversalClient::open(Vec::<String>::new());
308 assert!(result.is_err());
309 }
310
311 #[test]
312 fn open_single_addr_is_client() {
313 let result = UniversalClient::open(vec!["redis://127.0.0.1:6379"]);
314 assert!(result.unwrap().is_client());
315 }
316
317 #[test]
318 fn open_multiple_addrs_is_cluster() {
319 let result =
320 UniversalClient::open(vec!["redis://127.0.0.1:7000", "redis://127.0.0.1:7001"]);
321 assert!(result.unwrap().is_cluster());
322 }
323
324 #[test]
325 fn builder_empty_addrs_error() {
326 let result = UniversalBuilder::new(Vec::<String>::new()).build();
327 assert!(result.is_err());
328 }
329
330 #[test]
331 fn builder_cluster_true_forces_cluster() {
332 let result = UniversalBuilder::new(vec!["redis://127.0.0.1:6379".to_string()])
333 .cluster(true)
334 .build();
335 assert!(result.unwrap().is_cluster());
336 }
337
338 #[test]
339 fn builder_cluster_false_uses_first_addr() {
340 let result = UniversalBuilder::new(vec![
341 "redis://127.0.0.1:7000".to_string(),
342 "redis://127.0.0.1:7001".to_string(),
343 ])
344 .cluster(false)
345 .build();
346 assert!(result.unwrap().is_client());
347 }
348
349 #[test]
350 fn builder_with_password_is_client() {
351 let result = UniversalBuilder::new(vec!["redis://127.0.0.1:6379".to_string()])
352 .password("secret")
353 .build();
354 assert!(result.unwrap().is_client());
355 }
356
357 #[test]
358 fn builder_with_username_and_password_is_client() {
359 let result = UniversalBuilder::new(vec!["redis://127.0.0.1:6379".to_string()])
360 .username("alice")
361 .password("secret")
362 .build();
363 assert!(result.unwrap().is_client());
364 }
365
366 #[test]
367 fn builder_with_password_cluster_is_cluster() {
368 let result = UniversalBuilder::new(vec![
369 "redis://127.0.0.1:7000".to_string(),
370 "redis://127.0.0.1:7001".to_string(),
371 ])
372 .password("secret")
373 .cluster(true)
374 .build();
375 assert!(result.unwrap().is_cluster());
376 }
377
378 #[cfg(any(feature = "tls-native-tls", feature = "tls-rustls"))]
379 #[test]
380 fn builder_tls_secure_is_client() {
381 let result = UniversalBuilder::new(vec!["redis://127.0.0.1:6380".to_string()])
382 .tls(redis::TlsMode::Secure)
383 .build();
384 assert!(result.unwrap().is_client());
385 }
386
387 #[cfg(any(feature = "tls-native-tls", feature = "tls-rustls"))]
388 #[test]
389 fn builder_tls_insecure_is_client() {
390 let result = UniversalBuilder::new(vec!["redis://127.0.0.1:6380".to_string()])
391 .tls(redis::TlsMode::Insecure)
392 .build();
393 assert!(result.unwrap().is_client());
394 }
395
396 #[cfg(any(feature = "tls-native-tls", feature = "tls-rustls"))]
397 #[test]
398 fn builder_tls_cluster_is_cluster() {
399 let result = UniversalBuilder::new(vec![
400 "redis://127.0.0.1:7000".to_string(),
401 "redis://127.0.0.1:7001".to_string(),
402 ])
403 .tls(redis::TlsMode::Secure)
404 .cluster(true)
405 .build();
406 assert!(result.unwrap().is_cluster());
407 }
408}