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