Skip to main content

rskit_testutil/golden/
normalize.rs

1use regex::{NoExpand, Regex};
2use rskit_errors::{AppError, AppResult};
3
4/// How a [`Rule`] locates the spans to rewrite.
5#[derive(Debug, Clone)]
6enum RuleMatcher {
7    Literal(String),
8    Pattern(Regex),
9}
10
11/// One ordered substitution rule: a matched span becomes a stable placeholder.
12///
13/// Rules are supplied by the caller (an absolute temp path → `<ROOT>`, a
14/// duration → `<DUR>`, a hex digest → `<HASH>`), keeping the normalizer free of
15/// any domain knowledge. Placeholders are inserted literally — no capture-group
16/// expansion.
17#[derive(Debug, Clone)]
18pub struct Rule {
19    matcher: RuleMatcher,
20    placeholder: String,
21}
22
23impl Rule {
24    /// A rule replacing every occurrence of the literal `text`.
25    #[must_use]
26    pub fn literal(text: impl Into<String>, placeholder: impl Into<String>) -> Self {
27        Self {
28            matcher: RuleMatcher::Literal(text.into()),
29            placeholder: placeholder.into(),
30        }
31    }
32
33    /// A rule replacing every match of the regex `pattern`.
34    ///
35    /// # Errors
36    ///
37    /// Returns a typed [`AppError`] (cause preserved) when `pattern` is not a valid regex.
38    pub fn pattern(pattern: &str, placeholder: impl Into<String>) -> AppResult<Self> {
39        let regex = Regex::new(pattern).map_err(|err| {
40            AppError::invalid_input("pattern", "failed to compile normalization pattern")
41                .with_cause(err)
42        })?;
43        Ok(Self {
44            matcher: RuleMatcher::Pattern(regex),
45            placeholder: placeholder.into(),
46        })
47    }
48
49    fn apply(&self, input: &str) -> String {
50        match &self.matcher {
51            RuleMatcher::Literal(text) => input.replace(text, &self.placeholder),
52            RuleMatcher::Pattern(regex) => regex
53                .replace_all(input, NoExpand(&self.placeholder))
54                .into_owned(),
55        }
56    }
57}
58
59/// An ordered list of substitution [`Rule`]s applied to raw output.
60///
61/// Rules run in the order given: a span rewritten by an earlier rule is no
62/// longer visible to later ones, so callers order rules from most to least
63/// specific.
64#[derive(Debug, Clone, Default)]
65pub struct Normalizer {
66    rules: Vec<Rule>,
67}
68
69impl Normalizer {
70    /// A normalizer applying `rules` in order.
71    #[must_use]
72    pub fn new(rules: Vec<Rule>) -> Self {
73        Self { rules }
74    }
75
76    /// Rewrite `input` by applying every rule in order.
77    #[must_use]
78    pub fn apply(&self, input: &str) -> String {
79        self.rules
80            .iter()
81            .fold(input.to_owned(), |text, rule| rule.apply(&text))
82    }
83}