Skip to main content

asknothingx2_util/api/
error.rs

1use std::fmt;
2
3use ::http::header::{InvalidHeaderName, InvalidHeaderValue};
4
5pub struct Error {
6    inner: Box<Inner>,
7}
8
9#[derive(Debug)]
10struct Inner {
11    kind: Kind,
12    message: Option<String>,
13    input: Option<String>,
14    source: Option<BoxError>,
15}
16
17#[derive(Debug)]
18pub enum Kind {
19    RequestBuild,
20    HttpInvalidHeader,
21    AuthInvalidScheme,
22    ContentTypeInvalid,
23    ContentTypeUnsupported,
24}
25
26impl Kind {
27    pub fn category(self) -> ErrorCategory {
28        match self {
29            Kind::RequestBuild => ErrorCategory::Request,
30            Kind::HttpInvalidHeader => ErrorCategory::Http,
31            Kind::AuthInvalidScheme => ErrorCategory::Authentication,
32            Kind::ContentTypeInvalid | Kind::ContentTypeUnsupported => ErrorCategory::ContentType,
33        }
34    }
35}
36
37impl fmt::Display for Kind {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            Kind::RequestBuild => f.write_str("failed to build request"),
41            Kind::HttpInvalidHeader => f.write_str("invalid HTTP header"),
42            Kind::AuthInvalidScheme => f.write_str("invalid authentication scheme"),
43            Kind::ContentTypeInvalid => f.write_str("invalid content type"),
44            Kind::ContentTypeUnsupported => f.write_str("unsupported content type"),
45        }
46    }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum ErrorCategory {
51    Request,
52    Http,
53    Authentication,
54    ContentType,
55}
56
57type BoxError = Box<dyn std::error::Error + Send + Sync>;
58
59impl Error {
60    pub fn new(kind: Kind) -> Self {
61        Self {
62            inner: Box::new(Inner {
63                kind,
64                message: None,
65                input: None,
66                source: None,
67            }),
68        }
69    }
70
71    pub fn with_message(kind: Kind, message: impl Into<String>) -> Self {
72        Self {
73            inner: Box::new(Inner {
74                kind,
75                message: Some(message.into()),
76                input: None,
77                source: None,
78            }),
79        }
80    }
81
82    pub fn with_source(kind: Kind, source: impl Into<BoxError>) -> Self {
83        Self {
84            inner: Box::new(Inner {
85                kind,
86                message: None,
87                input: None,
88                source: Some(source.into()),
89            }),
90        }
91    }
92
93    pub fn with_message_and_source(
94        kind: Kind,
95        message: impl Into<String>,
96        source: impl Into<BoxError>,
97    ) -> Self {
98        Self {
99            inner: Box::new(Inner {
100                kind,
101                message: Some(message.into()),
102                input: None,
103                source: Some(source.into()),
104            }),
105        }
106    }
107
108    pub fn with_input(mut self, input: impl Into<String>) -> Self {
109        self.inner.input = Some(input.into());
110        self
111    }
112
113    pub fn message(&self) -> Option<&str> {
114        self.inner.message.as_deref()
115    }
116
117    pub fn input(&self) -> Option<&str> {
118        self.inner.input.as_deref()
119    }
120
121    pub fn is_request(&self) -> bool {
122        matches!(self.inner.kind, Kind::RequestBuild)
123    }
124}
125
126impl fmt::Debug for Error {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        let mut builder = f.debug_struct("asknothingx2-util::api::Error");
129
130        builder.field("kind", &self.inner.kind);
131
132        if let Some(ref message) = self.inner.message {
133            builder.field("message", message);
134        }
135
136        if let Some(ref input) = self.inner.input {
137            builder.field("input", input);
138        }
139
140        if let Some(ref source) = self.inner.source {
141            builder.field("source", source);
142        }
143
144        builder.finish()
145    }
146}
147
148impl fmt::Display for Error {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        if let Some(ref message) = self.inner.message {
151            write!(f, "{message}")?;
152        } else {
153            write!(f, "{}", self.inner.kind)?;
154        }
155
156        if let Some(ref input) = self.inner.input {
157            let truncated = truncate_input(input);
158            if !truncated.is_empty() {
159                write!(
160                    f,
161                    " [input: {}{}]",
162                    truncated,
163                    if input.len() > truncated.len() {
164                        "..."
165                    } else {
166                        ""
167                    }
168                )?;
169            }
170        }
171
172        if let Some(ref source) = self.inner.source {
173            write!(f, " -> {source})")?;
174        }
175
176        Ok(())
177    }
178}
179
180impl std::error::Error for Error {
181    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
182        self.inner.source.as_ref().map(|e| &**e as _)
183    }
184}
185
186pub mod request {
187    use super::{BoxError, Error, Kind};
188
189    pub fn build<E: Into<BoxError>>(source: E) -> Error {
190        Error::with_source(Kind::RequestBuild, source)
191    }
192}
193
194pub mod http {
195    use super::{BoxError, Error, Kind};
196    pub fn invalid_header<E: Into<BoxError>>(source: E) -> Error {
197        Error::with_source(Kind::HttpInvalidHeader, source)
198    }
199}
200
201pub mod auth {
202    use super::{Error, Kind};
203
204    pub fn invalid_scheme<S: Into<String>>(scheme: S) -> Error {
205        Error::with_message(
206            Kind::AuthInvalidScheme,
207            format!("invalid authentication scheme '{}'", scheme.into()),
208        )
209    }
210}
211
212pub mod content {
213    use super::{Error, Kind};
214
215    pub fn invalid_type<T: Into<String>>(content_type: T) -> Error {
216        Error::with_message(
217            Kind::ContentTypeInvalid,
218            format!("invalid content type '{}'", content_type.into()),
219        )
220    }
221
222    pub fn unsupported<T: Into<String>>(content_type: T) -> Error {
223        Error::with_message(
224            Kind::ContentTypeUnsupported,
225            format!("unsupported content type '{}'", content_type.into()),
226        )
227    }
228}
229
230fn truncate_input(input: &str) -> &str {
231    const MAX_LEN: usize = 80;
232    if input.len() <= MAX_LEN {
233        input
234    } else {
235        &input[..MAX_LEN]
236    }
237}
238
239impl From<InvalidHeaderName> for Error {
240    fn from(err: InvalidHeaderName) -> Self {
241        Error::with_source(Kind::HttpInvalidHeader, err)
242    }
243}
244
245impl From<InvalidHeaderValue> for Error {
246    fn from(err: InvalidHeaderValue) -> Self {
247        Error::with_source(Kind::HttpInvalidHeader, err)
248    }
249}