/// Exception hierarchy. /// /// Everything derives from [CepisException], so callers who only care that /// *something* went wrong can catch one type. It is `sealed`, so a `switch` /// over it is checked for exhaustiveness by the compiler. /// /// Below that, the split is by **the decision the caller has to make**, not by /// the HTTP status that caused it: /// /// - [InvalidCepException] and [CepNotFoundException] are problems with the /// *request*. Retrying is pointless — the same CEP will fail again. /// - [NetworkException], [ServerException] and [RateLimitedException] are /// problems with *transport*. Retrying may help, and the client already /// retried on its own before throwing any of them. /// /// That split is why [CepNotFoundException] is not "the HTTP 404 exception": /// the client also throws [InvalidCepException] without making any request at /// all, when the CEP is malformed before it ever leaves the machine. library; import 'models.dart'; /// Base of every error this library throws. Catch this to handle any failure. /// /// Sealed: the subtypes below are the complete set, so a `switch` on one is /// exhaustive without a default branch. sealed class CepisException implements Exception { /// Creates an exception carrying [message]. const CepisException(this.message); /// Human-readable description of what went wrong. final String message; @override String toString() => '$runtimeType: $message'; } /// The CEP is not 8 digits. /// /// Deliberately also a [FormatException]: this is a malformed-input error, and /// code that already handles `FormatException` while parsing user input keeps /// working without knowing this library exists. /// /// Usually thrown **without** a request ever being made — the client checks the /// format before going to the network. The one exception is a `400` from the /// API, which carries the API's own message: the value already passed local /// validation, so the server knows something this client does not. final class InvalidCepException extends CepisException implements FormatException { /// Creates the error for [message], optionally recording the [source] value /// that was rejected. const InvalidCepException(super.message, {this.source, this.offset}); /// The value that was rejected, as the caller supplied it. @override final Object? source; /// Always `null`; present only to satisfy [FormatException]. @override final int? offset; } /// The CEP is well-formed but does not exist in the DNE database. /// /// [faixa] may name the municipality the CEP would have belonged to. It does /// not soften the error: this is still a 404, and there is still no address. final class CepNotFoundException extends CepisException { /// Creates the error for [message], recording the normalised [cep] and the /// [faixa] the API named, when it named one. const CepNotFoundException(super.message, {this.cep, this.faixa}); /// The CEP that was looked up, normalised to 8 digits. final String? cep; /// The municipality that owns the numeric band this CEP fell in, or `null`. /// /// `null` when the CEP falls in a band no municipality owns — the API /// **omits** the block there rather than sending null — and also whenever the /// block cannot be read. See [Faixa], and read [Faixa.cepIni]/[Faixa.cepFim] /// as the extremes that exist, not as a range of valid CEPs. final Faixa? faixa; /// Mentions the municipality when the API named one. /// /// The count comes along deliberately: `118 CEPs` across nearly five /// thousand numbers is what stops the two bounds from reading like a range /// where anything in between would work. @override String toString() { final band = faixa; if (band == null) { return super.toString(); } return '$runtimeType: $message — that band belongs to ' '${band.localidade}/${band.uf}, which has ${band.ceps} CEPs scattered ' 'between ${band.cepIni} and ${band.cepFim}'; } } /// The request never completed: DNS, refused connection, timeout, TLS. /// /// Thrown only after every configured attempt has failed. final class NetworkException extends CepisException { /// Creates the error for [message], recording the underlying [cause]. const NetworkException(super.message, {this.cause}); /// The transport error that ended the last attempt, when there was one. final Object? cause; } /// The API answered 5xx on every attempt. final class ServerException extends CepisException { /// Creates the error for [message] and the [status] seen last. const ServerException(super.message, {required this.status}); /// The HTTP status seen on the last attempt. final int status; } /// The API (or the edge in front of it) answered 429. /// /// [retryAfter] is what the server asked for, or `null` if it did not say. The /// client already honoured that value across the attempts it made — capped at /// the client's `backoffMax` — and it is exposed here for callers who want to /// reschedule the work. final class RateLimitedException extends CepisException { /// Creates the error for [message] and the server's [retryAfter], if any. const RateLimitedException(super.message, {this.retryAfter}); /// How long the server asked the caller to wait, or `null` if it did not say. final Duration? retryAfter; } /// 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. final class InvalidResponseException extends CepisException { /// Creates the error for [message]. const InvalidResponseException(super.message); }