Skip to main content

dynomite/conf/
error.rs

1//! Typed errors emitted by configuration parsing and validation.
2
3use std::path::PathBuf;
4
5use thiserror::Error;
6
7/// Errors that can occur while loading or validating a [`Config`].
8///
9/// [`Config`]: crate::conf::Config
10///
11/// # Examples
12///
13/// ```
14/// use dynomite::conf::{Config, ConfError};
15/// let err = Config::parse_str("").unwrap_err();
16/// assert!(matches!(err, ConfError::Yaml { .. } | ConfError::EmptyDocument));
17/// ```
18#[derive(Debug, Error)]
19pub enum ConfError {
20    /// I/O error while reading a configuration file.
21    #[error("conf: failed to read configuration file '{path}': {source}")]
22    Io {
23        /// The path that triggered the failure.
24        path: PathBuf,
25        /// The underlying I/O error.
26        #[source]
27        source: std::io::Error,
28    },
29
30    /// The YAML document was empty or missing the top-level pool.
31    #[error("conf: configuration document is empty")]
32    EmptyDocument,
33
34    /// The top-level mapping had more than one pool.
35    #[error("conf: configuration must contain exactly one pool, found {0}")]
36    TooManyPools(usize),
37
38    /// The top-level pool name was the empty string.
39    #[error("conf: pool name must not be empty")]
40    EmptyPoolName,
41
42    /// A directive name in the YAML is not recognized.
43    #[error("conf: directive '{name}' is unknown")]
44    UnknownKey {
45        /// The unrecognized YAML key.
46        name: String,
47    },
48
49    /// A required directive is absent from the configuration.
50    #[error("conf: directive '{0}' is missing")]
51    MissingRequired(&'static str),
52
53    /// `listen` / `dyn_listen` / `stats_listen` address could not be parsed.
54    #[error("conf: '{field}' has an invalid address '{value}': {reason}")]
55    BadAddr {
56        /// The directive whose value failed to parse.
57        field: &'static str,
58        /// The string that failed to parse.
59        value: String,
60        /// Human-readable parse failure reason.
61        reason: String,
62    },
63
64    /// A token list could not be parsed as comma-separated big-ints.
65    #[error("conf: token list '{value}' is not valid: {reason}")]
66    BadToken {
67        /// The token-list string.
68        value: String,
69        /// Human-readable parse failure reason.
70        reason: String,
71    },
72
73    /// `read_consistency` / `write_consistency` value is not a known level.
74    #[error("conf: directive '{field}' must be one of 'DC_ONE', 'DC_QUORUM', 'DC_SAFE_QUORUM', 'DC_EACH_SAFE_QUORUM', got '{value}'")]
75    BadConsistency {
76        /// The directive: `read_consistency` or `write_consistency`.
77        field: &'static str,
78        /// The unrecognized value.
79        value: String,
80    },
81
82    /// `secure_server_option` value is not a known mode.
83    #[error("conf: directive 'secure_server_option' must be one of 'none', 'rack', 'datacenter', 'all', got '{0}'")]
84    BadSecure(String),
85
86    /// `data_store` value is not 0 (Valkey), 1 (Memcache), or 2
87    /// (Dyniak). The string forms `valkey` (and the back-compat
88    /// alias `redis`), `memcache`, and `dyniak` are also accepted
89    /// on the YAML side and are translated to these integers
90    /// before validation.
91    #[error(
92        "conf: directive 'data_store' must be 0 (valkey), 1 (memcache), or 2 (dyniak), got {0}"
93    )]
94    BadDataStore(i64),
95
96    /// `data_store: dyniak` was selected but `noxu_path` was not
97    /// supplied or `dynomited` was built without `--features
98    /// riak`.
99    #[error("conf: {0}")]
100    BadDyniakConfig(&'static str),
101
102    /// `hash` value is not a recognized hash algorithm name.
103    #[error("conf: directive 'hash' is not a valid hash function, got '{0}'")]
104    BadHash(String),
105
106    /// `distribution` value is not a recognized distribution
107    /// algorithm name.
108    #[error("conf: directive 'distribution' is not a valid distribution, got '{0}'")]
109    BadDistribution(String),
110
111    /// A numeric directive is out of its allowed range.
112    #[error("conf: directive '{field}' value {value} is out of range: {reason}")]
113    OutOfRange {
114        /// The directive name.
115        field: &'static str,
116        /// The offending value.
117        value: i64,
118        /// What range was expected.
119        reason: &'static str,
120    },
121
122    /// A server / `dyn_seeds` entry is malformed.
123    #[error("conf: '{field}' entry '{value}' is invalid: {reason}")]
124    BadServer {
125        /// The directive (`servers` or `dyn_seeds`).
126        field: &'static str,
127        /// The malformed entry.
128        value: String,
129        /// Human-readable failure reason.
130        reason: String,
131    },
132
133    /// `hash_tag` must be exactly two characters.
134    #[error("conf: directive 'hash_tag' must be a string of exactly 2 characters, got '{0}'")]
135    BadHashTag(String),
136
137    /// The YAML failed to parse at the document level.
138    #[error("conf: yaml parse error: {message}")]
139    Yaml {
140        /// The YAML error message (with location, if available).
141        message: String,
142    },
143}
144
145impl ConfError {
146    pub(crate) fn from_yaml(err: &serde_yaml::Error) -> Self {
147        let message = err.to_string();
148        // serde_yaml emits unknown-field errors as
149        //   `<key>: unknown field \`<name>\`, expected one of ...`.
150        // Pull the field name out so callers can pattern-match
151        // on the typed `UnknownKey` variant directly.
152        if let Some(start) = message.find("unknown field `") {
153            let after = &message[start + "unknown field `".len()..];
154            if let Some(end) = after.find('`') {
155                return ConfError::UnknownKey {
156                    name: after[..end].to_string(),
157                };
158            }
159        }
160        ConfError::Yaml { message }
161    }
162}