1#![expect(
2 clippy::indexing_slicing,
3 reason = "Error rendering indexes only validated structured diagnostic entries."
4)]
5
6use std::borrow::Cow;
7use std::fmt;
8
9use anyhow::{Error, Result};
10
11pub const ERR_READ_FILE: &str = "failed to read file";
13pub const ERR_WRITE_FILE: &str = "failed to write file";
14pub const ERR_READ_DIR: &str = "failed to read directory";
15pub const ERR_CREATE_DIR: &str = "failed to create directory";
16pub const ERR_REMOVE_FILE: &str = "failed to remove file";
17pub const ERR_REMOVE_DIR: &str = "failed to remove directory";
18pub const ERR_READ_DIR_ENTRY: &str = "failed to read directory entry";
19pub const ERR_GET_FILE_TYPE: &str = "failed to read file type";
20pub const ERR_GET_METADATA: &str = "failed to read file metadata";
21pub const ERR_CANONICALIZE_PATH: &str = "failed to canonicalize path";
22pub const ERR_READ_SYMLINK: &str = "failed to read symlink";
23
24pub const ERR_CREATE_SKILLS_DIR: &str = "failed to create skills directory";
26pub const ERR_CREATE_SKILL_DIR: &str = "failed to create skill directory";
27pub const ERR_READ_SKILL_CODE: &str = "failed to read skill code";
28pub const ERR_WRITE_SKILL_CODE: &str = "failed to write skill code";
29pub const ERR_READ_SKILL_METADATA: &str = "failed to read skill metadata";
30pub const ERR_WRITE_SKILL_METADATA: &str = "failed to write skill metadata";
31pub const ERR_PARSE_SKILL_METADATA: &str = "failed to parse skill metadata";
32pub const ERR_WRITE_SKILL_DOCS: &str = "failed to write skill documentation";
33pub const ERR_DELETE_SKILL: &str = "failed to delete skill";
34pub const ERR_READ_SKILLS_DIR: &str = "failed to read skills directory";
35pub const ERR_TOOL_DENIED: &str = "tool denied or unavailable by policy";
36
37pub const ERR_CREATE_AUDIT_DIR: &str = "Failed to create audit directory";
39pub const ERR_WRITE_AUDIT_LOG: &str = "failed to write audit log";
40
41pub const ERR_CREATE_CHECKPOINT_DIR: &str = "failed to create checkpoint directory";
43pub const ERR_WRITE_CHECKPOINT: &str = "failed to write checkpoint";
44pub const ERR_READ_CHECKPOINT: &str = "failed to read checkpoint";
45
46pub const ERR_CREATE_POLICY_DIR: &str = "Failed to create directory for tool policy config";
48pub const ERR_CREATE_WORKSPACE_POLICY_DIR: &str = "Failed to create workspace policy directory";
49
50pub const ERR_SERIALIZE_METADATA: &str = "failed to serialize skill metadata";
52pub const ERR_SERIALIZE_STATE: &str = "failed to serialize state";
53pub const ERR_DESERIALIZE: &str = "failed to deserialize data";
54
55pub const ERR_CREATE_IPC_DIR: &str = "failed to create IPC directory";
57pub const ERR_READ_REQUEST_FILE: &str = "failed to read request file";
58pub const ERR_READ_REQUEST_JSON: &str = "failed to read request JSON";
59pub const ERR_PARSE_REQUEST_JSON: &str = "failed to parse request JSON";
60pub const ERR_PARSE_ARGS: &str = "failed to parse tokenized args";
61pub const ERR_PARSE_RESULT: &str = "failed to parse de-tokenized result";
62
63#[macro_export]
69macro_rules! file_err {
70 ($path:expr, read) => {
71 format!("failed to read {}", $path)
72 };
73 ($path:expr, write) => {
74 format!("failed to write {}", $path)
75 };
76 ($path:expr, delete) => {
77 format!("failed to delete {}", $path)
78 };
79 ($path:expr, create) => {
80 format!("failed to create {}", $path)
81 };
82}
83
84#[macro_export]
87macro_rules! ctx_err {
88 ($op:expr, $ctx:expr) => {
89 format!("{}: {}", $op, $ctx)
90 };
91}
92
93pub trait ErrorFormatter: Send + Sync {
97 fn format_error(&self, error: &Error) -> Cow<'_, str>;
99}
100
101pub trait ErrorReporter: Send + Sync {
103 fn capture(&self, error: &Error) -> Result<()>;
105
106 fn capture_message(&self, message: impl Into<Cow<'static, str>>) -> Result<()> {
108 let message: Cow<'static, str> = message.into();
109 self.capture(&Error::msg(message))
110 }
111}
112
113#[derive(Debug, Default, Clone, Copy)]
116pub struct NoopErrorReporter;
117
118impl ErrorReporter for NoopErrorReporter {
119 fn capture(&self, _error: &Error) -> Result<()> {
120 Ok(())
121 }
122}
123
124#[derive(Debug, Default, Clone, Copy)]
126pub struct DisplayErrorFormatter;
127
128impl ErrorFormatter for DisplayErrorFormatter {
129 fn format_error(&self, error: &Error) -> Cow<'_, str> {
130 Cow::Owned(format!("{error}"))
131 }
132}
133
134#[derive(Debug, Clone)]
158pub struct MultiErrors<E = Error> {
159 errors: Vec<E>,
160}
161
162impl<E> MultiErrors<E> {
163 pub fn new() -> Self {
165 Self { errors: Vec::new() }
166 }
167
168 pub fn push(&mut self, error: E) {
170 self.errors.push(error);
171 }
172
173 fn extend(&mut self, iter: impl IntoIterator<Item = E>) {
175 self.errors.extend(iter);
176 }
177
178 #[must_use]
180 pub fn is_empty(&self) -> bool {
181 self.errors.is_empty()
182 }
183
184 #[must_use]
186 pub fn len(&self) -> usize {
187 self.errors.len()
188 }
189
190 #[must_use]
192 fn into_inner(self) -> Vec<E> {
193 self.errors
194 }
195
196 #[must_use]
198 pub fn as_slice(&self) -> &[E] {
199 &self.errors
200 }
201
202 pub fn iter(&self) -> std::slice::Iter<'_, E> {
204 self.errors.iter()
205 }
206
207 pub fn ok(self) -> std::result::Result<(), Self> {
209 if self.errors.is_empty() { Ok(()) } else { Err(self) }
210 }
211
212 pub fn clear(&mut self) {
214 self.errors.clear();
215 }
216
217 pub fn collect_result<T, F>(&mut self, result: std::result::Result<T, F>) -> Option<T>
222 where
223 F: Into<E>,
224 {
225 match result {
226 Ok(val) => Some(val),
227 Err(e) => {
228 self.errors.push(e.into());
229 None
230 }
231 }
232 }
233
234 #[must_use]
236 pub fn to_anyhow(&self) -> Error
237 where
238 E: fmt::Display,
239 {
240 Error::msg(self.to_string())
241 }
242}
243
244impl<E> Default for MultiErrors<E> {
245 fn default() -> Self {
246 Self::new()
247 }
248}
249
250impl<E> From<Vec<E>> for MultiErrors<E> {
251 fn from(errors: Vec<E>) -> Self {
252 Self { errors }
253 }
254}
255
256impl<E> IntoIterator for MultiErrors<E> {
257 type Item = E;
258 type IntoIter = std::vec::IntoIter<E>;
259
260 fn into_iter(self) -> Self::IntoIter {
261 self.errors.into_iter()
262 }
263}
264
265impl<E: serde::Serialize> serde::Serialize for MultiErrors<E> {
266 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
267 self.errors.serialize(serializer)
268 }
269}
270
271impl<'de, E: serde::Deserialize<'de>> serde::Deserialize<'de> for MultiErrors<E> {
272 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
273 Vec::<E>::deserialize(deserializer).map(|errors| Self { errors })
274 }
275}
276
277impl<E: fmt::Display> fmt::Display for MultiErrors<E> {
278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 match self.errors.len() {
280 0 => write!(f, "no errors"),
281 1 => write!(f, "{}", self.errors[0]),
282 _ => {
283 for (i, error) in self.errors.iter().enumerate() {
284 if i > 0 {
285 writeln!(f)?;
286 }
287 write!(f, " {}. {error}", i + 1)?;
288 }
289 Ok(())
290 }
291 }
292 }
293}
294
295impl<E: std::error::Error + 'static> std::error::Error for MultiErrors<E> {
296 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
297 self.errors.first().map(|e| e as &(dyn std::error::Error + 'static))
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn file_err_operations_format_path() {
307 let path = "config/vtcode.toml";
310 assert_eq!(file_err!(path, read), "failed to read config/vtcode.toml");
311 assert_eq!(file_err!(path, write), "failed to write config/vtcode.toml");
312 assert_eq!(file_err!(path, delete), "failed to delete config/vtcode.toml");
313 assert_eq!(file_err!(path, create), "failed to create config/vtcode.toml");
314 }
315
316 #[test]
317 fn ctx_err_joins_operation_and_context() {
318 assert_eq!(ctx_err!("llm factory", "lock poisoned"), "llm factory: lock poisoned");
319 }
320
321 #[test]
322 fn formatter_uses_display() {
323 let formatter = DisplayErrorFormatter;
324 let error = Error::msg("test error");
325 assert_eq!(formatter.format_error(&error), "test error");
326 }
327
328 #[test]
329 fn noop_reporter_drops_errors() {
330 let reporter = NoopErrorReporter;
331 let error = Error::msg("test");
332 assert!(reporter.capture(&error).is_ok());
333 assert!(reporter.capture_message("message").is_ok());
334 }
335
336 #[test]
337 fn multi_errors_new_is_empty() {
338 let errors: MultiErrors<String> = MultiErrors::new();
339 assert!(errors.is_empty());
340 assert_eq!(errors.len(), 0);
341 }
342
343 #[test]
344 fn multi_errors_push_and_len() {
345 let mut errors = MultiErrors::new();
346 errors.push("error 1".to_string());
347 errors.push("error 2".to_string());
348 assert!(!errors.is_empty());
349 assert_eq!(errors.len(), 2);
350 }
351
352 #[test]
353 fn multi_errors_collect_result_ok() {
354 let mut errors: MultiErrors<String> = MultiErrors::new();
355 let value: i32 = errors.collect_result(Ok::<_, String>(42)).unwrap_or(0);
356 assert_eq!(value, 42);
357 assert!(errors.is_empty());
358 }
359
360 #[test]
361 fn multi_errors_collect_result_err() {
362 let mut errors: MultiErrors<String> = MultiErrors::new();
363 let value: i32 = errors.collect_result(Err::<i32, String>("bad".to_string())).unwrap_or(0);
364 assert_eq!(value, 0);
365 assert_eq!(errors.len(), 1);
366 }
367
368 #[test]
369 fn multi_errors_ok_succeeds_when_empty() {
370 let errors: MultiErrors<String> = MultiErrors::new();
371 assert!(errors.ok().is_ok());
372 }
373
374 #[test]
375 fn multi_errors_ok_fails_when_not_empty() {
376 let mut errors = MultiErrors::new();
377 errors.push("error".to_string());
378 assert!(errors.ok().is_err());
379 }
380
381 #[test]
382 fn multi_errors_display_empty() {
383 let errors: MultiErrors<String> = MultiErrors::new();
384 assert_eq!(errors.to_string(), "no errors");
385 }
386
387 #[test]
388 fn multi_errors_display_single() {
389 let mut errors = MultiErrors::new();
390 errors.push("something failed".to_string());
391 assert_eq!(errors.to_string(), "something failed");
392 }
393
394 #[test]
395 fn multi_errors_display_multiple() {
396 let mut errors = MultiErrors::new();
397 errors.push("first issue".to_string());
398 errors.push("second issue".to_string());
399 let display = errors.to_string();
400 assert!(display.contains("1. first issue"));
401 assert!(display.contains("2. second issue"));
402 }
403
404 #[test]
405 fn multi_errors_extend() {
406 let mut errors = MultiErrors::new();
407 errors.extend(vec!["a".to_string(), "b".to_string()]);
408 assert_eq!(errors.len(), 2);
409 }
410
411 #[test]
412 fn multi_errors_into_inner() {
413 let mut errors = MultiErrors::new();
414 errors.push("test".to_string());
415 let inner: Vec<String> = errors.into_inner();
416 assert_eq!(inner.len(), 1);
417 }
418
419 #[test]
420 fn multi_errors_from_vec() {
421 let errors: MultiErrors<String> = MultiErrors::from(vec!["a".to_string()]);
422 assert_eq!(errors.len(), 1);
423 }
424
425 #[test]
426 fn multi_errors_into_iterator() {
427 let mut errors = MultiErrors::new();
428 errors.push("a".to_string());
429 errors.push("b".to_string());
430 let collected: Vec<String> = errors.into_iter().collect();
431 assert_eq!(collected, vec!["a", "b"]);
432 }
433
434 #[test]
435 fn multi_errors_slice_access() {
436 let mut errors = MultiErrors::new();
437 errors.push("err".to_string());
438 assert_eq!(errors.as_slice(), &["err".to_string()]);
439 }
440
441 #[test]
442 fn multi_errors_to_anyhow() {
443 let mut errors = MultiErrors::new();
444 errors.push("something broke".to_string());
445 let err = errors.to_anyhow();
446 assert!(err.to_string().contains("something broke"));
447 }
448}