icydb_error/
lib.rs

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