Skip to main content

dynomite/conf/
mod.rs

1//! YAML configuration: schema, parsing, defaulting, validation.
2//!
3//! The top-level YAML document is a single-key mapping from a pool name
4//! to a [`ConfPool`]. [`Config`] wraps both. The typical lifecycle is:
5//!
6//! 1. [`Config::parse_str`] (or [`Config::parse_file`]) - parse YAML and
7//!    apply structural checks.
8//! 2. [`Config::finalize`] - apply defaults to fields that were left
9//!    unset.
10//! 3. [`Config::validate`] - run the full set of cross-field checks.
11//!
12//! [`Config::test_conf`] is the convenience used by the `-t` flag of the
13//! server binary and runs `finalize` + `validate` and returns a short
14//! status string.
15//!
16//! # Examples
17//!
18//! ```
19//! use dynomite::conf::Config;
20//!
21//! let yaml = r#"
22//! dyn_o_mite:
23//!   listen: 127.0.0.1:8102
24//!   dyn_listen: 127.0.0.1:8101
25//!   tokens: '101134286'
26//!   servers:
27//!   - 127.0.0.1:22122:1
28//!   data_store: 0
29//!   mbuf_size: 16384
30//!   max_msgs: 300000
31//! "#;
32//!
33//! let mut cfg = Config::parse_str(yaml).unwrap();
34//! cfg.finalize();
35//! cfg.validate().unwrap();
36//! assert_eq!(cfg.pool_name(), "dyn_o_mite");
37//! ```
38
39mod endpoint;
40mod enums;
41mod error;
42mod pool;
43mod server;
44mod tokens;
45
46pub use endpoint::{ConfListen, EndpointKind};
47pub use enums::{ConsistencyLevel, DataStore, Distribution, HashType, SecureServerOption};
48pub use error::ConfError;
49pub use pool::{
50    ConfBucketType, ConfPool, ConfRiak, ConfRiakWasmModule, ConfTlsProfile, ObservabilityConfig,
51    Servers,
52};
53pub use server::{ConfDynSeed, ConfServer};
54pub use tokens::{TokenComponent, TokenList};
55
56use std::collections::BTreeMap;
57use std::path::Path;
58use std::sync::atomic::{AtomicBool, Ordering};
59
60/// Process-wide flag set by an embedder to declare that the
61/// `data_store: noxu` configuration value is supported by this
62/// build.
63///
64/// The engine ships with the flag off; the `dynomited` binary
65/// flips it to `true` at startup when compiled with
66/// `--features riak` (which is the gate that pulls in the
67/// `dyniak::NoxuDatastore` type). Pool validation rejects
68/// `data_store: noxu` with [`ConfError::BadNoxuConfig`] when
69/// the flag is `false`.
70///
71/// # Examples
72///
73/// ```
74/// use dynomite::conf::{is_noxu_supported, set_noxu_supported};
75/// let prev = is_noxu_supported();
76/// set_noxu_supported(true);
77/// assert!(is_noxu_supported());
78/// set_noxu_supported(prev);
79/// ```
80static NOXU_SUPPORTED: AtomicBool = AtomicBool::new(false);
81
82/// Set the process-wide "noxu data_store is supported" flag.
83///
84/// Idempotent. See [`NOXU_SUPPORTED`] for the contract.
85pub fn set_noxu_supported(on: bool) {
86    NOXU_SUPPORTED.store(on, Ordering::SeqCst);
87}
88
89/// Read the process-wide "noxu data_store is supported" flag.
90#[must_use]
91pub fn is_noxu_supported() -> bool {
92    NOXU_SUPPORTED.load(Ordering::SeqCst)
93}
94
95/// Top-level configuration value: a single named [`ConfPool`].
96///
97/// The YAML document mirrors the C reference: a top-level mapping with
98/// exactly one key, the pool name, whose value is the pool body.
99#[derive(Debug, Clone)]
100pub struct Config {
101    pool_name: String,
102    pool: ConfPool,
103}
104
105impl Config {
106    /// Parse a YAML configuration document from a string.
107    ///
108    /// Performs structural validation (exactly one pool, no unknown
109    /// keys) but does not apply defaults. Call [`Config::finalize`]
110    /// before [`Config::validate`] to fully prepare the config.
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// use dynomite::conf::Config;
116    /// let yaml = "p:\n  listen: 127.0.0.1:1\n  dyn_listen: 127.0.0.1:2\n  tokens: '1'\n  servers:\n  - 127.0.0.1:3:1\n  data_store: 0\n";
117    /// let cfg = Config::parse_str(yaml).unwrap();
118    /// assert_eq!(cfg.pool_name(), "p");
119    /// assert!(Config::parse_str("").is_err());
120    /// ```
121    pub fn parse_str(input: &str) -> Result<Self, ConfError> {
122        let raw: BTreeMap<String, ConfPool> =
123            serde_yaml::from_str(input).map_err(|e| ConfError::from_yaml(&e))?;
124        if raw.is_empty() {
125            return Err(ConfError::EmptyDocument);
126        }
127        if raw.len() != 1 {
128            return Err(ConfError::TooManyPools(raw.len()));
129        }
130        let (pool_name, pool) = raw
131            .into_iter()
132            .next()
133            .expect("invariant: raw.len() == 1, checked above");
134        if pool_name.is_empty() {
135            return Err(ConfError::EmptyPoolName);
136        }
137        Ok(Self { pool_name, pool })
138    }
139
140    /// Parse a YAML configuration document from a filesystem path.
141    ///
142    /// # Examples
143    ///
144    /// ```
145    /// use std::io::Write;
146    /// use dynomite::conf::Config;
147    /// let mut f = tempfile::NamedTempFile::new().unwrap();
148    /// writeln!(f, "p:\n  listen: 127.0.0.1:1\n  dyn_listen: 127.0.0.1:2\n  tokens: '1'\n  servers:\n  - 127.0.0.1:3:1\n  data_store: 0\n").unwrap();
149    /// let cfg = Config::parse_file(f.path()).unwrap();
150    /// assert_eq!(cfg.pool_name(), "p");
151    /// ```
152    pub fn parse_file(path: &Path) -> Result<Self, ConfError> {
153        let bytes = std::fs::read_to_string(path).map_err(|e| ConfError::Io {
154            path: path.to_path_buf(),
155            source: e,
156        })?;
157        Self::parse_str(&bytes)
158    }
159
160    /// The configured pool name (the single top-level YAML key).
161    ///
162    /// # Examples
163    ///
164    /// ```
165    /// use dynomite::conf::Config;
166    /// let cfg = Config::parse_str("my_pool:\n  listen: 127.0.0.1:1\n  dyn_listen: 127.0.0.1:2\n  tokens: '1'\n  servers:\n  - 127.0.0.1:3:1\n  data_store: 0\n").unwrap();
167    /// assert_eq!(cfg.pool_name(), "my_pool");
168    /// ```
169    pub fn pool_name(&self) -> &str {
170        &self.pool_name
171    }
172
173    /// Borrow the inner [`ConfPool`].
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// use dynomite::conf::Config;
179    /// let cfg = Config::parse_str("p:\n  listen: 127.0.0.1:8102\n  dyn_listen: 127.0.0.1:2\n  tokens: '1'\n  servers:\n  - 127.0.0.1:3:1\n  data_store: 0\n").unwrap();
180    /// assert_eq!(cfg.pool().listen.as_ref().unwrap().port(), 8102);
181    /// ```
182    pub fn pool(&self) -> &ConfPool {
183        &self.pool
184    }
185
186    /// Mutably borrow the inner [`ConfPool`].
187    ///
188    /// # Examples
189    ///
190    /// ```
191    /// use dynomite::conf::Config;
192    /// let mut cfg = Config::parse_str("p:\n  listen: 127.0.0.1:1\n  dyn_listen: 127.0.0.1:2\n  tokens: '1'\n  servers:\n  - 127.0.0.1:3:1\n  data_store: 0\n").unwrap();
193    /// cfg.pool_mut().preconnect = Some(true);
194    /// assert_eq!(cfg.pool().preconnect, Some(true));
195    /// ```
196    pub fn pool_mut(&mut self) -> &mut ConfPool {
197        &mut self.pool
198    }
199
200    /// Apply default values to any field left unset by the YAML.
201    ///
202    /// # Examples
203    ///
204    /// ```
205    /// use dynomite::conf::Config;
206    /// let mut cfg = Config::parse_str("p:\n  listen: 127.0.0.1:1\n  dyn_listen: 127.0.0.1:2\n  tokens: '1'\n  servers:\n  - 127.0.0.1:3:1\n  data_store: 0\n").unwrap();
207    /// assert!(cfg.pool().rack.is_none());
208    /// cfg.finalize();
209    /// assert!(cfg.pool().rack.is_some());
210    /// ```
211    pub fn finalize(&mut self) {
212        self.pool.apply_defaults();
213    }
214
215    /// Run the full validation pass.
216    ///
217    /// # Examples
218    ///
219    /// ```
220    /// use dynomite::conf::Config;
221    /// let mut cfg = Config::parse_str("p:\n  listen: 127.0.0.1:1\n  dyn_listen: 127.0.0.1:2\n  tokens: '1'\n  servers:\n  - 127.0.0.1:3:1\n  data_store: 0\n").unwrap();
222    /// cfg.finalize();
223    /// cfg.validate().unwrap();
224    /// ```
225    pub fn validate(&self) -> Result<(), ConfError> {
226        self.pool.validate(&self.pool_name)
227    }
228
229    /// Equivalent of `dynomite -t -c <file>`: finalize, validate, and
230    /// produce a short status string.
231    ///
232    /// # Examples
233    ///
234    /// ```
235    /// use dynomite::conf::Config;
236    /// let cfg = Config::parse_str("p:\n  listen: 127.0.0.1:1\n  dyn_listen: 127.0.0.1:2\n  tokens: '1'\n  servers:\n  - 127.0.0.1:3:1\n  data_store: 0\n").unwrap();
237    /// assert!(cfg.test_conf().unwrap().contains("is valid"));
238    /// ```
239    pub fn test_conf(&self) -> Result<String, ConfError> {
240        let mut owned = self.clone();
241        owned.finalize();
242        owned.validate()?;
243        Ok(format!(
244            "configuration file with pool '{}' is valid",
245            owned.pool_name
246        ))
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    const MINIMAL: &str = r"
255dyn_o_mite:
256  listen: 127.0.0.1:8102
257  dyn_listen: 127.0.0.1:8101
258  tokens: '101134286'
259  servers:
260  - 127.0.0.1:22122:1
261  data_store: 0
262";
263
264    #[test]
265    fn parse_minimal() {
266        let cfg = Config::parse_str(MINIMAL).unwrap();
267        assert_eq!(cfg.pool_name(), "dyn_o_mite");
268        assert_eq!(cfg.pool().listen.as_ref().unwrap().port(), 8102);
269    }
270
271    #[test]
272    fn finalize_sets_defaults() {
273        let mut cfg = Config::parse_str(MINIMAL).unwrap();
274        cfg.finalize();
275        assert_eq!(cfg.pool().rack.as_deref(), Some("localrack"));
276        assert_eq!(cfg.pool().datacenter.as_deref(), Some("localdc"));
277        assert_eq!(cfg.pool().timeout, Some(5000));
278    }
279
280    #[test]
281    fn validate_minimal() {
282        let mut cfg = Config::parse_str(MINIMAL).unwrap();
283        cfg.finalize();
284        cfg.validate().unwrap();
285    }
286
287    #[test]
288    fn empty_document_rejected() {
289        let err = Config::parse_str("").unwrap_err();
290        assert!(matches!(
291            err,
292            ConfError::Yaml { .. } | ConfError::EmptyDocument
293        ));
294    }
295
296    #[test]
297    fn too_many_pools_rejected() {
298        let yaml = "a:\n  listen: 1.2.3.4:80\nb:\n  listen: 1.2.3.4:81\n";
299        let err = Config::parse_str(yaml).unwrap_err();
300        assert!(matches!(err, ConfError::TooManyPools(2)));
301    }
302
303    #[test]
304    fn unknown_key_rejected() {
305        let yaml = "p:\n  listen: 127.0.0.1:1\n  bogus_key: 42\n";
306        let err = Config::parse_str(yaml).unwrap_err();
307        match err {
308            ConfError::UnknownKey { name } => assert_eq!(name, "bogus_key"),
309            other => panic!("unexpected error: {other:?}"),
310        }
311    }
312
313    #[test]
314    fn test_conf_reports_pool_name() {
315        let cfg = Config::parse_str(MINIMAL).unwrap();
316        let report = cfg.test_conf().unwrap();
317        assert!(report.contains("dyn_o_mite"));
318    }
319}