clnrm_template/
toml.rs

1//! Comprehensive TOML file operations for Cleanroom templates
2//!
3//! Provides TOML-specific functionality for template development:
4//! - TOML file loading and parsing
5//! - TOML validation and schema checking
6//! - TOML file writing and formatting
7//! - TOML merging and composition
8//! - TOML diff and patch operations
9//! - Template file organization and management
10
11use crate::error::{Result, TemplateError};
12use serde_json::Value;
13use std::collections::{HashMap, HashSet};
14use std::fs;
15use std::io::Write;
16use std::path::{Path, PathBuf};
17
18/// TOML file representation with metadata
19#[derive(Debug, Clone)]
20pub struct TomlFile {
21    /// File path
22    pub path: PathBuf,
23    /// TOML content as string
24    pub content: String,
25    /// Parsed TOML as JSON Value for manipulation
26    pub parsed: Value,
27    /// File metadata
28    pub metadata: TomlMetadata,
29}
30
31/// TOML file metadata for tracking and validation
32#[derive(Debug, Clone)]
33pub struct TomlMetadata {
34    /// File size in bytes
35    pub size: u64,
36    /// Last modification time
37    pub modified: std::time::SystemTime,
38    /// File permissions
39    pub permissions: std::fs::Permissions,
40    /// Template variables used (for analysis)
41    pub variables_used: HashSet<String>,
42    /// Template functions used (for analysis)
43    pub functions_used: HashSet<String>,
44}
45
46/// TOML file loader with comprehensive parsing capabilities
47#[derive(Debug, Clone)]
48pub struct TomlLoader {
49    /// Base directories to search for TOML files
50    search_paths: Vec<PathBuf>,
51    /// File extensions to consider (default: toml, clnrm.toml)
52    extensions: Vec<String>,
53    /// Enable recursive directory scanning
54    recursive: bool,
55    /// Validation rules to apply during loading
56    validation_rules: Vec<crate::validation::ValidationRule>,
57}
58
59impl Default for TomlLoader {
60    fn default() -> Self {
61        Self {
62            search_paths: Vec::new(),
63            extensions: vec!["toml".to_string(), "clnrm.toml".to_string()],
64            recursive: true,
65            validation_rules: Vec::new(),
66        }
67    }
68}
69
70impl TomlLoader {
71    /// Create new TOML loader
72    pub fn new() -> Self {
73        Self::default()
74    }
75
76    /// Add search path for TOML files
77    pub fn with_search_path<P: AsRef<Path>>(mut self, path: P) -> Self {
78        self.search_paths.push(path.as_ref().to_path_buf());
79        self
80    }
81
82    /// Add multiple search paths
83    pub fn with_search_paths<I, P>(mut self, paths: I) -> Self
84    where
85        I: IntoIterator<Item = P>,
86        P: AsRef<Path>,
87    {
88        for path in paths {
89            self.search_paths.push(path.as_ref().to_path_buf());
90        }
91        self
92    }
93
94    /// Set file extensions to include
95    pub fn with_extensions<I, S>(mut self, extensions: I) -> Self
96    where
97        I: IntoIterator<Item = S>,
98        S: Into<String>,
99    {
100        self.extensions = extensions.into_iter().map(|s| s.into()).collect();
101        self
102    }
103
104    /// Enable/disable recursive scanning
105    pub fn recursive(mut self, recursive: bool) -> Self {
106        self.recursive = recursive;
107        self
108    }
109
110    /// Add validation rule for loaded TOML files
111    pub fn with_validation_rule(mut self, rule: crate::validation::ValidationRule) -> Self {
112        self.validation_rules.push(rule);
113        self
114    }
115
116    /// Load single TOML file
117    ///
118    /// # Arguments
119    /// * `path` - Path to TOML file
120    pub fn load_file<P: AsRef<Path>>(&self, path: P) -> Result<TomlFile> {
121        let path = path.as_ref();
122
123        if !path.exists() {
124            return Err(TemplateError::IoError(format!(
125                "TOML file not found: {}",
126                path.display()
127            )));
128        }
129
130        if !path.is_file() {
131            return Err(TemplateError::IoError(format!(
132                "Path is not a file: {}",
133                path.display()
134            )));
135        }
136
137        // Check file extension
138        if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
139            if !self.extensions.contains(&ext.to_string()) {
140                return Err(TemplateError::ValidationError(format!(
141                    "File extension '{}' not supported. Expected: {:?}",
142                    ext, self.extensions
143                )));
144            }
145        }
146
147        let content = fs::read_to_string(path)
148            .map_err(|e| TemplateError::IoError(format!("Failed to read TOML file: {}", e)))?;
149
150        let parsed = toml::from_str::<Value>(&content)
151            .map_err(|e| TemplateError::ValidationError(format!("Invalid TOML format: {}", e)))?;
152
153        let metadata = path
154            .metadata()
155            .map_err(|e| TemplateError::IoError(format!("Failed to read file metadata: {}", e)))?;
156
157        let file = TomlFile {
158            path: path.to_path_buf(),
159            content,
160            parsed,
161            metadata: TomlMetadata {
162                size: metadata.len(),
163                modified: metadata.modified().map_err(|e| {
164                    TemplateError::IoError(format!("Failed to get modification time: {}", e))
165                })?,
166                permissions: metadata.permissions(),
167                variables_used: HashSet::new(),
168                functions_used: HashSet::new(),
169            },
170        };
171
172        // Apply validation rules
173        for rule in &self.validation_rules {
174            rule.validate(&file.parsed, &file.path.to_string_lossy())?;
175        }
176
177        Ok(file)
178    }
179
180    /// Load all TOML files from search paths
181    ///
182    /// Returns map of file paths to TomlFile objects
183    pub fn load_all(&self) -> Result<HashMap<PathBuf, TomlFile>> {
184        let mut files = HashMap::new();
185
186        for search_path in &self.search_paths {
187            self.scan_directory(search_path, &mut files)?;
188        }
189
190        Ok(files)
191    }
192
193    /// Scan directory for TOML files
194    fn scan_directory(&self, dir: &Path, files: &mut HashMap<PathBuf, TomlFile>) -> Result<()> {
195        use walkdir::WalkDir;
196
197        let walker = if self.recursive {
198            WalkDir::new(dir)
199        } else {
200            WalkDir::new(dir).max_depth(1)
201        };
202
203        for entry in walker {
204            let entry = entry.map_err(|e| {
205                TemplateError::IoError(format!("Failed to read directory entry: {}", e))
206            })?;
207
208            if entry.file_type().is_file() {
209                let path = entry.path();
210
211                // Check if file has supported extension
212                if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
213                    if self.extensions.contains(&ext.to_string()) {
214                        match self.load_file(path) {
215                            Ok(file) => {
216                                files.insert(path.to_path_buf(), file);
217                            }
218                            Err(e) => {
219                                eprintln!("Warning: Failed to load TOML file {:?}: {}", path, e);
220                            }
221                        }
222                    }
223                }
224            }
225        }
226
227        Ok(())
228    }
229
230    /// Load TOML files matching glob pattern
231    pub fn load_glob(&self, pattern: &str) -> Result<HashMap<PathBuf, TomlFile>> {
232        use globset::{Glob, GlobSetBuilder};
233
234        let glob = Glob::new(pattern).map_err(|e| {
235            TemplateError::ConfigError(format!("Invalid glob pattern '{}': {}", pattern, e))
236        })?;
237
238        let glob_set = GlobSetBuilder::new()
239            .add(glob)
240            .build()
241            .map_err(|e| TemplateError::ConfigError(format!("Failed to build glob set: {}", e)))?;
242
243        let mut files = HashMap::new();
244
245        for search_path in &self.search_paths {
246            self.scan_glob_pattern(search_path, &glob_set, &mut files)?;
247        }
248
249        Ok(files)
250    }
251
252    /// Scan directory with glob pattern
253    fn scan_glob_pattern(
254        &self,
255        dir: &Path,
256        glob_set: &globset::GlobSet,
257        files: &mut HashMap<PathBuf, TomlFile>,
258    ) -> Result<()> {
259        use walkdir::WalkDir;
260
261        let walker = if self.recursive {
262            WalkDir::new(dir)
263        } else {
264            WalkDir::new(dir).max_depth(1)
265        };
266
267        for entry in walker {
268            let entry = entry.map_err(|e| {
269                TemplateError::IoError(format!("Failed to read directory entry: {}", e))
270            })?;
271
272            if entry.file_type().is_file() {
273                let path_str = entry.path().to_string_lossy();
274                if glob_set.is_match(&*path_str) {
275                    match self.load_file(entry.path()) {
276                        Ok(file) => {
277                            files.insert(entry.path().to_path_buf(), file);
278                        }
279                        Err(e) => {
280                            eprintln!(
281                                "Warning: Failed to load TOML file {:?}: {}",
282                                entry.path(),
283                                e
284                            );
285                        }
286                    }
287                }
288            }
289        }
290
291        Ok(())
292    }
293}
294
295/// TOML file writer with formatting and validation
296#[derive(Debug, Clone)]
297pub struct TomlWriter {
298    /// Enable pretty formatting
299    pretty: bool,
300    /// Backup files before writing
301    backup: bool,
302    /// Validate before writing
303    validate: bool,
304    /// Custom header comment for generated files
305    header: Option<String>,
306}
307
308impl Default for TomlWriter {
309    fn default() -> Self {
310        Self {
311            pretty: true,
312            backup: true,
313            validate: true,
314            header: Some("# Generated by clnrm-template".to_string()),
315        }
316    }
317}
318
319impl TomlWriter {
320    /// Create new TOML writer
321    pub fn new() -> Self {
322        Self::default()
323    }
324
325    /// Enable/disable pretty formatting
326    pub fn pretty(mut self, pretty: bool) -> Self {
327        self.pretty = pretty;
328        self
329    }
330
331    /// Enable/disable backup creation
332    pub fn backup(mut self, backup: bool) -> Self {
333        self.backup = backup;
334        self
335    }
336
337    /// Enable/disable validation before writing
338    pub fn validate(mut self, validate: bool) -> Self {
339        self.validate = validate;
340        self
341    }
342
343    /// Set custom header comment
344    pub fn with_header<S: Into<String>>(mut self, header: S) -> Self {
345        self.header = Some(header.into());
346        self
347    }
348
349    /// Write TOML content to file
350    ///
351    /// # Arguments
352    /// * `path` - Target file path
353    /// * `content` - TOML content to write
354    /// * `validator` - Optional validator to run before writing
355    pub fn write_file<P: AsRef<Path>>(
356        &self,
357        path: P,
358        content: &str,
359        validator: Option<&crate::validation::TemplateValidator>,
360    ) -> Result<()> {
361        let path = path.as_ref();
362
363        // Validate before writing if enabled
364        if self.validate {
365            if let Some(validator) = validator {
366                validator.validate(content, &path.to_string_lossy())?;
367            }
368        }
369
370        // Create backup if enabled and file exists
371        if self.backup && path.exists() {
372            self.create_backup(path)?;
373        }
374
375        // Prepare content with header
376        let final_content = if let Some(ref header) = self.header {
377            format!("{}\n{}\n", header, content)
378        } else {
379            content.to_string()
380        };
381
382        // Write file
383        let mut file = fs::File::create(path)
384            .map_err(|e| TemplateError::IoError(format!("Failed to create file: {}", e)))?;
385
386        file.write_all(final_content.as_bytes())
387            .map_err(|e| TemplateError::IoError(format!("Failed to write file: {}", e)))?;
388
389        file.sync_all()
390            .map_err(|e| TemplateError::IoError(format!("Failed to sync file: {}", e)))?;
391
392        Ok(())
393    }
394
395    /// Create backup of existing file
396    fn create_backup(&self, path: &Path) -> Result<()> {
397        let _backup_path = self.backup_path(path);
398
399        fs::copy(path, &_backup_path)
400            .map_err(|e| TemplateError::IoError(format!("Failed to create backup: {}", e)))?;
401
402        Ok(())
403    }
404
405    /// Generate backup file path
406    fn backup_path(&self, path: &Path) -> PathBuf {
407        let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
408        let stem = path.file_stem().unwrap_or_default().to_string_lossy();
409        let _ext = path.extension().unwrap_or_default().to_string_lossy();
410
411        path.with_file_name(format!("{}.{}.bak", stem, timestamp))
412    }
413}
414
415/// TOML merger for combining multiple TOML sources
416pub struct TomlMerger {
417    /// Merge strategy for conflicting keys
418    strategy: MergeStrategy,
419    /// Preserve comments and formatting
420    preserve_formatting: bool,
421    /// Deep merge nested structures
422    deep_merge: bool,
423}
424
425pub enum MergeStrategy {
426    /// Overwrite existing values (default)
427    Overwrite,
428    /// Merge arrays by concatenation
429    MergeArrays,
430    /// Preserve existing values
431    Preserve,
432    /// Custom merge function
433    Custom,
434}
435
436impl std::fmt::Debug for MergeStrategy {
437    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438        match self {
439            MergeStrategy::Overwrite => write!(f, "Overwrite"),
440            MergeStrategy::MergeArrays => write!(f, "MergeArrays"),
441            MergeStrategy::Preserve => write!(f, "Preserve"),
442            MergeStrategy::Custom => write!(f, "Custom"),
443        }
444    }
445}
446
447impl Clone for MergeStrategy {
448    fn clone(&self) -> Self {
449        match self {
450            MergeStrategy::Overwrite => MergeStrategy::Overwrite,
451            MergeStrategy::MergeArrays => MergeStrategy::MergeArrays,
452            MergeStrategy::Preserve => MergeStrategy::Preserve,
453            MergeStrategy::Custom => MergeStrategy::Custom,
454        }
455    }
456}
457
458impl Default for TomlMerger {
459    fn default() -> Self {
460        Self {
461            strategy: MergeStrategy::Overwrite,
462            preserve_formatting: false,
463            deep_merge: true,
464        }
465    }
466}
467
468impl TomlMerger {
469    /// Create new TOML merger
470    pub fn new() -> Self {
471        Self::default()
472    }
473
474    /// Set merge strategy
475    pub fn with_strategy(mut self, strategy: MergeStrategy) -> Self {
476        self.strategy = strategy;
477        self
478    }
479
480    /// Enable/disable formatting preservation
481    pub fn preserve_formatting(mut self, preserve: bool) -> Self {
482        self.preserve_formatting = preserve;
483        self
484    }
485
486    /// Enable/disable deep merging
487    pub fn deep_merge(mut self, deep: bool) -> Self {
488        self.deep_merge = deep;
489        self
490    }
491
492    /// Merge two TOML values
493    ///
494    /// # Arguments
495    /// * `base` - Base TOML value
496    /// * `overlay` - TOML value to merge on top
497    pub fn merge(&self, base: &Value, overlay: &Value) -> Result<Value> {
498        match (&base, &overlay) {
499            (Value::Object(base_obj), Value::Object(overlay_obj)) => {
500                let mut result = base_obj.clone();
501
502                for (key, overlay_value) in overlay_obj {
503                    if let Some(base_value) = base_obj.get(key) {
504                        let merged = self.merge_values(base_value, overlay_value)?;
505                        result.insert(key.clone(), merged);
506                    } else {
507                        result.insert(key.clone(), overlay_value.clone());
508                    }
509                }
510
511                Ok(Value::Object(result))
512            }
513            _ => {
514                // For non-objects, use overlay strategy
515                Ok(overlay.clone())
516            }
517        }
518    }
519
520    /// Merge individual values based on strategy
521    fn merge_values(&self, base: &Value, overlay: &Value) -> Result<Value> {
522        match &self.strategy {
523            MergeStrategy::Overwrite => Ok(overlay.clone()),
524            MergeStrategy::Preserve => Ok(base.clone()),
525            MergeStrategy::MergeArrays => {
526                if let (Value::Array(base_arr), Value::Array(overlay_arr)) = (base, overlay) {
527                    let mut merged = base_arr.clone();
528                    merged.extend(overlay_arr.iter().cloned());
529                    Ok(Value::Array(merged))
530                } else {
531                    Ok(overlay.clone())
532                }
533            }
534            MergeStrategy::Custom => Ok(overlay.clone()), // Simplified for now
535        }
536    }
537
538    /// Merge multiple TOML files
539    ///
540    /// # Arguments
541    /// * `files` - Vector of TomlFile objects to merge
542    pub fn merge_files(&self, files: &[&TomlFile]) -> Result<TomlFile> {
543        if files.is_empty() {
544            return Err(TemplateError::ValidationError(
545                "No files to merge".to_string(),
546            ));
547        }
548
549        let mut merged_value = files[0].parsed.clone();
550
551        for file in &files[1..] {
552            merged_value = self.merge(&merged_value, &file.parsed)?;
553        }
554
555        // Create new TomlFile with merged content
556        let merged_content = if self.preserve_formatting {
557            // Try to preserve formatting (simplified)
558            toml::to_string_pretty(&merged_value)
559                .unwrap_or_else(|_| toml::to_string(&merged_value).unwrap_or_default())
560        } else {
561            toml::to_string(&merged_value).map_err(|e| {
562                TemplateError::ValidationError(format!("Failed to serialize merged TOML: {}", e))
563            })?
564        };
565
566        Ok(TomlFile {
567            path: files[0].path.with_extension("merged.toml"),
568            content: merged_content,
569            parsed: merged_value,
570            metadata: files[0].metadata.clone(), // Use first file's metadata
571        })
572    }
573}
574
575/// TOML file utilities for common operations
576pub struct TomlUtils;
577
578impl TomlUtils {
579    /// Extract variables from TOML content for template analysis
580    ///
581    /// # Arguments
582    /// * `content` - TOML content as string
583    pub fn extract_variables(content: &str) -> Result<HashSet<String>> {
584        let parsed = toml::from_str::<Value>(content).map_err(|e| {
585            TemplateError::ValidationError(format!("Invalid TOML for variable extraction: {}", e))
586        })?;
587
588        let mut variables = HashSet::new();
589        Self::extract_variables_recursive(&parsed, &mut variables, "");
590        Ok(variables)
591    }
592
593    /// Recursively extract variable references from TOML
594    fn extract_variables_recursive(value: &Value, variables: &mut HashSet<String>, prefix: &str) {
595        match value {
596            Value::String(s) => {
597                // Look for template variable patterns {{ variable }}
598                if s.contains("{{") && s.contains("}}") {
599                    // Simple extraction - in real implementation would use regex
600                    if let Some(start) = s.find("{{") {
601                        if let Some(end) = s.find("}}") {
602                            let var_part = &s[start + 2..end];
603                            if !var_part.trim().is_empty() {
604                                let var_name = if prefix.is_empty() {
605                                    var_part.trim().to_string()
606                                } else {
607                                    format!("{}.{}", prefix, var_part.trim())
608                                };
609                                variables.insert(var_name);
610                            }
611                        }
612                    }
613                }
614            }
615            Value::Object(obj) => {
616                for (key, value) in obj {
617                    let new_prefix = if prefix.is_empty() {
618                        key.clone()
619                    } else {
620                        format!("{}.{}", prefix, key)
621                    };
622                    Self::extract_variables_recursive(value, variables, &new_prefix);
623                }
624            }
625            Value::Array(arr) => {
626                for (i, value) in arr.iter().enumerate() {
627                    let new_prefix = if prefix.is_empty() {
628                        format!("{}", i)
629                    } else {
630                        format!("{}.{}", prefix, i)
631                    };
632                    Self::extract_variables_recursive(value, variables, &new_prefix);
633                }
634            }
635            _ => {} // Other types don't contain variables
636        }
637    }
638
639    /// Validate TOML file structure
640    ///
641    /// # Arguments
642    /// * `file` - TomlFile to validate
643    /// * `required_sections` - Required top-level sections
644    pub fn validate_structure(file: &TomlFile, required_sections: &[&str]) -> Result<()> {
645        let obj = file
646            .parsed
647            .as_object()
648            .ok_or_else(|| TemplateError::ValidationError("TOML must be an object".to_string()))?;
649
650        for section in required_sections {
651            if !obj.contains_key(*section) {
652                return Err(TemplateError::ValidationError(format!(
653                    "Required section '{}' missing in TOML file: {}",
654                    section,
655                    file.path.display()
656                )));
657            }
658        }
659
660        Ok(())
661    }
662
663    /// Compare two TOML files for differences
664    ///
665    /// # Arguments
666    /// * `file1` - First TOML file
667    /// * `file2` - Second TOML file
668    pub fn diff(file1: &TomlFile, file2: &TomlFile) -> TomlDiff {
669        let mut added = Vec::new();
670        let mut removed = Vec::new();
671        let mut changed = Vec::new();
672
673        // Compare top-level keys
674        if let (Some(obj1), Some(obj2)) = (file1.parsed.as_object(), file2.parsed.as_object()) {
675            for (key, value2) in obj2 {
676                if let Some(value1) = obj1.get(key) {
677                    if value1 != value2 {
678                        changed.push((key.clone(), value1.clone(), value2.clone()));
679                    }
680                } else {
681                    added.push((key.clone(), value2.clone()));
682                }
683            }
684
685            for (key, _) in obj1 {
686                if !obj2.contains_key(key) {
687                    removed.push(key.clone());
688                }
689            }
690        }
691
692        TomlDiff {
693            added,
694            removed,
695            changed,
696        }
697    }
698
699    /// Pretty format TOML content
700    ///
701    /// # Arguments
702    /// * `content` - TOML content to format
703    pub fn format_toml(content: &str) -> Result<String> {
704        let parsed = toml::from_str::<Value>(content).map_err(|e| {
705            TemplateError::ValidationError(format!("Invalid TOML for formatting: {}", e))
706        })?;
707
708        toml::to_string_pretty(&parsed)
709            .map_err(|e| TemplateError::ValidationError(format!("Failed to format TOML: {}", e)))
710    }
711
712    /// Validate TOML syntax and structure
713    ///
714    /// # Arguments
715    /// * `content` - TOML content to validate
716    pub fn validate_toml_syntax(content: &str) -> Result<()> {
717        // Parse to check syntax
718        toml::from_str::<Value>(content)
719            .map_err(|e| TemplateError::ValidationError(format!("Invalid TOML syntax: {}", e)))?;
720
721        Ok(())
722    }
723
724    /// Extract all keys from TOML content
725    ///
726    /// # Arguments
727    /// * `content` - TOML content
728    pub fn extract_keys(content: &str) -> Result<HashSet<String>> {
729        let parsed = toml::from_str::<Value>(content).map_err(|e| {
730            TemplateError::ValidationError(format!("Invalid TOML for key extraction: {}", e))
731        })?;
732
733        let mut keys = HashSet::new();
734        Self::extract_keys_recursive(&parsed, &mut keys, "");
735        Ok(keys)
736    }
737
738    /// Recursively extract all keys from TOML
739    fn extract_keys_recursive(value: &Value, keys: &mut HashSet<String>, prefix: &str) {
740        match value {
741            Value::Object(obj) => {
742                for (key, value) in obj {
743                    let full_key = if prefix.is_empty() {
744                        key.clone()
745                    } else {
746                        format!("{}.{}", prefix, key)
747                    };
748                    keys.insert(full_key.clone());
749                    Self::extract_keys_recursive(value, keys, &full_key);
750                }
751            }
752            Value::Array(arr) => {
753                for (i, value) in arr.iter().enumerate() {
754                    let index_key = if prefix.is_empty() {
755                        format!("{}", i)
756                    } else {
757                        format!("{}.{}", prefix, i)
758                    };
759                    Self::extract_keys_recursive(value, keys, &index_key);
760                }
761            }
762            _ => {} // Leaf values don't have keys
763        }
764    }
765
766    /// Check if TOML content contains template variables
767    ///
768    /// # Arguments
769    /// * `content` - TOML content
770    pub fn contains_templates(content: &str) -> bool {
771        content.contains("{{") || content.contains("{%") || content.contains("{#")
772    }
773
774    /// Count template variables in TOML content
775    ///
776    /// # Arguments
777    /// * `content` - TOML content
778    pub fn count_variables(content: &str) -> usize {
779        let mut count = 0;
780        let mut in_braces = false;
781
782        for ch in content.chars() {
783            match ch {
784                '{' => {
785                    if let Some(next) = content.chars().nth(count + 1) {
786                        if next == '{' {
787                            in_braces = true;
788                        }
789                    }
790                }
791                '}' => {
792                    if let Some(prev) = content.chars().nth(count - 1) {
793                        if prev == '}' && in_braces {
794                            in_braces = false;
795                        }
796                    }
797                }
798                _ => {
799                    if in_braces {
800                        // Count variables (simplified - would need proper parsing)
801                        count += 1;
802                    }
803                }
804            }
805        }
806
807        count
808    }
809}
810
811/// TOML file differences for comparison
812#[derive(Debug, Clone)]
813pub struct TomlDiff {
814    /// Keys added in second file
815    pub added: Vec<(String, Value)>,
816    /// Keys removed from first file
817    pub removed: Vec<String>,
818    /// Keys with different values
819    pub changed: Vec<(String, Value, Value)>,
820}
821
822/// Fluent API for TOML file operations
823pub struct TomlFileBuilder {
824    loader: TomlLoader,
825    writer: TomlWriter,
826    merger: TomlMerger,
827}
828
829impl TomlFileBuilder {
830    /// Start building TOML file operations
831    pub fn new() -> Self {
832        Self {
833            loader: TomlLoader::new(),
834            writer: TomlWriter::new(),
835            merger: TomlMerger::new(),
836        }
837    }
838
839    /// Configure loader
840    pub fn loader<F>(mut self, f: F) -> Self
841    where
842        F: FnOnce(TomlLoader) -> TomlLoader,
843    {
844        self.loader = f(self.loader);
845        self
846    }
847
848    /// Configure writer
849    pub fn writer<F>(mut self, f: F) -> Self
850    where
851        F: FnOnce(TomlWriter) -> TomlWriter,
852    {
853        self.writer = f(self.writer);
854        self
855    }
856
857    /// Configure merger
858    pub fn merger<F>(mut self, f: F) -> Self
859    where
860        F: FnOnce(TomlMerger) -> TomlMerger,
861    {
862        self.merger = f(self.merger);
863        self
864    }
865
866    /// Load TOML file
867    pub fn load<P: AsRef<Path>>(self, path: P) -> Result<TomlFile> {
868        self.loader.load_file(path)
869    }
870
871    /// Write TOML file
872    pub fn write<P: AsRef<Path>>(
873        self,
874        path: P,
875        content: &str,
876        validator: Option<&crate::validation::TemplateValidator>,
877    ) -> Result<()> {
878        self.writer.write_file(path, content, validator)
879    }
880
881    /// Merge TOML files
882    pub fn merge(self, files: &[&TomlFile]) -> Result<TomlFile> {
883        self.merger.merge_files(files)
884    }
885}
886
887impl Default for TomlFileBuilder {
888    fn default() -> Self {
889        Self::new()
890    }
891}
892
893#[cfg(test)]
894mod tests {
895    use super::*;
896    use tempfile::tempdir;
897
898    #[test]
899    fn test_toml_file_loading() {
900        let temp_dir = tempdir().unwrap();
901        let toml_file = temp_dir.path().join("test.toml");
902
903        let content = r#"
904[service]
905name = "test-service"
906
907[meta]
908version = "1.0.0"
909        "#;
910
911        fs::write(&toml_file, content).unwrap();
912
913        let loader = TomlLoader::new()
914            .with_search_path(&temp_dir)
915            .with_extensions(vec!["toml"]);
916
917        let file = loader.load_file(&toml_file).unwrap();
918
919        assert_eq!(file.path, toml_file);
920        assert_eq!(file.content, content);
921        assert!(file.parsed.get("service").is_some());
922        assert!(file.parsed.get("meta").is_some());
923    }
924
925    #[test]
926    fn test_toml_merging() {
927        let base_content = r#"
928[service]
929name = "base-service"
930
931[meta]
932version = "1.0.0"
933        "#;
934
935        let overlay_content = r#"
936[service]
937description = "overlay description"
938
939[config]
940debug = true
941        "#;
942
943        let base_parsed = toml::from_str::<Value>(base_content).unwrap();
944        let overlay_parsed = toml::from_str::<Value>(overlay_content).unwrap();
945
946        let merger = TomlMerger::new();
947        let merged = merger.merge(&base_parsed, &overlay_parsed).unwrap();
948
949        // Should have both service.name and service.description
950        assert!(merged.get("service").unwrap().get("name").is_some());
951        assert!(merged.get("service").unwrap().get("description").is_some());
952        assert!(merged.get("meta").is_some());
953        assert!(merged.get("config").is_some());
954    }
955
956    #[test]
957    fn test_variable_extraction() {
958        let content = r#"
959service = "{{ service_name }}"
960config = "{{ config.env }}"
961        "#;
962
963        let variables = TomlUtils::extract_variables(content).unwrap();
964        assert!(variables.contains("service_name"));
965        assert!(variables.contains("config.env"));
966    }
967
968    #[test]
969    fn test_toml_validation() {
970        let temp_dir = tempdir().unwrap();
971        let toml_file = temp_dir.path().join("config.toml");
972
973        let content = r#"
974[service]
975name = "test-service"
976
977[meta]
978version = "1.0.0"
979        "#;
980
981        fs::write(&toml_file, content).unwrap();
982
983        let file = TomlLoader::new().load_file(&toml_file).unwrap();
984        TomlUtils::validate_structure(&file, &["service", "meta"]).unwrap();
985    }
986
987    #[test]
988    fn test_toml_formatting() {
989        let content = r#"[service]name="test"[meta]version="1.0.0""#;
990        let formatted = TomlUtils::format_toml(content).unwrap();
991
992        assert!(formatted.contains("[service]"));
993        assert!(formatted.contains("[meta]"));
994        assert!(formatted.contains("name = \"test\""));
995        assert!(formatted.contains("version = \"1.0.0\""));
996    }
997
998    #[test]
999    fn test_toml_key_extraction() {
1000        let content = r#"
1001[service]
1002name = "test"
1003
1004[config]
1005debug = true
1006
1007[database]
1008host = "localhost"
1009        "#;
1010
1011        let keys = TomlUtils::extract_keys(content).unwrap();
1012        assert!(keys.contains("service"));
1013        assert!(keys.contains("service.name"));
1014        assert!(keys.contains("config"));
1015        assert!(keys.contains("config.debug"));
1016        assert!(keys.contains("database"));
1017        assert!(keys.contains("database.host"));
1018    }
1019}