diff options
Diffstat (limited to 'src/error.rs')
| -rw-r--r-- | src/error.rs | 150 |
1 files changed, 150 insertions, 0 deletions
diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..40a3049 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,150 @@ +//! Error type. +//! +//! Everything the library can fail with is one [`Error`], so a caller who only +//! cares that *something* went wrong matches one type and is done. +//! +//! Below that, the split is by **the decision the caller has to make**, not by +//! the HTTP status that caused it: +//! +//! - [`Error::InvalidCep`] and [`Error::NotFound`] are problems with the +//! *request*. Retrying is pointless — the same CEP will fail again. +//! - [`Error::Network`], [`Error::Server`] and [`Error::RateLimited`] are +//! problems with *transport*. Retrying may help, and the client already +//! retried on its own before returning any of them. +//! +//! That split is why `NotFound` is not "the HTTP 404 variant": the client also +//! returns `InvalidCep` without making any request at all, when the CEP is +//! malformed before it ever leaves the machine. + +use std::error::Error as StdError; +use std::fmt; +use std::time::Duration; + +/// The result of every fallible operation in this crate. +pub type Result<T> = std::result::Result<T, Error>; + +/// Everything that can go wrong when talking to the API. +/// +/// Use [`Error::is_retriable`] when the choice is simply "give up or try again +/// later"; match on the variants when the caller needs to tell a bad CEP from a +/// bad network. +#[derive(Debug)] +#[non_exhaustive] +pub enum Error { + /// The CEP is not 8 digits. + /// + /// Usually returned **without** a request ever being made — the client + /// checks the format before going to the network. It is also what a 400 + /// from the API becomes, in which case the message is the API's own. + InvalidCep { + /// Human-readable explanation, including the offending value. + message: String, + }, + + /// The CEP is well-formed but does not exist in the DNE database. + NotFound { + /// The CEP that was looked up, unmasked. + cep: String, + }, + + /// The API (or the edge in front of it) answered 429 on every attempt. + RateLimited { + /// Human-readable explanation. + message: String, + /// What the server asked for via `Retry-After`, or `None` if it did not + /// say. The client already honoured it across the attempts it made; it + /// is exposed here for callers who want to reschedule the work. + retry_after: Option<Duration>, + }, + + /// The API answered 5xx on every attempt. + Server { + /// Human-readable explanation. + message: String, + /// The HTTP status seen on the last attempt. + status: u16, + }, + + /// The request never completed: DNS, refused connection, timeout, TLS. + /// + /// Returned only after every configured attempt has failed. + Network { + /// Human-readable explanation. + message: String, + /// The underlying transport error, when there is one. + source: Option<Box<dyn StdError + Send + Sync>>, + }, + + /// A success status came back with a body that cannot be parsed. + /// + /// In practice this means something between you and the API is answering + /// instead of it — a captive Wi-Fi portal, a corporate proxy, an HTML error + /// page. Useful as a diagnosis: the problem is not in your code. + InvalidResponse { + /// Human-readable explanation. + message: String, + }, +} + +impl Error { + /// Whether trying the same call again later could plausibly succeed. + /// + /// `true` for [`Error::RateLimited`], [`Error::Server`] and + /// [`Error::Network`]; `false` for a bad or unknown CEP, which fails the + /// same way forever. + /// + /// The client has already retried internally by the time any of these are + /// returned, so "later" means later — not immediately. + pub fn is_retriable(&self) -> bool { + matches!( + self, + Error::RateLimited { .. } | Error::Server { .. } | Error::Network { .. } + ) + } + + pub(crate) fn invalid_cep(message: impl Into<String>) -> Self { + Error::InvalidCep { + message: message.into(), + } + } + + pub(crate) fn invalid_response(message: impl Into<String>) -> Self { + Error::InvalidResponse { + message: message.into(), + } + } + + pub(crate) fn network( + message: impl Into<String>, + source: Option<Box<dyn StdError + Send + Sync>>, + ) -> Self { + Error::Network { + message: message.into(), + source, + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::InvalidCep { message } => f.write_str(message), + Error::NotFound { cep } => { + write!(f, "CEP {cep} does not exist in the DNE database") + } + Error::RateLimited { message, .. } => f.write_str(message), + Error::Server { message, .. } => f.write_str(message), + Error::Network { message, .. } => f.write_str(message), + Error::InvalidResponse { message } => f.write_str(message), + } + } +} + +impl StdError for Error { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + Error::Network { source, .. } => source.as_ref().map(|e| e.as_ref() as &dyn StdError), + _ => None, + } + } +} |