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 MissingResource(String),
63 MissingAnnotations(String),
65 MissingLabel(String),
67 InvalidParameters(String),
69 FeatureNotEnabled(String),
71 EmptyToken,
73 InvalidToken,
75 TokenExpired,
77 Unauthorized,
79 InvalidEtag(String),
81 StorageError(String),
83 #[cfg(feature = "polars")]
85 PolarsError(polars::error::PolarsError),
86 CocoError(String),
88 ZipError(String),
90 TaskNotFound(crate::api::TaskID),
92 PermissionDenied(String),
95 PayloadTooLarge {
99 method: String,
100 size_hint: Option<u64>,
101 },
102 InsecureUrl(String),
108}
109
110impl From<std::io::Error> for Error {
111 fn from(err: std::io::Error) -> Self {
112 Error::IoError(err)
113 }
114}
115
116impl From<config::ConfigError> for Error {
117 fn from(err: config::ConfigError) -> Self {
118 Error::ConfigError(err)
119 }
120}
121
122impl From<serde_json::Error> for Error {
123 fn from(err: serde_json::Error) -> Self {
124 Error::JsonError(err)
125 }
126}
127
128impl From<reqwest::Error> for Error {
129 fn from(err: reqwest::Error) -> Self {
130 Error::HttpError(err)
131 }
132}
133
134impl From<url::ParseError> for Error {
135 fn from(err: url::ParseError) -> Self {
136 Error::UrlParseError(err)
137 }
138}
139
140impl From<std::env::VarError> for Error {
141 fn from(err: std::env::VarError) -> Self {
142 Error::EnvError(err)
143 }
144}
145
146impl From<AcquireError> for Error {
147 fn from(err: AcquireError) -> Self {
148 Error::SemaphoreError(err)
149 }
150}
151
152impl From<tokio::task::JoinError> for Error {
153 fn from(err: tokio::task::JoinError) -> Self {
154 Error::JoinError(err)
155 }
156}
157
158impl From<watch::error::SendError<Progress>> for Error {
159 fn from(err: watch::error::SendError<Progress>) -> Self {
160 Error::ProgressSendError(err)
161 }
162}
163
164impl From<watch::error::RecvError> for Error {
165 fn from(err: watch::error::RecvError) -> Self {
166 Error::ProgressRecvError(err)
167 }
168}
169
170impl From<std::path::StripPrefixError> for Error {
171 fn from(err: std::path::StripPrefixError) -> Self {
172 Error::StripPrefixError(err)
173 }
174}
175
176impl From<std::num::ParseIntError> for Error {
177 fn from(err: std::num::ParseIntError) -> Self {
178 Error::ParseIntError(err)
179 }
180}
181
182impl From<crate::storage::StorageError> for Error {
183 fn from(err: crate::storage::StorageError) -> Self {
184 Error::StorageError(err.to_string())
185 }
186}
187
188#[cfg(feature = "polars")]
189impl From<polars::error::PolarsError> for Error {
190 fn from(err: polars::error::PolarsError) -> Self {
191 Error::PolarsError(err)
192 }
193}
194
195impl From<zip::result::ZipError> for Error {
196 fn from(err: zip::result::ZipError) -> Self {
197 Error::ZipError(err.to_string())
198 }
199}
200
201impl std::fmt::Display for Error {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 match self {
204 Error::IoError(e) => write!(f, "I/O error: {}", e),
205 Error::ConfigError(e) => write!(f, "Configuration error: {}", e),
206 Error::JsonError(e) => write!(f, "JSON error: {}", e),
207 Error::HttpError(e) => write!(f, "HTTP error: {}", e),
208 Error::MaxRetriesExceeded(n) => write!(f, "Maximum retries ({}) exceeded", n),
209 Error::UrlParseError(e) => write!(f, "URL parse error: {}", e),
210 Error::RpcError(code, msg) => write!(f, "RPC error {}: {}", code, msg),
211 Error::InvalidRpcId(id) => write!(f, "Invalid RPC ID: {}", id),
212 Error::EnvError(e) => write!(f, "Environment variable error: {}", e),
213 Error::SemaphoreError(e) => write!(f, "Semaphore error: {}", e),
214 Error::JoinError(e) => write!(f, "Task join error: {}", e),
215 Error::ProgressSendError(e) => write!(f, "Progress send error: {}", e),
216 Error::ProgressRecvError(e) => write!(f, "Progress receive error: {}", e),
217 Error::StripPrefixError(e) => write!(f, "Path prefix error: {}", e),
218 Error::ParseIntError(e) => write!(f, "Integer parse error: {}", e),
219 Error::InvalidResponse => write!(f, "Invalid server response"),
220 Error::NotImplemented => write!(f, "Not implemented"),
221 Error::PartTooLarge => write!(f, "File part size exceeds maximum limit"),
222 Error::InvalidFileType(s) => write!(
225 f,
226 "Invalid file type: {}. Valid types: image, lidar.pcd, lidar.png, \
227 lidar.jpg, radar.pcd, radar.png, all (aliases also accepted: \
228 lidar.depth, depth.png, depthmap, lidar.jpeg, lidar.reflect, pcd, cube)",
229 s
230 ),
231 Error::InvalidAnnotationType(s) => write!(f, "Invalid annotation type: {}", s),
232 Error::UnsupportedFormat(s) => write!(f, "Unsupported format: {}", s),
233 Error::MissingImages(s) => write!(f, "Missing images: {}", s),
234 Error::MissingResource(s) => write!(f, "Missing resource: {}", s),
235 Error::MissingAnnotations(s) => write!(f, "Missing annotations: {}", s),
236 Error::MissingLabel(s) => write!(f, "Missing label: {}", s),
237 Error::InvalidParameters(s) => write!(f, "Invalid parameters: {}", s),
238 Error::FeatureNotEnabled(s) => write!(f, "Feature not enabled: {}", s),
239 Error::EmptyToken => write!(f, "Authentication token is empty"),
240 Error::InvalidToken => write!(f, "Invalid authentication token"),
241 Error::TokenExpired => write!(f, "Authentication token has expired"),
242 Error::Unauthorized => write!(f, "Unauthorized access"),
243 Error::InvalidEtag(s) => write!(f, "Invalid ETag header: {}", s),
244 Error::StorageError(s) => write!(f, "Token storage error: {}", s),
245 #[cfg(feature = "polars")]
246 Error::PolarsError(e) => write!(f, "Polars error: {}", e),
247 Error::CocoError(s) => write!(f, "COCO format error: {}", s),
248 Error::ZipError(s) => write!(f, "ZIP error: {}", s),
249 Error::TaskNotFound(id) => write!(f, "task not found: {}", id),
250 Error::PermissionDenied(op) => write!(f, "permission denied: {}", op),
251 Error::PayloadTooLarge { method, .. } => write!(f, "payload too large for {}", method),
252 Error::InsecureUrl(url) => write!(
253 f,
254 "refusing insecure URL '{}': Studio bearer tokens require HTTPS \
255 (loopback http is allowed for tests/dev)",
256 url
257 ),
258 }
259 }
260}
261
262impl std::error::Error for Error {
263 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
264 match self {
265 Error::IoError(e) => Some(e),
266 Error::ConfigError(e) => Some(e),
267 Error::JsonError(e) => Some(e),
268 Error::HttpError(e) => Some(e),
269 Error::UrlParseError(e) => Some(e),
270 Error::EnvError(e) => Some(e),
271 Error::JoinError(e) => Some(e),
272 Error::StripPrefixError(e) => Some(e),
273 Error::ParseIntError(e) => Some(e),
274 #[cfg(feature = "polars")]
275 Error::PolarsError(e) => Some(e),
276 _ => None,
277 }
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use std::path::Path;
285
286 #[test]
294 fn test_io_error_wrapping() {
295 let inner_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
297 let inner_str = inner_err.to_string();
299 let wrapped_err: Error = inner_err.into();
301 let wrapped_str = wrapped_err.to_string();
303 assert!(
305 wrapped_str.contains(&inner_str),
306 "Wrapped error '{}' should contain inner error '{}'",
307 wrapped_str,
308 inner_str
309 );
310 assert!(wrapped_str.starts_with("I/O error: "));
311 }
312
313 #[test]
314 fn test_config_error_wrapping() {
315 #[derive(Debug, serde::Deserialize)]
318 #[allow(dead_code)]
319 struct RequiredField {
320 required: String,
321 }
322
323 let inner_err = config::Config::builder()
324 .build()
325 .unwrap()
326 .try_deserialize::<RequiredField>()
327 .unwrap_err();
328 let inner_str = inner_err.to_string();
330 let wrapped_err: Error = inner_err.into();
332 let wrapped_str = wrapped_err.to_string();
334 assert!(
336 wrapped_str.contains(&inner_str),
337 "Wrapped error '{}' should contain inner error '{}'",
338 wrapped_str,
339 inner_str
340 );
341 assert!(wrapped_str.starts_with("Configuration error: "));
342 }
343
344 #[test]
345 fn test_json_error_wrapping() {
346 let inner_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
348 let inner_str = inner_err.to_string();
350 let wrapped_err: Error = inner_err.into();
352 let wrapped_str = wrapped_err.to_string();
354 assert!(
356 wrapped_str.contains(&inner_str),
357 "Wrapped error '{}' should contain inner error '{}'",
358 wrapped_str,
359 inner_str
360 );
361 assert!(wrapped_str.starts_with("JSON error: "));
362 }
363
364 #[test]
365 fn test_url_parse_error_wrapping() {
366 let inner_err = url::Url::parse("not a valid url").unwrap_err();
368 let inner_str = inner_err.to_string();
370 let wrapped_err: Error = inner_err.into();
372 let wrapped_str = wrapped_err.to_string();
374 assert!(
376 wrapped_str.contains(&inner_str),
377 "Wrapped error '{}' should contain inner error '{}'",
378 wrapped_str,
379 inner_str
380 );
381 assert!(wrapped_str.starts_with("URL parse error: "));
382 }
383
384 #[test]
385 fn test_env_error_wrapping() {
386 let inner_err = std::env::var("NONEXISTENT_VAR_12345").unwrap_err();
388 let inner_str = inner_err.to_string();
390 let wrapped_err: Error = inner_err.into();
392 let wrapped_str = wrapped_err.to_string();
394 assert!(
396 wrapped_str.contains(&inner_str),
397 "Wrapped error '{}' should contain inner error '{}'",
398 wrapped_str,
399 inner_str
400 );
401 assert!(wrapped_str.starts_with("Environment variable error: "));
402 }
403
404 #[test]
405 fn test_strip_prefix_error_wrapping() {
406 let path = Path::new("/foo/bar");
408 let prefix = Path::new("/baz");
409 let inner_err = path.strip_prefix(prefix).unwrap_err();
410 let inner_str = inner_err.to_string();
412 let wrapped_err: Error = inner_err.into();
414 let wrapped_str = wrapped_err.to_string();
416 assert!(
418 wrapped_str.contains(&inner_str),
419 "Wrapped error '{}' should contain inner error '{}'",
420 wrapped_str,
421 inner_str
422 );
423 assert!(wrapped_str.starts_with("Path prefix error: "));
424 }
425
426 #[test]
427 fn test_parse_int_error_wrapping() {
428 let inner_err = "not a number".parse::<i32>().unwrap_err();
430 let inner_str = inner_err.to_string();
432 let wrapped_err: Error = inner_err.into();
434 let wrapped_str = wrapped_err.to_string();
436 assert!(
438 wrapped_str.contains(&inner_str),
439 "Wrapped error '{}' should contain inner error '{}'",
440 wrapped_str,
441 inner_str
442 );
443 assert!(wrapped_str.starts_with("Integer parse error: "));
444 }
445
446 #[cfg(feature = "polars")]
447 #[test]
448 fn test_polars_error_wrapping() {
449 use polars::prelude::*;
451 let inner_err = DataFrame::new_infer_height(vec![
452 Series::new("a".into(), &[1, 2, 3]).into(),
453 Series::new("a".into(), &[4, 5, 6]).into(),
454 ])
455 .unwrap_err();
456 let inner_str = inner_err.to_string();
458 let wrapped_err: Error = inner_err.into();
460 let wrapped_str = wrapped_err.to_string();
462 assert!(
464 wrapped_str.contains(&inner_str),
465 "Wrapped error '{}' should contain inner error '{}'",
466 wrapped_str,
467 inner_str
468 );
469 assert!(wrapped_str.starts_with("Polars error: "));
470 }
471
472 #[test]
480 fn test_max_retries_exceeded() {
481 let retry_count = 42u32;
483 let primitive_str = retry_count.to_string();
485 let wrapped_err = Error::MaxRetriesExceeded(retry_count);
487 let wrapped_str = wrapped_err.to_string();
489 assert!(
491 wrapped_str.contains(&primitive_str),
492 "Wrapped error '{}' should contain retry count '{}'",
493 wrapped_str,
494 primitive_str
495 );
496 assert!(wrapped_str.starts_with("Maximum retries"));
497 }
498
499 #[test]
500 fn test_rpc_error() {
501 let error_code = -32600;
503 let error_msg = "Invalid Request";
504 let code_str = error_code.to_string();
506 let wrapped_err = Error::RpcError(error_code, error_msg.to_string());
508 let wrapped_str = wrapped_err.to_string();
510 assert!(
512 wrapped_str.contains(&code_str),
513 "Wrapped error '{}' should contain error code '{}'",
514 wrapped_str,
515 code_str
516 );
517 assert!(
518 wrapped_str.contains(error_msg),
519 "Wrapped error '{}' should contain error message '{}'",
520 wrapped_str,
521 error_msg
522 );
523 assert!(wrapped_str.starts_with("RPC error"));
524 }
525
526 #[test]
527 fn test_invalid_rpc_id() {
528 let invalid_id = "not-a-valid-id-123";
530 let wrapped_err = Error::InvalidRpcId(invalid_id.to_string());
533 let wrapped_str = wrapped_err.to_string();
535 assert!(
537 wrapped_str.contains(invalid_id),
538 "Wrapped error '{}' should contain invalid ID '{}'",
539 wrapped_str,
540 invalid_id
541 );
542 assert!(wrapped_str.starts_with("Invalid RPC ID: "));
543 }
544
545 #[test]
546 fn test_invalid_file_type() {
547 let file_type = "unknown_format";
549 let wrapped_err = Error::InvalidFileType(file_type.to_string());
552 let wrapped_str = wrapped_err.to_string();
554 assert!(
556 wrapped_str.contains(file_type),
557 "Wrapped error '{}' should contain file type '{}'",
558 wrapped_str,
559 file_type
560 );
561 assert!(wrapped_str.starts_with("Invalid file type: "));
562 assert!(
567 !wrapped_str.contains(" "),
568 "message must not contain doubled spaces from continuation indent: {wrapped_str:?}"
569 );
570 assert!(wrapped_str.contains(
571 "Valid types: image, lidar.pcd, lidar.png, lidar.jpg, radar.pcd, radar.png, all"
572 ));
573 for alias in [
576 "lidar.depth",
577 "depth.png",
578 "depthmap",
579 "lidar.jpeg",
580 "lidar.reflect",
581 "pcd",
582 "cube",
583 ] {
584 assert!(
585 wrapped_str.contains(alias),
586 "message should mention accepted alias '{alias}': {wrapped_str:?}"
587 );
588 }
589 }
590
591 #[test]
592 fn test_invalid_annotation_type() {
593 let annotation_type = "unsupported_annotation";
595 let wrapped_err = Error::InvalidAnnotationType(annotation_type.to_string());
598 let wrapped_str = wrapped_err.to_string();
600 assert!(
602 wrapped_str.contains(annotation_type),
603 "Wrapped error '{}' should contain annotation type '{}'",
604 wrapped_str,
605 annotation_type
606 );
607 assert!(wrapped_str.starts_with("Invalid annotation type: "));
608 }
609
610 #[test]
611 fn test_unsupported_format() {
612 let format = "xyz_format";
614 let wrapped_err = Error::UnsupportedFormat(format.to_string());
617 let wrapped_str = wrapped_err.to_string();
619 assert!(
621 wrapped_str.contains(format),
622 "Wrapped error '{}' should contain format '{}'",
623 wrapped_str,
624 format
625 );
626 assert!(wrapped_str.starts_with("Unsupported format: "));
627 }
628
629 #[test]
630 fn test_missing_images() {
631 let details = "image001.jpg, image002.jpg";
633 let wrapped_err = Error::MissingImages(details.to_string());
636 let wrapped_str = wrapped_err.to_string();
638 assert!(
640 wrapped_str.contains(details),
641 "Wrapped error '{}' should contain details '{}'",
642 wrapped_str,
643 details
644 );
645 assert!(wrapped_str.starts_with("Missing images: "));
646 }
647
648 #[test]
649 fn test_missing_annotations() {
650 let details = "annotations.json";
652 let wrapped_err = Error::MissingAnnotations(details.to_string());
655 let wrapped_str = wrapped_err.to_string();
657 assert!(
659 wrapped_str.contains(details),
660 "Wrapped error '{}' should contain details '{}'",
661 wrapped_str,
662 details
663 );
664 assert!(wrapped_str.starts_with("Missing annotations: "));
665 }
666
667 #[test]
668 fn test_missing_label() {
669 let label = "person";
671 let wrapped_err = Error::MissingLabel(label.to_string());
674 let wrapped_str = wrapped_err.to_string();
676 assert!(
678 wrapped_str.contains(label),
679 "Wrapped error '{}' should contain label '{}'",
680 wrapped_str,
681 label
682 );
683 assert!(wrapped_str.starts_with("Missing label: "));
684 }
685
686 #[test]
687 fn test_invalid_parameters() {
688 let params = "batch_size must be positive";
690 let wrapped_err = Error::InvalidParameters(params.to_string());
693 let wrapped_str = wrapped_err.to_string();
695 assert!(
697 wrapped_str.contains(params),
698 "Wrapped error '{}' should contain params '{}'",
699 wrapped_str,
700 params
701 );
702 assert!(wrapped_str.starts_with("Invalid parameters: "));
703 }
704
705 #[test]
706 fn test_feature_not_enabled() {
707 let feature = "polars";
709 let wrapped_err = Error::FeatureNotEnabled(feature.to_string());
712 let wrapped_str = wrapped_err.to_string();
714 assert!(
716 wrapped_str.contains(feature),
717 "Wrapped error '{}' should contain feature '{}'",
718 wrapped_str,
719 feature
720 );
721 assert!(wrapped_str.starts_with("Feature not enabled: "));
722 }
723
724 #[test]
725 fn test_invalid_etag() {
726 let etag = "malformed-etag-value";
728 let wrapped_err = Error::InvalidEtag(etag.to_string());
731 let wrapped_str = wrapped_err.to_string();
733 assert!(
735 wrapped_str.contains(etag),
736 "Wrapped error '{}' should contain etag '{}'",
737 wrapped_str,
738 etag
739 );
740 assert!(wrapped_str.starts_with("Invalid ETag header: "));
741 }
742
743 #[test]
747 fn test_invalid_response() {
748 let err = Error::InvalidResponse;
749 let err_str = err.to_string();
750 assert_eq!(err_str, "Invalid server response");
751 }
752
753 #[test]
754 fn test_not_implemented() {
755 let err = Error::NotImplemented;
756 let err_str = err.to_string();
757 assert_eq!(err_str, "Not implemented");
758 }
759
760 #[test]
761 fn test_part_too_large() {
762 let err = Error::PartTooLarge;
763 let err_str = err.to_string();
764 assert_eq!(err_str, "File part size exceeds maximum limit");
765 }
766
767 #[test]
768 fn test_empty_token() {
769 let err = Error::EmptyToken;
770 let err_str = err.to_string();
771 assert_eq!(err_str, "Authentication token is empty");
772 }
773
774 #[test]
775 fn test_invalid_token() {
776 let err = Error::InvalidToken;
777 let err_str = err.to_string();
778 assert_eq!(err_str, "Invalid authentication token");
779 }
780
781 #[test]
782 fn test_token_expired() {
783 let err = Error::TokenExpired;
784 let err_str = err.to_string();
785 assert_eq!(err_str, "Authentication token has expired");
786 }
787
788 #[test]
789 fn test_unauthorized() {
790 let err = Error::Unauthorized;
791 let err_str = err.to_string();
792 assert_eq!(err_str, "Unauthorized access");
793 }
794
795 #[test]
800 fn test_task_not_found_display_contains_id() {
801 let task_id = crate::api::TaskID::from(0x1092u64);
803 let err = Error::TaskNotFound(task_id);
804 let err_str = err.to_string();
805 assert!(
806 err_str.contains("task-1092"),
807 "Display should include the task ID prefix+hex, got: {err_str}"
808 );
809 assert!(err_str.starts_with("task not found"));
810 }
811
812 #[test]
813 fn test_permission_denied_display_contains_method() {
814 let err = Error::PermissionDenied("task.chart.add".to_string());
815 let err_str = err.to_string();
816 assert!(
817 err_str.contains("task.chart.add"),
818 "Display should include the method name, got: {err_str}"
819 );
820 assert!(err_str.starts_with("permission denied"));
821 }
822
823 #[test]
824 fn test_payload_too_large_display_contains_method() {
825 let err = Error::PayloadTooLarge {
826 method: "val.data.upload".to_string(),
827 size_hint: Some(123456),
828 };
829 let err_str = err.to_string();
830 assert!(
831 err_str.contains("val.data.upload"),
832 "Display should include the method name, got: {err_str}"
833 );
834 assert!(err_str.starts_with("payload too large"));
835 }
836
837 #[test]
838 fn test_payload_too_large_with_no_size_hint() {
839 let err = Error::PayloadTooLarge {
841 method: "task.data.upload".to_string(),
842 size_hint: None,
843 };
844 let err_str = err.to_string();
845 assert!(err_str.contains("task.data.upload"));
846 }
847
848 #[test]
849 fn test_typed_variants_have_no_source() {
850 use std::error::Error as _;
854
855 let task_not_found = Error::TaskNotFound(crate::api::TaskID::from(1u64));
856 assert!(task_not_found.source().is_none());
857
858 let perm_denied = Error::PermissionDenied("op".into());
859 assert!(perm_denied.source().is_none());
860
861 let too_large = Error::PayloadTooLarge {
862 method: "m".into(),
863 size_hint: Some(1),
864 };
865 assert!(too_large.source().is_none());
866 }
867
868 #[test]
873 fn test_coco_error_display() {
874 let err = Error::CocoError("missing categories array".into());
875 let err_str = err.to_string();
876 assert!(err_str.contains("missing categories array"));
877 assert!(err_str.starts_with("COCO format error:"));
878 }
879
880 #[test]
881 fn test_zip_error_display() {
882 let err = Error::ZipError("invalid central directory".into());
883 let err_str = err.to_string();
884 assert!(err_str.contains("invalid central directory"));
885 assert!(err_str.starts_with("ZIP error:"));
886 }
887
888 #[test]
889 fn test_storage_error_display() {
890 let err = Error::StorageError("keychain locked".into());
891 let err_str = err.to_string();
892 assert!(err_str.contains("keychain locked"));
893 assert!(err_str.starts_with("Token storage error:"));
894 }
895}