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