Skip to main content

icydb_model/
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    /// Add an error message to the current level.
26    pub fn add<M: ToString>(&mut self, message: M) {
27        self.messages.push(message.to_string());
28    }
29
30    /// Format and append an error message.
31    pub fn addf(&mut self, args: fmt::Arguments) {
32        self.messages.push(format!("{args}"));
33    }
34
35    /// Merge another `ErrorTree` into this one, combining children recursively.
36    pub fn merge(&mut self, other: Self) {
37        self.messages.extend(other.messages);
38        for (key, child_errors) in other.children {
39            self.children.entry(key).or_default().merge(child_errors);
40        }
41    }
42
43    /// Merge another `ErrorTree` under one child route key.
44    pub fn merge_for<K: ToString>(&mut self, key: K, other: Self) {
45        self.children
46            .entry(key.to_string())
47            .or_default()
48            .merge(other);
49    }
50
51    /// Check if there are any errors.
52    #[must_use]
53    pub fn is_empty(&self) -> bool {
54        self.messages.is_empty() && self.children.is_empty()
55    }
56
57    /// Borrow top-level messages.
58    #[must_use]
59    pub fn messages(&self) -> &[String] {
60        &self.messages
61    }
62
63    /// Borrow child error trees keyed by route/field.
64    #[must_use]
65    pub const fn children(&self) -> &HashMap<String, Self> {
66        &self.children
67    }
68
69    /// Flatten the error hierarchy without consuming `self`.
70    #[must_use]
71    pub fn flatten_ref(&self) -> Vec<(String, String)> {
72        let mut result = Vec::new();
73        self.flatten_helper_ref(String::new(), &mut result);
74        result
75    }
76
77    fn flatten_helper_ref(&self, prefix: String, result: &mut Vec<(String, String)>) {
78        // Add messages at the current level.
79        for msg in &self.messages {
80            result.push((prefix.clone(), msg.clone()));
81        }
82        // Process child errors recursively.
83        for (key, child) in &self.children {
84            let new_prefix = if prefix.is_empty() {
85                key.clone()
86            } else {
87                format!("{prefix}.{key}")
88            };
89            child.flatten_helper_ref(new_prefix, result);
90        }
91    }
92
93    /// Consume `self` and return `Ok(())` if there are no errors, or `Err(self)` otherwise.
94    pub fn result(self) -> Result<(), Self> {
95        if self.is_empty() { Ok(()) } else { Err(self) }
96    }
97}
98
99#[macro_export]
100macro_rules! err {
101    ($errs:expr, $($arg:tt)*) => {{
102        $errs.addf(format_args!($($arg)*));
103    }};
104}
105
106impl fmt::Display for ErrorTree {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        for (key, msg) in self.flatten_ref() {
109            if key.is_empty() {
110                writeln!(f, "{msg}")?;
111            } else {
112                writeln!(f, "{key}: {msg}")?;
113            }
114        }
115
116        Ok(())
117    }
118}
119
120impl From<&str> for ErrorTree {
121    fn from(err: &str) -> Self {
122        let mut tree = Self::new();
123        tree.add(err.to_string());
124
125        tree
126    }
127}
128
129impl From<String> for ErrorTree {
130    fn from(s: String) -> Self {
131        let mut tree = Self::new();
132        tree.add(s);
133
134        tree
135    }
136}
137
138//
139// TESTS
140//
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_empty_errors() {
148        let errs = ErrorTree::new();
149        assert!(errs.is_empty());
150        assert_eq!(errs.result(), Ok(()));
151    }
152
153    #[test]
154    fn test_add_and_merge() {
155        let mut errs = ErrorTree::new();
156        errs.add("top-level error");
157
158        let mut child_errs = ErrorTree::new();
159        child_errs.add("child error 1");
160        child_errs.add("child error 2");
161        errs.merge_for("field", ErrorTree::from("field error"));
162        errs.merge_for("nested", child_errs);
163
164        // Check hierarchical structure.
165        assert_eq!(errs.messages().len(), 1);
166        assert!(errs.children().contains_key("field"));
167        assert!(errs.children().contains_key("nested"));
168
169        // Flatten and check that errors include keys.
170        let flat = errs.flatten_ref();
171        assert_eq!(flat.len(), 4);
172    }
173}