Skip to main content

snapbox/filter/
mod.rs

1//! Filter `actual` or `expected` [`Data`]
2//!
3//! This can be done for
4//! - Making snapshots consistent across platforms or conditional compilation
5//! - Focusing snapshots on the characteristics of the data being tested
6
7mod pattern;
8mod redactions;
9#[cfg(test)]
10mod test;
11#[cfg(test)]
12mod test_redactions;
13#[cfg(test)]
14mod test_unordered_redactions;
15
16use crate::data::DataInner;
17use crate::Data;
18
19pub use pattern::NormalizeToExpected;
20pub use redactions::RedactedValue;
21pub use redactions::Redactions;
22
23pub trait Filter {
24    fn filter(&self, data: Data) -> Data;
25}
26
27pub struct FilterNewlines;
28impl Filter for FilterNewlines {
29    fn filter(&self, data: Data) -> Data {
30        let source = data.source;
31        let filters = data.filters;
32        let inner = match data.inner {
33            DataInner::Error(err) => DataInner::Error(err),
34            DataInner::Binary(bin) => DataInner::Binary(bin),
35            DataInner::Text(text) => {
36                let lines = normalize_lines(&text);
37                DataInner::Text(lines)
38            }
39            #[cfg(feature = "json")]
40            DataInner::Json(value) => {
41                let mut value = value;
42                normalize_json_string(&mut value, &normalize_lines);
43                DataInner::Json(value)
44            }
45            #[cfg(feature = "json")]
46            DataInner::JsonLines(value) => {
47                let mut value = value;
48                normalize_json_string(&mut value, &normalize_lines);
49                DataInner::JsonLines(value)
50            }
51            #[cfg(feature = "term-svg")]
52            DataInner::TermSvg(text) => {
53                let lines = normalize_lines(&text);
54                DataInner::TermSvg(lines)
55            }
56        };
57        Data {
58            inner,
59            source,
60            filters,
61        }
62    }
63}
64
65/// Normalize line endings
66pub fn normalize_lines(data: &str) -> String {
67    normalize_lines_chars(data.chars()).collect()
68}
69
70fn normalize_lines_chars(data: impl Iterator<Item = char>) -> impl Iterator<Item = char> {
71    normalize_line_endings::normalized(data)
72}
73
74pub struct FilterPaths;
75impl Filter for FilterPaths {
76    fn filter(&self, data: Data) -> Data {
77        let source = data.source;
78        let filters = data.filters;
79        let inner = match data.inner {
80            DataInner::Error(err) => DataInner::Error(err),
81            DataInner::Binary(bin) => DataInner::Binary(bin),
82            DataInner::Text(text) => {
83                let lines = normalize_paths(&text);
84                DataInner::Text(lines)
85            }
86            #[cfg(feature = "json")]
87            DataInner::Json(value) => {
88                let mut value = value;
89                normalize_json_string(&mut value, &normalize_paths);
90                DataInner::Json(value)
91            }
92            #[cfg(feature = "json")]
93            DataInner::JsonLines(value) => {
94                let mut value = value;
95                normalize_json_string(&mut value, &normalize_paths);
96                DataInner::JsonLines(value)
97            }
98            #[cfg(feature = "term-svg")]
99            DataInner::TermSvg(text) => {
100                let lines = normalize_paths(&text);
101                DataInner::TermSvg(lines)
102            }
103        };
104        Data {
105            inner,
106            source,
107            filters,
108        }
109    }
110}
111
112/// Normalize path separators
113///
114/// [`std::path::MAIN_SEPARATOR`] can vary by platform, so make it consistent
115///
116/// Note: this cannot distinguish between when a character is being used as a path separator or not
117/// and can "normalize" unrelated data
118pub fn normalize_paths(data: &str) -> String {
119    normalize_paths_chars(data.chars()).collect()
120}
121
122fn normalize_paths_chars(data: impl Iterator<Item = char>) -> impl Iterator<Item = char> {
123    data.map(|c| if c == '\\' { '/' } else { c })
124}
125
126struct NormalizeRedactions<'r> {
127    redactions: &'r Redactions,
128}
129impl Filter for NormalizeRedactions<'_> {
130    fn filter(&self, data: Data) -> Data {
131        let source = data.source;
132        let filters = data.filters;
133        let inner = match data.inner {
134            DataInner::Error(err) => DataInner::Error(err),
135            DataInner::Binary(bin) => DataInner::Binary(bin),
136            DataInner::Text(text) => {
137                let lines = self.redactions.redact(&text);
138                DataInner::Text(lines)
139            }
140            #[cfg(feature = "json")]
141            DataInner::Json(value) => {
142                let mut value = value;
143                normalize_json_string(&mut value, &|s| self.redactions.redact(s));
144                DataInner::Json(value)
145            }
146            #[cfg(feature = "json")]
147            DataInner::JsonLines(value) => {
148                let mut value = value;
149                normalize_json_string(&mut value, &|s| self.redactions.redact(s));
150                DataInner::JsonLines(value)
151            }
152            #[cfg(feature = "term-svg")]
153            DataInner::TermSvg(text) => {
154                let lines = self.redactions.redact(&text);
155                DataInner::TermSvg(lines)
156            }
157        };
158        Data {
159            inner,
160            source,
161            filters,
162        }
163    }
164}
165
166#[cfg(feature = "structured-data")]
167fn normalize_json_string(value: &mut serde_json::Value, op: &dyn Fn(&str) -> String) {
168    match value {
169        serde_json::Value::String(str) => {
170            *str = op(str);
171        }
172        serde_json::Value::Array(arr) => {
173            for value in arr.iter_mut() {
174                normalize_json_string(value, op);
175            }
176        }
177        serde_json::Value::Object(obj) => {
178            for (key, mut value) in std::mem::replace(obj, serde_json::Map::new()) {
179                let key = op(&key);
180                normalize_json_string(&mut value, op);
181                obj.insert(key, value);
182            }
183        }
184        _ => {}
185    }
186}