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/// High-level configuration manager with format preservation and change tracking
16///
17/// [`Config`] provides a comprehensive API for managing configurations
18/// throughout their lifecycle. It maintains both the resolved values (for fast access)
19/// and format-specific preservation data (for round-trip editing).
20///
21/// ## Key Features
22///
23/// - **Format Preservation**: Maintains comments, whitespace, and original formatting
24/// - **Change Tracking**: Automatic detection of modifications
25/// - **Type Safety**: Rich type conversion with comprehensive error handling
26/// - **Path-based Access**: Dot notation for nested value access
27/// - **Multi-format Support**: CONF, TOML, JSON, NOML formats
28/// - **Schema Validation**: Optional schema validation and enforcement
29/// - **Async Support**: Non-blocking file operations (with feature flag)
30///
31/// ## Examples
32///
33/// ```rust
34/// use config_lib::Config;
35///
36/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
37/// // Load from string
38/// let mut config = Config::from_string("port = 8080\nname = \"MyApp\"", None)?;
39///
40/// // Access values
41/// let port = config.get("port").unwrap().as_integer()?;
42/// let name = config.get("name").unwrap().as_string()?;
43///
44/// // Modify values
45/// config.set("port", 9000)?;
46///
47/// # Ok(())
48/// # }
49/// ```
50pub struct Config {
51    /// The resolved configuration values
52    values: Value,
53    
54    /// Path to the source file (if loaded from file)
55    file_path: Option<PathBuf>,
56    
57    /// Detected or specified format
58    format: String,
59    
60    /// Change tracking - has the config been modified?
61    modified: bool,
62    
63    /// Format-specific preservation data
64    #[cfg(feature = "noml")]
65    noml_document: Option<noml::Document>,
66}
67
68impl Config {
69    /// Create a new empty configuration
70    pub fn new() -> Self {
71        Self {
72            values: Value::table(BTreeMap::new()),
73            file_path: None,
74            format: "conf".to_string(),
75            modified: false,
76            #[cfg(feature = "noml")]
77            noml_document: None,
78        }
79    }
80
81    /// Load configuration from a string
82    pub fn from_string(source: &str, format: Option<&str>) -> Result<Self> {
83        let detected_format = format.unwrap_or_else(|| {
84            parsers::detect_format(source)
85        });
86
87        let values = parsers::parse_string(source, Some(detected_format))?;
88        
89        let config = Self {
90            values,
91            file_path: None,
92            format: detected_format.to_string(),
93            modified: false,
94            #[cfg(feature = "noml")]
95            noml_document: None,
96        };
97
98        // Store format-specific preservation data
99        #[cfg(feature = "noml")]
100        if detected_format == "noml" || detected_format == "toml" {
101            if let Ok(document) = noml::parse_string(source, None) {
102                config.noml_document = Some(document);
103            }
104        }
105
106        Ok(config)
107    }
108
109    /// Load configuration from a file
110    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
111        let path = path.as_ref();
112        let content = std::fs::read_to_string(path)
113            .map_err(|e| Error::io(path.display().to_string(), e))?;
114
115        let format = parsers::detect_format_from_path(path)
116            .unwrap_or_else(|| parsers::detect_format(&content));
117
118        let mut config = Self::from_string(&content, Some(format))?;
119        config.file_path = Some(path.to_path_buf());
120        
121        Ok(config)
122    }
123
124    /// Async version of from_file
125    #[cfg(feature = "async")]
126    pub async fn from_file_async<P: AsRef<Path>>(path: P) -> Result<Self> {
127        let path = path.as_ref();
128        let content = tokio::fs::read_to_string(path)
129            .await
130            .map_err(|e| Error::io(path.display().to_string(), e))?;
131
132        let format = parsers::detect_format_from_path(path)
133            .unwrap_or_else(|| parsers::detect_format(&content));
134
135        let mut config = Self::from_string(&content, Some(format))?;
136        config.file_path = Some(path.to_path_buf());
137        
138        Ok(config)
139    }
140
141    /// Get a value by path
142    pub fn get(&self, path: &str) -> Option<&Value> {
143        self.values.get(path)
144    }
145
146    /// Get a mutable reference to a value by path
147    pub fn get_mut(&mut self, path: &str) -> Result<&mut Value> {
148        self.values.get_mut_nested(path)
149    }
150
151    /// Set a value by path
152    pub fn set<V: Into<Value>>(&mut self, path: &str, value: V) -> Result<()> {
153        self.values.set_nested(path, value.into())?;
154        self.modified = true;
155        Ok(())
156    }
157
158    /// Remove a value by path  
159    pub fn remove(&mut self, path: &str) -> Result<Option<Value>> {
160        let result = self.values.remove(path)?;
161        if result.is_some() {
162            self.modified = true;
163        }
164        Ok(result)
165    }
166
167    /// Check if a path exists
168    pub fn contains_key(&self, path: &str) -> bool {
169        self.values.contains_key(path)
170    }
171
172    /// Get all keys in the configuration
173    pub fn keys(&self) -> Result<Vec<&str>> {
174        self.values.keys()
175    }
176
177    /// Check if the configuration has been modified
178    pub fn is_modified(&self) -> bool {
179        self.modified
180    }
181
182    /// Mark the configuration as unmodified
183    pub fn mark_clean(&mut self) {
184        self.modified = false;
185    }
186
187    /// Get the configuration format
188    pub fn format(&self) -> &str {
189        &self.format
190    }
191
192    /// Get the file path (if loaded from file)
193    pub fn file_path(&self) -> Option<&Path> {
194        self.file_path.as_deref()
195    }
196
197    /// Save the configuration to its original file
198    pub fn save(&mut self) -> Result<()> {
199        match &self.file_path {
200            Some(path) => {
201                self.save_to_file(path.clone())?;
202                self.modified = false;
203                Ok(())
204            },
205            None => Err(Error::internal(
206                "Cannot save configuration that wasn't loaded from a file"
207            )),
208        }
209    }
210
211    /// Save the configuration to a specific file
212    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
213        let serialized = self.serialize()?;
214        std::fs::write(path, serialized)
215            .map_err(|e| Error::io("save".to_string(), e))?;
216        Ok(())
217    }
218
219    /// Async version of save
220    #[cfg(feature = "async")]
221    pub async fn save_async(&mut self) -> Result<()> {
222        match &self.file_path {
223            Some(path) => {
224                self.save_to_file_async(path.clone()).await?;
225                self.modified = false;
226                Ok(())
227            },
228            None => Err(Error::internal(
229                "Cannot save configuration that wasn't loaded from a file"
230            )),
231        }
232    }
233
234    /// Async version of save_to_file
235    #[cfg(feature = "async")]
236    pub async fn save_to_file_async<P: AsRef<Path>>(&self, path: P) -> Result<()> {
237        let serialized = self.serialize()?;
238        tokio::fs::write(path, serialized)
239            .await
240            .map_err(|e| Error::io("save".to_string(), e))?;
241        Ok(())
242    }
243
244    /// Serialize the configuration to string format
245    pub fn serialize(&self) -> Result<String> {
246        match self.format.as_str() {
247            "json" => {
248                #[cfg(feature = "json")]
249                return crate::parsers::json_parser::serialize(&self.values);
250                #[cfg(not(feature = "json"))]
251                return Err(Error::feature_not_enabled("json"));
252            }
253            "toml" => {
254                #[cfg(feature = "toml")]
255                {
256                    // Use NOML's serializer for format preservation
257                    if let Some(ref document) = self.noml_document {
258                        return Ok(noml::serialize_document(document));
259                    } else {
260                        // Fallback to basic serialization
261                        return self.serialize_as_toml();
262                    }
263                }
264                #[cfg(not(feature = "toml"))]
265                return Err(Error::feature_not_enabled("toml"));
266            }
267            "noml" => {
268                #[cfg(feature = "noml")]
269                {
270                    if let Some(ref document) = self.noml_document {
271                        return Ok(noml::serialize_document(document));
272                    } else {
273                        return Err(Error::internal("NOML document not preserved"));
274                    }
275                }
276                #[cfg(not(feature = "noml"))]
277                return Err(Error::feature_not_enabled("noml"));
278            }
279            "conf" => self.serialize_as_conf(),
280            _ => Err(Error::unknown_format(&self.format)),
281        }
282    }
283
284    /// Serialize as CONF format
285    fn serialize_as_conf(&self) -> Result<String> {
286        let mut output = String::new();
287        if let Value::Table(table) = &self.values {
288            self.write_conf_table(&mut output, table, "")?;
289        }
290        Ok(output)
291    }
292
293    /// Helper to write CONF format table
294    fn write_conf_table(
295        &self,
296        output: &mut String,
297        table: &BTreeMap<String, Value>,
298        section_prefix: &str,
299    ) -> Result<()> {
300        // First pass: write simple key-value pairs
301        for (key, value) in table {
302            if !value.is_table() {
303                let formatted_value = self.format_conf_value(value)?;
304                output.push_str(&format!("{} = {}\n", key, formatted_value));
305            }
306        }
307
308        // Second pass: write sections
309        for (key, value) in table {
310            if let Value::Table(nested_table) = value {
311                let section_name = if section_prefix.is_empty() {
312                    key.clone()
313                } else {
314                    format!("{}.{}", section_prefix, key)
315                };
316                
317                output.push_str(&format!("\n[{}]\n", section_name));
318                self.write_conf_table(output, nested_table, &section_name)?;
319            }
320        }
321        
322        Ok(())
323    }
324
325    /// Format a value for CONF output
326    fn format_conf_value(&self, value: &Value) -> Result<String> {
327        match value {
328            Value::Null => Ok("null".to_string()),
329            Value::Bool(b) => Ok(b.to_string()),
330            Value::Integer(i) => Ok(i.to_string()),
331            Value::Float(f) => Ok(f.to_string()),
332            Value::String(s) => {
333                if s.contains(' ') || s.contains('\t') || s.contains('\n') {
334                    Ok(format!("\"{}\"", s.replace('"', "\\\"")))
335                } else {
336                    Ok(s.clone())
337                }
338            }
339            Value::Array(arr) => {
340                let items: Result<Vec<String>> = arr
341                    .iter()
342                    .map(|v| self.format_conf_value(v))
343                    .collect();
344                Ok(items?.join(" "))
345            }
346            Value::Table(_) => Err(Error::type_error(
347                "Cannot serialize nested table as value",
348                "primitive",
349                "table",
350            )),
351            #[cfg(feature = "chrono")]
352            Value::DateTime(dt) => Ok(dt.to_rfc3339()),
353        }
354    }
355
356    /// Serialize as TOML format (basic implementation)
357    #[cfg(feature = "toml")]
358    fn serialize_as_toml(&self) -> Result<String> {
359        // This is a simplified TOML serializer
360        // In practice, you'd use the NOML library for proper TOML serialization
361        Err(Error::internal(
362            "Basic TOML serialization not implemented - use NOML library"
363        ))
364    }
365
366    /// Validate the configuration against a schema
367    #[cfg(feature = "schema")]
368    pub fn validate_schema(&self, schema: &Schema) -> Result<()> {
369        schema.validate(&self.values)
370    }
371
372    /// Get the underlying Value
373    pub fn as_value(&self) -> &Value {
374        &self.values
375    }
376
377    /// Merge another configuration into this one
378    pub fn merge(&mut self, other: &Config) -> Result<()> {
379        self.merge_value(&other.values)?;
380        self.modified = true;
381        Ok(())
382    }
383
384    /// Helper to merge values recursively
385    fn merge_value(&mut self, other: &Value) -> Result<()> {
386        match (&mut self.values, other) {
387            (Value::Table(self_table), Value::Table(other_table)) => {
388                for (key, other_value) in other_table {
389                    match self_table.get_mut(key) {
390                        Some(self_value) => {
391                            if let (Value::Table(_), Value::Table(_)) = (&*self_value, other_value) {
392                                // Create a temporary config for recursive merging
393                                let mut temp_config = Config::new();
394                                temp_config.values = self_value.clone();
395                                temp_config.merge_value(other_value)?;
396                                *self_value = temp_config.values;
397                            } else {
398                                // Replace value
399                                *self_value = other_value.clone();
400                            }
401                        }
402                        None => {
403                            // Insert new value
404                            self_table.insert(key.clone(), other_value.clone());
405                        }
406                    }
407                }
408            }
409            _ => {
410                // Replace entire value
411                self.values = other.clone();
412            }
413        }
414        Ok(())
415    }
416}
417
418impl Default for Config {
419    fn default() -> Self {
420        Self::new()
421    }
422}
423
424/// Convert Value to Config
425impl From<Value> for Config {
426    fn from(value: Value) -> Self {
427        Self {
428            values: value,
429            file_path: None,
430            format: "conf".to_string(),
431            modified: false,
432            #[cfg(feature = "noml")]
433            noml_document: None,
434        }
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    #[test]
443    fn test_config_creation() {
444        let config = Config::new();
445        assert!(!config.is_modified());
446        assert_eq!(config.format(), "conf");
447    }
448
449    #[test]
450    fn test_config_from_string() {
451        let config = Config::from_string(
452            "key = value\nport = 8080",
453            Some("conf")
454        ).unwrap();
455        
456        assert_eq!(config.get("key").unwrap().as_string().unwrap(), "value");
457        assert_eq!(config.get("port").unwrap().as_integer().unwrap(), 8080);
458    }
459
460    #[test]
461    fn test_config_modification() {
462        let mut config = Config::new();
463        assert!(!config.is_modified());
464        
465        config.set("key", "value").unwrap();
466        assert!(config.is_modified());
467        
468        config.mark_clean();
469        assert!(!config.is_modified());
470    }
471
472    #[test]
473    fn test_config_merge() {
474        let mut config1 = Config::new();
475        config1.set("a", 1).unwrap();
476        config1.set("b.x", 2).unwrap();
477        
478        let mut config2 = Config::new();
479        config2.set("b.y", 3).unwrap();
480        config2.set("c", 4).unwrap();
481        
482        config1.merge(&config2).unwrap();
483        
484        assert_eq!(config1.get("a").unwrap().as_integer().unwrap(), 1);
485        assert_eq!(config1.get("b.x").unwrap().as_integer().unwrap(), 2);
486        assert_eq!(config1.get("b.y").unwrap().as_integer().unwrap(), 3);
487        assert_eq!(config1.get("c").unwrap().as_integer().unwrap(), 4);
488    }
489}