config_lib/config.rs
1//! # High-Level Configuration Management
2//!
3//! Advanced configuration management API providing intuitive interfaces for
4//! loading, modifying, validating, and saving configurations with format preservation.
5
6use crate::error::{Error, Result};
7use crate::parsers;
8use crate::value::Value;
9use dashmap::DashMap;
10use std::collections::BTreeMap;
11use std::path::{Path, PathBuf};
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::sync::{Arc, RwLock};
14
15#[cfg(feature = "schema")]
16use crate::schema::Schema;
17
18#[cfg(feature = "validation")]
19use crate::validation::{ValidationError, ValidationRuleSet};
20
21/// High-level configuration manager with format preservation and change tracking
22///
23/// [`Config`] provides a comprehensive API for managing configurations
24/// throughout their lifecycle. It maintains both the resolved values (for fast access)
25/// and format-specific preservation data (for round-trip editing).
26///
27/// ## Key Features
28///
29/// - **Format Preservation**: Maintains comments, whitespace, and original formatting
30/// - **Change Tracking**: Automatic detection of modifications
31/// - **Type Safety**: Rich type conversion with comprehensive error handling
32/// - **Path-based Access**: Dot notation for nested value access
33/// - **Multi-format Support**: CONF, TOML, JSON, NOML formats
34/// - **Schema Validation**: Optional schema validation and enforcement
35/// - **Async Support**: Non-blocking file operations (with feature flag)
36///
37/// ## Examples
38///
39/// ```rust
40/// use config_lib::Config;
41///
42/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
43/// // Load from string
44/// let mut config = Config::from_string("port = 8080\nname = \"MyApp\"", None)?;
45///
46/// // Access values
47/// let port = config.get("port").unwrap().as_integer()?;
48/// let name = config.get("name").unwrap().as_string()?;
49///
50/// // Modify values
51/// config.set("port", 9000)?;
52///
53/// # Ok(())
54/// # }
55/// ```
56#[derive(Debug)]
57pub struct Config {
58 /// The resolved configuration values
59 values: Value,
60
61 /// Path to the source file (if loaded from file)
62 file_path: Option<PathBuf>,
63
64 /// Detected or specified format
65 format: String,
66
67 /// Change tracking - has the config been modified?
68 modified: bool,
69
70 /// Opt-out behavior knobs (read-only, cache sizing, etc.).
71 /// See [`ConfigOptions`].
72 options: ConfigOptions,
73
74 /// Cache hit counter (loaded via `Ordering::Relaxed` from
75 /// [`Config::cache_stats`]). Wired in v0.9.9 — populated by
76 /// [`Config::get_arc`] when the cache layer is enabled.
77 cache_hits: AtomicU64,
78
79 /// Cache miss counter. See [`Config::cache_stats`].
80 cache_misses: AtomicU64,
81
82 /// Resolved-path cache. `DashMap` is sharded so reads hold a
83 /// short shard-local lock rather than a global lock; under
84 /// typical config workloads (read-mostly, infrequent writes)
85 /// this provides effectively-lock-free reads. Storing
86 /// `Arc<Value>` keeps clones cheap on hit (refcount bump only).
87 ///
88 /// Population is lazy — only paths actually requested through
89 /// [`Config::get_arc`] enter the cache. `set` / `remove` /
90 /// `merge` clear the cache entirely (writes are rare in the
91 /// design envelope; a per-prefix invalidation strategy is
92 /// post-1.0 backlog).
93 cache: DashMap<Box<str>, Arc<Value>>,
94
95 /// Per-path defaults table consulted by [`Config::get_or_default`]
96 /// when the requested path is missing from `values`. Wrapped in
97 /// `Arc<RwLock<_>>` so defaults can be updated independently of
98 /// the main value tree, including under `read_only` mode (defaults
99 /// are operator-provided fallbacks, not user-supplied data).
100 defaults: Arc<RwLock<BTreeMap<String, Value>>>,
101
102 /// Format-specific preservation data
103 #[cfg(feature = "noml")]
104 noml_document: Option<noml::Document>,
105
106 /// Validation rules for this configuration
107 #[cfg(feature = "validation")]
108 validation_rules: Option<ValidationRuleSet>,
109}
110
111impl Config {
112 /// Create a new empty configuration
113 pub fn new() -> Self {
114 Self {
115 values: Value::table(BTreeMap::new()),
116 file_path: None,
117 format: "conf".to_string(),
118 modified: false,
119 options: ConfigOptions::default(),
120 cache_hits: AtomicU64::new(0),
121 cache_misses: AtomicU64::new(0),
122 cache: DashMap::new(),
123 defaults: Arc::new(RwLock::new(BTreeMap::new())),
124 #[cfg(feature = "noml")]
125 noml_document: None,
126 #[cfg(feature = "validation")]
127 validation_rules: None,
128 }
129 }
130
131 /// Load configuration from a string
132 pub fn from_string(source: &str, format: Option<&str>) -> Result<Self> {
133 let detected_format = format.unwrap_or_else(|| parsers::detect_format(source));
134
135 let values = parsers::parse_string(source, Some(detected_format))?;
136
137 #[cfg(feature = "noml")]
138 let mut config = Self {
139 values,
140 file_path: None,
141 format: detected_format.to_string(),
142 modified: false,
143 options: ConfigOptions::default(),
144 cache_hits: AtomicU64::new(0),
145 cache_misses: AtomicU64::new(0),
146 cache: DashMap::new(),
147 defaults: Arc::new(RwLock::new(BTreeMap::new())),
148 noml_document: None,
149 #[cfg(feature = "validation")]
150 validation_rules: None,
151 };
152
153 #[cfg(not(feature = "noml"))]
154 let config = Self {
155 values,
156 file_path: None,
157 format: detected_format.to_string(),
158 modified: false,
159 options: ConfigOptions::default(),
160 cache_hits: AtomicU64::new(0),
161 cache_misses: AtomicU64::new(0),
162 cache: DashMap::new(),
163 defaults: Arc::new(RwLock::new(BTreeMap::new())),
164 #[cfg(feature = "validation")]
165 validation_rules: None,
166 };
167
168 // Store format-specific preservation data
169 #[cfg(feature = "noml")]
170 if detected_format == "noml" || detected_format == "toml" {
171 if let Ok(document) = noml::parse_string(source, None) {
172 config.noml_document = Some(document);
173 }
174 }
175
176 Ok(config)
177 }
178
179 /// Load configuration from a file
180 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
181 let path = path.as_ref();
182 let content =
183 std::fs::read_to_string(path).map_err(|e| Error::io(path.display().to_string(), e))?;
184
185 let format = parsers::detect_format_from_path(path)
186 .unwrap_or_else(|| parsers::detect_format(&content));
187
188 let mut config = Self::from_string(&content, Some(format))?;
189 config.file_path = Some(path.to_path_buf());
190
191 Ok(config)
192 }
193
194 /// Async version of from_file
195 #[cfg(feature = "async")]
196 pub async fn from_file_async<P: AsRef<Path>>(path: P) -> Result<Self> {
197 let path = path.as_ref();
198 let content = tokio::fs::read_to_string(path)
199 .await
200 .map_err(|e| Error::io(path.display().to_string(), e))?;
201
202 let format = parsers::detect_format_from_path(path)
203 .unwrap_or_else(|| parsers::detect_format(&content));
204
205 let mut config = Self::from_string(&content, Some(format))?;
206 config.file_path = Some(path.to_path_buf());
207
208 Ok(config)
209 }
210
211 /// Get a value by path
212 pub fn get(&self, path: &str) -> Option<&Value> {
213 self.values.get(path)
214 }
215
216 /// Get a mutable reference to a value by path
217 pub fn get_mut(&mut self, path: &str) -> Result<&mut Value> {
218 self.values.get_mut_nested(path)
219 }
220
221 /// Set a value by path
222 ///
223 /// Invalidates the entire resolved-path cache (writes are rare in
224 /// the design envelope; a per-prefix invalidation strategy is
225 /// post-1.0 backlog).
226 ///
227 /// # Errors
228 ///
229 /// Returns an error if:
230 /// - The configuration was constructed with [`ConfigOptions::read_only`]
231 /// - The path is invalid (e.g. attempts to insert into a non-table value)
232 pub fn set<V: Into<Value>>(&mut self, path: &str, value: V) -> Result<()> {
233 self.ensure_writable()?;
234 self.values.set_nested(path, value.into())?;
235 self.modified = true;
236 self.cache.clear();
237 Ok(())
238 }
239
240 /// Remove a value by path
241 ///
242 /// Invalidates the entire resolved-path cache on success.
243 ///
244 /// # Errors
245 ///
246 /// Returns an error if:
247 /// - The configuration was constructed with [`ConfigOptions::read_only`]
248 /// - The path is malformed
249 pub fn remove(&mut self, path: &str) -> Result<Option<Value>> {
250 self.ensure_writable()?;
251 let result = self.values.remove(path)?;
252 if result.is_some() {
253 self.modified = true;
254 self.cache.clear();
255 }
256 Ok(result)
257 }
258
259 /// Check if a path exists
260 pub fn contains_key(&self, path: &str) -> bool {
261 self.values.contains_key(path)
262 }
263
264 /// Get all keys in the configuration
265 pub fn keys(&self) -> Result<Vec<&str>> {
266 self.values.keys()
267 }
268
269 /// Check if the configuration has been modified
270 pub fn is_modified(&self) -> bool {
271 self.modified
272 }
273
274 /// Mark the configuration as unmodified
275 pub fn mark_clean(&mut self) {
276 self.modified = false;
277 }
278
279 /// Get the configuration format
280 pub fn format(&self) -> &str {
281 &self.format
282 }
283
284 /// Get the file path (if loaded from file)
285 pub fn file_path(&self) -> Option<&Path> {
286 self.file_path.as_deref()
287 }
288
289 /// Save the configuration to its original file
290 pub fn save(&mut self) -> Result<()> {
291 match &self.file_path {
292 Some(path) => {
293 self.save_to_file(path.clone())?;
294 self.modified = false;
295 Ok(())
296 }
297 None => Err(Error::internal(
298 "Cannot save configuration that wasn't loaded from a file",
299 )),
300 }
301 }
302
303 /// Save the configuration to a specific file
304 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
305 let serialized = self.serialize()?;
306 std::fs::write(path, serialized).map_err(|e| Error::io("save".to_string(), e))?;
307 Ok(())
308 }
309
310 /// Async version of save
311 #[cfg(feature = "async")]
312 pub async fn save_async(&mut self) -> Result<()> {
313 match &self.file_path {
314 Some(path) => {
315 self.save_to_file_async(path.clone()).await?;
316 self.modified = false;
317 Ok(())
318 }
319 None => Err(Error::internal(
320 "Cannot save configuration that wasn't loaded from a file",
321 )),
322 }
323 }
324
325 /// Async version of save_to_file
326 #[cfg(feature = "async")]
327 pub async fn save_to_file_async<P: AsRef<Path>>(&self, path: P) -> Result<()> {
328 let serialized = self.serialize()?;
329 tokio::fs::write(path, serialized)
330 .await
331 .map_err(|e| Error::io("save".to_string(), e))?;
332 Ok(())
333 }
334
335 /// Serialize the configuration to string format
336 pub fn serialize(&self) -> Result<String> {
337 match self.format.as_str() {
338 "json" => {
339 #[cfg(feature = "json")]
340 return crate::parsers::json_parser::serialize(&self.values);
341 #[cfg(not(feature = "json"))]
342 return Err(Error::feature_not_enabled("json"));
343 }
344 "toml" => {
345 #[cfg(feature = "toml")]
346 {
347 // Use NOML's serializer for format preservation
348 #[cfg(feature = "noml")]
349 if let Some(ref document) = self.noml_document {
350 return Ok(noml::serialize_document(document)?);
351 }
352 // Fallback to basic serialization
353 self.serialize_as_toml()
354 }
355 #[cfg(not(feature = "toml"))]
356 return Err(Error::feature_not_enabled("toml"));
357 }
358 "noml" => {
359 #[cfg(feature = "noml")]
360 {
361 if let Some(ref document) = self.noml_document {
362 Ok(noml::serialize_document(document)?)
363 } else {
364 Err(Error::internal("NOML document not preserved"))
365 }
366 }
367 #[cfg(not(feature = "noml"))]
368 return Err(Error::feature_not_enabled("noml"));
369 }
370 "conf" => self.serialize_as_conf(),
371 _ => Err(Error::unknown_format(&self.format)),
372 }
373 }
374
375 /// Serialize as CONF format
376 fn serialize_as_conf(&self) -> Result<String> {
377 let mut output = String::new();
378 if let Value::Table(table) = &self.values {
379 self.write_conf_table(&mut output, table, "")?;
380 }
381 Ok(output)
382 }
383
384 /// Helper to write CONF format table
385 fn write_conf_table(
386 &self,
387 output: &mut String,
388 table: &BTreeMap<String, Value>,
389 section_prefix: &str,
390 ) -> Result<()> {
391 // First pass: write simple key-value pairs
392 for (key, value) in table {
393 if !value.is_table() {
394 let formatted_value = self.format_conf_value(value)?;
395 output.push_str(&format!("{key} = {formatted_value}\n"));
396 }
397 }
398
399 // Second pass: write sections
400 for (key, value) in table {
401 if let Value::Table(nested_table) = value {
402 let section_name = if section_prefix.is_empty() {
403 key.clone()
404 } else {
405 format!("{section_prefix}.{key}")
406 };
407
408 output.push_str(&format!("\n[{section_name}]\n"));
409 self.write_conf_table(output, nested_table, §ion_name)?;
410 }
411 }
412
413 Ok(())
414 }
415
416 /// Format a value for CONF output
417 #[allow(clippy::only_used_in_recursion)]
418 fn format_conf_value(&self, value: &Value) -> Result<String> {
419 match value {
420 Value::Null => Ok("null".to_string()),
421 Value::Bool(b) => Ok(b.to_string()),
422 Value::Integer(i) => Ok(i.to_string()),
423 Value::Float(f) => Ok(f.to_string()),
424 Value::String(s) => {
425 if s.contains(' ') || s.contains('\t') || s.contains('\n') {
426 Ok(format!("\"{}\"", s.replace('"', "\\\"")))
427 } else {
428 Ok(s.clone())
429 }
430 }
431 Value::Array(arr) => {
432 let items: Result<Vec<String>> =
433 arr.iter().map(|v| self.format_conf_value(v)).collect();
434 Ok(items?.join(" "))
435 }
436 Value::Table(_) => Err(Error::type_error(
437 "Cannot serialize nested table as value",
438 "primitive",
439 "table",
440 )),
441 #[cfg(feature = "chrono")]
442 Value::DateTime(dt) => Ok(dt.to_rfc3339()),
443 }
444 }
445
446 /// Serialize as TOML format (basic implementation)
447 #[cfg(feature = "toml")]
448 fn serialize_as_toml(&self) -> Result<String> {
449 // This is a simplified TOML serializer
450 // In practice, you'd use the NOML library for proper TOML serialization
451 Err(Error::internal(
452 "Basic TOML serialization not implemented - use NOML library",
453 ))
454 }
455
456 /// Validate the configuration against a schema
457 #[cfg(feature = "schema")]
458 pub fn validate_schema(&self, schema: &Schema) -> Result<()> {
459 schema.validate(&self.values)
460 }
461
462 /// Get the underlying Value
463 pub fn as_value(&self) -> &Value {
464 &self.values
465 }
466
467 /// Merge another configuration into this one
468 ///
469 /// Invalidates the entire resolved-path cache.
470 ///
471 /// # Errors
472 ///
473 /// Returns an error if the configuration was constructed with
474 /// [`ConfigOptions::read_only`].
475 pub fn merge(&mut self, other: &Config) -> Result<()> {
476 self.ensure_writable()?;
477 self.merge_value(&other.values)?;
478 self.modified = true;
479 self.cache.clear();
480 Ok(())
481 }
482
483 /// Helper to merge values recursively
484 fn merge_value(&mut self, other: &Value) -> Result<()> {
485 match (&mut self.values, other) {
486 (Value::Table(self_table), Value::Table(other_table)) => {
487 for (key, other_value) in other_table {
488 match self_table.get_mut(key) {
489 Some(self_value) => {
490 if let (Value::Table(_), Value::Table(_)) = (&*self_value, other_value)
491 {
492 // Create a temporary config for recursive merging
493 let mut temp_config = Config::new();
494 temp_config.values = self_value.clone();
495 temp_config.merge_value(other_value)?;
496 *self_value = temp_config.values;
497 } else {
498 // Replace value
499 *self_value = other_value.clone();
500 }
501 }
502 None => {
503 // Insert new value
504 self_table.insert(key.clone(), other_value.clone());
505 }
506 }
507 }
508 }
509 _ => {
510 // Replace entire value
511 self.values = other.clone();
512 }
513 }
514 Ok(())
515 }
516
517 // =====================================================================
518 // Validation Methods (Feature-gated)
519 // =====================================================================
520
521 // --- CONVENIENCE METHODS & BUILDER PATTERN ---
522
523 /// Get a value by path with a more ergonomic API
524 pub fn key(&self, path: &str) -> ConfigValue<'_> {
525 ConfigValue::new(self.get(path))
526 }
527
528 /// Check if configuration has any value at the given path
529 pub fn has(&self, path: &str) -> bool {
530 self.contains_key(path)
531 }
532
533 /// Get a value with a default fallback
534 pub fn get_or<V>(&self, path: &str, default: V) -> V
535 where
536 V: TryFrom<Value> + Clone,
537 V::Error: std::fmt::Debug,
538 {
539 self.get(path)
540 .and_then(|v| V::try_from(v.clone()).ok())
541 .unwrap_or(default)
542 }
543
544 // --- VALIDATION SUPPORT ---
545
546 /// Set validation rules for this configuration
547 #[cfg(feature = "validation")]
548 pub fn set_validation_rules(&mut self, rules: ValidationRuleSet) {
549 self.validation_rules = Some(rules);
550 }
551
552 /// Validate the current configuration against all registered rules
553 #[cfg(feature = "validation")]
554 pub fn validate(&mut self) -> Result<Vec<ValidationError>> {
555 match &mut self.validation_rules {
556 Some(rules) => {
557 if let Value::Table(table) = &self.values {
558 let mut errors = Vec::new();
559
560 // Validate each key-value pair
561 for (key, value) in table {
562 errors.extend(rules.validate(key, value));
563 }
564
565 // Also validate for required keys (if any RequiredKeyValidator exists)
566 // This is handled by individual rule implementations
567
568 Ok(errors)
569 } else {
570 Err(Error::validation(
571 "Configuration root must be a table for validation",
572 ))
573 }
574 }
575 None => Ok(Vec::new()), // No rules = no errors
576 }
577 }
578
579 /// Validate and return only critical errors
580 #[cfg(feature = "validation")]
581 pub fn validate_critical_only(&mut self) -> Result<Vec<ValidationError>> {
582 let all_errors = self.validate()?;
583 Ok(all_errors
584 .into_iter()
585 .filter(|e| e.severity == crate::validation::ValidationSeverity::Critical)
586 .collect())
587 }
588
589 /// Check if configuration is valid (has no critical errors)
590 #[cfg(feature = "validation")]
591 pub fn is_valid(&mut self) -> Result<bool> {
592 let critical_errors = self.validate_critical_only()?;
593 Ok(critical_errors.is_empty())
594 }
595
596 /// Validate a specific value at a path
597 #[cfg(feature = "validation")]
598 pub fn validate_path(&mut self, path: &str) -> Result<Vec<ValidationError>> {
599 // Get the value first to avoid borrowing conflicts, clone to own it
600 let value = self
601 .get(path)
602 .ok_or_else(|| Error::key_not_found(path))?
603 .clone();
604
605 match &mut self.validation_rules {
606 Some(rules) => Ok(rules.validate(path, &value)),
607 None => Ok(Vec::new()),
608 }
609 }
610}
611
612impl Default for Config {
613 fn default() -> Self {
614 Self::new()
615 }
616}
617
618// =========================================================================
619// ConfigOptions — opt-out behavior knobs (foundation for v0.9.5)
620// =========================================================================
621
622/// Opt-out behavior knobs for [`Config`].
623///
624/// `ConfigOptions` carries the small set of toggles that should not be
625/// enabled by default: making a `Config` read-only, sizing the cache,
626/// and so on. The struct is `#[non_exhaustive]` so v0.9.x can add new
627/// knobs without breaking SemVer. Users construct it via
628/// [`ConfigOptions::new`] or [`ConfigOptions::default`] and apply
629/// individual toggles through the consuming builder methods.
630///
631/// The field set lays the groundwork for the lock-free caching work
632/// landing in v0.9.5. In v0.9.4 the only knob that has runtime effect
633/// is [`ConfigOptions::read_only`]; the cache-related knobs are
634/// accepted today so that the public API surface does not change
635/// again when v0.9.5 switches the cache on.
636///
637/// # Examples
638///
639/// ```rust
640/// use config_lib::{Config, ConfigOptions};
641///
642/// // Default options — caching on, writes allowed.
643/// let _cfg = Config::with_options(ConfigOptions::default());
644///
645/// // Read-only configuration for a hot path that must never be mutated.
646/// let opts = ConfigOptions::new().read_only(true);
647/// let _cfg = Config::with_options(opts);
648/// ```
649#[derive(Debug, Clone)]
650#[non_exhaustive]
651pub struct ConfigOptions {
652 /// Reject every `set` / `remove` / `merge` call with
653 /// [`Error::general`] instead of mutating. Useful for once-loaded
654 /// configurations that must never change at runtime.
655 pub read_only: bool,
656
657 /// Whether the internal caching layer is active. **Reserved** — the
658 /// caching layer ships in v0.9.5; in v0.9.4 this field is accepted
659 /// for forward compatibility but does not yet change runtime
660 /// behavior (every `get` already hits an in-memory `Value`).
661 pub cache_enabled: bool,
662
663 /// Maximum number of resolved-key entries the cache will hold
664 /// before evicting. **Reserved for v0.9.5.**
665 pub cache_capacity: usize,
666}
667
668impl Default for ConfigOptions {
669 /// The canonical options for the Hive DB use case:
670 /// caching enabled, writes allowed, capacity tuned for typical
671 /// server config size.
672 fn default() -> Self {
673 Self {
674 read_only: false,
675 cache_enabled: true,
676 cache_capacity: 1024,
677 }
678 }
679}
680
681impl ConfigOptions {
682 /// Construct a `ConfigOptions` with default values
683 /// ([`ConfigOptions::default`]).
684 pub fn new() -> Self {
685 Self::default()
686 }
687
688 /// Set the read-only flag. See [`ConfigOptions::read_only`].
689 pub fn read_only(mut self, read_only: bool) -> Self {
690 self.read_only = read_only;
691 self
692 }
693
694 /// Toggle the caching layer. **Reserved for v0.9.5** — currently
695 /// a no-op at runtime; the setter exists so call-sites compile
696 /// against the same shape they will use after the cache lands.
697 pub fn cache_enabled(mut self, cache_enabled: bool) -> Self {
698 self.cache_enabled = cache_enabled;
699 self
700 }
701
702 /// Set the cache capacity. **Reserved for v0.9.5** — see
703 /// [`ConfigOptions::cache_enabled`].
704 pub fn cache_capacity(mut self, cache_capacity: usize) -> Self {
705 self.cache_capacity = cache_capacity;
706 self
707 }
708}
709
710impl Config {
711 /// Construct a new empty [`Config`] with the supplied
712 /// [`ConfigOptions`].
713 ///
714 /// This is the explicit opt-out constructor. For the canonical
715 /// defaults (caching on, writes allowed), prefer [`Config::new`].
716 pub fn with_options(options: ConfigOptions) -> Self {
717 let mut config = Self::new();
718 config.options = options;
719 config
720 }
721
722 /// Return the [`ConfigOptions`] currently in effect on this config.
723 pub fn options(&self) -> &ConfigOptions {
724 &self.options
725 }
726
727 /// Returns `true` if this configuration was constructed read-only
728 /// (see [`ConfigOptions::read_only`]).
729 pub fn is_read_only(&self) -> bool {
730 self.options.read_only
731 }
732
733 /// Helper used by mutating methods to short-circuit when the
734 /// configuration is in read-only mode.
735 fn ensure_writable(&self) -> Result<()> {
736 if self.options.read_only {
737 Err(Error::general("Configuration is read-only"))
738 } else {
739 Ok(())
740 }
741 }
742
743 /// Return a snapshot of the cache-hit / cache-miss counters.
744 ///
745 /// Counters are loaded with `Ordering::Relaxed` — the values are
746 /// statistics, not synchronisation primitives, and the hit/miss
747 /// classification is best-effort under concurrent reads.
748 pub fn cache_stats(&self) -> CacheStats {
749 let hits = self.cache_hits.load(Ordering::Relaxed);
750 let misses = self.cache_misses.load(Ordering::Relaxed);
751 let total = hits.saturating_add(misses);
752 let hit_ratio = if total == 0 {
753 0.0
754 } else {
755 hits as f64 / total as f64
756 };
757 CacheStats {
758 hits,
759 misses,
760 hit_ratio,
761 }
762 }
763
764 /// Resolve a dotted path to a cached `Arc<Value>`.
765 ///
766 /// `get_arc` is the cache-backed thread-safe accessor introduced
767 /// in v0.9.9. The first lookup of a given path walks the value
768 /// tree, allocates an `Arc<Value>` containing a clone of the
769 /// resolved node, and inserts it into the resolved-path cache.
770 /// Subsequent lookups of the same path hit the cache and return
771 /// a cheap refcount-bump clone of the `Arc<Value>`.
772 ///
773 /// This is the recommended accessor for:
774 ///
775 /// - **Multi-threaded reads.** `Arc<Value>` is `Send + Sync` and
776 /// independent of `&self`'s lifetime, so the value can be
777 /// handed off to other threads.
778 /// - **Hot loops.** Cache hits avoid the tree walk; the
779 /// refcount bump is a single relaxed atomic.
780 ///
781 /// The existing `Config::get(&self, path) -> Option<&Value>` is
782 /// still the right choice for single-threaded reads that just
783 /// peek and drop the borrow — no clone, no Arc bump.
784 ///
785 /// The cache is invalidated wholesale on any `set` / `remove` /
786 /// `merge`. The runtime knob [`ConfigOptions::cache_enabled`]
787 /// (default `true`) toggles the cache layer; with it disabled,
788 /// every `get_arc` call walks the tree and allocates a fresh
789 /// `Arc<Value>`.
790 pub fn get_arc(&self, path: &str) -> Option<Arc<Value>> {
791 if self.options.cache_enabled {
792 if let Some(entry) = self.cache.get(path) {
793 self.cache_hits.fetch_add(1, Ordering::Relaxed);
794 return Some(Arc::clone(entry.value()));
795 }
796 self.cache_misses.fetch_add(1, Ordering::Relaxed);
797 let resolved = self.values.get(path)?.clone();
798 let arc = Arc::new(resolved);
799 self.cache.insert(path.into(), Arc::clone(&arc));
800 Some(arc)
801 } else {
802 self.cache_misses.fetch_add(1, Ordering::Relaxed);
803 Some(Arc::new(self.values.get(path)?.clone()))
804 }
805 }
806
807 /// Clear the resolved-path cache.
808 ///
809 /// Idempotent; safe to call from any thread. Useful when an
810 /// out-of-band actor (e.g. a hot-reload watcher) has changed
811 /// the underlying value tree and the cache snapshot is now stale.
812 pub fn clear_cache(&self) {
813 self.cache.clear();
814 }
815
816 /// Set a default value for the given path.
817 ///
818 /// Defaults are an operator-supplied fallback table consulted by
819 /// [`Config::get_or_default`] when the requested path is missing
820 /// from the main value tree. Defaults are independent of the
821 /// `read_only` flag — a read-only configuration can still have
822 /// its defaults table populated (defaults are deployment-time
823 /// declarations, not user-supplied data).
824 ///
825 /// Calls to `set_default` do **not** invalidate the cache; the
826 /// cache is keyed on observed value-tree resolutions, not on
827 /// defaults-table membership.
828 ///
829 /// # Errors
830 ///
831 /// Returns an error if the defaults-table lock has been poisoned.
832 pub fn set_default<V: Into<Value>>(&self, path: &str, value: V) -> Result<()> {
833 let mut defaults = self
834 .defaults
835 .write()
836 .map_err(|_| Error::concurrency("defaults table lock poisoned"))?;
837 defaults.insert(path.to_string(), value.into());
838 Ok(())
839 }
840
841 /// Resolve a path against the main value tree, falling back to the
842 /// defaults table when not found.
843 ///
844 /// Returns `None` only when neither the value tree nor the defaults
845 /// table contains an entry for `path`. The return type is owned
846 /// (`Option<Value>`) because the defaults table sits behind an
847 /// `Arc<RwLock<_>>` and producing a borrowed reference would
848 /// require holding the lock guard across the caller's use.
849 pub fn get_or_default(&self, path: &str) -> Option<Value> {
850 if let Some(v) = self.values.get(path) {
851 return Some(v.clone());
852 }
853 let defaults = self.defaults.read().ok()?;
854 defaults.get(path).cloned()
855 }
856
857 /// Toggle this configuration into read-only mode at runtime.
858 ///
859 /// Equivalent to constructing with
860 /// `ConfigOptions::new().read_only(true)` but available as a
861 /// post-construction switch. Subsequent calls to `set` / `remove`
862 /// / `merge` return `Err(Error::general("Configuration is
863 /// read-only"))`.
864 ///
865 /// The toggle is **one-way** by design — there is no
866 /// `make_writable()` companion. A configuration that has been
867 /// declared immutable should not become mutable again; if you
868 /// need that flexibility, keep two `Config` handles instead.
869 pub fn make_read_only(&mut self) {
870 self.options.read_only = true;
871 }
872}
873
874// =========================================================================
875// CacheStats — read-only snapshot of cache performance counters
876// =========================================================================
877
878/// Snapshot of a [`Config`]'s cache-hit / cache-miss counters.
879///
880/// Returned by [`Config::cache_stats`]. The struct is `#[non_exhaustive]`
881/// so the v1.x SemVer contract can add new counter fields (e.g.
882/// `evictions`, `insertions`, per-shard breakdowns) in MINOR releases
883/// without breaking user code.
884///
885/// # Stability note for v0.9.5
886///
887/// In v0.9.5 every `CacheStats` returned by [`Config::cache_stats`]
888/// has `hits = 0`, `misses = 0`, `hit_ratio = 0.0`. The lock-free
889/// cache layer that populates these counters lands in a follow-up
890/// v0.9.5 implementation release. The struct is shipping now so the
891/// API surface is locked in ahead of the implementation.
892///
893/// # Examples
894///
895/// ```rust
896/// use config_lib::Config;
897///
898/// let cfg = Config::new();
899/// let stats = cfg.cache_stats();
900/// assert_eq!(stats.hits, 0);
901/// assert_eq!(stats.misses, 0);
902/// assert_eq!(stats.hit_ratio, 0.0);
903/// ```
904#[derive(Debug, Clone, Copy)]
905#[non_exhaustive]
906pub struct CacheStats {
907 /// Number of cache lookups that resolved to a cached value.
908 pub hits: u64,
909 /// Number of cache lookups that did not find a cached value and
910 /// fell through to the canonical storage.
911 pub misses: u64,
912 /// `hits / (hits + misses)` as an f64 in `[0.0, 1.0]`. Returns
913 /// `0.0` when no lookups have happened yet.
914 pub hit_ratio: f64,
915}
916
917/// Ergonomic wrapper for accessing configuration values
918pub struct ConfigValue<'a> {
919 value: Option<&'a Value>,
920}
921
922impl<'a> ConfigValue<'a> {
923 fn new(value: Option<&'a Value>) -> Self {
924 Self { value }
925 }
926
927 /// Get as string with default fallback
928 pub fn as_string(&self) -> Result<String> {
929 match self.value {
930 Some(v) => v.as_string().map(|s| s.to_string()),
931 None => Err(Error::key_not_found("value not found")),
932 }
933 }
934
935 /// Get as string with custom default
936 pub fn as_string_or(&self, default: &str) -> String {
937 self.value
938 .and_then(|v| v.as_string().ok())
939 .map(|s| s.to_string())
940 .unwrap_or_else(|| default.to_string())
941 }
942
943 /// Get as integer with default fallback
944 pub fn as_integer(&self) -> Result<i64> {
945 match self.value {
946 Some(v) => v.as_integer(),
947 None => Err(Error::key_not_found("value not found")),
948 }
949 }
950
951 /// Get as integer with custom default
952 pub fn as_integer_or(&self, default: i64) -> i64 {
953 self.value
954 .and_then(|v| v.as_integer().ok())
955 .unwrap_or(default)
956 }
957
958 /// Get as boolean with default fallback
959 pub fn as_bool(&self) -> Result<bool> {
960 match self.value {
961 Some(v) => v.as_bool(),
962 None => Err(Error::key_not_found("value not found")),
963 }
964 }
965
966 /// Get as boolean with custom default
967 pub fn as_bool_or(&self, default: bool) -> bool {
968 self.value.and_then(|v| v.as_bool().ok()).unwrap_or(default)
969 }
970
971 /// Check if the value exists
972 pub fn exists(&self) -> bool {
973 self.value.is_some()
974 }
975
976 /// Get the underlying Value reference if it exists
977 pub fn value(&self) -> Option<&'a Value> {
978 self.value
979 }
980}
981
982/// Builder pattern for Config creation
983pub struct ConfigBuilder {
984 format: Option<String>,
985 #[cfg(feature = "validation")]
986 validation_rules: Option<ValidationRuleSet>,
987}
988
989impl ConfigBuilder {
990 /// Create a new ConfigBuilder
991 pub fn new() -> Self {
992 Self {
993 format: None,
994 #[cfg(feature = "validation")]
995 validation_rules: None,
996 }
997 }
998
999 /// Set the configuration format
1000 pub fn format<S: Into<String>>(mut self, format: S) -> Self {
1001 self.format = Some(format.into());
1002 self
1003 }
1004
1005 /// Set validation rules
1006 #[cfg(feature = "validation")]
1007 pub fn validation_rules(mut self, rules: ValidationRuleSet) -> Self {
1008 self.validation_rules = Some(rules);
1009 self
1010 }
1011
1012 /// Build Config from string
1013 pub fn from_string(self, source: &str) -> Result<Config> {
1014 #[cfg(feature = "validation")]
1015 let mut config = Config::from_string(source, self.format.as_deref())?;
1016 #[cfg(not(feature = "validation"))]
1017 let config = Config::from_string(source, self.format.as_deref())?;
1018
1019 #[cfg(feature = "validation")]
1020 if let Some(rules) = self.validation_rules {
1021 config.set_validation_rules(rules);
1022 }
1023
1024 Ok(config)
1025 }
1026
1027 /// Build Config from file
1028 pub fn from_file<P: AsRef<Path>>(self, path: P) -> Result<Config> {
1029 #[cfg(feature = "validation")]
1030 let mut config = Config::from_file(path)?;
1031 #[cfg(not(feature = "validation"))]
1032 let config = Config::from_file(path)?;
1033
1034 #[cfg(feature = "validation")]
1035 if let Some(rules) = self.validation_rules {
1036 config.set_validation_rules(rules);
1037 }
1038
1039 Ok(config)
1040 }
1041}
1042
1043impl Default for ConfigBuilder {
1044 fn default() -> Self {
1045 Self::new()
1046 }
1047}
1048
1049/// Convert Value to Config
1050impl From<Value> for Config {
1051 fn from(value: Value) -> Self {
1052 Self {
1053 values: value,
1054 file_path: None,
1055 format: "conf".to_string(),
1056 modified: false,
1057 options: ConfigOptions::default(),
1058 cache_hits: AtomicU64::new(0),
1059 cache_misses: AtomicU64::new(0),
1060 cache: DashMap::new(),
1061 defaults: Arc::new(RwLock::new(BTreeMap::new())),
1062 #[cfg(feature = "noml")]
1063 noml_document: None,
1064 #[cfg(feature = "validation")]
1065 validation_rules: None,
1066 }
1067 }
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072 use super::*;
1073
1074 #[test]
1075 fn test_config_creation() {
1076 let config = Config::new();
1077 assert!(!config.is_modified());
1078 assert_eq!(config.format(), "conf");
1079 }
1080
1081 #[test]
1082 fn test_config_from_string() {
1083 let config = Config::from_string("key = value\nport = 8080", Some("conf")).unwrap();
1084
1085 assert_eq!(config.get("key").unwrap().as_string().unwrap(), "value");
1086 assert_eq!(config.get("port").unwrap().as_integer().unwrap(), 8080);
1087 }
1088
1089 #[test]
1090 fn test_config_modification() {
1091 let mut config = Config::new();
1092 assert!(!config.is_modified());
1093
1094 config.set("key", "value").unwrap();
1095 assert!(config.is_modified());
1096
1097 config.mark_clean();
1098 assert!(!config.is_modified());
1099 }
1100
1101 #[test]
1102 fn test_config_merge() {
1103 let mut config1 = Config::new();
1104 config1.set("a", 1).unwrap();
1105 config1.set("b.x", 2).unwrap();
1106
1107 let mut config2 = Config::new();
1108 config2.set("b.y", 3).unwrap();
1109 config2.set("c", 4).unwrap();
1110
1111 config1.merge(&config2).unwrap();
1112
1113 assert_eq!(config1.get("a").unwrap().as_integer().unwrap(), 1);
1114 assert_eq!(config1.get("b.x").unwrap().as_integer().unwrap(), 2);
1115 assert_eq!(config1.get("b.y").unwrap().as_integer().unwrap(), 3);
1116 assert_eq!(config1.get("c").unwrap().as_integer().unwrap(), 4);
1117 }
1118}