1use std::path::PathBuf;
2use std::sync::Arc;
3
4#[derive(Debug, thiserror::Error)]
5#[non_exhaustive]
6pub enum WsiError {
7 #[error("unsupported format: {0}")]
8 UnsupportedFormat(String),
9 #[error("TIFF error in {path}: {message}")]
10 Tiff { path: PathBuf, message: String },
11 #[error("JPEG decode error: {0}")]
12 Jpeg(String),
13 #[error("JPEG2000 decode error: {0}")]
14 Jp2k(String),
15 #[error("XML parse error: {0}")]
16 Xml(String),
17 #[error("invalid slide {path}: {message}")]
18 InvalidSlide { path: PathBuf, message: String },
19 #[error("tile read failed at ({col}, {row}) level {level}: {reason}")]
20 TileRead {
21 col: i64,
22 row: i64,
23 level: u32,
24 reason: String,
25 },
26 #[error("I/O error: {0}")]
27 Io(#[from] std::io::Error),
28 #[error("I/O error at {path}: {source}")]
29 IoWithPath {
30 #[source]
31 source: Arc<std::io::Error>,
32 path: PathBuf,
33 },
34
35 #[error("scene index {index} out of range (dataset has {count} scenes)")]
37 SceneOutOfRange { index: usize, count: usize },
38
39 #[error("series index {index} out of range (scene has {count} series)")]
40 SeriesOutOfRange { index: usize, count: usize },
41
42 #[error("level {level} out of range (series has {count} levels)")]
43 LevelOutOfRange { level: u32, count: u32 },
44
45 #[error("plane axis {axis} value {value} exceeds max {max}")]
46 PlaneOutOfRange { axis: String, value: u32, max: u32 },
47
48 #[error("associated image not found: {0}")]
49 AssociatedImageNotFound(String),
50
51 #[error("display conversion error: {0}")]
52 DisplayConversion(String),
53
54 #[error("codec error in {codec}: {source}")]
56 Codec {
57 codec: &'static str,
58 #[source]
59 source: Box<dyn std::error::Error + Send + Sync>,
60 },
61
62 #[error("unsupported: {reason}")]
64 Unsupported { reason: String },
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn error_display_formats() {
73 let err = WsiError::Tiff {
74 path: "/tmp/test.svs".into(),
75 message: "bad IFD".into(),
76 };
77 assert!(err.to_string().contains("test.svs"));
78 assert!(err.to_string().contains("bad IFD"));
79 }
80
81 #[test]
82 fn io_error_converts() {
83 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
84 let wsi_err: WsiError = io_err.into();
85 assert!(matches!(wsi_err, WsiError::Io(_)));
86 }
87
88 #[test]
89 fn scene_out_of_range_display() {
90 let err = WsiError::SceneOutOfRange { index: 2, count: 1 };
91 assert!(err.to_string().contains("2"));
92 assert!(err.to_string().contains("1"));
93 }
94
95 #[test]
96 fn series_out_of_range_display() {
97 let err = WsiError::SeriesOutOfRange { index: 3, count: 2 };
98 assert!(err.to_string().contains("3"));
99 }
100
101 #[test]
102 fn plane_out_of_range_display() {
103 let err = WsiError::PlaneOutOfRange {
104 axis: "z".into(),
105 value: 5,
106 max: 3,
107 };
108 assert!(err.to_string().contains("z"));
109 assert!(err.to_string().contains("5"));
110 }
111
112 #[test]
113 fn level_out_of_range_display() {
114 let err = WsiError::LevelOutOfRange {
115 level: 10,
116 count: 5,
117 };
118 assert!(err.to_string().contains("10"));
119 }
120
121 #[test]
122 fn associated_image_not_found_display() {
123 let err = WsiError::AssociatedImageNotFound("label".into());
124 assert!(err.to_string().contains("label"));
125 }
126
127 #[test]
128 fn display_conversion_display() {
129 let err = WsiError::DisplayConversion("non-uint8 requires windowing".into());
130 assert!(err.to_string().contains("windowing"));
131 }
132
133 #[test]
134 fn io_with_path_display() {
135 let err = WsiError::IoWithPath {
136 source: Arc::new(std::io::Error::new(
137 std::io::ErrorKind::NotFound,
138 "file not found",
139 )),
140 path: "/tmp/slide.svs".into(),
141 };
142 let msg = err.to_string();
143 assert!(msg.contains("/tmp/slide.svs"), "got: {msg}");
144 assert!(msg.contains("file not found"), "got: {msg}");
145 }
146
147 #[test]
148 fn codec_display_includes_codec_and_source() {
149 let inner: Box<dyn std::error::Error + Send + Sync> = "boom".into();
150 let err = WsiError::Codec {
151 codec: "jpeg",
152 source: inner,
153 };
154 let msg = err.to_string();
155 assert!(msg.contains("jpeg"), "got: {msg}");
156 assert!(msg.contains("boom"), "got: {msg}");
157 }
158
159 #[test]
160 fn codec_pattern_match_round_trips() {
161 let err = WsiError::Codec {
162 codec: "j2k",
163 source: "decode failed".into(),
164 };
165 match err {
166 WsiError::Codec { codec, source: _ } => assert_eq!(codec, "j2k"),
167 other => panic!("expected Codec, got {other:?}"),
168 }
169 }
170
171 #[test]
172 fn unsupported_display() {
173 let err = WsiError::Unsupported {
174 reason: "device backend unavailable".into(),
175 };
176 assert!(err.to_string().contains("device backend unavailable"));
177 }
178}