1use std::path::PathBuf;
4
5#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum AsrError {
12 #[error("model not found: {0}")]
14 ModelNotFound(String),
15
16 #[error("operation not supported: {0}")]
18 Unsupported(&'static str),
19
20 #[error("invalid input: {0}")]
23 InvalidInput(String),
24
25 #[error("audio I/O error at {}: {source}", path.display())]
27 AudioIo {
28 path: PathBuf,
30 #[source]
32 source: std::io::Error,
33 },
34
35 #[error("inference failed: {0}")]
38 Inference(String),
39
40 #[error("configuration error: {0}")]
42 Config(String),
43
44 #[error("network error at {url}: {message}")]
52 Network {
53 url: String,
55 message: String,
57 #[source]
59 source: Option<Box<dyn std::error::Error + Send + Sync>>,
60 },
61}
62
63impl AsrError {
64 pub fn audio_io(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
66 Self::AudioIo {
67 path: path.into(),
68 source,
69 }
70 }
71
72 pub fn network(
75 url: impl Into<String>,
76 message: impl Into<String>,
77 source: Option<Box<dyn std::error::Error + Send + Sync>>,
78 ) -> Self {
79 Self::Network {
80 url: url.into(),
81 message: message.into(),
82 source,
83 }
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use std::error::Error as _;
91
92 #[test]
93 fn display_messages_are_stable() {
94 assert_eq!(
95 AsrError::ModelNotFound("foo".into()).to_string(),
96 "model not found: foo"
97 );
98 assert_eq!(
99 AsrError::Unsupported("list_available").to_string(),
100 "operation not supported: list_available"
101 );
102 assert_eq!(
103 AsrError::InvalidInput("bad lang".into()).to_string(),
104 "invalid input: bad lang"
105 );
106 assert_eq!(
107 AsrError::Inference("NaN".into()).to_string(),
108 "inference failed: NaN"
109 );
110 assert_eq!(
111 AsrError::Config("missing tokenizer".into()).to_string(),
112 "configuration error: missing tokenizer"
113 );
114 }
115
116 #[test]
117 fn audio_io_helper_wraps_inner_error() {
118 let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "missing.wav");
119 let err = AsrError::audio_io("/tmp/missing.wav", inner);
120 match err {
121 AsrError::AudioIo { path, source } => {
122 assert_eq!(path, PathBuf::from("/tmp/missing.wav"));
123 assert_eq!(source.kind(), std::io::ErrorKind::NotFound);
124 }
125 other => panic!("expected AudioIo, got {other:?}"),
126 }
127 }
128
129 #[test]
130 fn audio_io_display_includes_path_and_source() {
131 let err = AsrError::audio_io("/data/x.wav", std::io::Error::other("disk gone"));
132 let rendered = err.to_string();
133 assert!(rendered.contains("/data/x.wav"), "{rendered}");
134 assert!(rendered.contains("disk gone"), "{rendered}");
135 }
136
137 #[test]
138 fn source_chain_is_walkable() {
139 let err = AsrError::audio_io("/p", std::io::Error::other("boom"));
140 let chain = err.source();
141 assert!(chain.is_some(), "audio_io must expose its inner io::Error");
142 let first = chain.expect("checked is_some");
143 assert_eq!(first.to_string(), "boom");
144 assert!(first.source().is_none());
145 }
146
147 #[test]
148 fn network_helper_constructs_variant_with_url_and_message() {
149 let inner = std::io::Error::other("connection reset");
150 let err = AsrError::network(
151 "https://huggingface.co/foo/bar/resolve/main/config.json",
152 "HTTP 503",
153 Some(Box::new(inner)),
154 );
155 match err {
156 AsrError::Network {
157 ref url,
158 ref message,
159 ref source,
160 } => {
161 assert_eq!(
162 url,
163 "https://huggingface.co/foo/bar/resolve/main/config.json"
164 );
165 assert_eq!(message, "HTTP 503");
166 let src = source.as_deref().expect("source must be present");
167 assert_eq!(src.to_string(), "connection reset");
168 }
169 other => panic!("expected Network, got {other:?}"),
170 }
171 }
172
173 #[test]
174 fn network_display_includes_url_and_message() {
175 let err = AsrError::network("https://huggingface.co/x", "DNS failure", None);
176 let rendered = err.to_string();
177 assert!(rendered.contains("https://huggingface.co/x"), "{rendered}");
178 assert!(rendered.contains("DNS failure"), "{rendered}");
179 }
180
181 #[test]
182 fn network_with_no_source_walks_to_none() {
183 let err = AsrError::network("u", "m", None);
184 assert!(err.source().is_none());
185 }
186}