summaryrefslogtreecommitdiffstats
path: root/src/error.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/error.rs')
-rw-r--r--src/error.rs150
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,
+ }
+ }
+}