dynomite/conf/enums.rs
1//! Typed enums for configuration values that arrive as free-form
2//! strings or small integer codes.
3
4use std::fmt;
5
6use serde::de::{self, Deserializer, Visitor};
7use serde::{Deserialize, Serialize};
8
9use super::error::ConfError;
10
11macro_rules! string_enum_serde {
12 ($t:ty) => {
13 impl Serialize for $t {
14 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
15 ser.serialize_str(self.as_str())
16 }
17 }
18
19 impl<'de> Deserialize<'de> for $t {
20 fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
21 struct V;
22 impl Visitor<'_> for V {
23 type Value = $t;
24 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 f.write_str(concat!("a string naming a ", stringify!($t)))
26 }
27 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
28 <$t>::parse(v).map_err(|e| E::custom(e.to_string()))
29 }
30 fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
31 self.visit_str(&v)
32 }
33 }
34 de.deserialize_str(V)
35 }
36 }
37 };
38}
39
40string_enum_serde!(SecureServerOption);
41string_enum_serde!(HashType);
42string_enum_serde!(Distribution);
43string_enum_serde!(Transport);
44string_enum_serde!(Membership);
45
46/// Membership / failure-detection backend selected by the pool's
47/// `membership:` directive.
48///
49/// `Gossip` is the historical default: Dynamo-style peer-state
50/// dissemination with a phi-accrual failure detector
51/// ([`crate::cluster::gossip`], [`crate::cluster::failure_detector`]).
52/// `Swim` selects the opt-in SWIM + Lifeguard backend
53/// ([`crate::cluster::swim`]), which trades all-to-all heartbeating
54/// for a randomised probe with indirect (PING-REQ) probes,
55/// incarnation-number refutation, and Lifeguard local health
56/// awareness -- aimed at faster convergence and fewer false peer-down
57/// verdicts as clusters grow and churn. SWIM is opt-in until the
58/// large-scale churn data justifies making it the default.
59///
60/// # Examples
61///
62/// ```
63/// use dynomite::conf::Membership;
64/// assert_eq!(Membership::parse("gossip").unwrap(), Membership::Gossip);
65/// assert_eq!(Membership::parse("swim").unwrap(), Membership::Swim);
66/// assert_eq!(Membership::default(), Membership::Gossip);
67/// ```
68#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
69pub enum Membership {
70 /// Dynamo-style gossip plus phi-accrual. The historical
71 /// default.
72 #[default]
73 Gossip,
74 /// SWIM + Lifeguard probe-based membership. Opt-in.
75 Swim,
76}
77
78impl Membership {
79 /// Parse a `membership:` value (case-insensitive).
80 ///
81 /// # Errors
82 /// Returns [`ConfError::BadServer`] when the value is not a
83 /// recognised backend.
84 ///
85 /// # Examples
86 ///
87 /// ```
88 /// use dynomite::conf::Membership;
89 /// assert_eq!(Membership::parse("SWIM").unwrap(), Membership::Swim);
90 /// assert!(Membership::parse("raft").is_err());
91 /// ```
92 pub fn parse(s: &str) -> Result<Self, ConfError> {
93 match s.to_ascii_lowercase().as_str() {
94 "gossip" => Ok(Membership::Gossip),
95 "swim" => Ok(Membership::Swim),
96 other => Err(ConfError::BadServer {
97 field: "membership",
98 value: other.to_string(),
99 reason: "membership must be 'gossip' or 'swim'".to_string(),
100 }),
101 }
102 }
103
104 /// Render back to the canonical YAML name.
105 ///
106 /// # Examples
107 ///
108 /// ```
109 /// use dynomite::conf::Membership;
110 /// assert_eq!(Membership::Gossip.as_str(), "gossip");
111 /// assert_eq!(Membership::Swim.as_str(), "swim");
112 /// ```
113 #[must_use]
114 pub const fn as_str(self) -> &'static str {
115 match self {
116 Membership::Gossip => "gossip",
117 Membership::Swim => "swim",
118 }
119 }
120}
121
122impl fmt::Display for Membership {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 f.write_str(self.as_str())
125 }
126}
127
128/// Transport selected by the pool's `transport:` directive.
129///
130/// Controls which network stack the proxy listener binds.
131/// `Tcp` is the historical default and the only option a build
132/// without the `quic` Cargo feature can satisfy.
133///
134/// # Examples
135///
136/// ```
137/// use dynomite::conf::Transport;
138/// assert_eq!(Transport::parse("tcp").unwrap(), Transport::Tcp);
139/// assert_eq!(Transport::parse("quic").unwrap(), Transport::Quic);
140/// assert_eq!(Transport::default(), Transport::Tcp);
141/// ```
142#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
143pub enum Transport {
144 /// TCP transport. The historical default and the only
145 /// option compiled in when the `quic` Cargo feature is
146 /// off.
147 #[default]
148 Tcp,
149 /// QUIC transport. Requires the `quic` Cargo feature on
150 /// the engine and a server cert / key pair supplied via
151 /// the pool's `quic_cert_file:` and `quic_key_file:`
152 /// directives.
153 Quic,
154}
155
156impl Transport {
157 /// Parse a `transport:` value (case-insensitive).
158 ///
159 /// # Errors
160 /// Returns [`ConfError::BadServer`] when the value is
161 /// not a recognised transport.
162 ///
163 /// # Examples
164 ///
165 /// ```
166 /// use dynomite::conf::Transport;
167 /// assert_eq!(Transport::parse("TCP").unwrap(), Transport::Tcp);
168 /// assert!(Transport::parse("http").is_err());
169 /// ```
170 pub fn parse(s: &str) -> Result<Self, ConfError> {
171 match s.to_ascii_lowercase().as_str() {
172 "tcp" => Ok(Transport::Tcp),
173 "quic" => Ok(Transport::Quic),
174 other => Err(ConfError::BadServer {
175 field: "transport",
176 value: other.to_string(),
177 reason: "transport must be 'tcp' or 'quic'".to_string(),
178 }),
179 }
180 }
181
182 /// Render back to the canonical YAML name.
183 ///
184 /// # Examples
185 ///
186 /// ```
187 /// use dynomite::conf::Transport;
188 /// assert_eq!(Transport::Tcp.as_str(), "tcp");
189 /// assert_eq!(Transport::Quic.as_str(), "quic");
190 /// ```
191 #[must_use]
192 pub const fn as_str(self) -> &'static str {
193 match self {
194 Transport::Tcp => "tcp",
195 Transport::Quic => "quic",
196 }
197 }
198}
199
200impl fmt::Display for Transport {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 f.write_str(self.as_str())
203 }
204}
205
206/// Distribution algorithm selected by the pool's `distribution:`
207/// directive.
208///
209/// `Vnode` is the historical default and was the only mode
210/// supported until `RandomSlicing` was added. `Ketama`, `Modula`,
211/// and `Random` are accepted for backward compatibility with the
212/// older configuration vocabulary; they collapse to `Vnode` at
213/// runtime and emit a deprecation warning at config-load time.
214///
215/// # Examples
216///
217/// ```
218/// use dynomite::conf::Distribution;
219/// assert_eq!(Distribution::parse("vnode").unwrap(), Distribution::Vnode);
220/// assert_eq!(
221/// Distribution::parse("random_slicing").unwrap(),
222/// Distribution::RandomSlicing
223/// );
224/// ```
225#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
226pub enum Distribution {
227 /// Per-rack continuum keyed by per-peer token lists. The
228 /// historical default.
229 #[default]
230 Vnode,
231 /// Compatibility alias; collapsed
232 /// to [`Self::Vnode`] at runtime with a deprecation warning.
233 Ketama,
234 /// Compatibility alias; collapsed
235 /// to [`Self::Vnode`] at runtime with a deprecation warning.
236 Modula,
237 /// Compatibility alias; collapsed
238 /// to [`Self::Vnode`] at runtime with a deprecation warning.
239 Random,
240 /// Random-slicing distribution: a small, gap-free `(name,
241 /// size)` partition table over the 64-bit hash space. See
242 /// [`crate::hashkit::random_slicing`].
243 RandomSlicing,
244}
245
246impl Distribution {
247 /// Parse a `distribution:` value (case-insensitive).
248 ///
249 /// # Errors
250 /// Returns [`ConfError::BadDistribution`] when the value is
251 /// not a recognised mode.
252 ///
253 /// # Examples
254 ///
255 /// ```
256 /// use dynomite::conf::Distribution;
257 /// assert_eq!(Distribution::parse("VNODE").unwrap(), Distribution::Vnode);
258 /// assert!(Distribution::parse("sphere").is_err());
259 /// ```
260 pub fn parse(s: &str) -> Result<Self, ConfError> {
261 Ok(match s.to_ascii_lowercase().as_str() {
262 "vnode" => Distribution::Vnode,
263 "ketama" => Distribution::Ketama,
264 "modula" => Distribution::Modula,
265 "random" => Distribution::Random,
266 "random_slicing" | "random-slicing" => Distribution::RandomSlicing,
267 _ => return Err(ConfError::BadDistribution(s.to_string())),
268 })
269 }
270
271 /// Render back to the canonical YAML name.
272 ///
273 /// # Examples
274 ///
275 /// ```
276 /// use dynomite::conf::Distribution;
277 /// assert_eq!(Distribution::Vnode.as_str(), "vnode");
278 /// assert_eq!(Distribution::RandomSlicing.as_str(), "random_slicing");
279 /// ```
280 #[must_use]
281 pub const fn as_str(self) -> &'static str {
282 match self {
283 Distribution::Vnode => "vnode",
284 Distribution::Ketama => "ketama",
285 Distribution::Modula => "modula",
286 Distribution::Random => "random",
287 Distribution::RandomSlicing => "random_slicing",
288 }
289 }
290
291 /// True for the modes honoured directly; `Ketama`, `Modula`,
292 /// and `Random` are accepted
293 /// for backward compatibility but collapse to `Vnode` at
294 /// runtime.
295 ///
296 /// # Examples
297 ///
298 /// ```
299 /// use dynomite::conf::Distribution;
300 /// assert!(Distribution::Vnode.is_supported());
301 /// assert!(Distribution::RandomSlicing.is_supported());
302 /// assert!(!Distribution::Ketama.is_supported());
303 /// ```
304 #[must_use]
305 pub const fn is_supported(self) -> bool {
306 matches!(self, Distribution::Vnode | Distribution::RandomSlicing)
307 }
308}
309
310impl fmt::Display for Distribution {
311 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312 f.write_str(self.as_str())
313 }
314}
315
316/// Datastore family selected by `data_store:`.
317///
318/// # Examples
319///
320/// ```
321/// use dynomite::conf::DataStore;
322/// assert_eq!(DataStore::from_int(0).unwrap(), DataStore::Valkey);
323/// assert_eq!(DataStore::Valkey.as_int(), 0);
324/// assert_eq!(DataStore::from_name("valkey").unwrap(), DataStore::Valkey);
325/// assert_eq!(DataStore::from_name("dyniak").unwrap(), DataStore::Dyniak);
326/// ```
327#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
328pub enum DataStore {
329 /// Valkey backend, spoken over RESP (the protocol Valkey
330 /// speaks). Encoded as `0` in YAML. The historical name
331 /// `redis` is accepted as a back-compat alias on the YAML
332 /// side and maps to this variant.
333 Valkey,
334 /// Memcached ASCII datastore. Encoded as `1` in YAML.
335 Memcache,
336 /// In-process dyniak datastore (Riak-shaped, backed by a
337 /// transactional Noxu environment). Encoded as `2` in YAML,
338 /// or as the string `dyniak`. Selecting this variant
339 /// requires `dynomited` to be built with `--features riak`
340 /// and a sibling `noxu_path:` knob. A dyniak pool serves the
341 /// Riak PBC / HTTP surface; it does not run a RESP client
342 /// proxy.
343 Dyniak,
344}
345
346impl DataStore {
347 /// Parse a `data_store:` value as it appears in YAML.
348 ///
349 /// # Examples
350 ///
351 /// ```
352 /// use dynomite::conf::DataStore;
353 /// assert_eq!(DataStore::from_int(1).unwrap(), DataStore::Memcache);
354 /// assert_eq!(DataStore::from_int(2).unwrap(), DataStore::Dyniak);
355 /// assert!(DataStore::from_int(7).is_err());
356 /// ```
357 pub fn from_int(v: i64) -> Result<Self, ConfError> {
358 match v {
359 0 => Ok(DataStore::Valkey),
360 1 => Ok(DataStore::Memcache),
361 2 => Ok(DataStore::Dyniak),
362 n => Err(ConfError::BadDataStore(n)),
363 }
364 }
365
366 /// Parse the textual form of a `data_store:` value, as
367 /// accepted in YAML alongside the integer form.
368 ///
369 /// Comparison is case-insensitive. `valkey` is the canonical
370 /// name for the RESP backend; `redis` is accepted as a
371 /// back-compat alias for the same variant so configs written
372 /// before the Valkey rename keep working. `memcache` and
373 /// `memcached` select [`DataStore::Memcache`]; `dyniak`
374 /// selects [`DataStore::Dyniak`].
375 ///
376 /// # Examples
377 ///
378 /// ```
379 /// use dynomite::conf::DataStore;
380 /// assert_eq!(DataStore::from_name("VALKEY").unwrap(), DataStore::Valkey);
381 /// assert_eq!(DataStore::from_name("redis").unwrap(), DataStore::Valkey);
382 /// assert!(DataStore::from_name("sql").is_err());
383 /// ```
384 pub fn from_name(s: &str) -> Result<Self, ConfError> {
385 if s.eq_ignore_ascii_case("valkey") || s.eq_ignore_ascii_case("redis") {
386 Ok(DataStore::Valkey)
387 } else if s.eq_ignore_ascii_case("memcache") || s.eq_ignore_ascii_case("memcached") {
388 Ok(DataStore::Memcache)
389 } else if s.eq_ignore_ascii_case("dyniak") {
390 Ok(DataStore::Dyniak)
391 } else {
392 Err(ConfError::BadDataStore(-1))
393 }
394 }
395
396 /// Encode back to the small integer used in YAML.
397 ///
398 /// # Examples
399 ///
400 /// ```
401 /// use dynomite::conf::DataStore;
402 /// assert_eq!(DataStore::Memcache.as_int(), 1);
403 /// assert_eq!(DataStore::Dyniak.as_int(), 2);
404 /// ```
405 pub fn as_int(self) -> i64 {
406 match self {
407 DataStore::Valkey => 0,
408 DataStore::Memcache => 1,
409 DataStore::Dyniak => 2,
410 }
411 }
412
413 /// Return the canonical lower-case textual name.
414 ///
415 /// # Examples
416 ///
417 /// ```
418 /// use dynomite::conf::DataStore;
419 /// assert_eq!(DataStore::Valkey.as_name(), "valkey");
420 /// assert_eq!(DataStore::Dyniak.as_name(), "dyniak");
421 /// ```
422 pub fn as_name(self) -> &'static str {
423 match self {
424 DataStore::Valkey => "valkey",
425 DataStore::Memcache => "memcache",
426 DataStore::Dyniak => "dyniak",
427 }
428 }
429}
430
431/// Inter-node security mode selected by `secure_server_option:`.
432///
433/// # Examples
434///
435/// ```
436/// use dynomite::conf::SecureServerOption;
437/// assert_eq!(
438/// SecureServerOption::parse("datacenter").unwrap(),
439/// SecureServerOption::Datacenter,
440/// );
441/// ```
442#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
443pub enum SecureServerOption {
444 /// No inter-node TLS.
445 None,
446 /// TLS only between racks (within a DC).
447 Rack,
448 /// TLS only between datacenters.
449 Datacenter,
450 /// TLS between all nodes.
451 All,
452}
453
454impl SecureServerOption {
455 /// Parse a `secure_server_option:` value, case-sensitively.
456 ///
457 /// # Examples
458 ///
459 /// ```
460 /// use dynomite::conf::SecureServerOption;
461 /// assert_eq!(SecureServerOption::parse("none").unwrap(), SecureServerOption::None);
462 /// assert!(SecureServerOption::parse("NONE").is_err());
463 /// ```
464 pub fn parse(s: &str) -> Result<Self, ConfError> {
465 match s {
466 "none" => Ok(SecureServerOption::None),
467 "rack" => Ok(SecureServerOption::Rack),
468 "datacenter" => Ok(SecureServerOption::Datacenter),
469 "all" => Ok(SecureServerOption::All),
470 other => Err(ConfError::BadSecure(other.to_string())),
471 }
472 }
473
474 /// Render back to the YAML string form.
475 ///
476 /// # Examples
477 ///
478 /// ```
479 /// use dynomite::conf::SecureServerOption;
480 /// assert_eq!(SecureServerOption::All.as_str(), "all");
481 /// ```
482 pub fn as_str(self) -> &'static str {
483 match self {
484 SecureServerOption::None => "none",
485 SecureServerOption::Rack => "rack",
486 SecureServerOption::Datacenter => "datacenter",
487 SecureServerOption::All => "all",
488 }
489 }
490}
491
492impl fmt::Display for SecureServerOption {
493 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
494 f.write_str(self.as_str())
495 }
496}
497
498/// Quorum policy for read or write paths.
499///
500/// # Examples
501///
502/// ```
503/// use dynomite::conf::ConsistencyLevel;
504/// let lvl = ConsistencyLevel::parse("read_consistency", "DC_QUORUM").unwrap();
505/// assert_eq!(lvl, ConsistencyLevel::DcQuorum);
506/// ```
507#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
508pub enum ConsistencyLevel {
509 /// Single replica acknowledgement.
510 DcOne,
511 /// Majority within a single datacenter.
512 DcQuorum,
513 /// Majority within a single datacenter with checksum repair.
514 DcSafeQuorum,
515 /// Majority within every datacenter, with checksum repair.
516 DcEachSafeQuorum,
517}
518
519impl ConsistencyLevel {
520 /// Parse a `read_consistency` or `write_consistency` value.
521 ///
522 /// Comparison is case-insensitive against the canonical names
523 /// `DC_ONE`, `DC_QUORUM`, `DC_SAFE_QUORUM`, and
524 /// `DC_EACH_SAFE_QUORUM`.
525 ///
526 /// # Examples
527 ///
528 /// ```
529 /// use dynomite::conf::ConsistencyLevel;
530 /// assert_eq!(
531 /// ConsistencyLevel::parse("read_consistency", "dc_one").unwrap(),
532 /// ConsistencyLevel::DcOne,
533 /// );
534 /// assert!(ConsistencyLevel::parse("read_consistency", "nope").is_err());
535 /// ```
536 pub fn parse(field: &'static str, s: &str) -> Result<Self, ConfError> {
537 if s.eq_ignore_ascii_case("dc_one") {
538 Ok(ConsistencyLevel::DcOne)
539 } else if s.eq_ignore_ascii_case("dc_quorum") {
540 Ok(ConsistencyLevel::DcQuorum)
541 } else if s.eq_ignore_ascii_case("dc_safe_quorum") {
542 Ok(ConsistencyLevel::DcSafeQuorum)
543 } else if s.eq_ignore_ascii_case("dc_each_safe_quorum") {
544 Ok(ConsistencyLevel::DcEachSafeQuorum)
545 } else {
546 Err(ConfError::BadConsistency {
547 field,
548 value: s.to_string(),
549 })
550 }
551 }
552
553 /// Render back to the canonical YAML name.
554 ///
555 /// # Examples
556 ///
557 /// ```
558 /// use dynomite::conf::ConsistencyLevel;
559 /// assert_eq!(ConsistencyLevel::DcSafeQuorum.as_str(), "DC_SAFE_QUORUM");
560 /// ```
561 pub fn as_str(self) -> &'static str {
562 match self {
563 ConsistencyLevel::DcOne => "DC_ONE",
564 ConsistencyLevel::DcQuorum => "DC_QUORUM",
565 ConsistencyLevel::DcSafeQuorum => "DC_SAFE_QUORUM",
566 ConsistencyLevel::DcEachSafeQuorum => "DC_EACH_SAFE_QUORUM",
567 }
568 }
569}
570
571impl fmt::Display for ConsistencyLevel {
572 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
573 f.write_str(self.as_str())
574 }
575}
576
577/// Hash algorithm selected by `hash:`.
578///
579/// The names mirror the algorithm tags accepted by the YAML parser.
580/// The [`crate::hashkit`] module owns the hashing math; this enum
581/// models only the configured choice so the parser can echo it back
582/// without depending on the
583/// hashkit module.
584///
585/// # Examples
586///
587/// ```
588/// use dynomite::conf::HashType;
589/// assert_eq!(HashType::parse("murmur3").unwrap(), HashType::Murmur3);
590/// assert_eq!(HashType::Md5.as_str(), "md5");
591/// ```
592#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
593pub enum HashType {
594 /// One-at-a-time hash.
595 OneAtATime,
596 /// MD5 (truncated for ketama).
597 Md5,
598 /// CRC-16.
599 Crc16,
600 /// CRC-32.
601 Crc32,
602 /// CRC-32 ARM.
603 Crc32a,
604 /// 64-bit FNV-1.
605 Fnv1_64,
606 /// 64-bit FNV-1a.
607 Fnv1a64,
608 /// 32-bit FNV-1.
609 Fnv1_32,
610 /// 32-bit FNV-1a.
611 Fnv1a32,
612 /// Paul Hsieh's hash.
613 Hsieh,
614 /// Murmur hash (32-bit, version 1).
615 Murmur,
616 /// Bob Jenkins's hash.
617 Jenkins,
618 /// Murmur hash 3 (128-bit).
619 Murmur3,
620 /// MurmurHash3 truncated to 64 bits (used by random
621 /// slicing).
622 #[allow(non_camel_case_types)]
623 Murmur3X64_64,
624}
625
626impl HashType {
627 /// Parse a `hash:` value (case-sensitive).
628 ///
629 /// # Examples
630 ///
631 /// ```
632 /// use dynomite::conf::HashType;
633 /// assert_eq!(HashType::parse("fnv1a_64").unwrap(), HashType::Fnv1a64);
634 /// assert!(HashType::parse("FNV1A_64").is_err());
635 /// ```
636 pub fn parse(s: &str) -> Result<Self, ConfError> {
637 Ok(match s {
638 "one_at_a_time" => HashType::OneAtATime,
639 "md5" => HashType::Md5,
640 "crc16" => HashType::Crc16,
641 "crc32" => HashType::Crc32,
642 "crc32a" => HashType::Crc32a,
643 "fnv1_64" => HashType::Fnv1_64,
644 "fnv1a_64" => HashType::Fnv1a64,
645 "fnv1_32" => HashType::Fnv1_32,
646 "fnv1a_32" => HashType::Fnv1a32,
647 "hsieh" => HashType::Hsieh,
648 "murmur" => HashType::Murmur,
649 "jenkins" => HashType::Jenkins,
650 "murmur3" => HashType::Murmur3,
651 "murmur3_x64_64" => HashType::Murmur3X64_64,
652 other => return Err(ConfError::BadHash(other.to_string())),
653 })
654 }
655
656 /// Render back to the canonical YAML name.
657 ///
658 /// # Examples
659 ///
660 /// ```
661 /// use dynomite::conf::HashType;
662 /// assert_eq!(HashType::Crc32a.as_str(), "crc32a");
663 /// ```
664 pub fn as_str(self) -> &'static str {
665 match self {
666 HashType::OneAtATime => "one_at_a_time",
667 HashType::Md5 => "md5",
668 HashType::Crc16 => "crc16",
669 HashType::Crc32 => "crc32",
670 HashType::Crc32a => "crc32a",
671 HashType::Fnv1_64 => "fnv1_64",
672 HashType::Fnv1a64 => "fnv1a_64",
673 HashType::Fnv1_32 => "fnv1_32",
674 HashType::Fnv1a32 => "fnv1a_32",
675 HashType::Hsieh => "hsieh",
676 HashType::Murmur => "murmur",
677 HashType::Jenkins => "jenkins",
678 HashType::Murmur3 => "murmur3",
679 HashType::Murmur3X64_64 => "murmur3_x64_64",
680 }
681 }
682}
683
684impl fmt::Display for HashType {
685 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
686 f.write_str(self.as_str())
687 }
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693
694 #[test]
695 fn data_store_round_trip() {
696 assert_eq!(DataStore::from_int(0).unwrap(), DataStore::Valkey);
697 assert_eq!(DataStore::from_int(1).unwrap(), DataStore::Memcache);
698 assert_eq!(DataStore::from_int(2).unwrap(), DataStore::Dyniak);
699 assert!(matches!(
700 DataStore::from_int(7),
701 Err(ConfError::BadDataStore(7))
702 ));
703 assert_eq!(DataStore::from_name("dyniak").unwrap(), DataStore::Dyniak);
704 assert_eq!(DataStore::from_name("VALKEY").unwrap(), DataStore::Valkey);
705 // `redis` stays accepted as a back-compat alias for the
706 // Valkey variant so pre-rename configs keep loading.
707 assert_eq!(DataStore::from_name("redis").unwrap(), DataStore::Valkey);
708 assert!(DataStore::from_name("sql").is_err());
709 assert_eq!(DataStore::Valkey.as_name(), "valkey");
710 assert_eq!(DataStore::Dyniak.as_name(), "dyniak");
711 }
712
713 #[test]
714 fn secure_round_trip() {
715 for s in ["none", "rack", "datacenter", "all"] {
716 assert_eq!(SecureServerOption::parse(s).unwrap().as_str(), s);
717 }
718 assert!(SecureServerOption::parse("nope").is_err());
719 }
720
721 #[test]
722 fn consistency_case_insensitive() {
723 assert_eq!(
724 ConsistencyLevel::parse("read_consistency", "dc_one").unwrap(),
725 ConsistencyLevel::DcOne
726 );
727 assert_eq!(
728 ConsistencyLevel::parse("read_consistency", "DC_SAFE_QUORUM").unwrap(),
729 ConsistencyLevel::DcSafeQuorum
730 );
731 assert!(ConsistencyLevel::parse("read_consistency", "garbage").is_err());
732 }
733
734 #[test]
735 fn hash_round_trip() {
736 for &name in &[
737 "one_at_a_time",
738 "md5",
739 "crc16",
740 "crc32",
741 "crc32a",
742 "fnv1_64",
743 "fnv1a_64",
744 "fnv1_32",
745 "fnv1a_32",
746 "hsieh",
747 "murmur",
748 "jenkins",
749 "murmur3",
750 "murmur3_x64_64",
751 ] {
752 assert_eq!(HashType::parse(name).unwrap().as_str(), name);
753 }
754 }
755
756 #[test]
757 fn distribution_round_trip() {
758 for &name in &["vnode", "ketama", "modula", "random", "random_slicing"] {
759 assert_eq!(Distribution::parse(name).unwrap().as_str(), name);
760 }
761 // Case-insensitive parse for back-compat with the C
762 // reference, which accepts upper-case.
763 assert_eq!(Distribution::parse("VNODE").unwrap(), Distribution::Vnode);
764 // Hyphenated alias accepted.
765 assert_eq!(
766 Distribution::parse("random-slicing").unwrap(),
767 Distribution::RandomSlicing
768 );
769 assert!(matches!(
770 Distribution::parse("sphere"),
771 Err(ConfError::BadDistribution(_))
772 ));
773 assert!(Distribution::Vnode.is_supported());
774 assert!(Distribution::RandomSlicing.is_supported());
775 assert!(!Distribution::Ketama.is_supported());
776 }
777
778 #[test]
779 fn distribution_default_is_vnode() {
780 assert_eq!(Distribution::default(), Distribution::Vnode);
781 }
782
783 #[test]
784 fn distribution_yaml_round_trip() {
785 // Serialise via serde, then parse back.
786 let raw = serde_yaml::to_string(&Distribution::RandomSlicing).unwrap();
787 let parsed: Distribution = serde_yaml::from_str(&raw).unwrap();
788 assert_eq!(parsed, Distribution::RandomSlicing);
789 }
790
791 #[test]
792 fn transport_round_trip() {
793 for &name in &["tcp", "quic"] {
794 assert_eq!(Transport::parse(name).unwrap().as_str(), name);
795 }
796 // Case-insensitive parse.
797 assert_eq!(Transport::parse("QUIC").unwrap(), Transport::Quic);
798 assert_eq!(Transport::parse("Tcp").unwrap(), Transport::Tcp);
799 assert!(Transport::parse("http").is_err());
800 }
801
802 #[test]
803 fn transport_default_is_tcp() {
804 assert_eq!(Transport::default(), Transport::Tcp);
805 }
806
807 #[test]
808 fn transport_yaml_round_trip() {
809 let raw = serde_yaml::to_string(&Transport::Quic).unwrap();
810 let parsed: Transport = serde_yaml::from_str(&raw).unwrap();
811 assert_eq!(parsed, Transport::Quic);
812 }
813
814 #[test]
815 fn membership_round_trip() {
816 for &name in &["gossip", "swim"] {
817 assert_eq!(Membership::parse(name).unwrap().as_str(), name);
818 }
819 // Case-insensitive parse.
820 assert_eq!(Membership::parse("SWIM").unwrap(), Membership::Swim);
821 assert_eq!(Membership::parse("Gossip").unwrap(), Membership::Gossip);
822 assert!(Membership::parse("raft").is_err());
823 }
824
825 #[test]
826 fn membership_default_is_gossip() {
827 assert_eq!(Membership::default(), Membership::Gossip);
828 }
829
830 #[test]
831 fn membership_yaml_round_trip() {
832 let raw = serde_yaml::to_string(&Membership::Swim).unwrap();
833 let parsed: Membership = serde_yaml::from_str(&raw).unwrap();
834 assert_eq!(parsed, Membership::Swim);
835 }
836}