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!(
220 f,
221 "Invalid file type: {}. Valid types: image, lidar.pcd, lidar.png, \
222 lidar.jpg, radar.pcd, radar.png, all (aliases also accepted: \
223 lidar.depth, depth.png, depthmap, lidar.jpeg, lidar.reflect, pcd, cube)",
224 s
225 ),
226 Error::InvalidAnnotationType(s) => write!(f, "Invalid annotation type: {}", s),
227 Error::UnsupportedFormat(s) => write!(f, "Unsupported format: {}", s),
228 Error::MissingImages(s) => write!(f, "Missing images: {}", s),
229 Error::MissingAnnotations(s) => write!(f, "Missing annotations: {}", s),
230 Error::MissingLabel(s) => write!(f, "Missing label: {}", s),
231 Error::InvalidParameters(s) => write!(f, "Invalid parameters: {}", s),
232 Error::FeatureNotEnabled(s) => write!(f, "Feature not enabled: {}", s),
233 Error::EmptyToken => write!(f, "Authentication token is empty"),
234 Error::InvalidToken => write!(f, "Invalid authentication token"),
235 Error::TokenExpired => write!(f, "Authentication token has expired"),
236 Error::Unauthorized => write!(f, "Unauthorized access"),
237 Error::InvalidEtag(s) => write!(f, "Invalid ETag header: {}", s),
238 Error::StorageError(s) => write!(f, "Token storage error: {}", s),
239 #[cfg(feature = "polars")]
240 Error::PolarsError(e) => write!(f, "Polars error: {}", e),
241 Error::CocoError(s) => write!(f, "COCO format error: {}", s),
242 Error::ZipError(s) => write!(f, "ZIP error: {}", s),
243 Error::TaskNotFound(id) => write!(f, "task not found: {}", id),
244 Error::PermissionDenied(op) => write!(f, "permission denied: {}", op),
245 Error::PayloadTooLarge { method, .. } => write!(f, "payload too large for {}", method),
246 Error::InsecureUrl(url) => write!(
247 f,
248 "refusing insecure URL '{}': Studio bearer tokens require HTTPS \
249 (loopback http is allowed for tests/dev)",
250 url
251 ),
252 }
253 }
254}
255
256impl std::error::Error for Error {
257 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
258 match self {
259 Error::IoError(e) => Some(e),
260 Error::ConfigError(e) => Some(e),
261 Error::JsonError(e) => Some(e),
262 Error::HttpError(e) => Some(e),
263 Error::UrlParseError(e) => Some(e),
264 Error::EnvError(e) => Some(e),
265 Error::JoinError(e) => Some(e),
266 Error::StripPrefixError(e) => Some(e),
267 Error::ParseIntError(e) => Some(e),
268 #[cfg(feature = "polars")]
269 Error::PolarsError(e) => Some(e),
270 _ => None,
271 }
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use std::path::Path;
279
280 #[test]
288 fn test_io_error_wrapping() {
289 let inner_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
291 let inner_str = inner_err.to_string();
293 let wrapped_err: Error = inner_err.into();
295 let wrapped_str = wrapped_err.to_string();
297 assert!(
299 wrapped_str.contains(&inner_str),
300 "Wrapped error '{}' should contain inner error '{}'",
301 wrapped_str,
302 inner_str
303 );
304 assert!(wrapped_str.starts_with("I/O error: "));
305 }
306
307 #[test]
308 fn test_config_error_wrapping() {
309 #[derive(Debug, serde::Deserialize)]
312 #[allow(dead_code)]
313 struct RequiredField {
314 required: String,
315 }
316
317 let inner_err = config::Config::builder()
318 .build()
319 .unwrap()
320 .try_deserialize::<RequiredField>()
321 .unwrap_err();
322 let inner_str = inner_err.to_string();
324 let wrapped_err: Error = inner_err.into();
326 let wrapped_str = wrapped_err.to_string();
328 assert!(
330 wrapped_str.contains(&inner_str),
331 "Wrapped error '{}' should contain inner error '{}'",
332 wrapped_str,
333 inner_str
334 );
335 assert!(wrapped_str.starts_with("Configuration error: "));
336 }
337
338 #[test]
339 fn test_json_error_wrapping() {
340 let inner_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
342 let inner_str = inner_err.to_string();
344 let wrapped_err: Error = inner_err.into();
346 let wrapped_str = wrapped_err.to_string();
348 assert!(
350 wrapped_str.contains(&inner_str),
351 "Wrapped error '{}' should contain inner error '{}'",
352 wrapped_str,
353 inner_str
354 );
355 assert!(wrapped_str.starts_with("JSON error: "));
356 }
357
358 #[test]
359 fn test_url_parse_error_wrapping() {
360 let inner_err = url::Url::parse("not a valid url").unwrap_err();
362 let inner_str = inner_err.to_string();
364 let wrapped_err: Error = inner_err.into();
366 let wrapped_str = wrapped_err.to_string();
368 assert!(
370 wrapped_str.contains(&inner_str),
371 "Wrapped error '{}' should contain inner error '{}'",
372 wrapped_str,
373 inner_str
374 );
375 assert!(wrapped_str.starts_with("URL parse error: "));
376 }
377
378 #[test]
379 fn test_env_error_wrapping() {
380 let inner_err = std::env::var("NONEXISTENT_VAR_12345").unwrap_err();
382 let inner_str = inner_err.to_string();
384 let wrapped_err: Error = inner_err.into();
386 let wrapped_str = wrapped_err.to_string();
388 assert!(
390 wrapped_str.contains(&inner_str),
391 "Wrapped error '{}' should contain inner error '{}'",
392 wrapped_str,
393 inner_str
394 );
395 assert!(wrapped_str.starts_with("Environment variable error: "));
396 }
397
398 #[test]
399 fn test_strip_prefix_error_wrapping() {
400 let path = Path::new("/foo/bar");
402 let prefix = Path::new("/baz");
403 let inner_err = path.strip_prefix(prefix).unwrap_err();
404 let inner_str = inner_err.to_string();
406 let wrapped_err: Error = inner_err.into();
408 let wrapped_str = wrapped_err.to_string();
410 assert!(
412 wrapped_str.contains(&inner_str),
413 "Wrapped error '{}' should contain inner error '{}'",
414 wrapped_str,
415 inner_str
416 );
417 assert!(wrapped_str.starts_with("Path prefix error: "));
418 }
419
420 #[test]
421 fn test_parse_int_error_wrapping() {
422 let inner_err = "not a number".parse::<i32>().unwrap_err();
424 let inner_str = inner_err.to_string();
426 let wrapped_err: Error = inner_err.into();
428 let wrapped_str = wrapped_err.to_string();
430 assert!(
432 wrapped_str.contains(&inner_str),
433 "Wrapped error '{}' should contain inner error '{}'",
434 wrapped_str,
435 inner_str
436 );
437 assert!(wrapped_str.starts_with("Integer parse error: "));
438 }
439
440 #[cfg(feature = "polars")]
441 #[test]
442 fn test_polars_error_wrapping() {
443 use polars::prelude::*;
445 let inner_err = DataFrame::new_infer_height(vec![
446 Series::new("a".into(), &[1, 2, 3]).into(),
447 Series::new("a".into(), &[4, 5, 6]).into(),
448 ])
449 .unwrap_err();
450 let inner_str = inner_err.to_string();
452 let wrapped_err: Error = inner_err.into();
454 let wrapped_str = wrapped_err.to_string();
456 assert!(
458 wrapped_str.contains(&inner_str),
459 "Wrapped error '{}' should contain inner error '{}'",
460 wrapped_str,
461 inner_str
462 );
463 assert!(wrapped_str.starts_with("Polars error: "));
464 }
465
466 #[test]
474 fn test_max_retries_exceeded() {
475 let retry_count = 42u32;
477 let primitive_str = retry_count.to_string();
479 let wrapped_err = Error::MaxRetriesExceeded(retry_count);
481 let wrapped_str = wrapped_err.to_string();
483 assert!(
485 wrapped_str.contains(&primitive_str),
486 "Wrapped error '{}' should contain retry count '{}'",
487 wrapped_str,
488 primitive_str
489 );
490 assert!(wrapped_str.starts_with("Maximum retries"));
491 }
492
493 #[test]
494 fn test_rpc_error() {
495 let error_code = -32600;
497 let error_msg = "Invalid Request";
498 let code_str = error_code.to_string();
500 let wrapped_err = Error::RpcError(error_code, error_msg.to_string());
502 let wrapped_str = wrapped_err.to_string();
504 assert!(
506 wrapped_str.contains(&code_str),
507 "Wrapped error '{}' should contain error code '{}'",
508 wrapped_str,
509 code_str
510 );
511 assert!(
512 wrapped_str.contains(error_msg),
513 "Wrapped error '{}' should contain error message '{}'",
514 wrapped_str,
515 error_msg
516 );
517 assert!(wrapped_str.starts_with("RPC error"));
518 }
519
520 #[test]
521 fn test_invalid_rpc_id() {
522 let invalid_id = "not-a-valid-id-123";
524 let wrapped_err = Error::InvalidRpcId(invalid_id.to_string());
527 let wrapped_str = wrapped_err.to_string();
529 assert!(
531 wrapped_str.contains(invalid_id),
532 "Wrapped error '{}' should contain invalid ID '{}'",
533 wrapped_str,
534 invalid_id
535 );
536 assert!(wrapped_str.starts_with("Invalid RPC ID: "));
537 }
538
539 #[test]
540 fn test_invalid_file_type() {
541 let file_type = "unknown_format";
543 let wrapped_err = Error::InvalidFileType(file_type.to_string());
546 let wrapped_str = wrapped_err.to_string();
548 assert!(
550 wrapped_str.contains(file_type),
551 "Wrapped error '{}' should contain file type '{}'",
552 wrapped_str,
553 file_type
554 );
555 assert!(wrapped_str.starts_with("Invalid file type: "));
556 assert!(
561 !wrapped_str.contains(" "),
562 "message must not contain doubled spaces from continuation indent: {wrapped_str:?}"
563 );
564 assert!(wrapped_str.contains(
565 "Valid types: image, lidar.pcd, lidar.png, lidar.jpg, radar.pcd, radar.png, all"
566 ));
567 for alias in [
570 "lidar.depth",
571 "depth.png",
572 "depthmap",
573 "lidar.jpeg",
574 "lidar.reflect",
575 "pcd",
576 "cube",
577 ] {
578 assert!(
579 wrapped_str.contains(alias),
580 "message should mention accepted alias '{alias}': {wrapped_str:?}"
581 );
582 }
583 }
584
585 #[test]
586 fn test_invalid_annotation_type() {
587 let annotation_type = "unsupported_annotation";
589 let wrapped_err = Error::InvalidAnnotationType(annotation_type.to_string());
592 let wrapped_str = wrapped_err.to_string();
594 assert!(
596 wrapped_str.contains(annotation_type),
597 "Wrapped error '{}' should contain annotation type '{}'",
598 wrapped_str,
599 annotation_type
600 );
601 assert!(wrapped_str.starts_with("Invalid annotation type: "));
602 }
603
604 #[test]
605 fn test_unsupported_format() {
606 let format = "xyz_format";
608 let wrapped_err = Error::UnsupportedFormat(format.to_string());
611 let wrapped_str = wrapped_err.to_string();
613 assert!(
615 wrapped_str.contains(format),
616 "Wrapped error '{}' should contain format '{}'",
617 wrapped_str,
618 format
619 );
620 assert!(wrapped_str.starts_with("Unsupported format: "));
621 }
622
623 #[test]
624 fn test_missing_images() {
625 let details = "image001.jpg, image002.jpg";
627 let wrapped_err = Error::MissingImages(details.to_string());
630 let wrapped_str = wrapped_err.to_string();
632 assert!(
634 wrapped_str.contains(details),
635 "Wrapped error '{}' should contain details '{}'",
636 wrapped_str,
637 details
638 );
639 assert!(wrapped_str.starts_with("Missing images: "));
640 }
641
642 #[test]
643 fn test_missing_annotations() {
644 let details = "annotations.json";
646 let wrapped_err = Error::MissingAnnotations(details.to_string());
649 let wrapped_str = wrapped_err.to_string();
651 assert!(
653 wrapped_str.contains(details),
654 "Wrapped error '{}' should contain details '{}'",
655 wrapped_str,
656 details
657 );
658 assert!(wrapped_str.starts_with("Missing annotations: "));
659 }
660
661 #[test]
662 fn test_missing_label() {
663 let label = "person";
665 let wrapped_err = Error::MissingLabel(label.to_string());
668 let wrapped_str = wrapped_err.to_string();
670 assert!(
672 wrapped_str.contains(label),
673 "Wrapped error '{}' should contain label '{}'",
674 wrapped_str,
675 label
676 );
677 assert!(wrapped_str.starts_with("Missing label: "));
678 }
679
680 #[test]
681 fn test_invalid_parameters() {
682 let params = "batch_size must be positive";
684 let wrapped_err = Error::InvalidParameters(params.to_string());
687 let wrapped_str = wrapped_err.to_string();
689 assert!(
691 wrapped_str.contains(params),
692 "Wrapped error '{}' should contain params '{}'",
693 wrapped_str,
694 params
695 );
696 assert!(wrapped_str.starts_with("Invalid parameters: "));
697 }
698
699 #[test]
700 fn test_feature_not_enabled() {
701 let feature = "polars";
703 let wrapped_err = Error::FeatureNotEnabled(feature.to_string());
706 let wrapped_str = wrapped_err.to_string();
708 assert!(
710 wrapped_str.contains(feature),
711 "Wrapped error '{}' should contain feature '{}'",
712 wrapped_str,
713 feature
714 );
715 assert!(wrapped_str.starts_with("Feature not enabled: "));
716 }
717
718 #[test]
719 fn test_invalid_etag() {
720 let etag = "malformed-etag-value";
722 let wrapped_err = Error::InvalidEtag(etag.to_string());
725 let wrapped_str = wrapped_err.to_string();
727 assert!(
729 wrapped_str.contains(etag),
730 "Wrapped error '{}' should contain etag '{}'",
731 wrapped_str,
732 etag
733 );
734 assert!(wrapped_str.starts_with("Invalid ETag header: "));
735 }
736
737 #[test]
741 fn test_invalid_response() {
742 let err = Error::InvalidResponse;
743 let err_str = err.to_string();
744 assert_eq!(err_str, "Invalid server response");
745 }
746
747 #[test]
748 fn test_not_implemented() {
749 let err = Error::NotImplemented;
750 let err_str = err.to_string();
751 assert_eq!(err_str, "Not implemented");
752 }
753
754 #[test]
755 fn test_part_too_large() {
756 let err = Error::PartTooLarge;
757 let err_str = err.to_string();
758 assert_eq!(err_str, "File part size exceeds maximum limit");
759 }
760
761 #[test]
762 fn test_empty_token() {
763 let err = Error::EmptyToken;
764 let err_str = err.to_string();
765 assert_eq!(err_str, "Authentication token is empty");
766 }
767
768 #[test]
769 fn test_invalid_token() {
770 let err = Error::InvalidToken;
771 let err_str = err.to_string();
772 assert_eq!(err_str, "Invalid authentication token");
773 }
774
775 #[test]
776 fn test_token_expired() {
777 let err = Error::TokenExpired;
778 let err_str = err.to_string();
779 assert_eq!(err_str, "Authentication token has expired");
780 }
781
782 #[test]
783 fn test_unauthorized() {
784 let err = Error::Unauthorized;
785 let err_str = err.to_string();
786 assert_eq!(err_str, "Unauthorized access");
787 }
788
789 #[test]
794 fn test_task_not_found_display_contains_id() {
795 let task_id = crate::api::TaskID::from(0x1092u64);
797 let err = Error::TaskNotFound(task_id);
798 let err_str = err.to_string();
799 assert!(
800 err_str.contains("task-1092"),
801 "Display should include the task ID prefix+hex, got: {err_str}"
802 );
803 assert!(err_str.starts_with("task not found"));
804 }
805
806 #[test]
807 fn test_permission_denied_display_contains_method() {
808 let err = Error::PermissionDenied("task.chart.add".to_string());
809 let err_str = err.to_string();
810 assert!(
811 err_str.contains("task.chart.add"),
812 "Display should include the method name, got: {err_str}"
813 );
814 assert!(err_str.starts_with("permission denied"));
815 }
816
817 #[test]
818 fn test_payload_too_large_display_contains_method() {
819 let err = Error::PayloadTooLarge {
820 method: "val.data.upload".to_string(),
821 size_hint: Some(123456),
822 };
823 let err_str = err.to_string();
824 assert!(
825 err_str.contains("val.data.upload"),
826 "Display should include the method name, got: {err_str}"
827 );
828 assert!(err_str.starts_with("payload too large"));
829 }
830
831 #[test]
832 fn test_payload_too_large_with_no_size_hint() {
833 let err = Error::PayloadTooLarge {
835 method: "task.data.upload".to_string(),
836 size_hint: None,
837 };
838 let err_str = err.to_string();
839 assert!(err_str.contains("task.data.upload"));
840 }
841
842 #[test]
843 fn test_typed_variants_have_no_source() {
844 use std::error::Error as _;
848
849 let task_not_found = Error::TaskNotFound(crate::api::TaskID::from(1u64));
850 assert!(task_not_found.source().is_none());
851
852 let perm_denied = Error::PermissionDenied("op".into());
853 assert!(perm_denied.source().is_none());
854
855 let too_large = Error::PayloadTooLarge {
856 method: "m".into(),
857 size_hint: Some(1),
858 };
859 assert!(too_large.source().is_none());
860 }
861
862 #[test]
867 fn test_coco_error_display() {
868 let err = Error::CocoError("missing categories array".into());
869 let err_str = err.to_string();
870 assert!(err_str.contains("missing categories array"));
871 assert!(err_str.starts_with("COCO format error:"));
872 }
873
874 #[test]
875 fn test_zip_error_display() {
876 let err = Error::ZipError("invalid central directory".into());
877 let err_str = err.to_string();
878 assert!(err_str.contains("invalid central directory"));
879 assert!(err_str.starts_with("ZIP error:"));
880 }
881
882 #[test]
883 fn test_storage_error_display() {
884 let err = Error::StorageError("keychain locked".into());
885 let err_str = err.to_string();
886 assert!(err_str.contains("keychain locked"));
887 assert!(err_str.starts_with("Token storage error:"));
888 }
889}