diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-27 20:33:24 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-27 20:33:24 -0300 |
| commit | d7ca2d6f4b1c5c1f5f1b6ce6ffad35aaffee5b0f (patch) | |
| tree | 21aca875c28f4d7d9ec3b0a807e7c7f15a6766d8 /Sources/Cepis/Client.swift | |
| parent | 72a4b451c9d06f7a97cac2f43f1b7f52fafcd3b7 (diff) | |
| download | swift-d7ca2d6f4b1c5c1f5f1b6ce6ffad35aaffee5b0f.tar.gz | |
A CEP that fails a lookup is rarely a random number: it is almost always
someone who knows the city and guessed the digits. The classic guess is
the -000, which 957 municipalities never received. Answering that with a
bare "not found" throws away what the API knows — that the number is in
Areal/RJ's band — and leaves the caller unable to tell a typo apart from
a city that has no such CEP.
The 404 now carries an optional faixa, so cepNotFound carries it too. It
decorates the error and never replaces it: the CEP still does not exist
and the call still throws. Parsing is deliberately incapable of
escalating — a faixa that is absent, null, empty or malformed becomes
nil, because losing the decoration must never cost the clean not-found
underneath it.
Faixa offers no contains(): cep_ini and cep_fim are the smallest and
largest CEP that actually exist, not the bounds the Correios publish,
and the range between them is sparse — Areal's 118 CEPs are spread over
some 4,900 numbers. Only a lookup can answer whether a CEP exists, and
an API that appeared to answer it from the pair would be lying.
Widening cepNotFound's payload breaks anyone matching the case with a
single binding, hence 0.2.0.
Diffstat (limited to 'Sources/Cepis/Client.swift')
| -rw-r--r-- | Sources/Cepis/Client.swift | 41 |
1 files changed, 37 insertions, 4 deletions
diff --git a/Sources/Cepis/Client.swift b/Sources/Cepis/Client.swift index e0d5135..f24effb 100644 --- a/Sources/Cepis/Client.swift +++ b/Sources/Cepis/Client.swift @@ -74,7 +74,7 @@ public struct Client: @unchecked Sendable { public static let maxBatch = 500 /// This library's version, as it appears in the default `User-Agent`. - public static let version = "0.1.0" + public static let version = "0.2.0" /// A shared client with the default configuration. /// @@ -176,9 +176,16 @@ public struct Client: @unchecked Sendable { /// the API's own message: the format already passed local validation, so /// the server knows something this client does not. /// + /// A 404 may carry a ``Faixa``: the municipality the CEP *would* have + /// belonged to, on + /// ``CepisError/cepNotFound(message:faixa:)``. It is context for the error + /// and nothing more — the CEP still does not exist, and the call still + /// throws. + /// /// - Throws: ``CepisError/invalidCEP(_:)`` when the CEP is not 8 digits, - /// before any request is made; ``CepisError/cepNotFound(_:)`` when it is - /// well-formed but absent from the database; + /// before any request is made; + /// ``CepisError/cepNotFound(message:faixa:)`` when it is well-formed but + /// absent from the database; /// ``CepisError/rateLimited(message:retryAfter:)``, /// ``CepisError/serverError(message:status:)`` or /// ``CepisError/networkError(message:)`` once every attempt has failed. @@ -187,7 +194,9 @@ public struct Client: @unchecked Sendable { let response = try await perform(path: "/\(number)") if response.status == 404 { - throw CepisError.cepNotFound("CEP \(number) does not exist in the DNE database") + throw CepisError.cepNotFound( + message: "CEP \(number) does not exist in the DNE database", + faixa: Client.faixa(response.body)) } if response.status == 400 { throw CepisError.invalidCEP( @@ -420,6 +429,30 @@ public struct Client: @unchecked Sendable { } } + /// Reads the optional `faixa` out of a 404 body, or gives up quietly. + /// + /// Every way this can go wrong ends in `nil`: a body that is not JSON, a + /// `faixa` that is absent, null, empty or malformed. That is deliberate. + /// The caller is already throwing ``CepisError/cepNotFound(message:faixa:)`` + /// and the faixa is only a decoration on it; letting a decode failure here + /// escalate would swap a clean not-found for an + /// ``CepisError/invalidResponse(_:)`` and lose the real answer. + private static func faixa(_ body: Data) -> Faixa? { + struct NotFound: Decodable { + let faixa: Faixa? + + enum CodingKeys: String, CodingKey { + case faixa + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.faixa = (try? container.decodeIfPresent(Faixa.self, forKey: .faixa)) ?? nil + } + } + return (try? JSONDecoder().decode(NotFound.self, from: body))?.faixa + } + private static func apiMessage(_ body: Data, fallback: String) -> String { struct APIError: Decodable { let mensagem: String? |