Skip to main content

bylaw_core/
selector.rs

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}