diff options
Diffstat (limited to 'lib/cepis/models.rb')
| -rw-r--r-- | lib/cepis/models.rb | 85 |
1 files changed, 85 insertions, 0 deletions
diff --git a/lib/cepis/models.rb b/lib/cepis/models.rb index a0181ff..a80fe37 100644 --- a/lib/cepis/models.rb +++ b/lib/cepis/models.rb @@ -145,6 +145,91 @@ module Cepis end end + # The municipality that owns the band a **nonexistent** CEP falls into. + # + # It is context for an error, never an address: it arrives on the 404 of + # {Client#lookup}, hanging off {CEPNotFound#faixa}, and says "this CEP does + # not exist, but the numbers around it belong to Areal/RJ". The lookup still + # failed — nothing here turns it into a result. + # + # It is worth having because a CEP that fails is rarely a random number: it + # is usually someone who knows the city and guessed the digits. The classic + # guess is the `-000`, which 957 municipalities never received. + # + # **{#cep_ini} and {#cep_fim} are the smallest and the largest CEP that + # actually exist in the municipality, not the bounds the Correios publish.** + # The DNE's own start for Areal is `25845000` — exactly the CEP that just + # failed, which is why the published bounds would be useless here. + # + # **The band is sparse.** Areal's 118 CEPs are scattered across the ~4,900 + # numbers between `25845001` and `25849899`. {#ceps} is there to say so. Do + # not present the pair as a range of valid CEPs, and do not iterate it: all + # but a fiftieth of it does not exist. + class Faixa + attr_reader :localidade, :uf, :estado, :regiao, :ibge, :cep_ini, :cep_fim, :ceps + + def initialize(localidade:, uf:, estado:, regiao:, ibge:, cep_ini:, cep_fim:, ceps:) + @localidade = localidade + @uf = uf + @estado = estado + @regiao = regiao + @ibge = ibge + @cep_ini = cep_ini + @cep_fim = cep_fim + @ceps = ceps + freeze + end + + # Build from the hash the API returned, or `nil` when the block is + # missing, empty or malformed. + # + # The API **omits** `faixa` — it never sends null — for a CEP falling in a + # band no municipality owns, so absence is the ordinary case and not an + # anomaly. Anything unreadable is treated as absence too: this is a bonus + # on top of a failure that has already happened, and it must never be the + # reason a clean not-found becomes something else. + def self.from_json(data) + return nil unless data.is_a?(Hash) + + block = data.transform_keys(&:to_s) + localidade = String(block["localidade"] || "").strip + return nil if localidade.empty? + + new( + localidade: localidade, + uf: String(block["uf"] || ""), + estado: String(block["estado"] || ""), + regiao: String(block["regiao"] || ""), + ibge: String(block["ibge"] || ""), + cep_ini: String(block["cep_ini"] || ""), + cep_fim: String(block["cep_fim"] || ""), + ceps: Integer(block["ceps"] || 0, exception: false) || 0 + ) + rescue TypeError, ArgumentError + nil + end + + def to_h + { + localidade: @localidade, uf: @uf, estado: @estado, regiao: @regiao, + ibge: @ibge, cep_ini: @cep_ini, cep_fim: @cep_fim, ceps: @ceps + } + end + + def ==(other) + other.is_a?(Faixa) && to_h == other.to_h + end + alias eql? == + + def hash + to_h.hash + end + + def to_s + "#<Cepis::Faixa #{@localidade}/#{@uf} #{@cep_ini}-#{@cep_fim} #{@ceps} CEPs>" + end + end + # The response of `GET /health`. # # Good for two things: checking that the API answers, and knowing **which |