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).case_insensitive(true).build()
298                    }
299                    .map_err(|e| {
300                        GuardianError::config(format!(
301                            "Invalid regex pattern in rule '{}': {}",
302                            rule.id, e
303                        ))
304                    })?;
305                }
306            }
307        }
308
309        Ok(())
310    }
311
312    /// Get all enabled rules across all categories
313    pub fn enabled_rules(&self) -> impl Iterator<Item = (&String, &PatternCategory, &PatternRule)> {
314        self.patterns.iter().filter(|(_, category)| category.enabled).flat_map(
315            |(name, category)| {
316                category
317                    .rules
318                    .iter()
319                    .filter(|rule| rule.enabled)
320                    .map(move |rule| (name, category, rule))
321            },
322        )
323    }
324
325    /// Get effective severity for a rule (rule override or category default)
326    pub fn effective_severity(&self, category: &PatternCategory, rule: &PatternRule) -> Severity {
327        rule.severity.unwrap_or(category.severity)
328    }
329
330    /// Convert to JSON for serialization
331    pub fn to_json(&self) -> GuardianResult<String> {
332        serde_json::to_string_pretty(self)
333            .map_err(|e| GuardianError::config(format!("Failed to serialize config: {e}")))
334    }
335
336    /// Create a fingerprint of the configuration for cache validation
337    pub fn fingerprint(&self) -> String {
338        use std::collections::hash_map::DefaultHasher;
339        use std::hash::{Hash, Hasher};
340
341        let mut hasher = DefaultHasher::new();
342
343        // Create a stable representation for hashing
344        // Sort patterns to ensure consistent ordering
345        let mut sorted_patterns: Vec<_> = self.patterns.iter().collect();
346        sorted_patterns.sort_by_key(|(name, _)| name.as_str());
347
348        // Hash version and path config
349        self.version.hash(&mut hasher);
350        self.paths.patterns.len().hash(&mut hasher);
351        for pattern in &self.paths.patterns {
352            pattern.hash(&mut hasher);
353        }
354        self.paths.ignore_file.hash(&mut hasher);
355
356        // Hash patterns in sorted order
357        for (category_name, category) in sorted_patterns {
358            category_name.hash(&mut hasher);
359            category.severity.hash(&mut hasher);
360            category.enabled.hash(&mut hasher);
361
362            // Sort rules for consistent ordering
363            let mut sorted_rules = category.rules.clone();
364            sorted_rules.sort_by_key(|rule| rule.id.clone());
365
366            for rule in sorted_rules {
367                rule.id.hash(&mut hasher);
368                rule.pattern.hash(&mut hasher);
369                rule.message.hash(&mut hasher);
370                rule.enabled.hash(&mut hasher);
371                rule.case_sensitive.hash(&mut hasher);
372            }
373        }
374
375        format!("{:x}", hasher.finish())
376    }
377}
378
379impl Default for GuardianConfig {
380    fn default() -> Self {
381        Self::with_defaults()
382    }
383}
384
385fn default_true() -> bool {
386    true
387}
388
389/// Build development marker pattern with simple, readable literals
390fn build_development_marker_pattern() -> String {
391    // Simple regex pattern for development markers
392    r"\b(TODO|FIXME|HACK|XXX|BUG|REFACTOR)\b".to_string()
393}
394
395/// Build implementation marker pattern with simple, readable literals
396fn build_temporary_marker_pattern() -> String {
397    // Simple regex pattern for temporary implementation markers
398    r"(?i)\b(for now|temporary|placeholder|stub|dummy|fake)\b".to_string()
399}
400
401/// Build unfinished macro pattern with simple, readable literals
402fn build_unfinished_macro_pattern() -> String {
403    // Simple pattern to detect incomplete macro calls
404    "macro_call:unimplemented|todo|panic".to_string()
405}
406
407/// Configuration builder for programmatic construction
408pub struct ConfigBuilder {
409    config: GuardianConfig,
410}
411
412impl ConfigBuilder {
413    /// Create a new builder with default configuration
414    pub fn new() -> Self {
415        Self { config: GuardianConfig::default() }
416    }
417
418    /// Add a path pattern
419    pub fn add_path_pattern(mut self, pattern: impl Into<String>) -> Self {
420        self.config.paths.patterns.push(pattern.into());
421        self
422    }
423
424    /// Set the ignore file name
425    pub fn ignore_file(mut self, filename: impl Into<String>) -> Self {
426        self.config.paths.ignore_file = Some(filename.into());
427        self
428    }
429
430    /// Add a pattern category
431    pub fn add_category(mut self, name: impl Into<String>, category: PatternCategory) -> Self {
432        self.config.patterns.insert(name.into(), category);
433        self
434    }
435
436    /// Build the final configuration
437    pub fn build(self) -> GuardianResult<GuardianConfig> {
438        self.config.validate()?;
439        Ok(self.config)
440    }
441}
442
443impl Default for ConfigBuilder {
444    fn default() -> Self {
445        Self::new()
446    }
447}
448
449impl GuardianConfig {
450    /// Self-validating configuration integrity check following architectural principles
451    /// Guardian validates itself rather than relying on external tests
452    pub fn verify_config_integrity(&self) -> GuardianResult<()> {
453        // Domain validates its own consistency
454        if self.version != "1.0" {
455            return Err(GuardianError::config(
456                "Configuration system requires version 1.0".to_string(),
457            ));
458        }
459
460        // Essential patterns must exist for guardian to function
461        if !self.patterns.contains_key("placeholders") {
462            return Err(GuardianError::config(
463                "Core pattern category 'placeholders' missing from guardian".to_string(),
464            ));
465        }
466
467        // Validate guardian can generate its own fingerprint consistently
468        let fingerprint1 = self.fingerprint();
469        let fingerprint2 = self.fingerprint();
470        if fingerprint1 != fingerprint2 {
471            return Err(GuardianError::config(
472                "Configuration system fingerprint inconsistent".to_string(),
473            ));
474        }
475
476        Ok(())
477    }
478
479    /// Verify configuration can evolve correctly (builder pattern validation)
480    pub fn verify_evolution_capability() -> GuardianResult<()> {
481        let evolved_config = ConfigBuilder::new()
482            .add_path_pattern("guardian/evolution/**")
483            .ignore_file(".guardian_ignore")
484            .build()?;
485
486        if !evolved_config.paths.patterns.contains(&"guardian/evolution/**".to_string()) {
487            return Err(GuardianError::config(
488                "Configuration evolution failed to integrate new patterns".to_string(),
489            ));
490        }
491
492        Ok(())
493    }
494
495    /// Verify serialization maintains configuration integrity
496    pub fn verify_serialization_fidelity(&self) -> GuardianResult<()> {
497        let yaml = serde_yaml::to_string(self).map_err(|e| {
498            GuardianError::config(format!("Configuration serialization failed: {e}"))
499        })?;
500
501        let rehydrated: Self = serde_yaml::from_str(&yaml)
502            .map_err(|e| GuardianError::config(format!("Configuration rehydration failed: {e}")))?;
503
504        if self.version != rehydrated.version {
505            return Err(GuardianError::config(
506                "Configuration version lost during serialization".to_string(),
507            ));
508        }
509
510        Ok(())
511    }
512}