Skip to main content

vtcode_commons/
errors.rs

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
11// File operation errors
12pub 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
24// Skill/Tool errors
25pub 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
37// Audit/Logging errors
38pub const ERR_CREATE_AUDIT_DIR: &str = "Failed to create audit directory";
39pub const ERR_WRITE_AUDIT_LOG: &str = "failed to write audit log";
40
41// Checkpoint/Snapshot errors
42pub 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
46// Policy errors
47pub 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
50// Serialization errors
51pub 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
55// IPC/SDK errors
56pub 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/// Helper macro for file operation errors with context.
64///
65/// Usage: `file_err!(path, read)` where `path` is an expression and the
66/// operation is matched as a bare identifier (not a string literal).
67/// Expands to `"failed to read <path>"`.
68#[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/// Helper macro for context errors
85/// Usage: ctx_err!(operation, context) -> "operation context"
86#[macro_export]
87macro_rules! ctx_err {
88    ($op:expr, $ctx:expr) => {
89        format!("{}: {}", $op, $ctx)
90    };
91}
92
93/// Formats an error into a user-facing description. This allows extracted
94/// components to present consistent error messaging without depending on the
95/// CLI presentation layer.
96pub trait ErrorFormatter: Send + Sync {
97    /// Render the error into a user-facing string.
98    fn format_error(&self, error: &Error) -> Cow<'_, str>;
99}
100
101/// Reports non-fatal errors to an observability backend.
102pub trait ErrorReporter: Send + Sync {
103    /// Capture the provided error for later inspection.
104    fn capture(&self, error: &Error) -> Result<()>;
105
106    /// Convenience helper to capture a simple message.
107    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/// Error reporting implementation that drops every event. Useful for tests or
114/// when a consumer does not yet integrate with error monitoring.
115#[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/// Default formatter that surfaces the error's display output.
125#[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/// A collection of errors that enables continuing work while collecting failures.
135///
136/// This type implements the "error parameter" pattern: instead of short-circuiting
137/// on the first error, processing continues and errors are accumulated. The caller
138/// can inspect the collection afterward to determine whether all operations
139/// succeeded.
140///
141/// # Ergonomic Result handling
142///
143/// [`collect_result`](MultiErrors::collect_result) lets you process a `Result<T, E>`
144/// while keeping the happy path dominant:
145///
146/// ```rust
147/// use vtcode_commons::MultiErrors;
148/// let mut errors: MultiErrors<String> = MultiErrors::new();
149/// let value: Option<i32> = errors.collect_result("42".parse::<i32>().map_err(|e| e.to_string()));
150/// assert_eq!(value, Some(42));
151/// ```
152///
153/// # Composing with traditional error handling
154///
155/// Use [`ok`](MultiErrors::ok) or [`to_anyhow`](MultiErrors::to_anyhow) to convert
156/// back into a traditional `Result`.
157#[derive(Debug, Clone)]
158pub struct MultiErrors<E = Error> {
159    errors: Vec<E>,
160}
161
162impl<E> MultiErrors<E> {
163    /// Create an empty error collection.
164    pub fn new() -> Self {
165        Self { errors: Vec::new() }
166    }
167
168    /// Add a single error to the collection.
169    pub fn push(&mut self, error: E) {
170        self.errors.push(error);
171    }
172
173    /// Extend the collection with multiple errors.
174    fn extend(&mut self, iter: impl IntoIterator<Item = E>) {
175        self.errors.extend(iter);
176    }
177
178    /// Returns `true` if no errors have been collected.
179    #[must_use]
180    pub fn is_empty(&self) -> bool {
181        self.errors.is_empty()
182    }
183
184    /// Returns the number of collected errors.
185    #[must_use]
186    pub fn len(&self) -> usize {
187        self.errors.len()
188    }
189
190    /// Consume the collector and return the underlying error vector.
191    #[must_use]
192    fn into_inner(self) -> Vec<E> {
193        self.errors
194    }
195
196    /// Returns a slice of all collected errors.
197    #[must_use]
198    pub fn as_slice(&self) -> &[E] {
199        &self.errors
200    }
201
202    /// Returns an iterator over the collected errors.
203    pub fn iter(&self) -> std::slice::Iter<'_, E> {
204        self.errors.iter()
205    }
206
207    /// Convert into `Result<()>` — succeeds if no errors were collected.
208    pub fn ok(self) -> std::result::Result<(), Self> {
209        if self.errors.is_empty() { Ok(()) } else { Err(self) }
210    }
211
212    /// Remove all errors from the collection.
213    pub fn clear(&mut self) {
214        self.errors.clear();
215    }
216
217    /// Process a `Result`, returning the success value or collecting the error.
218    ///
219    /// This is the key ergonomic method — it keeps the happy path as the primary
220    /// flow while silently collecting errors for later inspection.
221    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    /// Convert into an [`anyhow::Error`] for use with traditional error handling.
235    #[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        // `file_err!` has no shape test; other relative paths in macros were
308        // also left invariant. Keep the message contract pinned.
309        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}