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