Skip to main content

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}