1use thiserror::Error;
2
3pub const EXIT_SUCCESS: u8 = 0;
5pub const EXIT_USER_ERROR: u8 = 1;
7pub const EXIT_SYSTEM_ERROR: u8 = 2;
9pub const EXIT_INTERRUPTED: u8 = 130;
11
12#[derive(Error, Debug)]
19pub enum RecError {
20 #[error("IO error: {0}")]
22 Io(#[from] std::io::Error),
23
24 #[error("JSON serialization error: {0}")]
26 Json(#[from] serde_json::Error),
27
28 #[error("TOML parsing error: {0}")]
30 Toml(#[from] toml::de::Error),
31
32 #[error("Session not found: {0}")]
34 SessionNotFound(String),
35
36 #[error("Session already exists: {0}")]
38 SessionExists(String),
39
40 #[error("Invalid session format: {0}")]
42 InvalidSession(String),
43
44 #[error("Config error: {0}")]
46 Config(String),
47
48 #[error("Recording already in progress")]
50 RecordingInProgress,
51
52 #[error("No active recording")]
54 NoActiveRecording,
55
56 #[error("Stale lock from process {0}")]
58 StaleLock(String),
59
60 #[error("Invalid session name '{0}': only alphanumeric, dash, and underscore allowed")]
62 InvalidSessionName(String),
63
64 #[error("Invalid alias name '{0}': only alphanumeric, dash, and underscore allowed")]
66 InvalidAliasName(String),
67
68 #[error("Invalid tag name '{0}': only alphanumeric, dash, and underscore allowed")]
70 InvalidTagName(String),
71}
72
73impl RecError {
74 #[must_use]
80 pub fn exit_code(&self) -> u8 {
81 match self {
82 RecError::Io(_) | RecError::Json(_) | RecError::Toml(_) | RecError::StaleLock(_) => {
83 EXIT_SYSTEM_ERROR
84 }
85 RecError::SessionNotFound(_)
86 | RecError::SessionExists(_)
87 | RecError::InvalidSession(_)
88 | RecError::Config(_)
89 | RecError::RecordingInProgress
90 | RecError::NoActiveRecording
91 | RecError::InvalidSessionName(_)
92 | RecError::InvalidAliasName(_)
93 | RecError::InvalidTagName(_) => EXIT_USER_ERROR,
94 }
95 }
96}
97
98pub type Result<T> = std::result::Result<T, RecError>;
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn test_exit_code_user_errors() {
107 assert_eq!(
108 RecError::SessionNotFound("x".into()).exit_code(),
109 EXIT_USER_ERROR
110 );
111 assert_eq!(
112 RecError::SessionExists("x".into()).exit_code(),
113 EXIT_USER_ERROR
114 );
115 assert_eq!(
116 RecError::InvalidSession("x".into()).exit_code(),
117 EXIT_USER_ERROR
118 );
119 assert_eq!(RecError::Config("x".into()).exit_code(), EXIT_USER_ERROR);
120 assert_eq!(RecError::RecordingInProgress.exit_code(), EXIT_USER_ERROR);
121 assert_eq!(RecError::NoActiveRecording.exit_code(), EXIT_USER_ERROR);
122 assert_eq!(
123 RecError::InvalidSessionName("x".into()).exit_code(),
124 EXIT_USER_ERROR
125 );
126 assert_eq!(
127 RecError::InvalidAliasName("x".into()).exit_code(),
128 EXIT_USER_ERROR
129 );
130 assert_eq!(
131 RecError::InvalidTagName("x".into()).exit_code(),
132 EXIT_USER_ERROR
133 );
134 }
135
136 #[test]
137 fn test_exit_code_system_errors() {
138 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
139 assert_eq!(RecError::Io(io_err).exit_code(), EXIT_SYSTEM_ERROR);
140
141 let json_err: serde_json::Error =
142 serde_json::from_str::<serde_json::Value>("bad").unwrap_err();
143 assert_eq!(RecError::Json(json_err).exit_code(), EXIT_SYSTEM_ERROR);
144
145 assert_eq!(
146 RecError::StaleLock("1234".into()).exit_code(),
147 EXIT_SYSTEM_ERROR
148 );
149 }
150
151 #[test]
152 fn test_error_display() {
153 let err = RecError::SessionNotFound("test-session".to_string());
154 assert_eq!(err.to_string(), "Session not found: test-session");
155
156 let err = RecError::RecordingInProgress;
157 assert_eq!(err.to_string(), "Recording already in progress");
158
159 let err = RecError::Config("invalid key".to_string());
160 assert_eq!(err.to_string(), "Config error: invalid key");
161
162 let err = RecError::InvalidAliasName("bad/alias".to_string());
163 assert_eq!(
164 err.to_string(),
165 "Invalid alias name 'bad/alias': only alphanumeric, dash, and underscore allowed"
166 );
167
168 let err = RecError::InvalidTagName("bad@tag".to_string());
169 assert_eq!(
170 err.to_string(),
171 "Invalid tag name 'bad@tag': only alphanumeric, dash, and underscore allowed"
172 );
173 }
174
175 #[test]
176 fn test_io_error_conversion() {
177 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
178 let rec_err: RecError = io_err.into();
179
180 match rec_err {
181 RecError::Io(_) => {}
182 _ => panic!("Expected RecError::Io"),
183 }
184 }
185
186 #[test]
187 fn test_json_error_conversion() {
188 let json_str = "{ invalid json }";
189 let json_result: std::result::Result<serde_json::Value, _> = serde_json::from_str(json_str);
190 let json_err = json_result.unwrap_err();
191 let rec_err: RecError = json_err.into();
192
193 match rec_err {
194 RecError::Json(_) => {}
195 _ => panic!("Expected RecError::Json"),
196 }
197 }
198
199 #[test]
200 fn test_result_type() {
201 fn returns_error() -> Result<()> {
202 Err(RecError::NoActiveRecording)
203 }
204
205 assert!(returns_error().is_err());
206 }
207}