Skip to main content

voxora_traits/
error.rs

1//! Error type returned by every voxora operation.
2
3use std::path::PathBuf;
4
5/// All errors a voxora engine or model source may return.
6///
7/// `#[non_exhaustive]` so we can add variants in future minor releases
8/// without breaking downstream `match` arms.
9#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum AsrError {
12    /// The requested model could not be located by any known source.
13    #[error("model not found: {0}")]
14    ModelNotFound(String),
15
16    /// The requested operation is not supported by this engine / source.
17    #[error("operation not supported: {0}")]
18    Unsupported(&'static str),
19
20    /// Caller-supplied input was rejected (bad audio format, unknown
21    /// language code, out-of-range parameter, …).
22    #[error("invalid input: {0}")]
23    InvalidInput(String),
24
25    /// Audio file I/O failed.
26    #[error("audio I/O error at {}: {source}", path.display())]
27    AudioIo {
28        /// Path that failed to read or write.
29        path: PathBuf,
30        /// Underlying I/O error.
31        #[source]
32        source: std::io::Error,
33    },
34
35    /// The inference pass failed inside the engine (numerical error,
36    /// shape mismatch, OOM, …).
37    #[error("inference failed: {0}")]
38    Inference(String),
39
40    /// The model or runtime configuration is invalid.
41    #[error("configuration error: {0}")]
42    Config(String),
43
44    /// Network failure while acquiring a model (DNS, TCP, TLS, HTTP
45    /// transport, timeout, non-success status, or auth challenge).
46    ///
47    /// The `voxora-traits` crate stays offline-pure (no `reqwest`, no
48    /// `tokio`); this variant only carries a `String` URL, a `String`
49    /// message, and an optional boxed `std::error::Error`. The actual
50    /// network code lives in `voxora-hf`.
51    #[error("network error at {url}: {message}")]
52    Network {
53        /// URL that failed, if known.
54        url: String,
55        /// Human-readable description of the failure mode.
56        message: String,
57        /// Underlying error, when available.
58        #[source]
59        source: Option<Box<dyn std::error::Error + Send + Sync>>,
60    },
61}
62
63impl AsrError {
64    /// Construct an [`AsrError::AudioIo`] from an I/O error and a path.
65    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    /// Construct an [`AsrError::Network`] from a URL, a message, and an
73    /// optional inner error.
74    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}