//! 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. //! //! [`Error::NotFound`] may carry a [`Faixa`] — the municipality the CEP would //! have belonged to. That is context on the failure and changes nothing about //! it: the answer is still 404 and the CEP still does not exist. use std::error::Error as StdError; use std::fmt; use std::time::Duration; use crate::model::Faixa; /// 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 municipality this CEP *would* belong to, when the API knows it. /// /// Still an error, and still a 404 — see [`Faixa`] before showing any /// of it to a user. `None` when the CEP falls in a band no municipality /// owns, and also whenever the field cannot be read: an unparseable /// `faixa` never costs the not-found. /// /// Boxed to keep [`Error`] small: it travels in the `Err` of every /// [`Result`] in this crate, including the ones that never carry a /// `faixa`. [`Error::faixa`] hides the box at the call site. faixa: Option>, }, /// 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 { .. } ) } /// The municipality of a [`Error::NotFound`], when the API named one. /// /// `None` for every other variant, and for a not-found the API could not /// place. It is context for the failure, not a result: the CEP asked for /// still does not exist. /// /// ```no_run /// # let cep_from_form = "25845000"; /// if let Err(err) = cepis::lookup(cep_from_form) { /// match err.faixa() { /// Some(faixa) => println!("no such CEP in {}", faixa.municipality()), /// None => println!("no such CEP"), /// } /// } /// ``` pub fn faixa(&self) -> Option<&Faixa> { match self { Error::NotFound { faixa, .. } => faixa.as_deref(), _ => None, } } 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, faixa: None } => { write!(f, "CEP {cep} does not exist in the DNE database") } Error::NotFound { cep, faixa: Some(faixa), } => write!( f, "CEP {cep} does not exist in the DNE database (it would be in {})", faixa.municipality() ), 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, } } }