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 UnexpectedResponse(String),
52 NotImplemented,
54 PartTooLarge,
56 InvalidFileType(String),
58 InvalidAnnotationType(String),
60 UnsupportedFormat(String),
62 MissingImages(String),
64 MissingResource(String),
69 MissingAnnotations(String),
71 MissingLabel(String),
73 InvalidParameters(String),
75 FeatureNotEnabled(String),
77 EmptyToken,
79 InvalidToken,
81 TokenExpired,
83 Unauthorized,
85 InvalidEtag(String),
87 StorageError(String),
89 #[cfg(feature = "polars")]
91 PolarsError(polars::error::PolarsError),
92 CocoError(String),
94 ZipError(String),
96 TaskNotFound(crate::api::TaskID),
98 PermissionDenied(String),
101 PayloadTooLarge {
105 method: String,
106 size_hint: Option<u64>,
107 },
108 InsecureUrl(String),
114}
115
116impl From<std::io::Error> for Error {
117 fn from(err: std::io::Error) -> Self {
118 Error::IoError(err)
119 }
120}
121
122impl From<config::ConfigError> for Error {
123 fn from(err: config::ConfigError) -> Self {
124 Error::ConfigError(err)
125 }
126}
127
128impl From<serde_json::Error> for Error {
129 fn from(err: serde_json::Error) -> Self {
130 Error::JsonError(err)
131 }
132}
133
134impl From<reqwest::Error> for Error {
135 fn from(err: reqwest::Error) -> Self {
136 Error::HttpError(err)
137 }
138}
139
140impl From<url::ParseError> for Error {
141 fn from(err: url::ParseError) -> Self {
142 Error::UrlParseError(err)
143 }
144}
145
146impl From<std::env::VarError> for Error {
147 fn from(err: std::env::VarError) -> Self {
148 Error::EnvError(err)
149 }
150}
151
152impl From<AcquireError> for Error {
153 fn from(err: AcquireError) -> Self {
154 Error::SemaphoreError(err)
155 }
156}
157
158impl From<tokio::task::JoinError> for Error {
159 fn from(err: tokio::task::JoinError) -> Self {
160 Error::JoinError(err)
161 }
162}
163
164impl From<watch::error::SendError<Progress>> for Error {
165 fn from(err: watch::error::SendError<Progress>) -> Self {
166 Error::ProgressSendError(err)
167 }
168}
169
170impl From<watch::error::RecvError> for Error {
171 fn from(err: watch::error::RecvError) -> Self {
172 Error::ProgressRecvError(err)
173 }
174}
175
176impl From<std::path::StripPrefixError> for Error {
177 fn from(err: std::path::StripPrefixError) -> Self {
178 Error::StripPrefixError(err)
179 }
180}
181
182impl From<std::num::ParseIntError> for Error {
183 fn from(err: std::num::ParseIntError) -> Self {
184 Error::ParseIntError(err)
185 }
186}
187
188impl From<crate::storage::StorageError> for Error {
189 fn from(err: crate::storage::StorageError) -> Self {
190 Error::StorageError(err.to_string())
191 }
192}
193
194#[cfg(feature = "polars")]
195impl From<polars::error::PolarsError> for Error {
196 fn from(err: polars::error::PolarsError) -> Self {
197 Error::PolarsError(err)
198 }
199}
200
201impl From<zip::result::ZipError> for Error {
202 fn from(err: zip::result::ZipError) -> Self {
203 Error::ZipError(err.to_string())
204 }
205}
206
207impl std::fmt::Display for Error {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 match self {
210 Error::IoError(e) => write!(f, "I/O error: {}", e),
211 Error::ConfigError(e) => write!(f, "Configuration error: {}", e),
212 Error::JsonError(e) => write!(f, "JSON error: {}", e),
213 Error::HttpError(e) => write!(f, "HTTP error: {}", e),
214 Error::MaxRetriesExceeded(n) => write!(f, "Maximum retries ({}) exceeded", n),
215 Error::UrlParseError(e) => write!(f, "URL parse error: {}", e),
216 Error::RpcError(code, msg) => write!(f, "RPC error {}: {}", code, msg),
217 Error::InvalidRpcId(id) => write!(f, "Invalid RPC ID: {}", id),
218 Error::EnvError(e) => write!(f, "Environment variable error: {}", e),
219 Error::SemaphoreError(e) => write!(f, "Semaphore error: {}", e),
220 Error::JoinError(e) => write!(f, "Task join error: {}", e),
221 Error::ProgressSendError(e) => write!(f, "Progress send error: {}", e),
222 Error::ProgressRecvError(e) => write!(f, "Progress receive error: {}", e),
223 Error::StripPrefixError(e) => write!(f, "Path prefix error: {}", e),
224 Error::ParseIntError(e) => write!(f, "Integer parse error: {}", e),
225 Error::InvalidResponse => write!(f, "Invalid server response"),
226 Error::UnexpectedResponse(msg) => write!(f, "Unexpected server response: {}", msg),
227 Error::NotImplemented => write!(f, "Not implemented"),
228 Error::PartTooLarge => write!(f, "File part size exceeds maximum limit"),
229 Error::InvalidFileType(s) => write!(
232 f,
233 "Invalid file type: {}. Valid types: image, lidar.pcd, lidar.png, \
234 lidar.jpg, radar.pcd, radar.png, all (aliases also accepted: \
235 lidar.depth, depth.png, depthmap, lidar.jpeg, lidar.reflect, pcd, cube)",
236 s
237 ),
238 Error::InvalidAnnotationType(s) => write!(f, "Invalid annotation type: {}", s),
239 Error::UnsupportedFormat(s) => write!(f, "Unsupported format: {}", s),
240 Error::MissingImages(s) => write!(f, "Missing images: {}", s),
241 Error::MissingResource(s) => write!(f, "Missing resource: {}", s),
242 Error::MissingAnnotations(s) => write!(f, "Missing annotations: {}", s),
243 Error::MissingLabel(s) => write!(f, "Missing label: {}", s),
244 Error::InvalidParameters(s) => write!(f, "Invalid parameters: {}", s),
245 Error::FeatureNotEnabled(s) => write!(f, "Feature not enabled: {}", s),
246 Error::EmptyToken => write!(f, "Authentication token is empty"),
247 Error::InvalidToken => write!(f, "Invalid authentication token"),
248 Error::TokenExpired => write!(f, "Authentication token has expired"),
249 Error::Unauthorized => write!(f, "Unauthorized access"),
250 Error::InvalidEtag(s) => write!(f, "Invalid ETag header: {}", s),
251 Error::StorageError(s) => write!(f, "Token storage error: {}", s),
252 #[cfg(feature = "polars")]
253 Error::PolarsError(e) => write!(f, "Polars error: {}", e),
254 Error::CocoError(s) => write!(f, "COCO format error: {}", s),
255 Error::ZipError(s) => write!(f, "ZIP error: {}", s),
256 Error::TaskNotFound(id) => write!(f, "task not found: {}", id),
257 Error::PermissionDenied(op) => write!(f, "permission denied: {}", op),
258 Error::PayloadTooLarge { method, .. } => write!(f, "payload too large for {}", method),
259 Error::InsecureUrl(url) => write!(
260 f,
261 "refusing insecure URL '{}': Studio bearer tokens require HTTPS \
262 (loopback http is allowed for tests/dev)",
263 url
264 ),
265 }
266 }
267}
268
269impl std::error::Error for Error {
270 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
271 match self {
272 Error::IoError(e) => Some(e),
273 Error::ConfigError(e) => Some(e),
274 Error::JsonError(e) => Some(e),
275 Error::HttpError(e) => Some(e),
276 Error::UrlParseError(e) => Some(e),
277 Error::EnvError(e) => Some(e),
278 Error::JoinError(e) => Some(e),
279 Error::StripPrefixError(e) => Some(e),
280 Error::ParseIntError(e) => Some(e),
281 #[cfg(feature = "polars")]
282 Error::PolarsError(e) => Some(e),
283 _ => None,
284 }
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use std::path::Path;
292
293 #[test]
301 fn test_io_error_wrapping() {
302 let inner_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
304 let inner_str = inner_err.to_string();
306 let wrapped_err: Error = inner_err.into();
308 let wrapped_str = wrapped_err.to_string();
310 assert!(
312 wrapped_str.contains(&inner_str),
313 "Wrapped error '{}' should contain inner error '{}'",
314 wrapped_str,
315 inner_str
316 );
317 assert!(wrapped_str.starts_with("I/O error: "));
318 }
319
320 #[test]
321 fn test_config_error_wrapping() {
322 #[derive(Debug, serde::Deserialize)]
325 #[allow(dead_code)]
326 struct RequiredField {
327 required: String,
328 }
329
330 let inner_err = config::Config::builder()
331 .build()
332 .unwrap()
333 .try_deserialize::<RequiredField>()
334 .unwrap_err();
335 let inner_str = inner_err.to_string();
337 let wrapped_err: Error = inner_err.into();
339 let wrapped_str = wrapped_err.to_string();
341 assert!(
343 wrapped_str.contains(&inner_str),
344 "Wrapped error '{}' should contain inner error '{}'",
345 wrapped_str,
346 inner_str
347 );
348 assert!(wrapped_str.starts_with("Configuration error: "));
349 }
350
351 #[test]
352 fn test_json_error_wrapping() {
353 let inner_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
355 let inner_str = inner_err.to_string();
357 let wrapped_err: Error = inner_err.into();
359 let wrapped_str = wrapped_err.to_string();
361 assert!(
363 wrapped_str.contains(&inner_str),
364 "Wrapped error '{}' should contain inner error '{}'",
365 wrapped_str,
366 inner_str
367 );
368 assert!(wrapped_str.starts_with("JSON error: "));
369 }
370
371 #[test]
372 fn test_url_parse_error_wrapping() {
373 let inner_err = url::Url::parse("not a valid url").unwrap_err();
375 let inner_str = inner_err.to_string();
377 let wrapped_err: Error = inner_err.into();
379 let wrapped_str = wrapped_err.to_string();
381 assert!(
383 wrapped_str.contains(&inner_str),
384 "Wrapped error '{}' should contain inner error '{}'",
385 wrapped_str,
386 inner_str
387 );
388 assert!(wrapped_str.starts_with("URL parse error: "));
389 }
390
391 #[test]
392 fn test_env_error_wrapping() {
393 let inner_err = std::env::var("NONEXISTENT_VAR_12345").unwrap_err();
395 let inner_str = inner_err.to_string();
397 let wrapped_err: Error = inner_err.into();
399 let wrapped_str = wrapped_err.to_string();
401 assert!(
403 wrapped_str.contains(&inner_str),
404 "Wrapped error '{}' should contain inner error '{}'",
405 wrapped_str,
406 inner_str
407 );
408 assert!(wrapped_str.starts_with("Environment variable error: "));
409 }
410
411 #[test]
412 fn test_strip_prefix_error_wrapping() {
413 let path = Path::new("/foo/bar");
415 let prefix = Path::new("/baz");
416 let inner_err = path.strip_prefix(prefix).unwrap_err();
417 let inner_str = inner_err.to_string();
419 let wrapped_err: Error = inner_err.into();
421 let wrapped_str = wrapped_err.to_string();
423 assert!(
425 wrapped_str.contains(&inner_str),
426 "Wrapped error '{}' should contain inner error '{}'",
427 wrapped_str,
428 inner_str
429 );
430 assert!(wrapped_str.starts_with("Path prefix error: "));
431 }
432
433 #[test]
434 fn test_parse_int_error_wrapping() {
435 let inner_err = "not a number".parse::<i32>().unwrap_err();
437 let inner_str = inner_err.to_string();
439 let wrapped_err: Error = inner_err.into();
441 let wrapped_str = wrapped_err.to_string();
443 assert!(
445 wrapped_str.contains(&inner_str),
446 "Wrapped error '{}' should contain inner error '{}'",
447 wrapped_str,
448 inner_str
449 );
450 assert!(wrapped_str.starts_with("Integer parse error: "));
451 }
452
453 #[cfg(feature = "polars")]
454 #[test]
455 fn test_polars_error_wrapping() {
456 use polars::prelude::*;
458 let inner_err = DataFrame::new_infer_height(vec![
459 Series::new("a".into(), &[1, 2, 3]).into(),
460 Series::new("a".into(), &[4, 5, 6]).into(),
461 ])
462 .unwrap_err();
463 let inner_str = inner_err.to_string();
465 let wrapped_err: Error = inner_err.into();
467 let wrapped_str = wrapped_err.to_string();
469 assert!(
471 wrapped_str.contains(&inner_str),
472 "Wrapped error '{}' should contain inner error '{}'",
473 wrapped_str,
474 inner_str
475 );
476 assert!(wrapped_str.starts_with("Polars error: "));
477 }
478
479 #[test]
487 fn test_max_retries_exceeded() {
488 let retry_count = 42u32;
490 let primitive_str = retry_count.to_string();
492 let wrapped_err = Error::MaxRetriesExceeded(retry_count);
494 let wrapped_str = wrapped_err.to_string();
496 assert!(
498 wrapped_str.contains(&primitive_str),
499 "Wrapped error '{}' should contain retry count '{}'",
500 wrapped_str,
501 primitive_str
502 );
503 assert!(wrapped_str.starts_with("Maximum retries"));
504 }
505
506 #[test]
507 fn test_rpc_error() {
508 let error_code = -32600;
510 let error_msg = "Invalid Request";
511 let code_str = error_code.to_string();
513 let wrapped_err = Error::RpcError(error_code, error_msg.to_string());
515 let wrapped_str = wrapped_err.to_string();
517 assert!(
519 wrapped_str.contains(&code_str),
520 "Wrapped error '{}' should contain error code '{}'",
521 wrapped_str,
522 code_str
523 );
524 assert!(
525 wrapped_str.contains(error_msg),
526 "Wrapped error '{}' should contain error message '{}'",
527 wrapped_str,
528 error_msg
529 );
530 assert!(wrapped_str.starts_with("RPC error"));
531 }
532
533 #[test]
534 fn test_invalid_rpc_id() {
535 let invalid_id = "not-a-valid-id-123";
537 let wrapped_err = Error::InvalidRpcId(invalid_id.to_string());
540 let wrapped_str = wrapped_err.to_string();
542 assert!(
544 wrapped_str.contains(invalid_id),
545 "Wrapped error '{}' should contain invalid ID '{}'",
546 wrapped_str,
547 invalid_id
548 );
549 assert!(wrapped_str.starts_with("Invalid RPC ID: "));
550 }
551
552 #[test]
553 fn test_invalid_file_type() {
554 let file_type = "unknown_format";
556 let wrapped_err = Error::InvalidFileType(file_type.to_string());
559 let wrapped_str = wrapped_err.to_string();
561 assert!(
563 wrapped_str.contains(file_type),
564 "Wrapped error '{}' should contain file type '{}'",
565 wrapped_str,
566 file_type
567 );
568 assert!(wrapped_str.starts_with("Invalid file type: "));
569 assert!(
574 !wrapped_str.contains(" "),
575 "message must not contain doubled spaces from continuation indent: {wrapped_str:?}"
576 );
577 assert!(wrapped_str.contains(
578 "Valid types: image, lidar.pcd, lidar.png, lidar.jpg, radar.pcd, radar.png, all"
579 ));
580 for alias in [
583 "lidar.depth",
584 "depth.png",
585 "depthmap",
586 "lidar.jpeg",
587 "lidar.reflect",
588 "pcd",
589 "cube",
590 ] {
591 assert!(
592 wrapped_str.contains(alias),
593 "message should mention accepted alias '{alias}': {wrapped_str:?}"
594 );
595 }
596 }
597
598 #[test]
599 fn test_invalid_annotation_type() {
600 let annotation_type = "unsupported_annotation";
602 let wrapped_err = Error::InvalidAnnotationType(annotation_type.to_string());
605 let wrapped_str = wrapped_err.to_string();
607 assert!(
609 wrapped_str.contains(annotation_type),
610 "Wrapped error '{}' should contain annotation type '{}'",
611 wrapped_str,
612 annotation_type
613 );
614 assert!(wrapped_str.starts_with("Invalid annotation type: "));
615 }
616
617 #[test]
618 fn test_unsupported_format() {
619 let format = "xyz_format";
621 let wrapped_err = Error::UnsupportedFormat(format.to_string());
624 let wrapped_str = wrapped_err.to_string();
626 assert!(
628 wrapped_str.contains(format),
629 "Wrapped error '{}' should contain format '{}'",
630 wrapped_str,
631 format
632 );
633 assert!(wrapped_str.starts_with("Unsupported format: "));
634 }
635
636 #[test]
637 fn test_missing_images() {
638 let details = "image001.jpg, image002.jpg";
640 let wrapped_err = Error::MissingImages(details.to_string());
643 let wrapped_str = wrapped_err.to_string();
645 assert!(
647 wrapped_str.contains(details),
648 "Wrapped error '{}' should contain details '{}'",
649 wrapped_str,
650 details
651 );
652 assert!(wrapped_str.starts_with("Missing images: "));
653 }
654
655 #[test]
656 fn test_missing_annotations() {
657 let details = "annotations.json";
659 let wrapped_err = Error::MissingAnnotations(details.to_string());
662 let wrapped_str = wrapped_err.to_string();
664 assert!(
666 wrapped_str.contains(details),
667 "Wrapped error '{}' should contain details '{}'",
668 wrapped_str,
669 details
670 );
671 assert!(wrapped_str.starts_with("Missing annotations: "));
672 }
673
674 #[test]
675 fn test_missing_label() {
676 let label = "person";
678 let wrapped_err = Error::MissingLabel(label.to_string());
681 let wrapped_str = wrapped_err.to_string();
683 assert!(
685 wrapped_str.contains(label),
686 "Wrapped error '{}' should contain label '{}'",
687 wrapped_str,
688 label
689 );
690 assert!(wrapped_str.starts_with("Missing label: "));
691 }
692
693 #[test]
694 fn test_invalid_parameters() {
695 let params = "batch_size must be positive";
697 let wrapped_err = Error::InvalidParameters(params.to_string());
700 let wrapped_str = wrapped_err.to_string();
702 assert!(
704 wrapped_str.contains(params),
705 "Wrapped error '{}' should contain params '{}'",
706 wrapped_str,
707 params
708 );
709 assert!(wrapped_str.starts_with("Invalid parameters: "));
710 }
711
712 #[test]
713 fn test_feature_not_enabled() {
714 let feature = "polars";
716 let wrapped_err = Error::FeatureNotEnabled(feature.to_string());
719 let wrapped_str = wrapped_err.to_string();
721 assert!(
723 wrapped_str.contains(feature),
724 "Wrapped error '{}' should contain feature '{}'",
725 wrapped_str,
726 feature
727 );
728 assert!(wrapped_str.starts_with("Feature not enabled: "));
729 }
730
731 #[test]
732 fn test_invalid_etag() {
733 let etag = "malformed-etag-value";
735 let wrapped_err = Error::InvalidEtag(etag.to_string());
738 let wrapped_str = wrapped_err.to_string();
740 assert!(
742 wrapped_str.contains(etag),
743 "Wrapped error '{}' should contain etag '{}'",
744 wrapped_str,
745 etag
746 );
747 assert!(wrapped_str.starts_with("Invalid ETag header: "));
748 }
749
750 #[test]
754 fn test_invalid_response() {
755 let err = Error::InvalidResponse;
756 let err_str = err.to_string();
757 assert_eq!(err_str, "Invalid server response");
758 }
759
760 #[test]
761 fn test_not_implemented() {
762 let err = Error::NotImplemented;
763 let err_str = err.to_string();
764 assert_eq!(err_str, "Not implemented");
765 }
766
767 #[test]
768 fn test_part_too_large() {
769 let err = Error::PartTooLarge;
770 let err_str = err.to_string();
771 assert_eq!(err_str, "File part size exceeds maximum limit");
772 }
773
774 #[test]
775 fn test_empty_token() {
776 let err = Error::EmptyToken;
777 let err_str = err.to_string();
778 assert_eq!(err_str, "Authentication token is empty");
779 }
780
781 #[test]
782 fn test_invalid_token() {
783 let err = Error::InvalidToken;
784 let err_str = err.to_string();
785 assert_eq!(err_str, "Invalid authentication token");
786 }
787
788 #[test]
789 fn test_token_expired() {
790 let err = Error::TokenExpired;
791 let err_str = err.to_string();
792 assert_eq!(err_str, "Authentication token has expired");
793 }
794
795 #[test]
796 fn test_unauthorized() {
797 let err = Error::Unauthorized;
798 let err_str = err.to_string();
799 assert_eq!(err_str, "Unauthorized access");
800 }
801
802 #[test]
807 fn test_task_not_found_display_contains_id() {
808 let task_id = crate::api::TaskID::from(0x1092u64);
810 let err = Error::TaskNotFound(task_id);
811 let err_str = err.to_string();
812 assert!(
813 err_str.contains("task-1092"),
814 "Display should include the task ID prefix+hex, got: {err_str}"
815 );
816 assert!(err_str.starts_with("task not found"));
817 }
818
819 #[test]
820 fn test_permission_denied_display_contains_method() {
821 let err = Error::PermissionDenied("task.chart.add".to_string());
822 let err_str = err.to_string();
823 assert!(
824 err_str.contains("task.chart.add"),
825 "Display should include the method name, got: {err_str}"
826 );
827 assert!(err_str.starts_with("permission denied"));
828 }
829
830 #[test]
831 fn test_payload_too_large_display_contains_method() {
832 let err = Error::PayloadTooLarge {
833 method: "val.data.upload".to_string(),
834 size_hint: Some(123456),
835 };
836 let err_str = err.to_string();
837 assert!(
838 err_str.contains("val.data.upload"),
839 "Display should include the method name, got: {err_str}"
840 );
841 assert!(err_str.starts_with("payload too large"));
842 }
843
844 #[test]
845 fn test_payload_too_large_with_no_size_hint() {
846 let err = Error::PayloadTooLarge {
848 method: "task.data.upload".to_string(),
849 size_hint: None,
850 };
851 let err_str = err.to_string();
852 assert!(err_str.contains("task.data.upload"));
853 }
854
855 #[test]
856 fn test_typed_variants_have_no_source() {
857 use std::error::Error as _;
861
862 let task_not_found = Error::TaskNotFound(crate::api::TaskID::from(1u64));
863 assert!(task_not_found.source().is_none());
864
865 let perm_denied = Error::PermissionDenied("op".into());
866 assert!(perm_denied.source().is_none());
867
868 let too_large = Error::PayloadTooLarge {
869 method: "m".into(),
870 size_hint: Some(1),
871 };
872 assert!(too_large.source().is_none());
873 }
874
875 #[test]
880 fn test_coco_error_display() {
881 let err = Error::CocoError("missing categories array".into());
882 let err_str = err.to_string();
883 assert!(err_str.contains("missing categories array"));
884 assert!(err_str.starts_with("COCO format error:"));
885 }
886
887 #[test]
888 fn test_zip_error_display() {
889 let err = Error::ZipError("invalid central directory".into());
890 let err_str = err.to_string();
891 assert!(err_str.contains("invalid central directory"));
892 assert!(err_str.starts_with("ZIP error:"));
893 }
894
895 #[test]
896 fn test_storage_error_display() {
897 let err = Error::StorageError("keychain locked".into());
898 let err_str = err.to_string();
899 assert!(err_str.contains("keychain locked"));
900 assert!(err_str.starts_with("Token storage error:"));
901 }
902}