Skip to main content

oparry_core/
rule.rs

1//! Rule engine and rule definitions
2
3use crate::{Issue, IssueLevel, Result, ValidationResult};
4use regex::Regex;
5use serde::{Deserialize, Serialize};
6use std::sync::Arc;
7
8/// Rule trait - all validators implement this
9pub trait Rule: Send + Sync {
10    /// Get the rule name
11    fn name(&self) -> &str;
12
13    /// Get the rule code
14    fn code(&self) -> &str;
15
16    /// Get the default severity
17    fn severity(&self) -> IssueLevel;
18
19    /// Validate input
20    fn validate(&self, input: &str) -> Result<Vec<Issue>>;
21}
22
23/// Compiled rule with regex
24#[derive(Clone)]
25pub struct PatternRule {
26    name: String,
27    code: String,
28    severity: IssueLevel,
29    pattern: Regex,
30    message: String,
31    suggestion: Option<String>,
32}
33
34impl PatternRule {
35    /// Create a new pattern rule
36    pub fn new(
37        name: impl Into<String>,
38        code: impl Into<String>,
39        severity: IssueLevel,
40        pattern: &str,
41        message: impl Into<String>,
42    ) -> Result<Self> {
43        Ok(Self {
44            name: name.into(),
45            code: code.into(),
46            severity,
47            pattern: Regex::new(pattern)?,
48            message: message.into(),
49            suggestion: None,
50        })
51    }
52
53    /// Add suggestion
54    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
55        self.suggestion = Some(suggestion.into());
56        self
57    }
58}
59
60impl Rule for PatternRule {
61    fn name(&self) -> &str {
62        &self.name
63    }
64
65    fn code(&self) -> &str {
66        &self.code
67    }
68
69    fn severity(&self) -> IssueLevel {
70        self.severity
71    }
72
73    fn validate(&self, input: &str) -> Result<Vec<Issue>> {
74        let mut issues = Vec::new();
75
76        for mat in self.pattern.find_iter(input) {
77            let mut issue = Issue::new(self.severity, self.code.clone(), self.message.clone())
78                .with_context(mat.as_str().to_string());
79
80            if let Some(ref suggestion) = self.suggestion {
81                issue = issue.with_suggestion(suggestion.clone());
82            }
83
84            issues.push(issue);
85        }
86
87        Ok(issues)
88    }
89}
90
91/// Rule engine that runs multiple rules
92#[derive(Clone)]
93pub struct RuleEngine {
94    rules: Vec<Arc<dyn Rule>>,
95}
96
97impl RuleEngine {
98    /// Create a new rule engine
99    pub fn new() -> Self {
100        Self { rules: Vec::new() }
101    }
102
103    /// Add a rule
104    pub fn add_rule(&mut self, rule: Arc<dyn Rule>) -> &mut Self {
105        self.rules.push(rule);
106        self
107    }
108
109    /// Add multiple rules
110    pub fn extend_rules(&mut self, rules: impl IntoIterator<Item = Arc<dyn Rule>>) -> &mut Self {
111        self.rules.extend(rules);
112        self
113    }
114
115    /// Run all rules on input
116    pub fn validate(&self, input: &str) -> Result<ValidationResult> {
117        let mut result = ValidationResult::new();
118
119        for rule in &self.rules {
120            let issues = rule.validate(input)?;
121            for mut issue in issues {
122                // Ensure severity from rule
123                if issue.level < rule.severity() {
124                    issue.level = rule.severity();
125                }
126                result.add_issue(issue);
127            }
128        }
129
130        Ok(result)
131    }
132
133    /// Get all rules
134    pub fn rules(&self) -> &[Arc<dyn Rule>] {
135        &self.rules
136    }
137
138    /// Count rules
139    pub fn rule_count(&self) -> usize {
140        self.rules.len()
141    }
142}
143
144impl Default for RuleEngine {
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150/// Rule configuration
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct RuleConfig {
153    /// Rule name/code
154    pub name: String,
155
156    /// Enable/disable rule
157    #[serde(default = "default_enabled")]
158    pub enabled: bool,
159
160    /// Severity override
161    pub severity: Option<IssueLevel>,
162
163    /// Custom patterns
164    pub patterns: Vec<String>,
165
166    /// Custom message
167    pub message: Option<String>,
168
169    /// Custom suggestion
170    pub suggestion: Option<String>,
171}
172
173fn default_enabled() -> bool {
174    true
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_pattern_rule() {
183        let rule = PatternRule::new(
184            "no-console",
185            "no-console",
186            IssueLevel::Warning,
187            r"console\.(log|error|warn)\(",
188            "Don't use console in production",
189        )
190        .unwrap()
191        .with_suggestion("Use a proper logging library");
192
193        let code = r#"
194            function test() {
195                console.log("debug");
196                console.error("error");
197            }
198        "#;
199
200        let issues = rule.validate(code).unwrap();
201        assert_eq!(issues.len(), 2);
202        assert_eq!(issues[0].code, "no-console");
203    }
204
205    #[test]
206    fn test_rule_engine() {
207        let mut engine = RuleEngine::new();
208
209        let rule1 = PatternRule::new(
210            "no-debugger",
211            "no-debugger",
212            IssueLevel::Error,
213            r"debugger;",
214            "Remove debugger statement",
215        )
216        .unwrap();
217
218        let rule2 = PatternRule::new(
219            "no-alert",
220            "no-alert",
221            IssueLevel::Warning,
222            r"alert\(",
223            "Don't use alert()",
224        )
225        .unwrap();
226
227        engine.add_rule(Arc::new(rule1));
228        engine.add_rule(Arc::new(rule2));
229
230        let code = r#"
231            debugger;
232            alert("hello");
233        "#;
234
235        let result = engine.validate(code).unwrap();
236        assert_eq!(result.error_count(), 1);
237        assert_eq!(result.warning_count(), 1);
238        assert!(!result.passed);
239    }
240}