Skip to main content

toolpath_pi/
error.rs

1//! Error types for `toolpath-pi`.
2
3use std::path::PathBuf;
4use thiserror::Error;
5
6/// Errors produced by the `toolpath-pi` crate.
7#[derive(Debug, Error)]
8pub enum PiError {
9    /// Underlying I/O failure.
10    #[error("I/O error: {0}")]
11    Io(#[from] std::io::Error),
12
13    /// JSON (de)serialization failure.
14    #[error("JSON error: {0}")]
15    Json(#[from] serde_json::Error),
16
17    /// A session file was expected but could not be located.
18    #[error("session not found: {0}")]
19    SessionNotFound(String),
20
21    /// A project directory (encoded cwd) was expected but not found on disk.
22    #[error("project not found: {0}")]
23    ProjectNotFound(String),
24
25    /// A session JSONL file exists but cannot be interpreted.
26    ///
27    /// Carries the offending path and a short human-readable reason.
28    #[error("invalid session file {path}: {reason}")]
29    InvalidSessionFile {
30        /// Path to the offending file.
31        path: PathBuf,
32        /// Short human-readable reason.
33        reason: String,
34    },
35
36    /// A session header line was present but malformed (missing required fields,
37    /// unexpected shape, etc.).
38    #[error("malformed session header: {0}")]
39    MalformedHeader(String),
40
41    /// Wrapped error from `toolpath-convo`.
42    #[error("conversation error: {0}")]
43    Convo(#[from] toolpath_convo::ConvoError),
44
45    /// Catch-all for arbitrary `anyhow` errors bubbling up from dependencies.
46    #[error("{0}")]
47    Anyhow(#[from] anyhow::Error),
48
49    /// Generic free-form error.
50    #[error("{0}")]
51    Other(String),
52}
53
54impl PiError {
55    /// Construct a `SessionNotFound` error.
56    pub fn session_not_found(id: impl Into<String>) -> Self {
57        Self::SessionNotFound(id.into())
58    }
59
60    /// Construct a `ProjectNotFound` error.
61    pub fn project_not_found(cwd: impl Into<String>) -> Self {
62        Self::ProjectNotFound(cwd.into())
63    }
64
65    /// Construct an `InvalidSessionFile` error.
66    pub fn invalid_session_file(path: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
67        Self::InvalidSessionFile {
68            path: path.into(),
69            reason: reason.into(),
70        }
71    }
72
73    /// Construct a `MalformedHeader` error.
74    pub fn malformed_header(reason: impl Into<String>) -> Self {
75        Self::MalformedHeader(reason.into())
76    }
77
78    /// Construct an `Other` error.
79    pub fn other(msg: impl Into<String>) -> Self {
80        Self::Other(msg.into())
81    }
82}
83
84/// Convenience result alias.
85pub type Result<T> = std::result::Result<T, PiError>;
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use std::io;
91
92    #[test]
93    fn test_io_error_conversion() {
94        let io_err = io::Error::new(io::ErrorKind::NotFound, "missing");
95        let err: PiError = io_err.into();
96        match err {
97            PiError::Io(_) => {}
98            _ => panic!("expected Io variant"),
99        }
100    }
101
102    #[test]
103    fn test_json_error_display() {
104        let json_err = serde_json::from_str::<u32>("x").unwrap_err();
105        let err: PiError = json_err.into();
106        let msg = err.to_string();
107        assert!(
108            msg.to_lowercase().contains("json"),
109            "expected 'json' in display: {msg}"
110        );
111    }
112
113    #[test]
114    fn test_session_not_found_display() {
115        let err = PiError::SessionNotFound("abc".into());
116        assert!(err.to_string().contains("abc"));
117    }
118
119    #[test]
120    fn test_project_not_found_display() {
121        let err = PiError::ProjectNotFound("/Users/alex/project".into());
122        assert!(err.to_string().contains("/Users/alex/project"));
123    }
124
125    #[test]
126    fn test_other_display() {
127        let err = PiError::Other("something went wrong".into());
128        assert!(err.to_string().contains("something went wrong"));
129    }
130
131    #[test]
132    fn test_invalid_session_file_display() {
133        let err = PiError::invalid_session_file(PathBuf::from("/tmp/a.jsonl"), "bad line 3");
134        let msg = err.to_string();
135        assert!(msg.contains("/tmp/a.jsonl"));
136        assert!(msg.contains("bad line 3"));
137    }
138
139    #[test]
140    fn test_malformed_header_display() {
141        let err = PiError::malformed_header("missing session_id");
142        assert!(err.to_string().contains("missing session_id"));
143    }
144
145    #[test]
146    fn test_anyhow_conversion() {
147        let a: anyhow::Error = anyhow::anyhow!("boom");
148        let err: PiError = a.into();
149        assert!(err.to_string().contains("boom"));
150    }
151
152    #[test]
153    fn test_helper_constructors() {
154        assert!(matches!(
155            PiError::session_not_found("s"),
156            PiError::SessionNotFound(_)
157        ));
158        assert!(matches!(
159            PiError::project_not_found("p"),
160            PiError::ProjectNotFound(_)
161        ));
162        assert!(matches!(PiError::other("o"), PiError::Other(_)));
163    }
164}