//! 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 = std::result::Result; /// 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, }, /// 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>, }, /// 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) -> Self { Error::InvalidCep { message: message.into(), } } pub(crate) fn invalid_response(message: impl Into) -> Self { Error::InvalidResponse { message: message.into(), } } pub(crate) fn network( message: impl Into, source: Option>, ) -> 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, } } }