Skip to main content

boundary_core/
custom_rules.rs

1use anyhow::{Context, Result};
2use regex::Regex;
3
4use crate::config::CustomRuleConfig;
5use crate::graph::DependencyGraph;
6use crate::types::{Violation, ViolationKind};
7
8/// A compiled custom rule ready for evaluation.
9pub struct CompiledCustomRule {
10    pub name: String,
11    pub from_regex: Regex,
12    pub to_regex: Regex,
13    pub severity: crate::types::Severity,
14    pub message: Option<String>,
15}
16
17/// Compile custom rule configs into regex-based rules.
18pub fn compile_rules(configs: &[CustomRuleConfig]) -> Result<Vec<CompiledCustomRule>> {
19    configs
20        .iter()
21        .map(|cfg| {
22            let from_regex = Regex::new(&cfg.from_pattern)
23                .with_context(|| format!("invalid from_pattern in rule '{}'", cfg.name))?;
24            let to_regex = Regex::new(&cfg.to_pattern)
25                .with_context(|| format!("invalid to_pattern in rule '{}'", cfg.name))?;
26            Ok(CompiledCustomRule {
27                name: cfg.name.clone(),
28                from_regex,
29                to_regex,
30                severity: cfg.severity,
31                message: cfg.message.clone(),
32            })
33        })
34        .collect()
35}
36
37/// Evaluate custom rules against the dependency graph, returning any violations.
38pub fn evaluate_custom_rules(
39    graph: &DependencyGraph,
40    rules: &[CompiledCustomRule],
41) -> Vec<Violation> {
42    let mut violations = Vec::new();
43
44    for (src, tgt, edge) in graph.edges_with_nodes() {
45        let from_path = &src.id.0;
46        let to_path = edge.import_path.as_deref().unwrap_or(&tgt.id.0);
47
48        for rule in rules {
49            if rule.from_regex.is_match(from_path) && rule.to_regex.is_match(to_path) {
50                let message = rule.message.clone().unwrap_or_else(|| {
51                    format!(
52                        "Custom rule '{}' violated: {} -> {}",
53                        rule.name, from_path, to_path
54                    )
55                });
56
57                violations.push(Violation {
58                    kind: ViolationKind::CustomRule {
59                        rule_name: rule.name.clone(),
60                    },
61                    severity: rule.severity,
62                    location: edge.location.clone(),
63                    message,
64                    suggestion: Some(format!(
65                        "This dependency is forbidden by custom rule '{}'.",
66                        rule.name
67                    )),
68                });
69            }
70        }
71    }
72
73    violations
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::config::CustomRuleConfig;
80    use crate::graph::DependencyGraph;
81    use crate::types::*;
82    use std::path::PathBuf;
83
84    fn make_component(id: &str, name: &str, layer: Option<ArchLayer>) -> Component {
85        Component {
86            id: ComponentId(id.to_string()),
87            name: name.to_string(),
88            kind: ComponentKind::Entity(EntityInfo {
89                name: name.to_string(),
90                fields: vec![],
91                methods: vec![],
92                is_active_record: false,
93            }),
94            layer,
95            location: SourceLocation {
96                file: PathBuf::from("test.go"),
97                line: 1,
98                column: 1,
99            },
100            is_cross_cutting: false,
101            architecture_mode: ArchitectureMode::Ddd,
102        }
103    }
104
105    fn make_dep(from: &str, to: &str, import: &str) -> Dependency {
106        Dependency {
107            from: ComponentId(from.to_string()),
108            to: ComponentId(to.to_string()),
109            kind: DependencyKind::Import,
110            location: SourceLocation {
111                file: PathBuf::from("test.go"),
112                line: 10,
113                column: 1,
114            },
115            import_path: Some(import.to_string()),
116        }
117    }
118
119    #[test]
120    fn test_compile_and_evaluate_custom_rules() {
121        let configs = vec![CustomRuleConfig {
122            name: "no-internal-to-external".to_string(),
123            from_pattern: ".*/internal/.*".to_string(),
124            to_pattern: ".*/external/.*".to_string(),
125            action: "deny".to_string(),
126            severity: Severity::Error,
127            message: Some("Internal must not import external".to_string()),
128        }];
129
130        let rules = compile_rules(&configs).unwrap();
131        assert_eq!(rules.len(), 1);
132
133        let mut graph = DependencyGraph::new();
134        let c1 = make_component(
135            "app/internal/service",
136            "Service",
137            Some(ArchLayer::Application),
138        );
139        let c2 = make_component(
140            "app/external/client",
141            "Client",
142            Some(ArchLayer::Infrastructure),
143        );
144        graph.add_component(&c1);
145        graph.add_component(&c2);
146        graph.add_dependency(&make_dep(
147            "app/internal/service",
148            "app/external/client",
149            "app/external/client",
150        ));
151
152        let violations = evaluate_custom_rules(&graph, &rules);
153        assert_eq!(violations.len(), 1);
154        assert!(matches!(
155            violations[0].kind,
156            ViolationKind::CustomRule { .. }
157        ));
158    }
159
160    #[test]
161    fn test_no_match_no_violation() {
162        let configs = vec![CustomRuleConfig {
163            name: "no-internal-to-external".to_string(),
164            from_pattern: ".*/internal/.*".to_string(),
165            to_pattern: ".*/external/.*".to_string(),
166            action: "deny".to_string(),
167            severity: Severity::Error,
168            message: None,
169        }];
170
171        let rules = compile_rules(&configs).unwrap();
172
173        let mut graph = DependencyGraph::new();
174        let c1 = make_component("app/domain/user", "User", Some(ArchLayer::Domain));
175        let c2 = make_component("app/domain/order", "Order", Some(ArchLayer::Domain));
176        graph.add_component(&c1);
177        graph.add_component(&c2);
178        graph.add_dependency(&make_dep(
179            "app/domain/user",
180            "app/domain/order",
181            "app/domain/order",
182        ));
183
184        let violations = evaluate_custom_rules(&graph, &rules);
185        assert!(violations.is_empty());
186    }
187}