Skip to main content

oai_pmh/
error.rs

1use quick_xml::DeError;
2use reqwest::header::{InvalidHeaderName, InvalidHeaderValue};
3use thiserror::Error as ThisError;
4use url::ParseError;
5
6#[derive(Debug, ThisError)]
7pub enum Error {
8    /// HTTP request failed (network error, timeout, etc.)
9    #[error("HTTP request failed: {0}")]
10    Http(#[from] reqwest::Error),
11    /// The endpoint URL has an invalid scheme (must be http or https)
12    #[error("invalid endpoint: {0}")]
13    InvalidEndpoint(String),
14    /// Header name is invalid
15    #[error("invalid header name: {0}")]
16    InvalidHeaderName(#[from] InvalidHeaderName),
17    /// Header value is invalid
18    #[error("invalid header value: {0}")]
19    InvalidHeaderValue(#[from] InvalidHeaderValue),
20    /// Failed to serialize query parameters
21    #[error("query serialization failed: {0}")]
22    QuerySerialize(#[from] serde_qs::Error),
23    /// Response was not valid XML (e.g., HTML error page, plain text)
24    #[error("unexpected response{}: {body}", content_type_label(content_type.as_deref()))]
25    UnexpectedResponse {
26        /// The content-type header, if present
27        content_type: Option<String>,
28        /// The response body (truncated if too long)
29        body: String,
30    },
31    /// Failed to parse the endpoint URL
32    #[error("URL parsing failed: {0}")]
33    UrlParse(#[from] ParseError),
34    /// Failed to parse XML response
35    #[error("XML parsing failed: {0}")]
36    XmlParse(#[from] DeError),
37}
38
39fn content_type_label(content_type: Option<&str>) -> String {
40    match content_type {
41        Some(content_type) => format!(" (content-type: {content_type})"),
42        None => String::new(),
43    }
44}
45
46/// A specialized Result type for oai-pmh operations.
47pub type Result<T> = std::result::Result<T, Error>;
48
49#[cfg(test)]
50mod display_tests {
51    use super::*;
52    use std::error::Error as StdError;
53
54    #[test]
55    fn invalid_endpoint_display() {
56        let e = Error::InvalidEndpoint("bad".into());
57        assert_eq!(e.to_string(), "invalid endpoint: bad");
58        assert!(e.source().is_none());
59    }
60
61    #[test]
62    fn unexpected_response_with_content_type() {
63        let e = Error::UnexpectedResponse {
64            content_type: Some("text/html".into()),
65            body: "<html>".into(),
66        };
67        assert_eq!(
68            e.to_string(),
69            "unexpected response (content-type: text/html): <html>"
70        );
71        assert!(e.source().is_none());
72    }
73
74    #[test]
75    fn unexpected_response_without_content_type() {
76        let e = Error::UnexpectedResponse {
77            content_type: None,
78            body: "oops".into(),
79        };
80        assert_eq!(e.to_string(), "unexpected response: oops");
81    }
82
83    #[test]
84    fn url_parse_wraps_source() {
85        let inner = url::Url::parse("not a url").unwrap_err();
86        let e: Error = inner.into();
87        assert!(e.to_string().starts_with("URL parsing failed: "));
88        assert!(e.source().is_some());
89    }
90}