Skip to main content

apcore_toolkit/output/
errors.rs

1// Error types for output writers.
2
3use thiserror::Error;
4
5/// Raised when a writer fails to write an artifact to disk.
6#[derive(Debug, Error)]
7#[error("Failed to write {path}: {cause}")]
8pub struct WriteError {
9    /// The file path that could not be written.
10    pub path: String,
11    /// Description of the underlying error.
12    pub cause: String,
13    /// Underlying I/O error, when available.
14    ///
15    /// Preserves `io::Error` kind and errno so callers can inspect the root
16    /// cause. `None` for non-I/O errors (e.g. YAML serialization failures).
17    #[source]
18    pub io_source: Option<std::io::Error>,
19}
20
21impl WriteError {
22    /// Create a WriteError without an I/O source (e.g. for serialization failures).
23    pub fn new(path: String, cause: String) -> Self {
24        Self {
25            path,
26            cause,
27            io_source: None,
28        }
29    }
30
31    /// Create a WriteError wrapping an I/O error, preserving the source chain.
32    pub fn io(path: String, source: std::io::Error) -> Self {
33        let cause = source.to_string();
34        Self {
35            path,
36            cause,
37            io_source: Some(source),
38        }
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn test_write_error_display() {
48        let err = WriteError::new("/tmp/test.yaml".into(), "permission denied".into());
49        assert_eq!(
50            err.to_string(),
51            "Failed to write /tmp/test.yaml: permission denied"
52        );
53    }
54
55    #[test]
56    fn test_write_error_fields() {
57        let err = WriteError::new("/path".into(), "cause".into());
58        assert_eq!(err.path, "/path");
59        assert_eq!(err.cause, "cause");
60    }
61
62    #[test]
63    fn test_write_error_new_source_is_none() {
64        let err = WriteError::new("/file".into(), "io error".into());
65        let std_err: &dyn std::error::Error = &err;
66        assert!(std_err.source().is_none());
67    }
68
69    #[test]
70    fn test_write_error_io_source_is_some() {
71        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
72        let err = WriteError::io("/file".into(), io_err);
73        let std_err: &dyn std::error::Error = &err;
74        assert!(
75            std_err.source().is_some(),
76            "WriteError::io should expose the I/O error as source()"
77        );
78        assert_eq!(err.path, "/file");
79        assert_eq!(err.cause, "access denied");
80    }
81
82    #[test]
83    fn test_write_error_io_display() {
84        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
85        let err = WriteError::io("/missing.yaml".into(), io_err);
86        assert_eq!(
87            err.to_string(),
88            "Failed to write /missing.yaml: no such file"
89        );
90    }
91
92    #[test]
93    fn test_write_error_debug_format() {
94        let err = WriteError::new("/tmp/out.yaml".into(), "disk full".into());
95        let debug_str = format!("{err:?}");
96        assert!(debug_str.contains("WriteError"));
97        assert!(debug_str.contains("/tmp/out.yaml"));
98        assert!(debug_str.contains("disk full"));
99    }
100}