Skip to main content

dynomite/conf/
enums.rs

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