1use crate::Progress;
5use tokio::sync::{AcquireError, watch};
6
7#[derive(Debug)]
13pub enum Error {
14 IoError(std::io::Error),
16 ConfigError(config::ConfigError),
18 JsonError(serde_json::Error),
20 HttpError(reqwest::Error),
22 MaxRetriesExceeded(u32),
24 UrlParseError(url::ParseError),
26 RpcError(i32, String),
28 InvalidRpcId(String),
30 EnvError(std::env::VarError),
32 SemaphoreError(AcquireError),
34 JoinError(tokio::task::JoinError),
36 ProgressSendError(watch::error::SendError<Progress>),
38 ProgressRecvError(watch::error::RecvError),
40 StripPrefixError(std::path::StripPrefixError),
42 ParseIntError(std::num::ParseIntError),
44 InvalidResponse,
46 NotImplemented,
48 PartTooLarge,
50 InvalidFileType(String),
52 InvalidAnnotationType(String),
54 UnsupportedFormat(String),
56 MissingImages(String),
58 MissingAnnotations(String),
60 MissingLabel(String),
62 InvalidParameters(String),
64 FeatureNotEnabled(String),
66 EmptyToken,
68 InvalidToken,
70 TokenExpired,
72 Unauthorized,
74 InvalidEtag(String),
76 StorageError(String),
78 #[cfg(feature = "polars")]
80 PolarsError(polars::error::PolarsError),
81 CocoError(String),
83 ZipError(String),
85 TaskNotFound(crate::api::TaskID),
87 PermissionDenied(String),
90 PayloadTooLarge {
94 method: String,
95 size_hint: Option<u64>,
96 },
97 InsecureUrl(String),
103}
104
105impl From<std::io::Error> for Error {
106 fn from(err: std::io::Error) -> Self {
107 Error::IoError(err)
108 }
109}
110
111impl From<config::ConfigError> for Error {
112 fn from(err: config::ConfigError) -> Self {
113 Error::ConfigError(err)
114 }
115}
116
117impl From<serde_json::Error> for Error {
118 fn from(err: serde_json::Error) -> Self {
119 Error::JsonError(err)
120 }
121}
122
123impl From<reqwest::Error> for Error {
124 fn from(err: reqwest::Error) -> Self {
125 Error::HttpError(err)
126 }
127}
128
129impl From<url::ParseError> for Error {
130 fn from(err: url::ParseError) -> Self {
131 Error::UrlParseError(err)
132 }
133}
134
135impl From<std::env::VarError> for Error {
136 fn from(err: std::env::VarError) -> Self {
137 Error::EnvError(err)
138 }
139}
140
141impl From<AcquireError> for Error {
142 fn from(err: AcquireError) -> Self {
143 Error::SemaphoreError(err)
144 }
145}
146
147impl From<tokio::task::JoinError> for Error {
148 fn from(err: tokio::task::JoinError) -> Self {
149 Error::JoinError(err)
150 }
151}
152
153impl From<watch::error::SendError<Progress>> for Error {
154 fn from(err: watch::error::SendError<Progress>) -> Self {
155 Error::ProgressSendError(err)
156 }
157}
158
159impl From<watch::error::RecvError> for Error {
160 fn from(err: watch::error::RecvError) -> Self {
161 Error::ProgressRecvError(err)
162 }
163}
164
165impl From<std::path::StripPrefixError> for Error {
166 fn from(err: std::path::StripPrefixError) -> Self {
167 Error::StripPrefixError(err)
168 }
169}
170
171impl From<std::num::ParseIntError> for Error {
172 fn from(err: std::num::ParseIntError) -> Self {
173 Error::ParseIntError(err)
174 }
175}
176
177impl From<crate::storage::StorageError> for Error {
178 fn from(err: crate::storage::StorageError) -> Self {
179 Error::StorageError(err.to_string())
180 }
181}
182
183#[cfg(feature = "polars")]
184impl From<polars::error::PolarsError> for Error {
185 fn from(err: polars::error::PolarsError) -> Self {
186 Error::PolarsError(err)
187 }
188}
189
190impl From<zip::result::ZipError> for Error {
191 fn from(err: zip::result::ZipError) -> Self {
192 Error::ZipError(err.to_string())
193 }
194}
195
196impl std::fmt::Display for Error {
197 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198 match self {
199 Error::IoError(e) => write!(f, "I/O error: {}", e),
200 Error::ConfigError(e) => write!(f, "Configuration error: {}", e),
201 Error::JsonError(e) => write!(f, "JSON error: {}", e),
202 Error::HttpError(e) => write!(f, "HTTP error: {}", e),
203 Error::MaxRetriesExceeded(n) => write!(f, "Maximum retries ({}) exceeded", n),
204 Error::UrlParseError(e) => write!(f, "URL parse error: {}", e),
205 Error::RpcError(code, msg) => write!(f, "RPC error {}: {}", code, msg),
206 Error::InvalidRpcId(id) => write!(f, "Invalid RPC ID: {}", id),
207 Error::EnvError(e) => write!(f, "Environment variable error: {}", e),
208 Error::SemaphoreError(e) => write!(f, "Semaphore error: {}", e),
209 Error::JoinError(e) => write!(f, "Task join error: {}", e),
210 Error::ProgressSendError(e) => write!(f, "Progress send error: {}", e),
211 Error::ProgressRecvError(e) => write!(f, "Progress receive error: {}", e),
212 Error::StripPrefixError(e) => write!(f, "Path prefix error: {}", e),
213 Error::ParseIntError(e) => write!(f, "Integer parse error: {}", e),
214 Error::InvalidResponse => write!(f, "Invalid server response"),
215 Error::NotImplemented => write!(f, "Not implemented"),
216 Error::PartTooLarge => write!(f, "File part size exceeds maximum limit"),
217 Error::InvalidFileType(s) => write!(f, "Invalid file type: {}", s),
218 Error::InvalidAnnotationType(s) => write!(f, "Invalid annotation type: {}", s),
219 Error::UnsupportedFormat(s) => write!(f, "Unsupported format: {}", s),
220 Error::MissingImages(s) => write!(f, "Missing images: {}", s),
221 Error::MissingAnnotations(s) => write!(f, "Missing annotations: {}", s),
222 Error::MissingLabel(s) => write!(f, "Missing label: {}", s),
223 Error::InvalidParameters(s) => write!(f, "Invalid parameters: {}", s),
224 Error::FeatureNotEnabled(s) => write!(f, "Feature not enabled: {}", s),
225 Error::EmptyToken => write!(f, "Authentication token is empty"),
226 Error::InvalidToken => write!(f, "Invalid authentication token"),
227 Error::TokenExpired => write!(f, "Authentication token has expired"),
228 Error::Unauthorized => write!(f, "Unauthorized access"),
229 Error::InvalidEtag(s) => write!(f, "Invalid ETag header: {}", s),
230 Error::StorageError(s) => write!(f, "Token storage error: {}", s),
231 #[cfg(feature = "polars")]
232 Error::PolarsError(e) => write!(f, "Polars error: {}", e),
233 Error::CocoError(s) => write!(f, "COCO format error: {}", s),
234 Error::ZipError(s) => write!(f, "ZIP error: {}", s),
235 Error::TaskNotFound(id) => write!(f, "task not found: {}", id),
236 Error::PermissionDenied(op) => write!(f, "permission denied: {}", op),
237 Error::PayloadTooLarge { method, .. } => write!(f, "payload too large for {}", method),
238 Error::InsecureUrl(url) => write!(
239 f,
240 "refusing insecure URL '{}': Studio bearer tokens require HTTPS \
241 (loopback http is allowed for tests/dev)",
242 url
243 ),
244 }
245 }
246}
247
248impl std::error::Error for Error {
249 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
250 match self {
251 Error::IoError(e) => Some(e),
252 Error::ConfigError(e) => Some(e),
253 Error::JsonError(e) => Some(e),
254 Error::HttpError(e) => Some(e),
255 Error::UrlParseError(e) => Some(e),
256 Error::EnvError(e) => Some(e),
257 Error::JoinError(e) => Some(e),
258 Error::StripPrefixError(e) => Some(e),
259 Error::ParseIntError(e) => Some(e),
260 #[cfg(feature = "polars")]
261 Error::PolarsError(e) => Some(e),
262 _ => None,
263 }
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use std::path::Path;
271
272 #[test]
280 fn test_io_error_wrapping() {
281 let inner_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
283 let inner_str = inner_err.to_string();
285 let wrapped_err: Error = inner_err.into();
287 let wrapped_str = wrapped_err.to_string();
289 assert!(
291 wrapped_str.contains(&inner_str),
292 "Wrapped error '{}' should contain inner error '{}'",
293 wrapped_str,
294 inner_str
295 );
296 assert!(wrapped_str.starts_with("I/O error: "));
297 }
298
299 #[test]
300 fn test_config_error_wrapping() {
301 #[derive(Debug, serde::Deserialize)]
304 #[allow(dead_code)]
305 struct RequiredField {
306 required: String,
307 }
308
309 let inner_err = config::Config::builder()
310 .build()
311 .unwrap()
312 .try_deserialize::<RequiredField>()
313 .unwrap_err();
314 let inner_str = inner_err.to_string();
316 let wrapped_err: Error = inner_err.into();
318 let wrapped_str = wrapped_err.to_string();
320 assert!(
322 wrapped_str.contains(&inner_str),
323 "Wrapped error '{}' should contain inner error '{}'",
324 wrapped_str,
325 inner_str
326 );
327 assert!(wrapped_str.starts_with("Configuration error: "));
328 }
329
330 #[test]
331 fn test_json_error_wrapping() {
332 let inner_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
334 let inner_str = inner_err.to_string();
336 let wrapped_err: Error = inner_err.into();
338 let wrapped_str = wrapped_err.to_string();
340 assert!(
342 wrapped_str.contains(&inner_str),
343 "Wrapped error '{}' should contain inner error '{}'",
344 wrapped_str,
345 inner_str
346 );
347 assert!(wrapped_str.starts_with("JSON error: "));
348 }
349
350 #[test]
351 fn test_url_parse_error_wrapping() {
352 let inner_err = url::Url::parse("not a valid url").unwrap_err();
354 let inner_str = inner_err.to_string();
356 let wrapped_err: Error = inner_err.into();
358 let wrapped_str = wrapped_err.to_string();
360 assert!(
362 wrapped_str.contains(&inner_str),
363 "Wrapped error '{}' should contain inner error '{}'",
364 wrapped_str,
365 inner_str
366 );
367 assert!(wrapped_str.starts_with("URL parse error: "));
368 }
369
370 #[test]
371 fn test_env_error_wrapping() {
372 let inner_err = std::env::var("NONEXISTENT_VAR_12345").unwrap_err();
374 let inner_str = inner_err.to_string();
376 let wrapped_err: Error = inner_err.into();
378 let wrapped_str = wrapped_err.to_string();
380 assert!(
382 wrapped_str.contains(&inner_str),
383 "Wrapped error '{}' should contain inner error '{}'",
384 wrapped_str,
385 inner_str
386 );
387 assert!(wrapped_str.starts_with("Environment variable error: "));
388 }
389
390 #[test]
391 fn test_strip_prefix_error_wrapping() {
392 let path = Path::new("/foo/bar");
394 let prefix = Path::new("/baz");
395 let inner_err = path.strip_prefix(prefix).unwrap_err();
396 let inner_str = inner_err.to_string();
398 let wrapped_err: Error = inner_err.into();
400 let wrapped_str = wrapped_err.to_string();
402 assert!(
404 wrapped_str.contains(&inner_str),
405 "Wrapped error '{}' should contain inner error '{}'",
406 wrapped_str,
407 inner_str
408 );
409 assert!(wrapped_str.starts_with("Path prefix error: "));
410 }
411
412 #[test]
413 fn test_parse_int_error_wrapping() {
414 let inner_err = "not a number".parse::<i32>().unwrap_err();
416 let inner_str = inner_err.to_string();
418 let wrapped_err: Error = inner_err.into();
420 let wrapped_str = wrapped_err.to_string();
422 assert!(
424 wrapped_str.contains(&inner_str),
425 "Wrapped error '{}' should contain inner error '{}'",
426 wrapped_str,
427 inner_str
428 );
429 assert!(wrapped_str.starts_with("Integer parse error: "));
430 }
431
432 #[cfg(feature = "polars")]
433 #[test]
434 fn test_polars_error_wrapping() {
435 use polars::prelude::*;
437 let inner_err = DataFrame::new_infer_height(vec![
438 Series::new("a".into(), &[1, 2, 3]).into(),
439 Series::new("a".into(), &[4, 5, 6]).into(),
440 ])
441 .unwrap_err();
442 let inner_str = inner_err.to_string();
444 let wrapped_err: Error = inner_err.into();
446 let wrapped_str = wrapped_err.to_string();
448 assert!(
450 wrapped_str.contains(&inner_str),
451 "Wrapped error '{}' should contain inner error '{}'",
452 wrapped_str,
453 inner_str
454 );
455 assert!(wrapped_str.starts_with("Polars error: "));
456 }
457
458 #[test]
466 fn test_max_retries_exceeded() {
467 let retry_count = 42u32;
469 let primitive_str = retry_count.to_string();
471 let wrapped_err = Error::MaxRetriesExceeded(retry_count);
473 let wrapped_str = wrapped_err.to_string();
475 assert!(
477 wrapped_str.contains(&primitive_str),
478 "Wrapped error '{}' should contain retry count '{}'",
479 wrapped_str,
480 primitive_str
481 );
482 assert!(wrapped_str.starts_with("Maximum retries"));
483 }
484
485 #[test]
486 fn test_rpc_error() {
487 let error_code = -32600;
489 let error_msg = "Invalid Request";
490 let code_str = error_code.to_string();
492 let wrapped_err = Error::RpcError(error_code, error_msg.to_string());
494 let wrapped_str = wrapped_err.to_string();
496 assert!(
498 wrapped_str.contains(&code_str),
499 "Wrapped error '{}' should contain error code '{}'",
500 wrapped_str,
501 code_str
502 );
503 assert!(
504 wrapped_str.contains(error_msg),
505 "Wrapped error '{}' should contain error message '{}'",
506 wrapped_str,
507 error_msg
508 );
509 assert!(wrapped_str.starts_with("RPC error"));
510 }
511
512 #[test]
513 fn test_invalid_rpc_id() {
514 let invalid_id = "not-a-valid-id-123";
516 let wrapped_err = Error::InvalidRpcId(invalid_id.to_string());
519 let wrapped_str = wrapped_err.to_string();
521 assert!(
523 wrapped_str.contains(invalid_id),
524 "Wrapped error '{}' should contain invalid ID '{}'",
525 wrapped_str,
526 invalid_id
527 );
528 assert!(wrapped_str.starts_with("Invalid RPC ID: "));
529 }
530
531 #[test]
532 fn test_invalid_file_type() {
533 let file_type = "unknown_format";
535 let wrapped_err = Error::InvalidFileType(file_type.to_string());
538 let wrapped_str = wrapped_err.to_string();
540 assert!(
542 wrapped_str.contains(file_type),
543 "Wrapped error '{}' should contain file type '{}'",
544 wrapped_str,
545 file_type
546 );
547 assert!(wrapped_str.starts_with("Invalid file type: "));
548 }
549
550 #[test]
551 fn test_invalid_annotation_type() {
552 let annotation_type = "unsupported_annotation";
554 let wrapped_err = Error::InvalidAnnotationType(annotation_type.to_string());
557 let wrapped_str = wrapped_err.to_string();
559 assert!(
561 wrapped_str.contains(annotation_type),
562 "Wrapped error '{}' should contain annotation type '{}'",
563 wrapped_str,
564 annotation_type
565 );
566 assert!(wrapped_str.starts_with("Invalid annotation type: "));
567 }
568
569 #[test]
570 fn test_unsupported_format() {
571 let format = "xyz_format";
573 let wrapped_err = Error::UnsupportedFormat(format.to_string());
576 let wrapped_str = wrapped_err.to_string();
578 assert!(
580 wrapped_str.contains(format),
581 "Wrapped error '{}' should contain format '{}'",
582 wrapped_str,
583 format
584 );
585 assert!(wrapped_str.starts_with("Unsupported format: "));
586 }
587
588 #[test]
589 fn test_missing_images() {
590 let details = "image001.jpg, image002.jpg";
592 let wrapped_err = Error::MissingImages(details.to_string());
595 let wrapped_str = wrapped_err.to_string();
597 assert!(
599 wrapped_str.contains(details),
600 "Wrapped error '{}' should contain details '{}'",
601 wrapped_str,
602 details
603 );
604 assert!(wrapped_str.starts_with("Missing images: "));
605 }
606
607 #[test]
608 fn test_missing_annotations() {
609 let details = "annotations.json";
611 let wrapped_err = Error::MissingAnnotations(details.to_string());
614 let wrapped_str = wrapped_err.to_string();
616 assert!(
618 wrapped_str.contains(details),
619 "Wrapped error '{}' should contain details '{}'",
620 wrapped_str,
621 details
622 );
623 assert!(wrapped_str.starts_with("Missing annotations: "));
624 }
625
626 #[test]
627 fn test_missing_label() {
628 let label = "person";
630 let wrapped_err = Error::MissingLabel(label.to_string());
633 let wrapped_str = wrapped_err.to_string();
635 assert!(
637 wrapped_str.contains(label),
638 "Wrapped error '{}' should contain label '{}'",
639 wrapped_str,
640 label
641 );
642 assert!(wrapped_str.starts_with("Missing label: "));
643 }
644
645 #[test]
646 fn test_invalid_parameters() {
647 let params = "batch_size must be positive";
649 let wrapped_err = Error::InvalidParameters(params.to_string());
652 let wrapped_str = wrapped_err.to_string();
654 assert!(
656 wrapped_str.contains(params),
657 "Wrapped error '{}' should contain params '{}'",
658 wrapped_str,
659 params
660 );
661 assert!(wrapped_str.starts_with("Invalid parameters: "));
662 }
663
664 #[test]
665 fn test_feature_not_enabled() {
666 let feature = "polars";
668 let wrapped_err = Error::FeatureNotEnabled(feature.to_string());
671 let wrapped_str = wrapped_err.to_string();
673 assert!(
675 wrapped_str.contains(feature),
676 "Wrapped error '{}' should contain feature '{}'",
677 wrapped_str,
678 feature
679 );
680 assert!(wrapped_str.starts_with("Feature not enabled: "));
681 }
682
683 #[test]
684 fn test_invalid_etag() {
685 let etag = "malformed-etag-value";
687 let wrapped_err = Error::InvalidEtag(etag.to_string());
690 let wrapped_str = wrapped_err.to_string();
692 assert!(
694 wrapped_str.contains(etag),
695 "Wrapped error '{}' should contain etag '{}'",
696 wrapped_str,
697 etag
698 );
699 assert!(wrapped_str.starts_with("Invalid ETag header: "));
700 }
701
702 #[test]
706 fn test_invalid_response() {
707 let err = Error::InvalidResponse;
708 let err_str = err.to_string();
709 assert_eq!(err_str, "Invalid server response");
710 }
711
712 #[test]
713 fn test_not_implemented() {
714 let err = Error::NotImplemented;
715 let err_str = err.to_string();
716 assert_eq!(err_str, "Not implemented");
717 }
718
719 #[test]
720 fn test_part_too_large() {
721 let err = Error::PartTooLarge;
722 let err_str = err.to_string();
723 assert_eq!(err_str, "File part size exceeds maximum limit");
724 }
725
726 #[test]
727 fn test_empty_token() {
728 let err = Error::EmptyToken;
729 let err_str = err.to_string();
730 assert_eq!(err_str, "Authentication token is empty");
731 }
732
733 #[test]
734 fn test_invalid_token() {
735 let err = Error::InvalidToken;
736 let err_str = err.to_string();
737 assert_eq!(err_str, "Invalid authentication token");
738 }
739
740 #[test]
741 fn test_token_expired() {
742 let err = Error::TokenExpired;
743 let err_str = err.to_string();
744 assert_eq!(err_str, "Authentication token has expired");
745 }
746
747 #[test]
748 fn test_unauthorized() {
749 let err = Error::Unauthorized;
750 let err_str = err.to_string();
751 assert_eq!(err_str, "Unauthorized access");
752 }
753
754 #[test]
759 fn test_task_not_found_display_contains_id() {
760 let task_id = crate::api::TaskID::from(0x1092u64);
762 let err = Error::TaskNotFound(task_id);
763 let err_str = err.to_string();
764 assert!(
765 err_str.contains("task-1092"),
766 "Display should include the task ID prefix+hex, got: {err_str}"
767 );
768 assert!(err_str.starts_with("task not found"));
769 }
770
771 #[test]
772 fn test_permission_denied_display_contains_method() {
773 let err = Error::PermissionDenied("task.chart.add".to_string());
774 let err_str = err.to_string();
775 assert!(
776 err_str.contains("task.chart.add"),
777 "Display should include the method name, got: {err_str}"
778 );
779 assert!(err_str.starts_with("permission denied"));
780 }
781
782 #[test]
783 fn test_payload_too_large_display_contains_method() {
784 let err = Error::PayloadTooLarge {
785 method: "val.data.upload".to_string(),
786 size_hint: Some(123456),
787 };
788 let err_str = err.to_string();
789 assert!(
790 err_str.contains("val.data.upload"),
791 "Display should include the method name, got: {err_str}"
792 );
793 assert!(err_str.starts_with("payload too large"));
794 }
795
796 #[test]
797 fn test_payload_too_large_with_no_size_hint() {
798 let err = Error::PayloadTooLarge {
800 method: "task.data.upload".to_string(),
801 size_hint: None,
802 };
803 let err_str = err.to_string();
804 assert!(err_str.contains("task.data.upload"));
805 }
806
807 #[test]
808 fn test_typed_variants_have_no_source() {
809 use std::error::Error as _;
813
814 let task_not_found = Error::TaskNotFound(crate::api::TaskID::from(1u64));
815 assert!(task_not_found.source().is_none());
816
817 let perm_denied = Error::PermissionDenied("op".into());
818 assert!(perm_denied.source().is_none());
819
820 let too_large = Error::PayloadTooLarge {
821 method: "m".into(),
822 size_hint: Some(1),
823 };
824 assert!(too_large.source().is_none());
825 }
826
827 #[test]
832 fn test_coco_error_display() {
833 let err = Error::CocoError("missing categories array".into());
834 let err_str = err.to_string();
835 assert!(err_str.contains("missing categories array"));
836 assert!(err_str.starts_with("COCO format error:"));
837 }
838
839 #[test]
840 fn test_zip_error_display() {
841 let err = Error::ZipError("invalid central directory".into());
842 let err_str = err.to_string();
843 assert!(err_str.contains("invalid central directory"));
844 assert!(err_str.starts_with("ZIP error:"));
845 }
846
847 #[test]
848 fn test_storage_error_display() {
849 let err = Error::StorageError("keychain locked".into());
850 let err_str = err.to_string();
851 assert!(err_str.contains("keychain locked"));
852 assert!(err_str.starts_with("Token storage error:"));
853 }
854}