icydb_error/
lib.rs

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