Skip to main content

icydb_schema/
error.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    messages: Vec<String>,
14
15    /// Child errors indexed by field/key.
16    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    /// Merge another `ErrorTree` under one child route key.
75    pub fn merge_for<K: ToString>(&mut self, key: K, other: Self) {
76        self.children
77            .entry(key.to_string())
78            .or_default()
79            .merge(other);
80    }
81
82    /// Check if there are any errors.
83    #[must_use]
84    pub fn is_empty(&self) -> bool {
85        self.messages.is_empty() && self.children.is_empty()
86    }
87
88    /// Borrow top-level messages.
89    #[must_use]
90    pub fn messages(&self) -> &[String] {
91        &self.messages
92    }
93
94    /// Borrow child error trees keyed by route/field.
95    #[must_use]
96    pub const fn children(&self) -> &HashMap<String, Self> {
97        &self.children
98    }
99
100    /// Flatten the error hierarchy without consuming `self`.
101    #[must_use]
102    pub fn flatten_ref(&self) -> Vec<(String, String)> {
103        let mut result = Vec::new();
104        self.flatten_helper_ref(String::new(), &mut result);
105        result
106    }
107
108    fn flatten_helper_ref(&self, prefix: String, result: &mut Vec<(String, String)>) {
109        // Add messages at the current level.
110        for msg in &self.messages {
111            result.push((prefix.clone(), msg.clone()));
112        }
113        // Process child errors recursively.
114        for (key, child) in &self.children {
115            let new_prefix = if prefix.is_empty() {
116                key.clone()
117            } else {
118                format!("{prefix}.{key}")
119            };
120            child.flatten_helper_ref(new_prefix, result);
121        }
122    }
123
124    /// Consume `self` and return `Ok(())` if there are no errors, or `Err(self)` otherwise.
125    pub fn result(self) -> Result<(), Self> {
126        if self.is_empty() { Ok(()) } else { Err(self) }
127    }
128}
129
130#[macro_export]
131macro_rules! err {
132    ($errs:expr, $($arg:tt)*) => {{
133        $errs.addf(format_args!($($arg)*));
134    }};
135}
136
137impl fmt::Display for ErrorTree {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        for (key, msg) in self.flatten_ref() {
140            if key.is_empty() {
141                writeln!(f, "{msg}")?;
142            } else {
143                writeln!(f, "{key}: {msg}")?;
144            }
145        }
146
147        Ok(())
148    }
149}
150
151impl From<&str> for ErrorTree {
152    fn from(err: &str) -> Self {
153        let mut tree = Self::new();
154        tree.add(err.to_string());
155
156        tree
157    }
158}
159
160impl From<String> for ErrorTree {
161    fn from(s: String) -> Self {
162        let mut tree = Self::new();
163        tree.add(s);
164
165        tree
166    }
167}
168
169///
170/// TESTS
171///
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_empty_errors() {
179        let errs = ErrorTree::new();
180        assert!(errs.is_empty());
181        assert_eq!(errs.result(), Ok(()));
182    }
183
184    #[test]
185    fn test_add_and_merge() {
186        let mut errs = ErrorTree::new();
187        errs.add("top-level error");
188
189        let mut child_errs = ErrorTree::new();
190        child_errs.add("child error 1");
191        child_errs.add("child error 2");
192        errs.add_for("field", "field error");
193        errs.merge_for("nested", child_errs);
194
195        // Check hierarchical structure.
196        assert_eq!(errs.messages().len(), 1);
197        assert!(errs.children().contains_key("field") || errs.children().contains_key("nested"));
198
199        // Flatten and check that errors include keys.
200        let flat = errs.flatten_ref();
201        assert_eq!(flat.len(), 4);
202    }
203}