Skip to main content

autopulse_utils/
rewrite.rs

1use regex::Regex;
2use serde::{Deserialize, Serialize};
3
4/// Rewrites
5///
6/// This struct allows for a flexible way to define path rewrites using regex.
7///
8/// It can handle both single and multiple rewrites, where each rewrite consists of a `from` regex pattern and a `to` replacement string.
9///
10/// ```yml
11/// rewrite:
12///   from: '^/old/path/(.*)$'
13///   to: '/new/path/$1'
14/// ```
15/// or
16/// ```yml
17/// rewrite:
18///   - from: /testing
19///     to: /production
20///   - from: '^/old/path/(.*)$'
21///     to: '/new/path/$1'
22/// ``````
23#[derive(Serialize, Clone)]
24pub struct Rewrite {
25    pub(crate) rewrites: Vec<SingleRewrite>,
26    #[serde(skip)]
27    compiled: Vec<(Regex, String)>,
28}
29
30impl<'de> Deserialize<'de> for Rewrite {
31    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
32    where
33        D: serde::Deserializer<'de>,
34    {
35        #[derive(Deserialize)]
36        #[serde(untagged)]
37        enum Inner {
38            Single(SingleRewrite),
39            Multiple(Vec<SingleRewrite>),
40        }
41
42        let inner = Inner::deserialize(deserializer)?;
43
44        let rewrites = match inner {
45            Inner::Single(single) => {
46                vec![single]
47            }
48            Inner::Multiple(multiple) => multiple,
49        };
50
51        let compiled = rewrites
52            .iter()
53            .map(|r| {
54                let re = Regex::new(&r.from).map_err(|e| {
55                    serde::de::Error::custom(format!(
56                        "invalid regex '{}' in rewrite 'from' field: {e}",
57                        r.from
58                    ))
59                })?;
60                Ok((re, r.to.clone()))
61            })
62            .collect::<Result<Vec<_>, D::Error>>()?;
63
64        Ok(Self { rewrites, compiled })
65    }
66}
67
68#[derive(Serialize, Deserialize, Clone)]
69pub struct SingleRewrite {
70    /// Path to rewrite from
71    pub from: String,
72    /// Path to rewrite to
73    pub to: String,
74}
75
76impl Rewrite {
77    pub fn rewrite_path(&self, path: String) -> String {
78        let mut result = path;
79
80        for (from_regex, to) in &self.compiled {
81            result = from_regex.replace_all(&result, to).to_string();
82        }
83
84        result
85    }
86
87    #[cfg(test)]
88    pub fn single(from: &str, to: &str) -> Self {
89        let rewrites = vec![SingleRewrite {
90            from: from.to_string(),
91            to: to.to_string(),
92        }];
93        // In test helpers, an invalid pattern should fail loudly rather
94        // than silently produce a no-op rewrite that hides setup bugs.
95        let compiled = rewrites
96            .iter()
97            .map(|r| {
98                let re = Regex::new(&r.from)
99                    .unwrap_or_else(|e| panic!("invalid regex {:?} in test helper: {e}", r.from));
100                (re, r.to.clone())
101            })
102            .collect();
103        Self { rewrites, compiled }
104    }
105
106    #[cfg(test)]
107    pub fn multiple(rewrites: Vec<(&str, &str)>) -> Self {
108        let rewrites: Vec<SingleRewrite> = rewrites
109            .into_iter()
110            .map(|(from, to)| SingleRewrite {
111                from: from.to_string(),
112                to: to.to_string(),
113            })
114            .collect();
115        let compiled = rewrites
116            .iter()
117            .map(|r| {
118                let re = Regex::new(&r.from)
119                    .unwrap_or_else(|e| panic!("invalid regex {:?} in test helper: {e}", r.from));
120                (re, r.to.clone())
121            })
122            .collect();
123        Self { rewrites, compiled }
124    }
125}