rustrails_record/
normalization.rs1use std::collections::HashMap;
2
3use rustrails_support::string_ext::StringExt;
4use serde_json::Value;
5
6pub struct Normalization {
8 pub attribute: &'static str,
10 pub normalizer: fn(Value) -> Value,
12}
13
14pub trait Normalizable {
16 fn normalizations() -> &'static [Normalization] {
18 &[]
19 }
20
21 fn normalize_attributes(attrs: &mut HashMap<String, Value>) {
23 for normalization in Self::normalizations() {
24 if let Some(value) = attrs.remove(normalization.attribute) {
25 attrs.insert(
26 normalization.attribute.to_owned(),
27 (normalization.normalizer)(value),
28 );
29 }
30 }
31 }
32}
33
34fn normalize_string(value: Value, normalizer: impl FnOnce(&str) -> String) -> Value {
35 match value {
36 Value::String(content) => Value::String(normalizer(&content)),
37 other => other,
38 }
39}
40
41#[must_use]
43pub fn strip_normalizer(value: Value) -> Value {
44 normalize_string(value, |content| content.trim().to_owned())
45}
46
47#[must_use]
49pub fn downcase_normalizer(value: Value) -> Value {
50 normalize_string(value, |content| content.to_lowercase())
51}
52
53#[must_use]
55pub fn squish_normalizer(value: Value) -> Value {
56 normalize_string(value, StringExt::squish)
57}
58
59#[cfg(test)]
60mod tests {
61 use std::collections::HashMap;
62
63 use serde_json::{Value, json};
64
65 use super::{
66 Normalizable, Normalization, downcase_normalizer, squish_normalizer, strip_normalizer,
67 };
68
69 struct NormalizedUser;
70
71 impl Normalizable for NormalizedUser {
72 fn normalizations() -> &'static [Normalization] {
73 &[
74 Normalization {
75 attribute: "email",
76 normalizer: strip_normalizer,
77 },
78 Normalization {
79 attribute: "name",
80 normalizer: squish_normalizer,
81 },
82 Normalization {
83 attribute: "handle",
84 normalizer: downcase_normalizer,
85 },
86 ]
87 }
88 }
89
90 #[test]
91 fn strip_normalizer_trims_whitespace() {
92 assert_eq!(strip_normalizer(json!(" hello ")), json!("hello"));
93 }
94
95 #[test]
96 fn downcase_normalizer_lowercases_strings() {
97 assert_eq!(downcase_normalizer(json!("MiXeD")), json!("mixed"));
98 }
99
100 #[test]
101 fn squish_normalizer_collapses_internal_whitespace() {
102 assert_eq!(
103 squish_normalizer(json!(" hello \n world ")),
104 json!("hello world")
105 );
106 }
107
108 #[test]
109 fn normalize_attributes_applies_normalizers_to_matching_keys() {
110 let mut attrs = HashMap::from([
111 ("email".to_owned(), json!(" alice@example.com ")),
112 ("name".to_owned(), json!(" Alice Example ")),
113 ("handle".to_owned(), json!("AliceExample")),
114 ]);
115
116 NormalizedUser::normalize_attributes(&mut attrs);
117
118 assert_eq!(attrs.get("email"), Some(&json!("alice@example.com")));
119 assert_eq!(attrs.get("name"), Some(&json!("Alice Example")));
120 assert_eq!(attrs.get("handle"), Some(&json!("aliceexample")));
121 }
122
123 #[test]
124 fn normalize_attributes_ignores_keys_without_normalizers() {
125 let mut attrs = HashMap::from([
126 ("email".to_owned(), json!(" alice@example.com ")),
127 ("role".to_owned(), json!(" Admin ")),
128 ]);
129
130 NormalizedUser::normalize_attributes(&mut attrs);
131
132 assert_eq!(attrs.get("email"), Some(&json!("alice@example.com")));
133 assert_eq!(attrs.get("role"), Some(&json!(" Admin ")));
134 }
135
136 #[test]
137 fn normalizers_leave_non_string_values_unchanged() {
138 let value = json!(42);
139
140 assert_eq!(strip_normalizer(value.clone()), Value::from(42));
141 assert_eq!(downcase_normalizer(value.clone()), Value::from(42));
142 assert_eq!(squish_normalizer(value), Value::from(42));
143 }
144}