Skip to main content

doido_model/
normalization.rs

1//! Attribute normalization (Rails `normalizes :email, with: -> { ... }`).
2//!
3//! Build a [`Normalizer`] by chaining steps, then `apply` it to a value —
4//! typically inside a `before_save`/`before_validation` callback so the stored
5//! value is always canonical.
6
7type Step = Box<dyn Fn(String) -> String + Send + Sync>;
8
9/// An ordered pipeline of string transformations.
10#[derive(Default)]
11pub struct Normalizer {
12    steps: Vec<Step>,
13}
14
15impl Normalizer {
16    pub fn new() -> Self {
17        Self::default()
18    }
19
20    /// Trim leading and trailing whitespace.
21    pub fn strip(mut self) -> Self {
22        self.steps.push(Box::new(|s| s.trim().to_string()));
23        self
24    }
25
26    /// Lowercase the value.
27    pub fn downcase(mut self) -> Self {
28        self.steps.push(Box::new(|s| s.to_lowercase()));
29        self
30    }
31
32    /// Uppercase the value.
33    pub fn upcase(mut self) -> Self {
34        self.steps.push(Box::new(|s| s.to_uppercase()));
35        self
36    }
37
38    /// Collapse runs of whitespace to a single space and trim (Rails `squish`).
39    pub fn squish(mut self) -> Self {
40        self.steps.push(Box::new(|s| {
41            s.split_whitespace().collect::<Vec<_>>().join(" ")
42        }));
43        self
44    }
45
46    /// Add a custom transformation step.
47    pub fn custom(mut self, f: impl Fn(String) -> String + Send + Sync + 'static) -> Self {
48        self.steps.push(Box::new(f));
49        self
50    }
51
52    /// Run every step in order over `input`.
53    pub fn apply(&self, input: &str) -> String {
54        self.steps
55            .iter()
56            .fold(input.to_string(), |acc, step| step(acc))
57    }
58}