1use std::time::Duration;
4
5pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum Error {
12 #[error("{reason}")]
14 InvalidUrl {
15 url: String,
17 reason: String,
19 },
20
21 #[error("page load timed out after {}s", timeout.as_secs())]
23 Timeout {
24 url: String,
26 timeout: Duration,
28 },
29
30 #[error("address not allowed: {0}")]
32 AddressNotAllowed(String),
33
34 #[error("engine error: {0}")]
36 Engine(String),
37
38 #[error("JavaScript evaluation failed: {0}")]
40 JavaScript(String),
41
42 #[error("screenshot capture failed: {0}")]
44 Screenshot(String),
45
46 #[error(transparent)]
48 Extract(#[from] crate::extract::ExtractError),
49
50 #[error(transparent)]
52 Io(#[from] std::io::Error),
53
54 #[error("invalid glob pattern: {0}")]
56 InvalidGlob(#[from] globset::Error),
57}
58
59impl Error {
60 #[must_use]
62 pub fn is_timeout(&self) -> bool {
63 matches!(self, Self::Timeout { .. })
64 }
65
66 #[must_use]
68 pub fn is_network(&self) -> bool {
69 matches!(self, Self::Timeout { .. } | Self::AddressNotAllowed(_))
70 }
71
72 #[must_use]
74 pub fn url(&self) -> Option<&str> {
75 match self {
76 Self::InvalidUrl { url, .. } | Self::Timeout { url, .. } | Self::AddressNotAllowed(url) => Some(url),
77 _ => None,
78 }
79 }
80}
81
82#[allow(clippy::used_underscore_items)]
83const _: () = {
84 fn _assert<T: Send + Sync>() {}
85 fn _check() {
86 _assert::<Error>();
87 }
88};
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn timeout_is_timeout() {
96 let err = Error::Timeout {
97 url: "https://example.com".into(),
98 timeout: Duration::from_secs(30),
99 };
100 assert!(err.is_timeout());
101 assert!(err.is_network());
102 assert_eq!(err.url(), Some("https://example.com"));
103 }
104
105 #[test]
106 fn address_not_allowed_is_network() {
107 let err = Error::AddressNotAllowed("127.0.0.1".into());
108 assert!(!err.is_timeout());
109 assert!(err.is_network());
110 assert_eq!(err.url(), Some("127.0.0.1"));
111 }
112
113 #[test]
114 fn invalid_url_has_url() {
115 let err = Error::InvalidUrl {
116 url: "bad://url".into(),
117 reason: "scheme not allowed".into(),
118 };
119 assert!(!err.is_timeout());
120 assert!(!err.is_network());
121 assert_eq!(err.url(), Some("bad://url"));
122 }
123
124 #[test]
125 fn engine_error_has_no_url() {
126 let err = Error::Engine("crashed".into());
127 assert!(!err.is_timeout());
128 assert!(err.url().is_none());
129 }
130}