summaryrefslogtreecommitdiffstats
path: root/src/errors.rs
blob: 0c16d551d2c51bb82ada33d3bc5ae4bd7adf42fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
use std::string::FromUtf8Error;
use std::{error, fmt, io::Error as IoError, num::TryFromIntError};

use derive_is_enum_variant::is_enum_variant;
#[cfg(feature = "env")]
use envy::Error as EnvyError;
use reqwest::{header::ToStrError as HeaderStrError, Error as HttpError, StatusCode};
use serde::Deserialize;
use serde_json::Error as SerdeError;
use serde_urlencoded::ser::Error as UrlEncodedError;
#[cfg(feature = "toml")]
use tomlcrate::de::Error as TomlDeError;
#[cfg(feature = "toml")]
use tomlcrate::ser::Error as TomlSerError;
use url::ParseError as UrlError;

/// Convience type over `std::result::Result` with `Error` as the error type.
pub type Result<T> = ::std::result::Result<T, Error>;

/// enum of possible errors encountered using the mastodon API.
#[derive(Debug, thiserror::Error, is_enum_variant)]
pub enum Error {
    /// Error from the Mastodon API. This typically means something went
    /// wrong with your authentication or data.
    #[error("API error: status: {status:?}, response:\n{response:#?}")]
    Api {
        /// The response status.
        status: StatusCode,
        /// The JSON-decoded error response from the server.
        response: ApiError,
    },
    /// Error deserialising to json. Typically represents a breaking change in
    /// the Mastodon API
    #[error("error from serde")]
    Serde(#[from] SerdeError),
    /// Error serializing to url-encoded string
    #[error("error serializing to url-encoded string")]
    UrlEncoded(#[from] UrlEncodedError),
    /// Error encountered in the HTTP backend while requesting a route.
    #[error("Error encountered in the HTTP backend while requesting a route.")]
    Http(#[from] HttpError),
    /// Wrapper around the `std::io::Error` struct.
    #[error("io error")]
    Io(#[from] IoError),
    /// Wrapper around the `url::ParseError` struct.
    #[error("error parsing URL")]
    Url(#[from] UrlError),
    /// Missing Client Id.
    #[error("Missing Client Id.")]
    ClientIdRequired,
    /// Missing Client Secret.
    #[error("Missing Client Secret.")]
    ClientSecretRequired,
    /// Missing Access Token.
    #[error("Missing Access Token.")]
    AccessTokenRequired,
    /// MastodonBuilder & AppBuilder error
    #[error("builder required field {0:?} to be constructed")]
    MissingField(&'static str),
    #[cfg(feature = "toml")]
    /// Error serializing to toml
    #[error("Error serializing to toml")]
    TomlSer(#[from] TomlSerError),
    #[cfg(feature = "toml")]
    /// Error deserializing from toml
    #[error("Error deserializing from toml")]
    TomlDe(#[from] TomlDeError),

    #[cfg(any(feature = "toml", feature = "json"))]
    /// Error raised in the helpers::json::to_writer or helpers::toml::to_writer function if not
    /// all bytes were written to the writer
    #[error("Not all bytes were written")]
    NotAllBytesWritten,

    /// Error converting an http header to a string
    #[error("Error converting an http header to a string")]
    HeaderStrError(#[from] HeaderStrError),
    /// Error parsing the http Link header
    #[error("error parsing http link header")]
    LinkHeaderParse(#[from] parse_link_header::Error),
    /// Error returned when an unexpected rel was parsed.
    #[error("unrecognized rel {rel:?} in link header {link:?}")]
    UnrecognizedRel {
        /// The relation which was not recognized
        rel: String,
        /// The raw link header
        link: String,
    },
    #[cfg(feature = "env")]
    /// Error deserializing config from the environment
    #[error("Error deserializing config from the environment")]
    Envy(#[from] EnvyError),
    /// An integer conversion was attempted, but the value didn't fit into the
    /// target type.
    ///
    /// At the time of writing, this can only be triggered when a file is
    /// larger than the system's usize allows.
    #[error("integer didn't fit in the target size")]
    IntConversion(#[from] TryFromIntError),
    /// Error from mastodon-async-entities
    #[error(transparent)]
    Entities(#[from] mastodon_async_entities::error::Error),
    /// Error parsing UTF-8 string from bytes
    #[error(transparent)]
    FromUtf8(#[from] FromUtf8Error),
    /// Other errors
    #[error("other error: {0:?}")]
    Other(String),
}

/// Error returned from the Mastodon API.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ApiError {
    /// The error message.
    pub error: String,
    /// A longer description of the error, mainly provided with the OAuth API.
    pub error_description: Option<String>,
}

impl fmt::Display for ApiError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{self:?}")
    }
}

impl error::Error for ApiError {}

#[macro_export]
/// Used to easily create errors from strings
macro_rules! format_err {
    ( $( $arg:tt )* ) => {
        {
            $crate::Error::Other(format!($($arg)*))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io;

    macro_rules! assert_is {
        ($err:ident, $variant:pat) => {
            assert!(match $err {
                $variant => true,
                _ => false,
            });
        };
    }

    #[tokio::test]
    async fn from_http_error() {
        let err: HttpError = reqwest::get("not an actual URL").await.unwrap_err();
        let err: Error = Error::from(err);
        assert_is!(err, Error::Http(..));
    }

    #[test]
    fn from_io_error() {
        let err: IoError = io::Error::new(io::ErrorKind::Other, "other error");
        let err: Error = Error::from(err);
        assert_is!(err, Error::Io(..));
    }

    #[test]
    fn from_serde_error() {
        let err: SerdeError = serde_json::from_str::<()>("not valid json").unwrap_err();
        let err: Error = Error::from(err);
        assert_is!(err, Error::Serde(..));
    }

    #[test]
    fn from_url_encoded_error() {
        let err: UrlEncodedError = serde_urlencoded::ser::Error::Custom("error".into());
        let err: Error = Error::from(err);
        assert_is!(err, Error::UrlEncoded(..));
    }

    #[test]
    fn from_url_error() {
        let err: UrlError = UrlError::EmptyHost;
        let err: Error = Error::from(err);
        assert_is!(err, Error::Url(..));
    }

    #[cfg(feature = "toml")]
    #[test]
    fn from_toml_de_error() {
        use tomlcrate;
        let err: TomlDeError = tomlcrate::from_str::<()>("not valid toml").unwrap_err();
        let err: Error = Error::from(err);
        assert_is!(err, Error::TomlDe(..));
    }
}