Skip to main content

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, &section_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}