1use std::fmt;
10
11use serde::de::{self, Deserializer, Visitor};
12use serde::{Deserialize, Serialize};
13
14use super::error::ConfError;
15use super::tokens::TokenList;
16
17const KETAMA_DEFAULT_PORT: u16 = 11_211;
21
22#[derive(Debug, Clone, Eq, PartialEq)]
35pub struct ConfServer {
36 pname: String,
37 name: String,
38 host: String,
39 port: u16,
40 weight: u32,
41 is_unix: bool,
42}
43
44impl ConfServer {
45 pub fn parse(raw: &str) -> Result<Self, ConfError> {
57 let bad = |reason: &str| ConfError::BadServer {
58 field: "servers",
59 value: raw.to_string(),
60 reason: reason.to_string(),
61 };
62
63 if raw.is_empty() {
64 return Err(bad("empty value"));
65 }
66
67 let (head, friendly_name) = split_optional_friendly_name(raw);
68 let head = head.trim_end();
69
70 if let Some(rest) = head.strip_prefix('/') {
71 let (path_no_prefix, weight) =
73 split_last_colon(rest).ok_or_else(|| bad("unix path requires ':weight' suffix"))?;
74 let weight = parse_weight(weight).ok_or_else(|| bad("invalid weight"))?;
75 let path = format!("/{path_no_prefix}");
76 let name = friendly_name.map_or_else(|| path.clone(), str::to_string);
77 let pname = head.to_string();
78 return Ok(Self {
79 pname,
80 name,
81 host: path,
82 port: 0,
83 weight,
84 is_unix: true,
85 });
86 }
87
88 let (head_no_weight, weight_str) =
90 split_last_colon(head).ok_or_else(|| bad("expected 'host:port:weight'"))?;
91 let weight = parse_weight(weight_str).ok_or_else(|| bad("invalid weight"))?;
92
93 let (host, port_str) =
94 split_last_colon(head_no_weight).ok_or_else(|| bad("expected 'host:port:weight'"))?;
95 let port = parse_port(port_str).ok_or_else(|| bad("port must be in 1..=65535"))?;
96 if host.is_empty() {
97 return Err(bad("empty host"));
98 }
99
100 let name = match friendly_name {
101 Some(n) => n.to_string(),
102 None => {
103 if port == KETAMA_DEFAULT_PORT {
104 host.to_string()
105 } else {
106 format!("{host}:{port_str}")
107 }
108 }
109 };
110
111 Ok(Self {
112 pname: head.to_string(),
113 name,
114 host: host.to_string(),
115 port,
116 weight,
117 is_unix: false,
118 })
119 }
120
121 pub fn pname(&self) -> &str {
131 &self.pname
132 }
133 pub fn name(&self) -> &str {
144 &self.name
145 }
146 pub fn host(&self) -> &str {
155 &self.host
156 }
157 pub fn port(&self) -> u16 {
167 self.port
168 }
169 pub fn weight(&self) -> u32 {
179 self.weight
180 }
181 pub fn is_unix(&self) -> bool {
191 self.is_unix
192 }
193}
194
195impl fmt::Display for ConfServer {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 f.write_str(&self.pname)
198 }
199}
200
201impl Serialize for ConfServer {
202 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
203 ser.serialize_str(&self.pname)
204 }
205}
206
207impl<'de> Deserialize<'de> for ConfServer {
208 fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
209 struct V;
210 impl Visitor<'_> for V {
211 type Value = ConfServer;
212 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 f.write_str("a 'host:port:weight' or '/path:weight' server entry")
214 }
215 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
216 ConfServer::parse(v).map_err(|e| E::custom(e.to_string()))
217 }
218 }
219 de.deserialize_str(V)
220 }
221}
222
223#[derive(Debug, Clone, Eq, PartialEq)]
234pub struct ConfDynSeed {
235 pname: String,
236 name: String,
237 host: String,
238 port: u16,
239 rack: String,
240 dc: String,
241 tokens: TokenList,
242}
243
244impl ConfDynSeed {
245 pub fn parse(raw: &str) -> Result<Self, ConfError> {
257 let bad = |reason: &str| ConfError::BadServer {
258 field: "dyn_seeds",
259 value: raw.to_string(),
260 reason: reason.to_string(),
261 };
262
263 if raw.is_empty() {
264 return Err(bad("empty value"));
265 }
266
267 let (head, friendly_name) = split_optional_friendly_name(raw);
268 let head = head.trim_end();
269
270 let (head, tokens_str) =
272 split_last_colon(head).ok_or_else(|| bad("expected 'host:port:rack:dc:tokens'"))?;
273 let (head, dc) =
275 split_last_colon(head).ok_or_else(|| bad("expected 'host:port:rack:dc:tokens'"))?;
276 let (head, rack) =
278 split_last_colon(head).ok_or_else(|| bad("expected 'host:port:rack:dc:tokens'"))?;
279 let (host, port_str) =
281 split_last_colon(head).ok_or_else(|| bad("expected 'host:port:rack:dc:tokens'"))?;
282
283 if host.is_empty() {
284 return Err(bad("empty host"));
285 }
286 if rack.is_empty() {
287 return Err(bad("empty rack"));
288 }
289 if dc.is_empty() {
290 return Err(bad("empty dc"));
291 }
292
293 let port = parse_port(port_str).ok_or_else(|| bad("port must be in 1..=65535"))?;
294 let tokens = TokenList::parse(tokens_str).map_err(|e| ConfError::BadServer {
295 field: "dyn_seeds",
296 value: raw.to_string(),
297 reason: e.to_string(),
298 })?;
299
300 let name = match friendly_name {
301 Some(n) => n.to_string(),
302 None => {
303 if port == KETAMA_DEFAULT_PORT {
304 host.to_string()
305 } else {
306 format!("{host}:{port_str}")
307 }
308 }
309 };
310
311 Ok(Self {
312 pname: head.to_string(),
313 name,
314 host: host.to_string(),
315 port,
316 rack: rack.to_string(),
317 dc: dc.to_string(),
318 tokens,
319 })
320 }
321
322 pub fn pname(&self) -> &str {
333 &self.pname
334 }
335 pub fn name(&self) -> &str {
347 &self.name
348 }
349 pub fn host(&self) -> &str {
358 &self.host
359 }
360 pub fn port(&self) -> u16 {
369 self.port
370 }
371 pub fn rack(&self) -> &str {
380 &self.rack
381 }
382 pub fn dc(&self) -> &str {
391 &self.dc
392 }
393 pub fn tokens(&self) -> &TokenList {
402 &self.tokens
403 }
404}
405
406impl fmt::Display for ConfDynSeed {
407 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408 f.write_str(&self.pname)
409 }
410}
411
412impl Serialize for ConfDynSeed {
413 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
414 ser.serialize_str(&self.pname)
415 }
416}
417
418impl<'de> Deserialize<'de> for ConfDynSeed {
419 fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
420 struct V;
421 impl Visitor<'_> for V {
422 type Value = ConfDynSeed;
423 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424 f.write_str("a 'host:port:rack:dc:tokens' dyn_seeds entry")
425 }
426 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
427 ConfDynSeed::parse(v).map_err(|e| E::custom(e.to_string()))
428 }
429 }
430 de.deserialize_str(V)
431 }
432}
433
434fn split_optional_friendly_name(raw: &str) -> (&str, Option<&str>) {
437 if let Some(idx) = raw.rfind(' ') {
438 let (head, tail) = raw.split_at(idx);
442 let tail = &tail[1..];
443 if !tail.is_empty() && head.contains(':') {
444 return (head, Some(tail));
445 }
446 }
447 (raw, None)
448}
449
450fn split_last_colon(s: &str) -> Option<(&str, &str)> {
451 let idx = s.rfind(':')?;
452 Some((&s[..idx], &s[idx + 1..]))
453}
454
455fn parse_port(s: &str) -> Option<u16> {
456 let n: u16 = s.parse().ok()?;
457 if n > 0 {
458 Some(n)
459 } else {
460 None
461 }
462}
463
464fn parse_weight(s: &str) -> Option<u32> {
465 s.parse().ok()
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[test]
473 fn server_basic() {
474 let s = ConfServer::parse("127.0.0.1:22122:1").unwrap();
475 assert_eq!(s.host(), "127.0.0.1");
476 assert_eq!(s.port(), 22122);
477 assert_eq!(s.weight(), 1);
478 assert_eq!(s.name(), "127.0.0.1:22122");
479 assert!(!s.is_unix());
480 }
481
482 #[test]
483 fn server_with_friendly_name() {
484 let s = ConfServer::parse("127.0.0.1:6379:1 redis_a").unwrap();
485 assert_eq!(s.host(), "127.0.0.1");
486 assert_eq!(s.port(), 6379);
487 assert_eq!(s.name(), "redis_a");
488 assert_eq!(s.pname(), "127.0.0.1:6379:1");
489 }
490
491 #[test]
492 fn server_default_ketama_port_drops_port_from_name() {
493 let s = ConfServer::parse("10.0.0.1:11211:1").unwrap();
494 assert_eq!(s.name(), "10.0.0.1");
495 }
496
497 #[test]
498 fn server_unix_socket() {
499 let s = ConfServer::parse("/tmp/redis.sock:1").unwrap();
500 assert!(s.is_unix());
501 assert_eq!(s.host(), "/tmp/redis.sock");
502 assert_eq!(s.port(), 0);
503 }
504
505 #[test]
506 fn server_bad_format() {
507 assert!(ConfServer::parse("just-a-host").is_err());
508 assert!(ConfServer::parse("a:b:c").is_err());
509 assert!(ConfServer::parse("").is_err());
510 }
511
512 #[test]
513 fn dyn_seed_basic() {
514 let s = ConfDynSeed::parse("127.0.0.2:8101:rack2:dc2:1383429731").unwrap();
515 assert_eq!(s.host(), "127.0.0.2");
516 assert_eq!(s.port(), 8101);
517 assert_eq!(s.rack(), "rack2");
518 assert_eq!(s.dc(), "dc2");
519 assert_eq!(s.tokens().to_string(), "1383429731");
520 }
521
522 #[test]
523 fn dyn_seed_multi_tokens() {
524 let s = ConfDynSeed::parse("h:1:r:d:1,2,3 friendly").unwrap();
525 assert_eq!(s.tokens().len(), 3);
526 assert_eq!(s.name(), "friendly");
527 }
528
529 #[test]
530 fn dyn_seed_bad() {
531 assert!(ConfDynSeed::parse("a:b:c:d").is_err());
532 assert!(ConfDynSeed::parse("h:1:r::1").is_err());
533 }
534}