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};
11
12#[cfg(feature = "schema")]
13use crate::schema::Schema;
14
15#[cfg(feature = "validation")]
16use crate::validation::{ValidationError, ValidationRuleSet};
17
18/// High-level configuration manager with format preservation and change tracking
19///
20/// [`Config`] provides a comprehensive API for managing configurations
21/// throughout their lifecycle. It maintains both the resolved values (for fast access)
22/// and format-specific preservation data (for round-trip editing).
23///
24/// ## Key Features
25///
26/// - **Format Preservation**: Maintains comments, whitespace, and original formatting
27/// - **Change Tracking**: Automatic detection of modifications
28/// - **Type Safety**: Rich type conversion with comprehensive error handling
29/// - **Path-based Access**: Dot notation for nested value access
30/// - **Multi-format Support**: CONF, TOML, JSON, NOML formats
31/// - **Schema Validation**: Optional schema validation and enforcement
32/// - **Async Support**: Non-blocking file operations (with feature flag)
33///
34/// ## Examples
35///
36/// ```rust
37/// use config_lib::Config;
38///
39/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
40/// // Load from string
41/// let mut config = Config::from_string("port = 8080\nname = \"MyApp\"", None)?;
42///
43/// // Access values
44/// let port = config.get("port").unwrap().as_integer()?;
45/// let name = config.get("name").unwrap().as_string()?;
46///
47/// // Modify values
48/// config.set("port", 9000)?;
49///
50/// # Ok(())
51/// # }
52/// ```
53pub struct Config {
54    /// The resolved configuration values
55    values: Value,
56
57    /// Path to the source file (if loaded from file)
58    file_path: Option<PathBuf>,
59
60    /// Detected or specified format
61    format: String,
62
63    /// Change tracking - has the config been modified?
64    modified: bool,
65
66    /// Format-specific preservation data
67    #[cfg(feature = "noml")]
68    noml_document: Option<noml::Document>,
69
70    /// Validation rules for this configuration
71    #[cfg(feature = "validation")]
72    validation_rules: Option<ValidationRuleSet>,
73}
74
75impl Config {
76    /// Create a new empty configuration
77    pub fn new() -> Self {
78        Self {
79            values: Value::table(BTreeMap::new()),
80            file_path: None,
81            format: "conf".to_string(),
82            modified: false,
83            #[cfg(feature = "noml")]
84            noml_document: None,
85            #[cfg(feature = "validation")]
86            validation_rules: None,
87        }
88    }
89
90    /// Load configuration from a string
91    pub fn from_string(source: &str, format: Option<&str>) -> Result<Self> {
92        let detected_format = format.unwrap_or_else(|| parsers::detect_format(source));
93
94        let values = parsers::parse_string(source, Some(detected_format))?;
95
96        #[cfg(feature = "noml")]
97        let mut config = Self {
98            values,
99            file_path: None,
100            format: detected_format.to_string(),
101            modified: false,
102            noml_document: None,
103            #[cfg(feature = "validation")]
104            validation_rules: None,
105        };
106
107        #[cfg(not(feature = "noml"))]
108        let config = Self {
109            values,
110            file_path: None,
111            format: detected_format.to_string(),
112            modified: false,
113            #[cfg(feature = "validation")]
114            validation_rules: None,
115        };
116
117        // Store format-specific preservation data
118        #[cfg(feature = "noml")]
119        if detected_format == "noml" || detected_format == "toml" {
120            if let Ok(document) = noml::parse_string(source, None) {
121                config.noml_document = Some(document);
122            }
123        }
124
125        Ok(config)
126    }
127
128    /// Load configuration from a file
129    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
130        let path = path.as_ref();
131        let content =
132            std::fs::read_to_string(path).map_err(|e| Error::io(path.display().to_string(), e))?;
133
134        let format = parsers::detect_format_from_path(path)
135            .unwrap_or_else(|| parsers::detect_format(&content));
136
137        let mut config = Self::from_string(&content, Some(format))?;
138        config.file_path = Some(path.to_path_buf());
139
140        Ok(config)
141    }
142
143    /// Async version of from_file
144    #[cfg(feature = "async")]
145    pub async fn from_file_async<P: AsRef<Path>>(path: P) -> Result<Self> {
146        let path = path.as_ref();
147        let content = tokio::fs::read_to_string(path)
148            .await
149            .map_err(|e| Error::io(path.display().to_string(), e))?;
150
151        let format = parsers::detect_format_from_path(path)
152            .unwrap_or_else(|| parsers::detect_format(&content));
153
154        let mut config = Self::from_string(&content, Some(format))?;
155        config.file_path = Some(path.to_path_buf());
156
157        Ok(config)
158    }
159
160    /// Get a value by path
161    pub fn get(&self, path: &str) -> Option<&Value> {
162        self.values.get(path)
163    }
164
165    /// Get a mutable reference to a value by path
166    pub fn get_mut(&mut self, path: &str) -> Result<&mut Value> {
167        self.values.get_mut_nested(path)
168    }
169
170    /// Set a value by path
171    pub fn set<V: Into<Value>>(&mut self, path: &str, value: V) -> Result<()> {
172        self.values.set_nested(path, value.into())?;
173        self.modified = true;
174        Ok(())
175    }
176
177    /// Remove a value by path  
178    pub fn remove(&mut self, path: &str) -> Result<Option<Value>> {
179        let result = self.values.remove(path)?;
180        if result.is_some() {
181            self.modified = true;
182        }
183        Ok(result)
184    }
185
186    /// Check if a path exists
187    pub fn contains_key(&self, path: &str) -> bool {
188        self.values.contains_key(path)
189    }
190
191    /// Get all keys in the configuration
192    pub fn keys(&self) -> Result<Vec<&str>> {
193        self.values.keys()
194    }
195
196    /// Check if the configuration has been modified
197    pub fn is_modified(&self) -> bool {
198        self.modified
199    }
200
201    /// Mark the configuration as unmodified
202    pub fn mark_clean(&mut self) {
203        self.modified = false;
204    }
205
206    /// Get the configuration format
207    pub fn format(&self) -> &str {
208        &self.format
209    }
210
211    /// Get the file path (if loaded from file)
212    pub fn file_path(&self) -> Option<&Path> {
213        self.file_path.as_deref()
214    }
215
216    /// Save the configuration to its original file
217    pub fn save(&mut self) -> Result<()> {
218        match &self.file_path {
219            Some(path) => {
220                self.save_to_file(path.clone())?;
221                self.modified = false;
222                Ok(())
223            }
224            None => Err(Error::internal(
225                "Cannot save configuration that wasn't loaded from a file",
226            )),
227        }
228    }
229
230    /// Save the configuration to a specific file
231    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
232        let serialized = self.serialize()?;
233        std::fs::write(path, serialized).map_err(|e| Error::io("save".to_string(), e))?;
234        Ok(())
235    }
236
237    /// Async version of save
238    #[cfg(feature = "async")]
239    pub async fn save_async(&mut self) -> Result<()> {
240        match &self.file_path {
241            Some(path) => {
242                self.save_to_file_async(path.clone()).await?;
243                self.modified = false;
244                Ok(())
245            }
246            None => Err(Error::internal(
247                "Cannot save configuration that wasn't loaded from a file",
248            )),
249        }
250    }
251
252    /// Async version of save_to_file
253    #[cfg(feature = "async")]
254    pub async fn save_to_file_async<P: AsRef<Path>>(&self, path: P) -> Result<()> {
255        let serialized = self.serialize()?;
256        tokio::fs::write(path, serialized)
257            .await
258            .map_err(|e| Error::io("save".to_string(), e))?;
259        Ok(())
260    }
261
262    /// Serialize the configuration to string format
263    pub fn serialize(&self) -> Result<String> {
264        match self.format.as_str() {
265            "json" => {
266                #[cfg(feature = "json")]
267                return crate::parsers::json_parser::serialize(&self.values);
268                #[cfg(not(feature = "json"))]
269                return Err(Error::feature_not_enabled("json"));
270            }
271            "toml" => {
272                #[cfg(feature = "toml")]
273                {
274                    // Use NOML's serializer for format preservation
275                    #[cfg(feature = "noml")]
276                    if let Some(ref document) = self.noml_document {
277                        return Ok(noml::serialize_document(document)?);
278                    }
279                    // Fallback to basic serialization
280                    self.serialize_as_toml()
281                }
282                #[cfg(not(feature = "toml"))]
283                return Err(Error::feature_not_enabled("toml"));
284            }
285            "noml" => {
286                #[cfg(feature = "noml")]
287                {
288                    if let Some(ref document) = self.noml_document {
289                        Ok(noml::serialize_document(document)?)
290                    } else {
291                        Err(Error::internal("NOML document not preserved"))
292                    }
293                }
294                #[cfg(not(feature = "noml"))]
295                return Err(Error::feature_not_enabled("noml"));
296            }
297            "conf" => self.serialize_as_conf(),
298            _ => Err(Error::unknown_format(&self.format)),
299        }
300    }
301
302    /// Serialize as CONF format
303    fn serialize_as_conf(&self) -> Result<String> {
304        let mut output = String::new();
305        if let Value::Table(table) = &self.values {
306            self.write_conf_table(&mut output, table, "")?;
307        }
308        Ok(output)
309    }
310
311    /// Helper to write CONF format table
312    fn write_conf_table(
313        &self,
314        output: &mut String,
315        table: &BTreeMap<String, Value>,
316        section_prefix: &str,
317    ) -> Result<()> {
318        // First pass: write simple key-value pairs
319        for (key, value) in table {
320            if !value.is_table() {
321                let formatted_value = self.format_conf_value(value)?;
322                output.push_str(&format!("{key} = {formatted_value}\n"));
323            }
324        }
325
326        // Second pass: write sections
327        for (key, value) in table {
328            if let Value::Table(nested_table) = value {
329                let section_name = if section_prefix.is_empty() {
330                    key.clone()
331                } else {
332                    format!("{section_prefix}.{key}")
333                };
334
335                output.push_str(&format!("\n[{section_name}]\n"));
336                self.write_conf_table(output, nested_table, &section_name)?;
337            }
338        }
339
340        Ok(())
341    }
342
343    /// Format a value for CONF output
344    #[allow(clippy::only_used_in_recursion)]
345    fn format_conf_value(&self, value: &Value) -> Result<String> {
346        match value {
347            Value::Null => Ok("null".to_string()),
348            Value::Bool(b) => Ok(b.to_string()),
349            Value::Integer(i) => Ok(i.to_string()),
350            Value::Float(f) => Ok(f.to_string()),
351            Value::String(s) => {
352                if s.contains(' ') || s.contains('\t') || s.contains('\n') {
353                    Ok(format!("\"{}\"", s.replace('"', "\\\"")))
354                } else {
355                    Ok(s.clone())
356                }
357            }
358            Value::Array(arr) => {
359                let items: Result<Vec<String>> =
360                    arr.iter().map(|v| self.format_conf_value(v)).collect();
361                Ok(items?.join(" "))
362            }
363            Value::Table(_) => Err(Error::type_error(
364                "Cannot serialize nested table as value",
365                "primitive",
366                "table",
367            )),
368            #[cfg(feature = "chrono")]
369            Value::DateTime(dt) => Ok(dt.to_rfc3339()),
370        }
371    }
372
373    /// Serialize as TOML format (basic implementation)
374    #[cfg(feature = "toml")]
375    fn serialize_as_toml(&self) -> Result<String> {
376        // This is a simplified TOML serializer
377        // In practice, you'd use the NOML library for proper TOML serialization
378        Err(Error::internal(
379            "Basic TOML serialization not implemented - use NOML library",
380        ))
381    }
382
383    /// Validate the configuration against a schema
384    #[cfg(feature = "schema")]
385    pub fn validate_schema(&self, schema: &Schema) -> Result<()> {
386        schema.validate(&self.values)
387    }
388
389    /// Get the underlying Value
390    pub fn as_value(&self) -> &Value {
391        &self.values
392    }
393
394    /// Merge another configuration into this one
395    pub fn merge(&mut self, other: &Config) -> Result<()> {
396        self.merge_value(&other.values)?;
397        self.modified = true;
398        Ok(())
399    }
400
401    /// Helper to merge values recursively
402    fn merge_value(&mut self, other: &Value) -> Result<()> {
403        match (&mut self.values, other) {
404            (Value::Table(self_table), Value::Table(other_table)) => {
405                for (key, other_value) in other_table {
406                    match self_table.get_mut(key) {
407                        Some(self_value) => {
408                            if let (Value::Table(_), Value::Table(_)) = (&*self_value, other_value)
409                            {
410                                // Create a temporary config for recursive merging
411                                let mut temp_config = Config::new();
412                                temp_config.values = self_value.clone();
413                                temp_config.merge_value(other_value)?;
414                                *self_value = temp_config.values;
415                            } else {
416                                // Replace value
417                                *self_value = other_value.clone();
418                            }
419                        }
420                        None => {
421                            // Insert new value
422                            self_table.insert(key.clone(), other_value.clone());
423                        }
424                    }
425                }
426            }
427            _ => {
428                // Replace entire value
429                self.values = other.clone();
430            }
431        }
432        Ok(())
433    }
434
435    // =====================================================================
436    // Validation Methods (Feature-gated)
437    // =====================================================================
438
439    // --- CONVENIENCE METHODS & BUILDER PATTERN ---
440
441    /// Get a value by path with a more ergonomic API
442    pub fn key(&self, path: &str) -> ConfigValue {
443        ConfigValue::new(self.get(path))
444    }
445
446    /// Check if configuration has any value at the given path
447    pub fn has(&self, path: &str) -> bool {
448        self.contains_key(path)
449    }
450
451    /// Get a value with a default fallback
452    pub fn get_or<V>(&self, path: &str, default: V) -> V
453    where
454        V: TryFrom<Value> + Clone,
455        V::Error: std::fmt::Debug,
456    {
457        self.get(path)
458            .and_then(|v| V::try_from(v.clone()).ok())
459            .unwrap_or(default)
460    }
461
462    // --- VALIDATION SUPPORT ---
463
464    /// Set validation rules for this configuration
465    #[cfg(feature = "validation")]
466    pub fn set_validation_rules(&mut self, rules: ValidationRuleSet) {
467        self.validation_rules = Some(rules);
468    }
469
470    /// Validate the current configuration against all registered rules
471    #[cfg(feature = "validation")]
472    pub fn validate(&mut self) -> Result<Vec<ValidationError>> {
473        match &mut self.validation_rules {
474            Some(rules) => {
475                if let Value::Table(table) = &self.values {
476                    let mut errors = Vec::new();
477
478                    // Validate each key-value pair
479                    for (key, value) in table {
480                        errors.extend(rules.validate(key, value));
481                    }
482
483                    // Also validate for required keys (if any RequiredKeyValidator exists)
484                    // This is handled by individual rule implementations
485
486                    Ok(errors)
487                } else {
488                    Err(Error::validation(
489                        "Configuration root must be a table for validation",
490                    ))
491                }
492            }
493            None => Ok(Vec::new()), // No rules = no errors
494        }
495    }
496
497    /// Validate and return only critical errors
498    #[cfg(feature = "validation")]
499    pub fn validate_critical_only(&mut self) -> Result<Vec<ValidationError>> {
500        let all_errors = self.validate()?;
501        Ok(all_errors
502            .into_iter()
503            .filter(|e| e.severity == crate::validation::ValidationSeverity::Critical)
504            .collect())
505    }
506
507    /// Check if configuration is valid (has no critical errors)
508    #[cfg(feature = "validation")]
509    pub fn is_valid(&mut self) -> Result<bool> {
510        let critical_errors = self.validate_critical_only()?;
511        Ok(critical_errors.is_empty())
512    }
513
514    /// Validate a specific value at a path
515    #[cfg(feature = "validation")]
516    pub fn validate_path(&mut self, path: &str) -> Result<Vec<ValidationError>> {
517        // Get the value first to avoid borrowing conflicts, clone to own it
518        let value = self
519            .get(path)
520            .ok_or_else(|| Error::key_not_found(path))?
521            .clone();
522
523        match &mut self.validation_rules {
524            Some(rules) => Ok(rules.validate(path, &value)),
525            None => Ok(Vec::new()),
526        }
527    }
528}
529
530impl Default for Config {
531    fn default() -> Self {
532        Self::new()
533    }
534}
535
536/// Ergonomic wrapper for accessing configuration values
537pub struct ConfigValue<'a> {
538    value: Option<&'a Value>,
539}
540
541impl<'a> ConfigValue<'a> {
542    fn new(value: Option<&'a Value>) -> Self {
543        Self { value }
544    }
545
546    /// Get as string with default fallback
547    pub fn as_string(&self) -> Result<String> {
548        match self.value {
549            Some(v) => v.as_string().map(|s| s.to_string()),
550            None => Err(Error::key_not_found("value not found")),
551        }
552    }
553
554    /// Get as string with custom default
555    pub fn as_string_or(&self, default: &str) -> String {
556        self.value
557            .and_then(|v| v.as_string().ok())
558            .map(|s| s.to_string())
559            .unwrap_or_else(|| default.to_string())
560    }
561
562    /// Get as integer with default fallback
563    pub fn as_integer(&self) -> Result<i64> {
564        match self.value {
565            Some(v) => v.as_integer(),
566            None => Err(Error::key_not_found("value not found")),
567        }
568    }
569
570    /// Get as integer with custom default
571    pub fn as_integer_or(&self, default: i64) -> i64 {
572        self.value
573            .and_then(|v| v.as_integer().ok())
574            .unwrap_or(default)
575    }
576
577    /// Get as boolean with default fallback
578    pub fn as_bool(&self) -> Result<bool> {
579        match self.value {
580            Some(v) => v.as_bool(),
581            None => Err(Error::key_not_found("value not found")),
582        }
583    }
584
585    /// Get as boolean with custom default
586    pub fn as_bool_or(&self, default: bool) -> bool {
587        self.value.and_then(|v| v.as_bool().ok()).unwrap_or(default)
588    }
589
590    /// Check if the value exists
591    pub fn exists(&self) -> bool {
592        self.value.is_some()
593    }
594
595    /// Get the underlying Value reference if it exists
596    pub fn value(&self) -> Option<&'a Value> {
597        self.value
598    }
599}
600
601/// Builder pattern for Config creation
602pub struct ConfigBuilder {
603    format: Option<String>,
604    #[cfg(feature = "validation")]
605    validation_rules: Option<ValidationRuleSet>,
606}
607
608impl ConfigBuilder {
609    /// Create a new ConfigBuilder
610    pub fn new() -> Self {
611        Self {
612            format: None,
613            #[cfg(feature = "validation")]
614            validation_rules: None,
615        }
616    }
617
618    /// Set the configuration format
619    pub fn format<S: Into<String>>(mut self, format: S) -> Self {
620        self.format = Some(format.into());
621        self
622    }
623
624    /// Set validation rules
625    #[cfg(feature = "validation")]
626    pub fn validation_rules(mut self, rules: ValidationRuleSet) -> Self {
627        self.validation_rules = Some(rules);
628        self
629    }
630
631    /// Build Config from string
632    pub fn from_string(self, source: &str) -> Result<Config> {
633        #[cfg(feature = "validation")]
634        let mut config = Config::from_string(source, self.format.as_deref())?;
635        #[cfg(not(feature = "validation"))]
636        let config = Config::from_string(source, self.format.as_deref())?;
637
638        #[cfg(feature = "validation")]
639        if let Some(rules) = self.validation_rules {
640            config.set_validation_rules(rules);
641        }
642
643        Ok(config)
644    }
645
646    /// Build Config from file
647    pub fn from_file<P: AsRef<Path>>(self, path: P) -> Result<Config> {
648        #[cfg(feature = "validation")]
649        let mut config = Config::from_file(path)?;
650        #[cfg(not(feature = "validation"))]
651        let config = Config::from_file(path)?;
652
653        #[cfg(feature = "validation")]
654        if let Some(rules) = self.validation_rules {
655            config.set_validation_rules(rules);
656        }
657
658        Ok(config)
659    }
660}
661
662impl Default for ConfigBuilder {
663    fn default() -> Self {
664        Self::new()
665    }
666}
667
668/// Convert Value to Config
669impl From<Value> for Config {
670    fn from(value: Value) -> Self {
671        Self {
672            values: value,
673            file_path: None,
674            format: "conf".to_string(),
675            modified: false,
676            #[cfg(feature = "noml")]
677            noml_document: None,
678            #[cfg(feature = "validation")]
679            validation_rules: None,
680        }
681    }
682}
683
684#[cfg(test)]
685mod tests {
686    use super::*;
687
688    #[test]
689    fn test_config_creation() {
690        let config = Config::new();
691        assert!(!config.is_modified());
692        assert_eq!(config.format(), "conf");
693    }
694
695    #[test]
696    fn test_config_from_string() {
697        let config = Config::from_string("key = value\nport = 8080", Some("conf")).unwrap();
698
699        assert_eq!(config.get("key").unwrap().as_string().unwrap(), "value");
700        assert_eq!(config.get("port").unwrap().as_integer().unwrap(), 8080);
701    }
702
703    #[test]
704    fn test_config_modification() {
705        let mut config = Config::new();
706        assert!(!config.is_modified());
707
708        config.set("key", "value").unwrap();
709        assert!(config.is_modified());
710
711        config.mark_clean();
712        assert!(!config.is_modified());
713    }
714
715    #[test]
716    fn test_config_merge() {
717        let mut config1 = Config::new();
718        config1.set("a", 1).unwrap();
719        config1.set("b.x", 2).unwrap();
720
721        let mut config2 = Config::new();
722        config2.set("b.y", 3).unwrap();
723        config2.set("c", 4).unwrap();
724
725        config1.merge(&config2).unwrap();
726
727        assert_eq!(config1.get("a").unwrap().as_integer().unwrap(), 1);
728        assert_eq!(config1.get("b.x").unwrap().as_integer().unwrap(), 2);
729        assert_eq!(config1.get("b.y").unwrap().as_integer().unwrap(), 3);
730        assert_eq!(config1.get("c").unwrap().as_integer().unwrap(), 4);
731    }
732}