Skip to main content

icydb_schema/
error.rs

1use serde::{Deserialize, Serialize};
2use std::{collections::HashMap, fmt};
3
4//
5// ErrorTree
6// Hierarchical error aggregator used by validation to keep nested context.
7//
8
9#[derive(Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
10pub struct ErrorTree {
11    /// Errors at the current level.
12    messages: Vec<String>,
13
14    /// Child errors indexed by field/key.
15    children: HashMap<String, Self>,
16}
17
18impl ErrorTree {
19    /// Create an empty error tree with no messages or children.
20    #[must_use]
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    /// Merge a sequence of `Result` values, collecting every `ErrorTree` into one.
26    pub fn collect<I>(iter: I) -> Result<(), Self>
27    where
28        I: IntoIterator<Item = Result<(), Self>>,
29    {
30        let mut errs = Self::new();
31        for res in iter {
32            if let Err(e) = res {
33                errs.merge(e);
34            }
35        }
36
37        errs.result()
38    }
39
40    /// Add an error message to the current level.
41    pub fn add<M: ToString>(&mut self, message: M) {
42        self.messages.push(message.to_string());
43    }
44
45    /// Push an error message only when the supplied result is `Err`.
46    pub fn add_result<M: ToString>(&mut self, error: Result<(), M>) {
47        if let Err(e) = error {
48            self.messages.push(e.to_string());
49        }
50    }
51
52    /// Format and append an error message.
53    pub fn addf(&mut self, args: fmt::Arguments) {
54        self.messages.push(format!("{args}"));
55    }
56
57    /// Add an error message under a specific child key, creating nodes as needed.
58    pub fn add_for<K: ToString, M: ToString>(&mut self, key: K, message: M) {
59        self.children
60            .entry(key.to_string())
61            .or_default()
62            .add(message);
63    }
64
65    /// Merge another `ErrorTree` into this one, combining children recursively.
66    pub fn merge(&mut self, other: Self) {
67        self.messages.extend(other.messages);
68        for (key, child_errors) in other.children {
69            self.children.entry(key).or_default().merge(child_errors);
70        }
71    }
72
73    /// Merge another `ErrorTree` under one child route key.
74    pub fn merge_for<K: ToString>(&mut self, key: K, other: Self) {
75        self.children
76            .entry(key.to_string())
77            .or_default()
78            .merge(other);
79    }
80
81    /// Check if there are any errors.
82    #[must_use]
83    pub fn is_empty(&self) -> bool {
84        self.messages.is_empty() && self.children.is_empty()
85    }
86
87    /// Borrow top-level messages.
88    #[must_use]
89    pub fn messages(&self) -> &[String] {
90        &self.messages
91    }
92
93    /// Borrow child error trees keyed by route/field.
94    #[must_use]
95    pub const fn children(&self) -> &HashMap<String, Self> {
96        &self.children
97    }
98
99    /// Flatten the error hierarchy without consuming `self`.
100    #[must_use]
101    pub fn flatten_ref(&self) -> Vec<(String, String)> {
102        let mut result = Vec::new();
103        self.flatten_helper_ref(String::new(), &mut result);
104        result
105    }
106
107    fn flatten_helper_ref(&self, prefix: String, result: &mut Vec<(String, String)>) {
108        // Add messages at the current level.
109        for msg in &self.messages {
110            result.push((prefix.clone(), msg.clone()));
111        }
112        // Process child errors recursively.
113        for (key, child) in &self.children {
114            let new_prefix = if prefix.is_empty() {
115                key.clone()
116            } else {
117                format!("{prefix}.{key}")
118            };
119            child.flatten_helper_ref(new_prefix, result);
120        }
121    }
122
123    /// Consume `self` and return `Ok(())` if there are no errors, or `Err(self)` otherwise.
124    pub fn result(self) -> Result<(), Self> {
125        if self.is_empty() { Ok(()) } else { Err(self) }
126    }
127}
128
129#[macro_export]
130macro_rules! err {
131    ($errs:expr, $($arg:tt)*) => {{
132        $errs.addf(format_args!($($arg)*));
133    }};
134}
135
136impl fmt::Display for ErrorTree {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        for (key, msg) in self.flatten_ref() {
139            if key.is_empty() {
140                writeln!(f, "{msg}")?;
141            } else {
142                writeln!(f, "{key}: {msg}")?;
143            }
144        }
145
146        Ok(())
147    }
148}
149
150impl From<&str> for ErrorTree {
151    fn from(err: &str) -> Self {
152        let mut tree = Self::new();
153        tree.add(err.to_string());
154
155        tree
156    }
157}
158
159impl From<String> for ErrorTree {
160    fn from(s: String) -> Self {
161        let mut tree = Self::new();
162        tree.add(s);
163
164        tree
165    }
166}
167
168//
169// TESTS
170//
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_empty_errors() {
178        let errs = ErrorTree::new();
179        assert!(errs.is_empty());
180        assert_eq!(errs.result(), Ok(()));
181    }
182
183    #[test]
184    fn test_add_and_merge() {
185        let mut errs = ErrorTree::new();
186        errs.add("top-level error");
187
188        let mut child_errs = ErrorTree::new();
189        child_errs.add("child error 1");
190        child_errs.add("child error 2");
191        errs.add_for("field", "field error");
192        errs.merge_for("nested", child_errs);
193
194        // Check hierarchical structure.
195        assert_eq!(errs.messages().len(), 1);
196        assert!(errs.children().contains_key("field") || errs.children().contains_key("nested"));
197
198        // Flatten and check that errors include keys.
199        let flat = errs.flatten_ref();
200        assert_eq!(flat.len(), 4);
201    }
202}