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