diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-27 20:32:01 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-27 20:32:01 -0300 |
| commit | b402de683eecef4c890081dd72edbcaf57f375e0 (patch) | |
| tree | 13e184ad13b3cad394e5c84c25b48df3b2052781 /lib/cepis/client.rb | |
| parent | 36a0ddb01a4fcb8fb2fefd7c407ecca0e3e56e7c (diff) | |
| download | ruby-84a6d224754979f76ce590e0175b9b939438a36f.tar.gz | |
A CEP that fails a lookup is rarely a random number: it is someone who
knows the city and guessed the digits, most often the -000 that 957
municipalities never received. The API now answers that 404 with an
optional faixa naming the municipality those numbers belong to, so the
caller can say "not found, but you seem to be looking at Areal/RJ"
instead of just "not found".
It stays an error. Faixa hangs off CEPNotFound and nothing about it
promotes a nonexistent CEP into an address. Its cep_ini/cep_fim are the
smallest and largest CEP that really exist there, not the published
bounds — the DNE's own start for Areal is the CEP that just failed — and
the band is sparse, which is what ceps is for; both facts are spelled
out in the docs so nobody iterates the pair.
Parsing is deliberately toothless: a missing, empty or malformed faixa,
or a 404 body that is not JSON at all, all yield nil. Losing the
consolation prize must never replace a clean not-found with a parse
failure.
Diffstat (limited to 'lib/cepis/client.rb')
| -rw-r--r-- | lib/cepis/client.rb | 41 |
1 files changed, 39 insertions, 2 deletions
diff --git a/lib/cepis/client.rb b/lib/cepis/client.rb index f451061..8480315 100644 --- a/lib/cepis/client.rb +++ b/lib/cepis/client.rb @@ -113,10 +113,15 @@ module Cepis # message: the format already passed local validation, so the server knows # something this client does not. # + # A 404 may carry {CEPNotFound#faixa}: the municipality that owns the band + # of numbers the CEP fell into. It is still a 404 and still an exception — + # the context does not make a nonexistent CEP into an address. See {Faixa}. + # # @param cep [String] masked or unmasked. # @return [Address] # @raise [InvalidCEP] the CEP is not 8 digits (no request is made). - # @raise [CEPNotFound] well-formed, but not in the database. + # @raise [CEPNotFound] well-formed, but not in the database. Carries + # {CEPNotFound#faixa} when the API named a municipality for the band. # @raise [RateLimited] 429 after every attempt. # @raise [ServerError] 5xx after every attempt. # @raise [NetworkError] the request never completed. @@ -125,7 +130,7 @@ module Cepis number = Cepis.normalize_cep(cep) status, body = request("/#{number}") - raise CEPNotFound, "CEP #{number} does not exist in the DNE database" if status == 404 + raise not_found(number, body) if status == 404 raise InvalidCEP, error_message(body, "CEP #{number} rejected") if status == 400 Address.from_json(parse_json(body)) @@ -219,6 +224,38 @@ module Cepis body.to_s.dup.force_encoding(Encoding::UTF_8).scrub end + # The {CEPNotFound} for a 404, with the band read off the body when it is + # there. + # + # Every step of reading it is allowed to fail into `nil`: an absent + # `faixa` is the ordinary case (the API omits it, it never sends null), + # and a body that is not JSON at all is a proxy's doing, not the caller's. + # Neither may promote a definitive, well-understood not-found into an + # {InvalidResponse} — the reason the lookup failed would be replaced by + # the reason the consolation prize failed to parse. + def not_found(number, body) + faixa = begin + data = JSON.parse(decode(body)) + data.is_a?(Hash) ? Faixa.from_json(data["faixa"]) : nil + rescue JSON::ParserError + nil + end + + CEPNotFound.new(not_found_message(number, faixa), faixa: faixa) + end + + # The message names the municipality when the API named one, because + # "does not exist" plus "the numbers around it are Areal/RJ" is what makes + # the log line diagnosable. The counts stay in the message too, so that + # nobody reads the two CEPs as the ends of a contiguous range. + def not_found_message(number, faixa) + base = "CEP #{number} does not exist in the DNE database" + return base if faixa.nil? + + "#{base} (it falls in the band of #{faixa.localidade}/#{faixa.uf}, which has " \ + "#{faixa.ceps} CEP(s) scattered between #{faixa.cep_ini} and #{faixa.cep_fim})" + end + def error_message(body, fallback) data = JSON.parse(decode(body)) data.is_a?(Hash) && data["mensagem"] ? String(data["mensagem"]) : fallback |