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}