icydb_error/
lib.rs

1use candid::CandidType;
2use serde::{Deserialize, Serialize};
3use std::{collections::HashMap, fmt};
4
5///
6/// ErrorTree
7///
8#[derive(CandidType, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
9pub struct ErrorTree {
10    /// errors at the current level
11    pub messages: Vec<String>,
12
13    /// child errors indexed by field/key
14    pub children: HashMap<String, ErrorTree>,
15}
16
17impl ErrorTree {
18    #[must_use]
19    pub fn new() -> Self {
20        Self::default()
21    }
22
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
38    // add an error message to the current level
39    pub fn add<M: ToString>(&mut self, message: M) {
40        self.messages.push(message.to_string());
41    }
42
43    // add_result
44    pub fn add_result<M: ToString>(&mut self, error: Result<(), M>) {
45        if let Err(e) = error {
46            self.messages.push(e.to_string());
47        }
48    }
49
50    // addf: format and add an error message
51    pub fn addf(&mut self, args: fmt::Arguments) {
52        self.messages.push(format!("{args}"));
53    }
54
55    // add_for
56    // add an error message under a specific key
57    pub fn add_for<K: ToString, M: ToString>(&mut self, key: K, message: M) {
58        self.children
59            .entry(key.to_string())
60            .or_default()
61            .add(message);
62    }
63
64    /// Merge another ErrorTree structure into this one.
65    /// Child errors are merged 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    /// Check if there are any errors.
74    #[must_use]
75    pub fn is_empty(&self) -> bool {
76        self.messages.is_empty() && self.children.is_empty()
77    }
78
79    // flatten the error hierarchy without consuming self
80    #[must_use]
81    pub fn flatten_ref(&self) -> Vec<(String, String)> {
82        let mut result = Vec::new();
83        self.flatten_helper_ref(String::new(), &mut result);
84        result
85    }
86
87    // flatten_helper_ref
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,
105    /// or Err(self) otherwise.
106    pub fn result(self) -> Result<(), Self> {
107        if self.is_empty() { Ok(()) } else { Err(self) }
108    }
109}
110
111#[macro_export]
112macro_rules! err {
113    ($errs:expr, $($arg:tt)*) => {{
114        $errs.addf(format_args!($($arg)*));
115    }};
116}
117
118impl fmt::Display for ErrorTree {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        for (key, msg) in self.flatten_ref() {
121            if key.is_empty() {
122                writeln!(f, "{msg}")?;
123            } else {
124                writeln!(f, "{key}: {msg}")?;
125            }
126        }
127
128        Ok(())
129    }
130}
131
132impl From<&str> for ErrorTree {
133    fn from(err: &str) -> Self {
134        let mut tree = Self::new();
135        tree.add(err.to_string());
136
137        tree
138    }
139}
140
141impl From<String> for ErrorTree {
142    fn from(s: String) -> Self {
143        let mut tree = Self::new();
144        tree.add(s);
145
146        tree
147    }
148}
149
150///
151/// TESTS
152///
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_empty_errors() {
160        let errs = ErrorTree::new();
161        assert!(errs.is_empty());
162        assert_eq!(errs.result(), Ok(()));
163    }
164
165    #[test]
166    fn test_add_and_merge() {
167        let mut errs = ErrorTree::new();
168        errs.add("top-level error");
169
170        let mut child_errs = ErrorTree::new();
171        child_errs.add("child error 1");
172        child_errs.add("child error 2");
173        errs.add_for("field", "field error");
174        errs.children
175            .entry("nested".to_string())
176            .or_default()
177            .merge(child_errs);
178
179        // Check hierarchical structure.
180        assert_eq!(errs.messages.len(), 1);
181        assert!(errs.children.contains_key("field") || errs.children.contains_key("nested"));
182
183        // Flatten and check that errors include keys.
184        let flat = errs.flatten_ref();
185        assert_eq!(flat.len(), 4);
186    }
187}