anyhow_http/
extension.rs

1use http::StatusCode;
2use std::{borrow::Cow, result::Result as StdResult};
3
4use crate::HttpError;
5
6/// Extension trait to map the error variant of a [`Result`] to a [`HttpError`].
7pub trait ResultExt {
8    type Item;
9
10    /// Maps a `Result<T, E>` to `Result<T, HttpError<R>>` by creating a [`HttpError`] with the
11    /// specified status code wrapping the error contained [`Err`].
12    ///
13    /// # Example
14    ///
15    /// ```
16    /// # use http::StatusCode;
17    /// # use anyhow_http::{http_error, HttpError, ResultExt};
18    ///
19    /// let err = "nan".parse::<i32>()
20    ///     .map_status(StatusCode::BAD_REQUEST)
21    ///     .unwrap_err();
22    /// assert_eq!(HttpError::from(err).status_code(), StatusCode::BAD_REQUEST);
23    /// ```
24    fn map_status(self, status_code: StatusCode) -> anyhow::Result<Self::Item>;
25
26    /// Maps a `Result<T, E>` to `Result<T, HttpError<R>>` by creating a [`HttpError`] with the
27    /// specified status code and reason wrapping the error contained [`Err`].
28    ///
29    /// # Example
30    ///
31    /// ```
32    /// # use http::StatusCode;
33    /// # use anyhow_http::{http_error, HttpError, ResultExt};
34    ///
35    /// let err = "nan".parse::<i32>()
36    ///     .map_http_error(StatusCode::BAD_REQUEST, "invalid number")
37    ///     .unwrap_err();
38    /// let http_err = HttpError::from(err);
39    /// assert_eq!(http_err.status_code(), StatusCode::BAD_REQUEST);
40    /// assert_eq!(http_err.reason().unwrap(), "invalid number");
41    /// ```
42    fn map_http_error<S>(self, status_code: StatusCode, reason: S) -> anyhow::Result<Self::Item>
43    where
44        S: Into<Cow<'static, str>>;
45}
46
47impl<E, T> ResultExt for StdResult<T, E>
48where
49    E: Into<anyhow::Error> + Send + Sync + 'static,
50{
51    type Item = T;
52
53    fn map_status(self, status_code: StatusCode) -> anyhow::Result<T> {
54        match self {
55            Ok(v) => Ok(v),
56            Err(e) => Err(HttpError::from_err(e).with_status_code(status_code).into()),
57        }
58    }
59
60    fn map_http_error<S>(self, status_code: StatusCode, reason: S) -> anyhow::Result<Self::Item>
61    where
62        S: Into<Cow<'static, str>>,
63    {
64        match self {
65            Ok(v) => Ok(v),
66            Err(e) => Err(HttpError::from_err(e)
67                .with_status_code(status_code)
68                .with_reason(reason.into())
69                .into()),
70        }
71    }
72}
73
74/// Extension trait to transform an [`Option`] to a [`HttpError`].
75pub trait OptionExt {
76    type Item;
77
78    /// Transforms the `Option<T>` into a `Result<T, HttpError<R>>`, mapping `Some(v)` to
79    /// `Ok(v)` and `None` to `Err(HttpError)` with status code.
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// # use http::StatusCode;
85    /// # use anyhow_http::{http_error, HttpError, OptionExt};
86    ///
87    /// let err = None::<()>.ok_or_status(StatusCode::BAD_REQUEST).unwrap_err();
88    /// assert_eq!(HttpError::from(err).status_code(), StatusCode::BAD_REQUEST);
89    /// ```
90    fn ok_or_status(self, status_code: StatusCode) -> anyhow::Result<Self::Item>;
91}
92
93impl<T> OptionExt for std::option::Option<T> {
94    type Item = T;
95
96    fn ok_or_status(self, status_code: StatusCode) -> anyhow::Result<T> {
97        match self {
98            Some(v) => Ok(v),
99            None => Err(HttpError::from_status_code(status_code).into()),
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use anyhow::anyhow;
107
108    use super::*;
109
110    #[test]
111    fn http_err_ext_result_map_status() {
112        let result: StdResult<(), _> = Err(anyhow!("error"));
113        let http_result = result.map_status(StatusCode::BAD_REQUEST);
114
115        let Err(e) = http_result else { unreachable!() };
116        let e: HttpError = e.into();
117        assert_eq!(e.status_code, StatusCode::BAD_REQUEST);
118        assert_eq!(e.source.unwrap().to_string(), "error".to_owned());
119    }
120
121    #[test]
122    fn http_err_ext_result_map_http_error() {
123        let s = "nan".parse::<i32>().map_status(StatusCode::BAD_REQUEST);
124        let Err(e) = s else { unreachable!() };
125        let e: HttpError = e.into();
126        assert_eq!(e.status_code, StatusCode::BAD_REQUEST);
127
128        let result = Err(anyhow!("error"));
129        let http_result: anyhow::Result<()> =
130            result.map_http_error(StatusCode::BAD_REQUEST, "invalid request");
131
132        let Err(e) = http_result else { unreachable!() };
133        let e: HttpError = e.into();
134        assert_eq!(e.status_code, StatusCode::BAD_REQUEST);
135        assert_eq!(e.source.unwrap().to_string(), "error".to_owned());
136        assert_eq!(e.reason, Some("invalid request".into()));
137    }
138
139    #[test]
140    fn http_err_ext_option() {
141        let opt: Option<()> = None;
142        let http_result = opt.ok_or_status(StatusCode::BAD_REQUEST);
143
144        let Err(e) = http_result else { unreachable!() };
145        let e: HttpError = e.into();
146        assert_eq!(e.status_code, StatusCode::BAD_REQUEST);
147        assert!(e.source.is_none());
148    }
149}