Skip to main content

rust_guardian/config/
mod.rs

1//! Configuration loading and management for Rust Guardian
2//!
3//! Architecture: Anti-Corruption Layer - Configuration translates external YAML formats
4//! - Raw YAML structures are converted to clean domain objects
5//! - Default configurations are embedded in the domain, not infrastructure
6//! - Configuration acts as a repository for pattern rules and path filters
7
8use crate::domain::violations::{GuardianError, GuardianResult, Severity};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::fs;
12use std::path::Path;
13
14/// Main configuration structure for Rust Guardian
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct GuardianConfig {
17    /// Configuration format version
18    pub version: String,
19    /// Path filtering configuration
20    pub paths: PathConfig,
21    /// Pattern definitions organized by category
22    pub patterns: HashMap<String, PatternCategory>,
23}
24
25/// Path filtering configuration
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct PathConfig {
28    /// Include/exclude patterns (gitignore-style)
29    pub patterns: Vec<String>,
30    /// Optional .guardianignore file name
31    pub ignore_file: Option<String>,
32}
33
34/// A category of patterns (e.g., "placeholders", "architectural_violations")
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct PatternCategory {
37    /// Default severity for patterns in this category
38    pub severity: Severity,
39    /// Whether this category is enabled
40    pub enabled: bool,
41    /// Individual pattern rules
42    pub rules: Vec<PatternRule>,
43}
44
45/// Individual pattern rule configuration
46#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
47pub struct PatternRule {
48    /// Unique identifier for this rule
49    pub id: String,
50    /// Type of pattern (regex, ast, semantic)
51    #[serde(rename = "type")]
52    pub rule_type: RuleType,
53    /// The pattern to match
54    pub pattern: String,
55    /// Human-readable message for violations
56    pub message: String,
57    /// Severity override (uses category default if not specified)
58    pub severity: Option<Severity>,
59    /// Whether this rule is enabled
60    #[serde(default = "default_true")]
61    pub enabled: bool,
62    /// Case sensitivity for regex patterns
63    #[serde(default)]
64    pub case_sensitive: bool,
65    /// Conditions that exclude matches from being violations
66    pub exclude_if: Option<ExcludeConditions>,
67}
68
69/// Types of pattern matching
70#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
71#[serde(rename_all = "snake_case")]
72pub enum RuleType {
73    /// Regular expression pattern matching
74    Regex,
75    /// Abstract syntax tree analysis
76    Ast,
77    /// Semantic code analysis
78    Semantic,
79    /// Import/dependency analysis
80    ImportAnalysis,
81}
82
83/// Conditions that can exclude a match from being reported as a violation
84#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
85pub struct ExcludeConditions {
86    /// Exclude if code has specific attributes (e.g., #[test])
87    pub attribute: Option<String>,
88    /// Exclude if in test files
89    #[serde(default)]
90    pub in_tests: bool,
91    /// Exclude if in specific file patterns
92    pub file_patterns: Option<Vec<String>>,
93}
94
95impl GuardianConfig {
96    /// Load configuration from a YAML file
97    pub fn load_from_file<P: AsRef<Path>>(path: P) -> GuardianResult<Self> {
98        let contents = fs::read_to_string(&path).map_err(|e| {
99            GuardianError::config(format!(
100                "Failed to read config file '{}': {}",
101                path.as_ref().display(),
102                e
103            ))
104        })?;
105
106        let config: Self = serde_yaml::from_str(&contents).map_err(|e| {
107            GuardianError::config(format!(
108                "Failed to parse config file '{}': {}",
109                path.as_ref().display(),
110                e
111            ))
112        })?;
113
114        config.validate()?;
115        Ok(config)
116    }
117
118    /// Load configuration from string content
119    pub fn load_from_str(content: &str) -> GuardianResult<Self> {
120        let config: Self = serde_yaml::from_str(content)
121            .map_err(|e| GuardianError::config(format!("Failed to parse config: {e}")))?;
122
123        config.validate()?;
124        Ok(config)
125    }
126
127    /// Get default configuration with built-in patterns
128    pub fn with_defaults() -> Self {
129        Self {
130            version: "1.0".to_string(),
131            paths: PathConfig {
132                patterns: vec![
133                    // Default exclusions
134                    "target/".to_string(),
135                    "**/node_modules/".to_string(),
136                    "**/.git/".to_string(),
137                    "**/*.generated.*".to_string(),
138                ],
139                ignore_file: Some(".guardianignore".to_string()),
140            },
141            patterns: Self::default_patterns(),
142        }
143    }
144
145    /// Get default pattern definitions
146    fn default_patterns() -> HashMap<String, PatternCategory> {
147        let mut patterns = HashMap::new();
148
149        // Development marker detection patterns
150        patterns.insert(
151            "placeholders".to_string(),
152            PatternCategory {
153                severity: Severity::Error,
154                enabled: true,
155                rules: vec![
156                    PatternRule {
157                        id: "todo_comments".to_string(),
158                        rule_type: RuleType::Regex,
159                        pattern: build_development_marker_pattern(),
160                        message: "Development marker detected: {match}".to_string(),
161                        severity: None,
162                        enabled: true,
163                        case_sensitive: false,
164                        exclude_if: None,
165                    },
166                    PatternRule {
167                        id: "temporary_markers".to_string(),
168                        rule_type: RuleType::Regex,
169                        pattern: build_temporary_marker_pattern(),
170                        message: "Implementation marker found: {match}".to_string(),
171                        severity: None,
172                        enabled: true,
173                        case_sensitive: false,
174                        exclude_if: Some(ExcludeConditions {
175                            attribute: None,
176                            in_tests: true,
177                            file_patterns: Some(vec!["**/tests/**".to_string()]),
178                        }),
179                    },
180                    PatternRule {
181                        id: "unimplemented_macros".to_string(),
182                        rule_type: RuleType::Ast,
183                        pattern: build_unfinished_macro_pattern(),
184                        message: "Unfinished macro {macro_name}! found".to_string(),
185                        severity: None,
186                        enabled: true,
187                        case_sensitive: true,
188                        exclude_if: Some(ExcludeConditions {
189                            attribute: Some("#[test]".to_string()),
190                            in_tests: true,
191                            file_patterns: None,
192                        }),
193                    },
194                ],
195            },
196        );
197
198        // Incomplete implementations
199        patterns.insert(
200            "incomplete_implementations".to_string(),
201            PatternCategory {
202                severity: Severity::Error,
203                enabled: true,
204                rules: vec![PatternRule {
205                    id: "empty_ok_return".to_string(),
206                    rule_type: RuleType::Ast,
207                    pattern: "return_ok_unit_with_no_logic".to_string(),
208                    message: "Function returns Ok(()) with no implementation".to_string(),
209                    severity: None,
210                    enabled: true,
211                    case_sensitive: true,
212                    exclude_if: Some(ExcludeConditions {
213                        attribute: Some("#[test]".to_string()),
214                        in_tests: true,
215                        file_patterns: None,
216                    }),
217                }],
218            },
219        );
220
221        // Architectural violations
222        patterns.insert(
223            "architectural_violations".to_string(),
224            PatternCategory {
225                severity: Severity::Warning,
226                enabled: true,
227                rules: vec![
228                    PatternRule {
229                        id: "hardcoded_paths".to_string(),
230                        rule_type: RuleType::Regex,
231                        pattern: r#"["\'](\./|/|\.\./)?(\.rust/)[^"']*["\']"#.to_string(),
232                        message: "Hardcoded path found - use configuration instead".to_string(),
233                        severity: None,
234                        enabled: true,
235                        case_sensitive: true,
236                        exclude_if: Some(ExcludeConditions {
237                            attribute: None,
238                            in_tests: true,
239                            file_patterns: Some(vec![
240                                "**/tests/**".to_string(),
241                                "**/examples/**".to_string(),
242                            ]),
243                        }),
244                    },
245                    PatternRule {
246                        id: "architectural_header_missing".to_string(),
247                        rule_type: RuleType::Regex,
248                        pattern: r"//!\s*(?:.*\n)*?\s*//!\s*Architecture:".to_string(),
249                        message: "File missing architectural principle header".to_string(),
250                        severity: Some(Severity::Info),
251                        enabled: false, // Disabled by default, can be enabled per project
252                        case_sensitive: false,
253                        exclude_if: Some(ExcludeConditions {
254                            attribute: None,
255                            in_tests: true,
256                            file_patterns: Some(vec![
257                                "**/tests/**".to_string(),
258                                "**/benches/**".to_string(),
259                                "**/examples/**".to_string(),
260                            ]),
261                        }),
262                    },
263                ],
264            },
265        );
266
267        patterns
268    }
269
270    /// Validate the configuration for consistency and correctness
271    pub fn validate(&self) -> GuardianResult<()> {
272        // Check version compatibility
273        if !["1.0"].contains(&self.version.as_str()) {
274            return Err(GuardianError::config(format!(
275                "Unsupported configuration version: {}. Supported versions: 1.0",
276                self.version
277            )));
278        }
279
280        // Validate patterns
281        for (category_name, category) in &self.patterns {
282            for rule in &category.rules {
283                // Validate rule IDs are unique within category
284                let duplicate_count = category.rules.iter().filter(|r| r.id == rule.id).count();
285                if duplicate_count > 1 {
286                    return Err(GuardianError::config(format!(
287                        "Duplicate rule ID '{}' in category '{}'",
288                        rule.id, category_name
289                    )));
290                }
291
292                // Validate regex patterns can compile
293                if matches!(rule.rule_type, RuleType::Regex) {
294                    if rule.case_sensitive {
295                        regex::Regex::new(&rule.pattern)
296                    } else {
297                        regex::RegexBuilder::new(&rule.pattern)
298                            .case_insensitive(true)
299                            .build()
300                    }
301                    .map_err(|e| {
302                        GuardianError::config(format!(
303                            "Invalid regex pattern in rule '{}': {}",
304                            rule.id, e
305                        ))
306                    })?;
307                }
308            }
309        }
310
311        Ok(())
312    }
313
314    /// Get all enabled rules across all categories
315    pub fn enabled_rules(&self) -> impl Iterator<Item = (&String, &PatternCategory, &PatternRule)> {
316        self.patterns
317            .iter()
318            .filter(|(_, category)| category.enabled)
319            .flat_map(|(name, category)| {
320                category
321                    .rules
322                    .iter()
323                    .filter(|rule| rule.enabled)
324                    .map(move |rule| (name, category, rule))
325            })
326    }
327
328    /// Get effective severity for a rule (rule override or category default)
329    pub fn effective_severity(&self, category: &PatternCategory, rule: &PatternRule) -> Severity {
330        rule.severity.unwrap_or(category.severity)
331    }
332
333    /// Convert to JSON for serialization
334    pub fn to_json(&self) -> GuardianResult<String> {
335        serde_json::to_string_pretty(self)
336            .map_err(|e| GuardianError::config(format!("Failed to serialize config: {e}")))
337    }
338
339    /// Create a fingerprint of the configuration for cache validation
340    pub fn fingerprint(&self) -> String {
341        use std::collections::hash_map::DefaultHasher;
342        use std::hash::{Hash, Hasher};
343
344        let mut hasher = DefaultHasher::new();
345
346        // Create a stable representation for hashing
347        // Sort patterns to ensure consistent ordering
348        let mut sorted_patterns: Vec<_> = self.patterns.iter().collect();
349        sorted_patterns.sort_by_key(|(name, _)| name.as_str());
350
351        // Hash version and path config
352        self.version.hash(&mut hasher);
353        self.paths.patterns.len().hash(&mut hasher);
354        for pattern in &self.paths.patterns {
355            pattern.hash(&mut hasher);
356        }
357        self.paths.ignore_file.hash(&mut hasher);
358
359        // Hash patterns in sorted order
360        for (category_name, category) in sorted_patterns {
361            category_name.hash(&mut hasher);
362            category.severity.hash(&mut hasher);
363            category.enabled.hash(&mut hasher);
364
365            // Sort rules for consistent ordering
366            let mut sorted_rules = category.rules.clone();
367            sorted_rules.sort_by_key(|rule| rule.id.clone());
368
369            for rule in sorted_rules {
370                rule.id.hash(&mut hasher);
371                rule.pattern.hash(&mut hasher);
372                rule.message.hash(&mut hasher);
373                rule.enabled.hash(&mut hasher);
374                rule.case_sensitive.hash(&mut hasher);
375            }
376        }
377
378        format!("{:x}", hasher.finish())
379    }
380}
381
382impl Default for GuardianConfig {
383    fn default() -> Self {
384        Self::with_defaults()
385    }
386}
387
388fn default_true() -> bool {
389    true
390}
391
392/// Build development marker pattern with simple, readable literals
393fn build_development_marker_pattern() -> String {
394    // Simple regex pattern for development markers
395    r"\b(TODO|FIXME|HACK|XXX|BUG|REFACTOR)\b".to_string()
396}
397
398/// Build implementation marker pattern with simple, readable literals
399fn build_temporary_marker_pattern() -> String {
400    // Simple regex pattern for temporary implementation markers
401    r"(?i)\b(for now|temporary|placeholder|stub|dummy|fake)\b".to_string()
402}
403
404/// Build unfinished macro pattern with simple, readable literals
405fn build_unfinished_macro_pattern() -> String {
406    // Simple pattern to detect incomplete macro calls
407    "macro_call:unimplemented|todo|panic".to_string()
408}
409
410/// Configuration builder for programmatic construction
411pub struct ConfigBuilder {
412    config: GuardianConfig,
413}
414
415impl ConfigBuilder {
416    /// Create a new builder with default configuration
417    pub fn new() -> Self {
418        Self {
419            config: GuardianConfig::default(),
420        }
421    }
422
423    /// Add a path pattern
424    pub fn add_path_pattern(mut self, pattern: impl Into<String>) -> Self {
425        self.config.paths.patterns.push(pattern.into());
426        self
427    }
428
429    /// Set the ignore file name
430    pub fn ignore_file(mut self, filename: impl Into<String>) -> Self {
431        self.config.paths.ignore_file = Some(filename.into());
432        self
433    }
434
435    /// Add a pattern category
436    pub fn add_category(mut self, name: impl Into<String>, category: PatternCategory) -> Self {
437        self.config.patterns.insert(name.into(), category);
438        self
439    }
440
441    /// Build the final configuration
442    pub fn build(self) -> GuardianResult<GuardianConfig> {
443        self.config.validate()?;
444        Ok(self.config)
445    }
446}
447
448impl Default for ConfigBuilder {
449    fn default() -> Self {
450        Self::new()
451    }
452}
453
454impl GuardianConfig {
455    /// Self-validating configuration integrity check following architectural principles
456    /// Guardian validates itself rather than relying on external tests
457    pub fn verify_config_integrity(&self) -> GuardianResult<()> {
458        // Domain validates its own consistency
459        if self.version != "1.0" {
460            return Err(GuardianError::config(
461                "Configuration system requires version 1.0".to_string(),
462            ));
463        }
464
465        // Essential patterns must exist for guardian to function
466        if !self.patterns.contains_key("placeholders") {
467            return Err(GuardianError::config(
468                "Core pattern category 'placeholders' missing from guardian".to_string(),
469            ));
470        }
471
472        // Validate guardian can generate its own fingerprint consistently
473        let fingerprint1 = self.fingerprint();
474        let fingerprint2 = self.fingerprint();
475        if fingerprint1 != fingerprint2 {
476            return Err(GuardianError::config(
477                "Configuration system fingerprint inconsistent".to_string(),
478            ));
479        }
480
481        Ok(())
482    }
483
484    /// Verify configuration can evolve correctly (builder pattern validation)
485    pub fn verify_evolution_capability() -> GuardianResult<()> {
486        let evolved_config = ConfigBuilder::new()
487            .add_path_pattern("guardian/evolution/**")
488            .ignore_file(".guardian_ignore")
489            .build()?;
490
491        if !evolved_config
492            .paths
493            .patterns
494            .contains(&"guardian/evolution/**".to_string())
495        {
496            return Err(GuardianError::config(
497                "Configuration evolution failed to integrate new patterns".to_string(),
498            ));
499        }
500
501        Ok(())
502    }
503
504    /// Verify serialization maintains configuration integrity
505    pub fn verify_serialization_fidelity(&self) -> GuardianResult<()> {
506        let yaml = serde_yaml::to_string(self).map_err(|e| {
507            GuardianError::config(format!("Configuration serialization failed: {e}"))
508        })?;
509
510        let rehydrated: Self = serde_yaml::from_str(&yaml)
511            .map_err(|e| GuardianError::config(format!("Configuration rehydration failed: {e}")))?;
512
513        if self.version != rehydrated.version {
514            return Err(GuardianError::config(
515                "Configuration version lost during serialization".to_string(),
516            ));
517        }
518
519        Ok(())
520    }
521}