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