api_err/
lib.rs

1#![warn(missing_docs)]
2
3//! Errors for conveniently attaching status codes to error cases.
4//!
5//! ```
6//! use api_err::{CategoryExt, Context};
7//!
8//! fn add_one(request: &str) -> api_err::Result<String> {
9//!    let input = request.parse::<i64>().bad_request()?;
10//!    let output = input.checked_add(1).context("Input too large").bad_request()?;
11//!    Ok(output.to_string())
12//! }
13//! ```
14//!
15//! Errors without a category attached default to the "Internal Server"-like error.
16
17mod category;
18
19#[cfg(feature = "http")]
20mod http;
21
22#[cfg(feature = "json_rpc")]
23mod json_rpc;
24
25use std::fmt::Display;
26
27pub use category::{Category, CategoryExt};
28
29/// [std::result::Result] bound to [Error].
30pub type Result<T> = std::result::Result<T, Error>;
31
32/// Core error type.
33///
34/// Combination of [anyhow::Error] and a [Category].
35pub struct Error {
36    anyhow: anyhow::Error,
37    category: Option<Category>,
38}
39
40impl Error {
41    /// Convert into the underlying [anyhow::Error].
42    pub fn into_anyhow(self) -> anyhow::Error {
43        self.anyhow
44    }
45
46    /// Get a reference to the error's category (if it exists).
47    pub fn category(&self) -> Option<&Category> {
48        self.category.as_ref()
49    }
50
51    /// Attach additional context to the error. See [anyhow::Error::context].
52    pub fn context<C>(mut self, context: C) -> Self
53    where
54        C: Display + Send + Sync + 'static,
55    {
56        self.anyhow = self.anyhow.context(context);
57        self
58    }
59
60    #[cfg(feature = "http")]
61    /// The HTTP status code that best corresponds to this error's category.
62    pub fn http_status(&self) -> u16 {
63        http::status_code(self.category.as_ref())
64    }
65
66    #[cfg(feature = "json_rpc")]
67    /// The JSON-RPC status code that best corresponds to this error's category.
68    pub fn json_rpc_status(&self) -> i32 {
69        json_rpc::status_code(self.category.as_ref())
70    }
71}
72
73impl<E> From<E> for Error
74where
75    anyhow::Error: From<E>,
76{
77    fn from(e: E) -> Self {
78        Error {
79            anyhow: anyhow::Error::from(e),
80            category: None,
81        }
82    }
83}
84
85/// [anyhow::Context]-like trait for adding contexts and getting [Error]s.
86pub trait Context<T, E> {
87    /// See [anyhow::Context::context].
88    fn context<C>(self, context: C) -> Result<T>
89    where
90        C: Display + Send + Sync + 'static;
91
92    /// See [anyhow::Context::with_context].
93    fn with_context<C, F>(self, f: F) -> Result<T>
94    where
95        C: Display + Send + Sync + 'static,
96        F: FnOnce() -> C;
97}
98
99impl<T, E> Context<T, E> for std::result::Result<T, E>
100where
101    E: Into<Error>,
102{
103    fn context<C>(self, context: C) -> Result<T>
104    where
105        C: Display + Send + Sync + 'static,
106    {
107        match self {
108            Ok(t) => Ok(t),
109            Err(e) => Err(e.into().context(context)),
110        }
111    }
112
113    fn with_context<C, F>(self, f: F) -> Result<T>
114    where
115        C: Display + Send + Sync + 'static,
116        F: FnOnce() -> C,
117    {
118        match self {
119            Ok(t) => Ok(t),
120            Err(e) => Err(e.into().context(f())),
121        }
122    }
123}
124
125impl<T> Context<T, std::convert::Infallible> for Option<T> {
126    fn context<C>(self, context: C) -> Result<T>
127    where
128        C: Display + Send + Sync + 'static,
129    {
130        match self {
131            Some(t) => Ok(t),
132            None => Err(anyhow::anyhow!(context.to_string()).into()),
133        }
134    }
135
136    fn with_context<C, F>(self, f: F) -> Result<T>
137    where
138        C: Display + Send + Sync + 'static,
139        F: FnOnce() -> C,
140    {
141        match self {
142            Some(t) => Ok(t),
143            None => Err(anyhow::anyhow!(f().to_string()).into()),
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn returns_with_question_mark() {
154        fn _api() -> super::Result<()> {
155            "foo".parse::<usize>()?;
156            Ok(())
157        }
158    }
159
160    #[test]
161    fn attaches_cause() {
162        let err = "foo".parse::<usize>().bad_request();
163
164        assert_eq!(err.unwrap_err().category(), Some(&Category::BadRequest));
165    }
166
167    #[test]
168    fn can_attach_context() {
169        let _ = "foo".parse::<usize>().bad_request().context("Some context");
170    }
171}