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}