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