a2ui_base/validate/
error.rs1#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum ValidationErrorCode {
17 DuplicateId,
18 MissingRoot,
19 DanglingReference,
20 SelfReference,
21 CircularReference,
22 OrphanComponent,
23 GlobalDepthExceeded,
24 FuncCallDepthExceeded,
25 InvalidPathSyntax,
26}
27
28#[derive(Debug, Clone)]
30pub struct ValidationError {
31 pub code: ValidationErrorCode,
32 pub message: String,
33 pub component_id: Option<String>,
34 pub path: Option<String>,
35}
36
37impl ValidationError {
38 pub fn duplicate_id(id: &str) -> Self {
41 Self {
42 code: ValidationErrorCode::DuplicateId,
43 message: format!("Duplicate component ID: {id}"),
44 component_id: Some(id.to_string()),
45 path: None,
46 }
47 }
48
49 pub fn missing_root(root_id: &str) -> Self {
50 Self {
51 code: ValidationErrorCode::MissingRoot,
52 message: format!("Missing root component: No component has id='{root_id}'"),
53 component_id: Some(root_id.to_string()),
54 path: None,
55 }
56 }
57
58 pub fn dangling(component_id: &str, ref_id: &str, field: &str) -> Self {
59 Self {
60 code: ValidationErrorCode::DanglingReference,
61 message: format!(
62 "Component '{component_id}' references non-existent component '{ref_id}' in field '{field}'"
63 ),
64 component_id: Some(component_id.to_string()),
65 path: Some(field.to_string()),
66 }
67 }
68
69 pub fn self_ref(component_id: &str, field: &str) -> Self {
70 Self {
71 code: ValidationErrorCode::SelfReference,
72 message: format!(
73 "Self-reference detected: Component '{component_id}' references itself in field '{field}'"
74 ),
75 component_id: Some(component_id.to_string()),
76 path: Some(field.to_string()),
77 }
78 }
79
80 pub fn circular(component_id: &str) -> Self {
81 Self {
82 code: ValidationErrorCode::CircularReference,
83 message: format!(
84 "Circular reference detected involving component '{component_id}'"
85 ),
86 component_id: Some(component_id.to_string()),
87 path: None,
88 }
89 }
90
91 pub fn orphan(component_id: &str, root_id: &str) -> Self {
92 Self {
93 code: ValidationErrorCode::OrphanComponent,
94 message: format!(
95 "Component '{component_id}' is not reachable from '{root_id}'"
96 ),
97 component_id: Some(component_id.to_string()),
98 path: None,
99 }
100 }
101
102 pub fn global_depth(component_id: &str) -> Self {
103 Self {
104 code: ValidationErrorCode::GlobalDepthExceeded,
105 message: format!("Global recursion limit exceeded: Depth > {}", super::integrity::MAX_GLOBAL_DEPTH),
106 component_id: Some(component_id.to_string()),
107 path: None,
108 }
109 }
110
111 pub fn func_depth() -> Self {
112 Self {
113 code: ValidationErrorCode::FuncCallDepthExceeded,
114 message: format!(
115 "Recursion limit exceeded: functionCall depth > {}",
116 super::integrity::MAX_FUNC_CALL_DEPTH
117 ),
118 component_id: None,
119 path: None,
120 }
121 }
122
123 pub fn invalid_path(path: &str) -> Self {
124 Self {
125 code: ValidationErrorCode::InvalidPathSyntax,
126 message: format!("Invalid path syntax: '{path}'"),
127 component_id: None,
128 path: Some(path.to_string()),
129 }
130 }
131}
132
133#[derive(Debug, Clone, Default)]
135pub struct ValidationReport {
136 pub errors: Vec<ValidationError>,
137}
138
139impl ValidationReport {
140 pub fn new() -> Self {
141 Self::default()
142 }
143
144 pub fn is_empty(&self) -> bool {
145 self.errors.is_empty()
146 }
147
148 pub fn push(&mut self, e: ValidationError) {
149 self.errors.push(e);
150 }
151
152 pub fn extend(&mut self, other: ValidationReport) {
153 self.errors.extend(other.errors);
154 }
155
156 pub fn into_result(self) -> std::result::Result<(), Self> {
158 if self.is_empty() {
159 Ok(())
160 } else {
161 Err(self)
162 }
163 }
164
165 #[cfg(test)]
167 pub fn has_code(&self, code: &ValidationErrorCode) -> bool {
168 self.errors.iter().any(|e| &e.code == code)
169 }
170}
171
172impl std::fmt::Display for ValidationReport {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 let msgs: Vec<&str> = self.errors.iter().map(|e| e.message.as_str()).collect();
175 write!(f, "{}", msgs.join("\n"))
176 }
177}
178
179impl From<ValidationReport> for crate::error::A2uiError {
180 fn from(report: ValidationReport) -> Self {
181 crate::error::A2uiError::Validation(report.to_string())
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn empty_report_is_ok() {
191 let r = ValidationReport::new();
192 assert!(r.is_empty());
193 assert!(r.into_result().is_ok());
194 }
195
196 #[test]
197 fn report_with_errors_is_err() {
198 let mut r = ValidationReport::new();
199 r.push(ValidationError::duplicate_id("dup"));
200 assert!(!r.is_empty());
201 assert!(r.into_result().is_err());
202 }
203
204 #[test]
205 fn display_joins_messages_with_newline() {
206 let mut r = ValidationReport::new();
207 r.push(ValidationError::duplicate_id("a"));
208 r.push(ValidationError::missing_root("root"));
209 let s = r.to_string();
210 assert!(s.contains("Duplicate component ID: a"));
211 assert!(s.contains("Missing root component"));
212 assert_eq!(s.matches('\n').count(), 1);
213 }
214
215 #[test]
216 fn from_report_to_a2ui_error() {
217 let mut r = ValidationReport::new();
218 r.push(ValidationError::dangling("root", "ghost", "child"));
219 let err: crate::error::A2uiError = r.into();
220 match err {
221 crate::error::A2uiError::Validation(msg) => {
222 assert!(msg.contains("references non-existent component 'ghost'"));
223 }
224 other => panic!("expected Validation variant, got {other:?}"),
225 }
226 }
227
228 #[test]
229 fn extend_merges_reports() {
230 let mut a = ValidationReport::new();
231 a.push(ValidationError::duplicate_id("x"));
232 let mut b = ValidationReport::new();
233 b.push(ValidationError::missing_root("root"));
234 a.extend(b);
235 assert_eq!(a.errors.len(), 2);
236 }
237}