1use std::path::PathBuf;
4use thiserror::Error;
5
6#[derive(Debug, Error)]
8pub enum PiError {
9 #[error("I/O error: {0}")]
11 Io(#[from] std::io::Error),
12
13 #[error("JSON error: {0}")]
15 Json(#[from] serde_json::Error),
16
17 #[error("session not found: {0}")]
19 SessionNotFound(String),
20
21 #[error("project not found: {0}")]
23 ProjectNotFound(String),
24
25 #[error("invalid session file {path}: {reason}")]
29 InvalidSessionFile {
30 path: PathBuf,
32 reason: String,
34 },
35
36 #[error("malformed session header: {0}")]
39 MalformedHeader(String),
40
41 #[error("conversation error: {0}")]
43 Convo(#[from] toolpath_convo::ConvoError),
44
45 #[error("{0}")]
47 Anyhow(#[from] anyhow::Error),
48
49 #[error("{0}")]
51 Other(String),
52}
53
54impl PiError {
55 pub fn session_not_found(id: impl Into<String>) -> Self {
57 Self::SessionNotFound(id.into())
58 }
59
60 pub fn project_not_found(cwd: impl Into<String>) -> Self {
62 Self::ProjectNotFound(cwd.into())
63 }
64
65 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 pub fn malformed_header(reason: impl Into<String>) -> Self {
75 Self::MalformedHeader(reason.into())
76 }
77
78 pub fn other(msg: impl Into<String>) -> Self {
80 Self::Other(msg.into())
81 }
82}
83
84pub 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}