Skip to main content

vtcode_commons/
errors.rs

1use std::borrow::Cow;
2use std::fmt;
3
4use anyhow::{Error, Result};
5
6// File operation errors
7pub const ERR_READ_FILE: &str = "failed to read file";
8pub const ERR_WRITE_FILE: &str = "failed to write file";
9pub const ERR_READ_DIR: &str = "failed to read directory";
10pub const ERR_CREATE_DIR: &str = "failed to create directory";
11pub const ERR_REMOVE_FILE: &str = "failed to remove file";
12pub const ERR_REMOVE_DIR: &str = "failed to remove directory";
13pub const ERR_READ_DIR_ENTRY: &str = "failed to read directory entry";
14pub const ERR_GET_FILE_TYPE: &str = "failed to read file type";
15pub const ERR_GET_METADATA: &str = "failed to read file metadata";
16pub const ERR_CANONICALIZE_PATH: &str = "failed to canonicalize path";
17pub const ERR_READ_SYMLINK: &str = "failed to read symlink";
18
19// Skill/Tool errors
20pub const ERR_CREATE_SKILLS_DIR: &str = "failed to create skills directory";
21pub const ERR_CREATE_SKILL_DIR: &str = "failed to create skill directory";
22pub const ERR_READ_SKILL_CODE: &str = "failed to read skill code";
23pub const ERR_WRITE_SKILL_CODE: &str = "failed to write skill code";
24pub const ERR_READ_SKILL_METADATA: &str = "failed to read skill metadata";
25pub const ERR_WRITE_SKILL_METADATA: &str = "failed to write skill metadata";
26pub const ERR_PARSE_SKILL_METADATA: &str = "failed to parse skill metadata";
27pub const ERR_WRITE_SKILL_DOCS: &str = "failed to write skill documentation";
28pub const ERR_DELETE_SKILL: &str = "failed to delete skill";
29pub const ERR_READ_SKILLS_DIR: &str = "failed to read skills directory";
30pub const ERR_TOOL_DENIED: &str = "tool denied or unavailable by policy";
31
32// Audit/Logging errors
33pub const ERR_CREATE_AUDIT_DIR: &str = "Failed to create audit directory";
34pub const ERR_WRITE_AUDIT_LOG: &str = "failed to write audit log";
35
36// Checkpoint/Snapshot errors
37pub const ERR_CREATE_CHECKPOINT_DIR: &str = "failed to create checkpoint directory";
38pub const ERR_WRITE_CHECKPOINT: &str = "failed to write checkpoint";
39pub const ERR_READ_CHECKPOINT: &str = "failed to read checkpoint";
40
41// Policy errors
42pub const ERR_CREATE_POLICY_DIR: &str = "Failed to create directory for tool policy config";
43pub const ERR_CREATE_WORKSPACE_POLICY_DIR: &str = "Failed to create workspace policy directory";
44
45// Serialization errors
46pub const ERR_SERIALIZE_METADATA: &str = "failed to serialize skill metadata";
47pub const ERR_SERIALIZE_STATE: &str = "failed to serialize state";
48pub const ERR_DESERIALIZE: &str = "failed to deserialize data";
49
50// IPC/SDK errors
51pub const ERR_CREATE_IPC_DIR: &str = "failed to create IPC directory";
52pub const ERR_READ_REQUEST_FILE: &str = "failed to read request file";
53pub const ERR_READ_REQUEST_JSON: &str = "failed to read request JSON";
54pub const ERR_PARSE_REQUEST_JSON: &str = "failed to parse request JSON";
55pub const ERR_PARSE_ARGS: &str = "failed to parse tokenized args";
56pub const ERR_PARSE_RESULT: &str = "failed to parse de-tokenized result";
57
58/// Helper macro for file operation errors with context
59/// Usage: file_err!("path", "read") -> "failed to read path"
60#[macro_export]
61macro_rules! file_err {
62    ($path:expr, read) => {
63        format!("failed to read {}", $path)
64    };
65    ($path:expr, write) => {
66        format!("failed to write {}", $path)
67    };
68    ($path:expr, delete) => {
69        format!("failed to delete {}", $path)
70    };
71    ($path:expr, create) => {
72        format!("failed to create {}", $path)
73    };
74}
75
76/// Helper macro for context errors
77/// Usage: ctx_err!(operation, context) -> "operation context"
78#[macro_export]
79macro_rules! ctx_err {
80    ($op:expr, $ctx:expr) => {
81        format!("{}: {}", $op, $ctx)
82    };
83}
84
85/// Formats an error into a user-facing description. This allows extracted
86/// components to present consistent error messaging without depending on the
87/// CLI presentation layer.
88pub trait ErrorFormatter: Send + Sync {
89    /// Render the error into a user-facing string.
90    fn format_error(&self, error: &Error) -> Cow<'_, str>;
91}
92
93/// Reports non-fatal errors to an observability backend.
94pub trait ErrorReporter: Send + Sync {
95    /// Capture the provided error for later inspection.
96    fn capture(&self, error: &Error) -> Result<()>;
97
98    /// Convenience helper to capture a simple message.
99    fn capture_message(&self, message: impl Into<Cow<'static, str>>) -> Result<()> {
100        let message: Cow<'static, str> = message.into();
101        self.capture(&Error::msg(message))
102    }
103}
104
105/// Error reporting implementation that drops every event. Useful for tests or
106/// when a consumer does not yet integrate with error monitoring.
107#[derive(Debug, Default, Clone, Copy)]
108pub struct NoopErrorReporter;
109
110impl ErrorReporter for NoopErrorReporter {
111    fn capture(&self, _error: &Error) -> Result<()> {
112        Ok(())
113    }
114}
115
116/// Default formatter that surfaces the error's display output.
117#[derive(Debug, Default, Clone, Copy)]
118pub struct DisplayErrorFormatter;
119
120impl ErrorFormatter for DisplayErrorFormatter {
121    fn format_error(&self, error: &Error) -> Cow<'_, str> {
122        Cow::Owned(format!("{error}"))
123    }
124}
125
126/// A collection of errors that enables continuing work while collecting failures.
127///
128/// This type implements the "error parameter" pattern: instead of short-circuiting
129/// on the first error, processing continues and errors are accumulated. The caller
130/// can inspect the collection afterward to determine whether all operations
131/// succeeded.
132///
133/// # Ergonomic Result handling
134///
135/// [`collect_result`](MultiErrors::collect_result) lets you process a `Result<T, E>`
136/// while keeping the happy path dominant:
137///
138/// ```rust
139/// use vtcode_commons::MultiErrors;
140/// let mut errors: MultiErrors<String> = MultiErrors::new();
141/// let value: Option<i32> = errors.collect_result("42".parse::<i32>().map_err(|e| e.to_string()));
142/// assert_eq!(value, Some(42));
143/// ```
144///
145/// # Composing with traditional error handling
146///
147/// Use [`ok`](MultiErrors::ok) or [`to_anyhow`](MultiErrors::to_anyhow) to convert
148/// back into a traditional `Result`.
149#[derive(Debug, Clone)]
150pub struct MultiErrors<E = Error> {
151    errors: Vec<E>,
152}
153
154impl<E> MultiErrors<E> {
155    /// Create an empty error collection.
156    pub fn new() -> Self {
157        Self { errors: Vec::new() }
158    }
159
160    /// Add a single error to the collection.
161    pub fn push(&mut self, error: E) {
162        self.errors.push(error);
163    }
164
165    /// Extend the collection with multiple errors.
166    pub fn extend(&mut self, iter: impl IntoIterator<Item = E>) {
167        self.errors.extend(iter);
168    }
169
170    /// Returns `true` if no errors have been collected.
171    #[must_use]
172    pub fn is_empty(&self) -> bool {
173        self.errors.is_empty()
174    }
175
176    /// Returns the number of collected errors.
177    #[must_use]
178    pub fn len(&self) -> usize {
179        self.errors.len()
180    }
181
182    /// Consume the collector and return the underlying error vector.
183    #[must_use]
184    pub fn into_inner(self) -> Vec<E> {
185        self.errors
186    }
187
188    /// Returns a slice of all collected errors.
189    #[must_use]
190    pub fn as_slice(&self) -> &[E] {
191        &self.errors
192    }
193
194    /// Returns an iterator over the collected errors.
195    pub fn iter(&self) -> std::slice::Iter<'_, E> {
196        self.errors.iter()
197    }
198
199    /// Convert into `Result<()>` — succeeds if no errors were collected.
200    pub fn ok(self) -> std::result::Result<(), Self> {
201        if self.errors.is_empty() {
202            Ok(())
203        } else {
204            Err(self)
205        }
206    }
207
208    /// Remove all errors from the collection.
209    pub fn clear(&mut self) {
210        self.errors.clear();
211    }
212
213    /// Process a `Result`, returning the success value or collecting the error.
214    ///
215    /// This is the key ergonomic method — it keeps the happy path as the primary
216    /// flow while silently collecting errors for later inspection.
217    pub fn collect_result<T, F>(&mut self, result: std::result::Result<T, F>) -> Option<T>
218    where
219        F: Into<E>,
220    {
221        match result {
222            Ok(val) => Some(val),
223            Err(e) => {
224                self.errors.push(e.into());
225                None
226            }
227        }
228    }
229
230    /// Convert into an [`anyhow::Error`] for use with traditional error handling.
231    #[must_use]
232    pub fn to_anyhow(&self) -> Error
233    where
234        E: fmt::Display,
235    {
236        Error::msg(self.to_string())
237    }
238}
239
240impl<E> Default for MultiErrors<E> {
241    fn default() -> Self {
242        Self::new()
243    }
244}
245
246impl<E> From<Vec<E>> for MultiErrors<E> {
247    fn from(errors: Vec<E>) -> Self {
248        Self { errors }
249    }
250}
251
252impl<E> IntoIterator for MultiErrors<E> {
253    type Item = E;
254    type IntoIter = std::vec::IntoIter<E>;
255
256    fn into_iter(self) -> Self::IntoIter {
257        self.errors.into_iter()
258    }
259}
260
261impl<E: serde::Serialize> serde::Serialize for MultiErrors<E> {
262    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
263        self.errors.serialize(serializer)
264    }
265}
266
267impl<'de, E: serde::Deserialize<'de>> serde::Deserialize<'de> for MultiErrors<E> {
268    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
269        Vec::<E>::deserialize(deserializer).map(|errors| Self { errors })
270    }
271}
272
273impl<E: fmt::Display> fmt::Display for MultiErrors<E> {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        match self.errors.len() {
276            0 => write!(f, "no errors"),
277            1 => write!(f, "{}", self.errors[0]),
278            _ => {
279                for (i, error) in self.errors.iter().enumerate() {
280                    if i > 0 {
281                        writeln!(f)?;
282                    }
283                    write!(f, "  {}. {error}", i + 1)?;
284                }
285                Ok(())
286            }
287        }
288    }
289}
290
291impl<E: std::error::Error + 'static> std::error::Error for MultiErrors<E> {
292    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
293        self.errors
294            .first()
295            .map(|e| e as &(dyn std::error::Error + 'static))
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn formatter_uses_display() {
305        let formatter = DisplayErrorFormatter;
306        let error = Error::msg("test error");
307        assert_eq!(formatter.format_error(&error), "test error");
308    }
309
310    #[test]
311    fn noop_reporter_drops_errors() {
312        let reporter = NoopErrorReporter;
313        let error = Error::msg("test");
314        assert!(reporter.capture(&error).is_ok());
315        assert!(reporter.capture_message("message").is_ok());
316    }
317
318    #[test]
319    fn multi_errors_new_is_empty() {
320        let errors: MultiErrors<String> = MultiErrors::new();
321        assert!(errors.is_empty());
322        assert_eq!(errors.len(), 0);
323    }
324
325    #[test]
326    fn multi_errors_push_and_len() {
327        let mut errors = MultiErrors::new();
328        errors.push("error 1".to_string());
329        errors.push("error 2".to_string());
330        assert!(!errors.is_empty());
331        assert_eq!(errors.len(), 2);
332    }
333
334    #[test]
335    fn multi_errors_collect_result_ok() {
336        let mut errors: MultiErrors<String> = MultiErrors::new();
337        let value: i32 = errors.collect_result(Ok::<_, String>(42)).unwrap_or(0);
338        assert_eq!(value, 42);
339        assert!(errors.is_empty());
340    }
341
342    #[test]
343    fn multi_errors_collect_result_err() {
344        let mut errors: MultiErrors<String> = MultiErrors::new();
345        let value: i32 = errors
346            .collect_result(Err::<i32, String>("bad".to_string()))
347            .unwrap_or(0);
348        assert_eq!(value, 0);
349        assert_eq!(errors.len(), 1);
350    }
351
352    #[test]
353    fn multi_errors_ok_succeeds_when_empty() {
354        let errors: MultiErrors<String> = MultiErrors::new();
355        assert!(errors.ok().is_ok());
356    }
357
358    #[test]
359    fn multi_errors_ok_fails_when_not_empty() {
360        let mut errors = MultiErrors::new();
361        errors.push("error".to_string());
362        assert!(errors.ok().is_err());
363    }
364
365    #[test]
366    fn multi_errors_display_empty() {
367        let errors: MultiErrors<String> = MultiErrors::new();
368        assert_eq!(errors.to_string(), "no errors");
369    }
370
371    #[test]
372    fn multi_errors_display_single() {
373        let mut errors = MultiErrors::new();
374        errors.push("something failed".to_string());
375        assert_eq!(errors.to_string(), "something failed");
376    }
377
378    #[test]
379    fn multi_errors_display_multiple() {
380        let mut errors = MultiErrors::new();
381        errors.push("first issue".to_string());
382        errors.push("second issue".to_string());
383        let display = errors.to_string();
384        assert!(display.contains("1. first issue"));
385        assert!(display.contains("2. second issue"));
386    }
387
388    #[test]
389    fn multi_errors_extend() {
390        let mut errors = MultiErrors::new();
391        errors.extend(vec!["a".to_string(), "b".to_string()]);
392        assert_eq!(errors.len(), 2);
393    }
394
395    #[test]
396    fn multi_errors_into_inner() {
397        let mut errors = MultiErrors::new();
398        errors.push("test".to_string());
399        let inner: Vec<String> = errors.into_inner();
400        assert_eq!(inner.len(), 1);
401    }
402
403    #[test]
404    fn multi_errors_from_vec() {
405        let errors: MultiErrors<String> = MultiErrors::from(vec!["a".to_string()]);
406        assert_eq!(errors.len(), 1);
407    }
408
409    #[test]
410    fn multi_errors_into_iterator() {
411        let mut errors = MultiErrors::new();
412        errors.push("a".to_string());
413        errors.push("b".to_string());
414        let collected: Vec<String> = errors.into_iter().collect();
415        assert_eq!(collected, vec!["a", "b"]);
416    }
417
418    #[test]
419    fn multi_errors_slice_access() {
420        let mut errors = MultiErrors::new();
421        errors.push("err".to_string());
422        assert_eq!(errors.as_slice(), &["err".to_string()]);
423    }
424
425    #[test]
426    fn multi_errors_to_anyhow() {
427        let mut errors = MultiErrors::new();
428        errors.push("something broke".to_string());
429        let err = errors.to_anyhow();
430        assert!(err.to_string().contains("something broke"));
431    }
432}