1use thiserror::Error;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct ModelPath(String);
14
15impl ModelPath {
16 pub fn new(s: &str) -> Result<Self, GigasttError> {
20 if s.is_empty() {
21 return Err(GigasttError::InvalidAudio {
22 reason: "empty model path".into(),
23 });
24 }
25 Ok(ModelPath(s.to_string()))
26 }
27
28 pub fn as_str(&self) -> &str {
35 &self.0
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct Reason(String);
44
45impl Reason {
46 pub fn new(s: &str) -> Result<Self, GigasttError> {
50 if s.is_empty() {
51 return Err(GigasttError::InvalidAudio {
52 reason: "empty error reason".into(),
53 });
54 }
55 Ok(Reason(s.to_string()))
56 }
57
58 pub fn as_str(&self) -> &str {
62 &self.0
63 }
64}
65
66#[derive(Debug, Error)]
88#[non_exhaustive]
89pub enum GigasttError {
90 #[error("model load error at {path}")]
92 ModelLoad {
93 path: String,
95 #[source]
97 source: Option<Box<dyn std::error::Error + Send + Sync>>,
98 },
99 #[error("inference failed")]
101 Inference {
102 #[source]
104 source: Box<dyn std::error::Error + Send + Sync>,
105 },
106 #[error("invalid audio: {reason}")]
108 InvalidAudio {
109 reason: String,
111 },
112 #[error(transparent)]
114 Io(#[from] std::io::Error),
115 #[error("invalid input: {message}")]
117 InvalidInput { message: String },
118 #[error("cancelled")]
125 Cancelled,
126 #[error("audio too long: {observed_secs:.0}s exceeds the maximum of {limit_secs:.0}s")]
137 AudioTooLong {
138 observed_secs: f64,
140 limit_secs: f64,
142 },
143}
144
145impl GigasttError {
146 pub fn code(&self) -> &'static str {
151 match self {
152 GigasttError::ModelLoad { .. } => "model_load_error",
153 GigasttError::Inference { .. } => "inference_error",
154 GigasttError::InvalidAudio { .. } => "invalid_audio",
155 GigasttError::Io(_) => "io_error",
156 GigasttError::InvalidInput { .. } => "invalid_input",
157 GigasttError::Cancelled => "cancelled",
158 GigasttError::AudioTooLong { .. } => "audio_too_long",
159 }
160 }
161}
162
163impl From<crate::runtime::RuntimeError> for GigasttError {
164 fn from(err: crate::runtime::RuntimeError) -> Self {
165 match err {
166 crate::runtime::RuntimeError::LoadFailed { path, message } => GigasttError::ModelLoad {
167 path: path.to_string_lossy().into_owned(),
168 source: Some(Box::new(std::io::Error::other(message))),
169 },
170 other => GigasttError::Inference {
171 source: Box::new(other),
172 },
173 }
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_error_code_maps_variants() {
183 assert_eq!(
184 GigasttError::Inference {
185 source: "boom".into()
186 }
187 .code(),
188 "inference_error"
189 );
190 assert_eq!(
191 GigasttError::InvalidAudio {
192 reason: "bad".into()
193 }
194 .code(),
195 "invalid_audio"
196 );
197 assert_eq!(
198 GigasttError::ModelLoad {
199 path: "x".into(),
200 source: None
201 }
202 .code(),
203 "model_load_error"
204 );
205 assert_eq!(
206 GigasttError::Io(std::io::Error::other("x")).code(),
207 "io_error"
208 );
209 assert_eq!(
210 GigasttError::InvalidInput {
211 message: "bad format".into()
212 }
213 .code(),
214 "invalid_input"
215 );
216 assert_eq!(GigasttError::Cancelled.code(), "cancelled");
217 assert_eq!(
218 GigasttError::AudioTooLong {
219 observed_secs: 4000.0,
220 limit_secs: 1800.0,
221 }
222 .code(),
223 "audio_too_long"
224 );
225 }
226
227 #[test]
228 fn test_cancelled_display() {
229 assert_eq!(GigasttError::Cancelled.to_string(), "cancelled");
230 }
231
232 #[test]
233 fn test_audio_too_long_display_rounds_seconds() {
234 let e = GigasttError::AudioTooLong {
235 observed_secs: 3661.4,
236 limit_secs: 1800.0,
237 };
238 assert_eq!(
239 e.to_string(),
240 "audio too long: 3661s exceeds the maximum of 1800s"
241 );
242 }
243
244 #[test]
245 fn test_audio_too_long_survives_anyhow_downcast() {
246 let err: anyhow::Error = GigasttError::AudioTooLong {
249 observed_secs: 5000.0,
250 limit_secs: 1800.0,
251 }
252 .into();
253 match err.downcast::<GigasttError>() {
254 Ok(GigasttError::AudioTooLong { limit_secs, .. }) => {
255 assert_eq!(limit_secs, 1800.0);
256 }
257 other => panic!("expected AudioTooLong, got {other:?}"),
258 }
259 }
260
261 #[test]
262 fn test_display_invalid_input() {
263 let e = GigasttError::InvalidInput {
264 message: "unsupported format".into(),
265 };
266 assert_eq!(e.to_string(), "invalid input: unsupported format");
267 }
268
269 #[test]
270 fn test_model_path_rejects_empty() {
271 assert!(ModelPath::new("").is_err());
272 }
273
274 #[test]
275 fn test_model_path_accepts_valid() {
276 let p = ModelPath::new("encoder.onnx").unwrap();
277 assert_eq!(p.as_str(), "encoder.onnx");
278 }
279
280 #[test]
281 fn test_reason_rejects_empty() {
282 assert!(Reason::new("").is_err());
283 }
284
285 #[test]
286 fn test_reason_accepts_valid() {
287 let r = Reason::new("too long").unwrap();
288 assert_eq!(r.as_str(), "too long");
289 }
290
291 #[test]
292 fn test_display_model_load() {
293 let e = GigasttError::ModelLoad {
294 path: "encoder.onnx".into(),
295 source: Some(Box::new(std::io::Error::other("missing weights"))),
296 };
297 assert!(e.to_string().contains("encoder.onnx"));
298 }
299
300 #[test]
301 fn test_display_inference() {
302 let e = GigasttError::Inference {
303 source: Box::new(std::io::Error::other("decoder failed")),
304 };
305 assert_eq!(e.to_string(), "inference failed");
306 }
307
308 #[test]
309 fn test_display_invalid_audio() {
310 let e = GigasttError::InvalidAudio {
311 reason: "too long".into(),
312 };
313 assert_eq!(e.to_string(), "invalid audio: too long");
314 }
315
316 #[test]
317 fn test_display_io() {
318 let e = GigasttError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "gone"));
319 assert!(e.to_string().contains("gone"));
320 }
321
322 #[test]
323 fn test_from_io_error() {
324 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
325 let e: GigasttError = io_err.into();
326 assert!(matches!(e, GigasttError::Io(_)));
327 }
328
329 #[test]
330 fn test_error_source_io() {
331 let e = GigasttError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "x"));
332 assert!(std::error::Error::source(&e).is_none());
333 }
334
335 #[test]
336 fn test_into_anyhow() {
337 fn returns_anyhow() -> anyhow::Result<()> {
339 Err(GigasttError::Inference {
340 source: Box::new(std::io::Error::other("test")),
341 })?;
342 Ok(())
343 }
344 assert!(returns_anyhow().is_err());
345 }
346}