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