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}