summaryrefslogtreecommitdiffstats
path: root/Sources/Cepis/Client.swift
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-27 20:33:24 -0300
committerMurilo Ijanc <[email protected]>2026-07-27 20:33:24 -0300
commitd7ca2d6f4b1c5c1f5f1b6ce6ffad35aaffee5b0f (patch)
tree21aca875c28f4d7d9ec3b0a807e7c7f15a6766d8 /Sources/Cepis/Client.swift
parent72a4b451c9d06f7a97cac2f43f1b7f52fafcd3b7 (diff)
downloadswift-0.2.0.tar.gz
Carry the municipality on a not-found CEPHEADv0.2.0main
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.swift41
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?