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