1use crate::{ArchitectureGraph, Component, ComponentKind, TargetKind};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fmt;
5use std::sync::Arc;
6use thiserror::Error;
7
8#[derive(Clone, Copy)]
9pub struct Candidate<'a> {
10 graph: &'a ArchitectureGraph,
11 component: &'a Component,
12}
13
14impl<'a> Candidate<'a> {
15 pub fn new(graph: &'a ArchitectureGraph, component: &'a Component) -> Self {
16 Self { graph, component }
17 }
18
19 pub fn graph(self) -> &'a ArchitectureGraph {
20 self.graph
21 }
22
23 pub fn component(self) -> &'a Component {
24 self.component
25 }
26}
27
28pub trait Selector: Send + Sync {
29 fn description(&self) -> &str;
30 fn matches(&self, candidate: Candidate<'_>) -> bool;
31}
32
33#[derive(Clone)]
34pub struct DescribedSelector {
35 inner: Arc<dyn Selector>,
36}
37
38impl fmt::Debug for DescribedSelector {
39 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40 formatter
41 .debug_struct("DescribedSelector")
42 .field("description", &self.description())
43 .finish_non_exhaustive()
44 }
45}
46
47impl DescribedSelector {
48 pub fn new<F>(description: impl Into<String>, predicate: F) -> Self
49 where
50 F: for<'a> Fn(Candidate<'a>) -> bool + Send + Sync + 'static,
51 {
52 Self {
53 inner: Arc::new(FunctionSelector {
54 description: description.into(),
55 predicate,
56 }),
57 }
58 }
59
60 pub fn all() -> Self {
61 Self::new("all components", |_| true)
62 }
63
64 pub fn description(&self) -> &str {
65 self.inner.description()
66 }
67
68 pub fn matches(&self, candidate: Candidate<'_>) -> bool {
69 self.inner.matches(candidate)
70 }
71
72 pub fn and(self, other: Self) -> Self {
73 let description = format!("{} and {}", self.description(), other.description());
74 Self::new(description, move |candidate| {
75 self.matches(candidate) && other.matches(candidate)
76 })
77 }
78
79 pub fn or(self, other: Self) -> Self {
80 let description = format!("{} or {}", self.description(), other.description());
81 Self::new(description, move |candidate| {
82 self.matches(candidate) || other.matches(candidate)
83 })
84 }
85
86 #[allow(clippy::should_implement_trait)]
87 pub fn not(self) -> Self {
88 let description = format!("not {}", self.description());
89 Self::new(description, move |candidate| !self.matches(candidate))
90 }
91}
92
93impl Selector for DescribedSelector {
94 fn description(&self) -> &str {
95 self.description()
96 }
97
98 fn matches(&self, candidate: Candidate<'_>) -> bool {
99 self.matches(candidate)
100 }
101}
102
103struct FunctionSelector<F> {
104 description: String,
105 predicate: F,
106}
107
108impl<F> Selector for FunctionSelector<F>
109where
110 F: for<'a> Fn(Candidate<'a>) -> bool + Send + Sync,
111{
112 fn description(&self) -> &str {
113 &self.description
114 }
115
116 fn matches(&self, candidate: Candidate<'_>) -> bool {
117 (self.predicate)(candidate)
118 }
119}
120
121#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
122#[serde(tag = "kind", rename_all = "kebab-case")]
123pub enum SelectorSpec {
124 All,
125 Packages { names: Vec<String> },
126 Crates { names: Vec<String> },
127 Modules { patterns: Vec<String> },
128 ExternalCrates { names: Vec<String> },
129 TargetKinds { kinds: Vec<TargetKind> },
130 ComponentKinds { kinds: Vec<ComponentKind> },
131 AnyOf { selectors: Vec<SelectorSpec> },
132 AllOf { selectors: Vec<SelectorSpec> },
133 Not { selector: Box<SelectorSpec> },
134}
135
136impl SelectorSpec {
137 pub fn compile(&self) -> Result<DescribedSelector, PathPatternError> {
138 match self {
139 Self::All => Ok(DescribedSelector::all()),
140 Self::Packages { names } => {
141 let names = names.clone();
142 let description = format!("packages [{}]", names.join(", "));
143 Ok(DescribedSelector::new(description, move |candidate| {
144 matches!(
145 candidate.component(),
146 Component::Crate(_) | Component::Module(_)
147 ) && names
148 .iter()
149 .any(|name| name == candidate.component().package_name())
150 }))
151 }
152 Self::Crates { names } => {
153 let names = names.clone();
154 let description = format!("crates [{}]", names.join(", "));
155 Ok(DescribedSelector::new(description, move |candidate| {
156 matches!(
157 candidate.component(),
158 Component::Crate(_) | Component::Module(_)
159 ) && names
160 .iter()
161 .any(|name| name == candidate.component().crate_name())
162 }))
163 }
164 Self::Modules { patterns } => {
165 let compiled = patterns
166 .iter()
167 .map(PathPattern::new)
168 .collect::<Result<Vec<_>, _>>()?;
169 let description = format!("modules [{}]", patterns.join(", "));
170 Ok(DescribedSelector::new(description, move |candidate| {
171 let Component::Module(module) = candidate.component() else {
172 return false;
173 };
174 compiled.iter().any(|pattern| pattern.matches(&module.path))
175 }))
176 }
177 Self::ExternalCrates { names } => {
178 let names = names.clone();
179 let description = format!("external crates [{}]", names.join(", "));
180 Ok(DescribedSelector::new(description, move |candidate| {
181 matches!(candidate.component(), Component::ExternalCrate(_))
182 && names
183 .iter()
184 .any(|name| name == candidate.component().crate_name())
185 }))
186 }
187 Self::TargetKinds { kinds } => {
188 let kinds = kinds.clone();
189 let description = format!(
190 "target kinds [{}]",
191 kinds
192 .iter()
193 .map(|kind| format!("{kind:?}"))
194 .collect::<Vec<_>>()
195 .join(", ")
196 );
197 Ok(DescribedSelector::new(description, move |candidate| {
198 let component = candidate.component();
199 let direct = component.target_kind();
200 let inherited = component.containing_crate().and_then(|crate_id| {
201 candidate
202 .graph()
203 .component(&crate::ComponentId::Crate(crate_id))
204 .and_then(Component::target_kind)
205 });
206 direct
207 .or(inherited)
208 .is_some_and(|kind| kinds.contains(kind))
209 }))
210 }
211 Self::ComponentKinds { kinds } => {
212 let kinds = kinds.clone();
213 let description = format!(
214 "component kinds [{}]",
215 kinds
216 .iter()
217 .map(|kind| format!("{kind:?}"))
218 .collect::<Vec<_>>()
219 .join(", ")
220 );
221 Ok(DescribedSelector::new(description, move |candidate| {
222 kinds.contains(&candidate.component().kind())
223 }))
224 }
225 Self::AnyOf { selectors } => {
226 let mut selectors = selectors.iter();
227 let Some(first) = selectors.next() else {
228 return Ok(DescribedSelector::new("no components", |_| false));
229 };
230 selectors.try_fold(first.compile()?, |combined, selector| {
231 Ok(combined.or(selector.compile()?))
232 })
233 }
234 Self::AllOf { selectors } => {
235 let mut selectors = selectors.iter();
236 let Some(first) = selectors.next() else {
237 return Ok(DescribedSelector::all());
238 };
239 selectors.try_fold(first.compile()?, |combined, selector| {
240 Ok(combined.and(selector.compile()?))
241 })
242 }
243 Self::Not { selector } => Ok(selector.compile()?.not()),
244 }
245 }
246}
247
248#[derive(Clone, Debug, Eq, PartialEq)]
249enum PatternSegment {
250 Exact(String),
251 One,
252 Many,
253}
254
255#[derive(Clone, Debug, Eq, PartialEq)]
256pub struct PathPattern {
257 source: String,
258 segments: Vec<PatternSegment>,
259}
260
261impl PathPattern {
262 pub fn new(pattern: impl Into<String>) -> Result<Self, PathPatternError> {
263 let source = pattern.into();
264 if source.is_empty() {
265 return Err(PathPatternError::Empty);
266 }
267 let mut segments = Vec::new();
268 for segment in source.split("::") {
269 if segment.is_empty() {
270 return Err(PathPatternError::EmptySegment(source));
271 }
272 let segment = match segment {
273 "*" => PatternSegment::One,
274 "**" => PatternSegment::Many,
275 value if value.contains('*') => {
276 return Err(PathPatternError::InvalidWildcard(value.to_owned()));
277 }
278 value => PatternSegment::Exact(value.to_owned()),
279 };
280 segments.push(segment);
281 }
282 Ok(Self { source, segments })
283 }
284
285 pub fn as_str(&self) -> &str {
286 &self.source
287 }
288
289 pub fn matches(&self, path: &str) -> bool {
290 let path = path.split("::").collect::<Vec<_>>();
291 let mut memo = HashMap::new();
292 self.matches_from(0, 0, &path, &mut memo)
293 }
294
295 fn matches_from(
296 &self,
297 pattern_index: usize,
298 path_index: usize,
299 path: &[&str],
300 memo: &mut HashMap<(usize, usize), bool>,
301 ) -> bool {
302 if let Some(result) = memo.get(&(pattern_index, path_index)) {
303 return *result;
304 }
305
306 let result = match self.segments.get(pattern_index) {
307 None => path_index == path.len(),
308 Some(PatternSegment::Exact(expected)) => {
309 path.get(path_index)
310 .is_some_and(|actual| *actual == expected)
311 && self.matches_from(pattern_index + 1, path_index + 1, path, memo)
312 }
313 Some(PatternSegment::One) => {
314 path_index < path.len()
315 && self.matches_from(pattern_index + 1, path_index + 1, path, memo)
316 }
317 Some(PatternSegment::Many) => {
318 self.matches_from(pattern_index + 1, path_index, path, memo)
319 || (path_index < path.len()
320 && self.matches_from(pattern_index, path_index + 1, path, memo))
321 }
322 };
323 memo.insert((pattern_index, path_index), result);
324 result
325 }
326}
327
328#[derive(Clone, Debug, Error, Eq, PartialEq)]
329pub enum PathPatternError {
330 #[error("path pattern cannot be empty")]
331 Empty,
332 #[error("path pattern `{0}` contains an empty segment")]
333 EmptySegment(String),
334 #[error("wildcards must occupy an entire path segment, found `{0}`")]
335 InvalidWildcard(String),
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use crate::{ExternalCrateId, ExternalCrateNode};
342
343 #[test]
344 fn path_pattern_matches_rust_segments() {
345 let pattern = PathPattern::new("shop::domain::**").unwrap();
346 assert!(pattern.matches("shop::domain"));
347 assert!(pattern.matches("shop::domain::order"));
348 assert!(!pattern.matches("shop::api::domain"));
349
350 let one = PathPattern::new("shop::*::model").unwrap();
351 assert!(one.matches("shop::domain::model"));
352 assert!(!one.matches("shop::deep::domain::model"));
353 }
354
355 #[test]
356 fn package_selectors_do_not_match_external_crates() {
357 let graph = ArchitectureGraph::default();
358 let external = Component::ExternalCrate(ExternalCrateNode {
359 id: ExternalCrateId::new("serde@1"),
360 package_name: "serde".to_owned(),
361 crate_name: "serde".to_owned(),
362 version: Some("1".to_owned()),
363 source: None,
364 toolchain: false,
365 });
366 let selector = SelectorSpec::Packages {
367 names: vec!["serde".to_owned()],
368 }
369 .compile()
370 .unwrap();
371
372 assert!(!selector.matches(Candidate::new(&graph, &external)));
373 }
374}