diff options
Diffstat (limited to 'cepis')
| -rw-r--r-- | cepis/client.lua | 42 | ||||
| -rw-r--r-- | cepis/errors.lua | 9 | ||||
| -rw-r--r-- | cepis/models.lua | 86 |
3 files changed, 130 insertions, 7 deletions
diff --git a/cepis/client.lua b/cepis/client.lua index 2a8ae2c..d8a42c9 100644 --- a/cepis/client.lua +++ b/cepis/client.lua @@ -24,7 +24,7 @@ local csv = require("cepis.csv") local M = {} --- Library version, as it appears in the default `User-Agent`. -M.VERSION = "0.1.0" +M.VERSION = "0.2.0" --- Where the API lives unless `base_url` says otherwise. M.DEFAULT_URL = "https://cep.is" @@ -191,6 +191,22 @@ local function error_message(body, fallback) return fallback end +--- The `faixa` of a 404 body, when it carries one. +-- +-- Every step is allowed to give up and return `nil`: the body may not be JSON +-- at all (an edge answering instead of the API), may not carry the block (the +-- API omits it, never nulls it, for a CEP in a band no municipality owns), or +-- may carry something unusable. None of that may turn a clean not-found into a +-- parse failure — the answer to the caller is the same 404 either way, only +-- with less context on it. +local function faixa_from(body) + local ok, data = pcall(json.decode, body or "", 1, nil) + if not ok or type(data) ~= "table" then + return nil + end + return models.new_faixa(data.faixa) +end + local function transport(url) if url:lower():find("^https://") then local ok, https = pcall(require, "ssl.https") @@ -430,6 +446,15 @@ end -- the format already passed local validation, so the server knows something -- this client does not. -- +-- A `not_found` may carry `err.faixa`, a `Faixa` naming the municipality the +-- CEP would have belonged to. It is context for the failure and nothing more — +-- the lookup still failed, and it is `nil` whenever the API omits the block: +-- +-- local address, err = client:lookup("25845000") +-- if not address and err.kind == cepis.NOT_FOUND and err.faixa then +-- print("did you mean a CEP in " .. err.faixa:label() .. "?") +-- end +-- -- @tparam string cep masked or unmasked -- @treturn[1] Address -- @treturn[2] nil @@ -448,12 +473,17 @@ function Client:lookup(cep) end if status == 404 then + local faixa = faixa_from(body) + local message = "CEP " .. number .. " does not exist in the DNE database" + if faixa then + message = message .. ", which would place it in " .. faixa:label() + end return nil, - errors.new( - errors.NOT_FOUND, - "CEP " .. number .. " does not exist in the DNE database", - { cep = number, status = 404 } - ) + errors.new(errors.NOT_FOUND, message, { + cep = number, + status = 404, + faixa = faixa, + }) end if status == 400 then return nil, diff --git a/cepis/errors.lua b/cepis/errors.lua index 0076b95..011014b 100644 --- a/cepis/errors.lua +++ b/cepis/errors.lua @@ -27,6 +27,12 @@ local M = {} M.INVALID_CEP = "invalid_cep" --- The CEP is well-formed but does not exist in the DNE database. +-- +-- The error may carry `faixa`, a `cepis.models.Faixa` naming the municipality +-- that CEP would have belonged to. It is context for the failure, never an +-- address: the lookup failed all the same. It is `nil` when the CEP falls in a +-- band no municipality owns — the API omits the block there rather than +-- sending null. M.NOT_FOUND = "not_found" --- The API (or the edge in front of it) answered 429 on every attempt. @@ -88,7 +94,8 @@ M.Error = Error -- -- @tparam string kind one of the six constants in this module -- @tparam string message human-readable, already complete --- @tparam[opt] table fields extra context: `cep`, `status`, `retry_after` +-- @tparam[opt] table fields extra context: `cep`, `status`, `retry_after`, +-- `faixa` -- @treturn table the error, with `tostring()` yielding `message` function M.new(kind, message, fields) local err = setmetatable({}, Error) diff --git a/cepis/models.lua b/cepis/models.lua index 61ad678..a433369 100644 --- a/cepis/models.lua +++ b/cepis/models.lua @@ -185,6 +185,92 @@ function M.new_address(data) return address end +--- The municipality a CEP that does not exist would have belonged to. +-- +-- This is **context for an error, never an address**. It arrives on the 404 of +-- `Client:lookup` and hangs off the `not_found` error as `err.faixa`; the +-- lookup still failed, and nothing here says otherwise. The CEP asked for does +-- not exist. What the API is saying is only *where it would have been*. +-- +-- It exists because a CEP that fails is rarely a random number: it is usually +-- someone who knows the city and guessed the digits, and the classic guess is +-- the `-000` that 957 municipalities never received. Knowing the guess landed +-- on Areal/RJ turns "CEP does not exist" into a message the user can act on. +-- +-- **`cep_ini` and `cep_fim` are the smallest and the largest CEP that actually +-- exist in that municipality — not the bounds the Correios publish.** For +-- Areal the DNE's own range starts at `25845000`, which is precisely the CEP +-- that just failed; the range reported here starts at `25845001`, the first +-- one really in the base. +-- +-- **The range is sparse.** Areal's 118 CEPs are spread over the roughly 4,900 +-- numbers between `25845001` and `25849899`. That is what `ceps` is for: the +-- pair is an interval, never a list of valid CEPs, and picking a number +-- between them has about one chance in forty of existing. +-- +-- @type Faixa +-- @field localidade string the municipality +-- @field uf string +-- @field estado string +-- @field regiao string +-- @field ibge string +-- @field cep_ini string smallest CEP that exists there, 8 digits +-- @field cep_fim string largest CEP that exists there, 8 digits +-- @field ceps number how many exist between the two — far fewer than the span +local Faixa = {} +Faixa.__index = Faixa +Faixa.__name = "cepis.Faixa" + +--- The municipality and its UF: `Areal/RJ`. +-- +-- The one-line form for a message shown to a person, so it does not become a +-- repeated concatenation in every project that uses this. +-- @treturn string +function Faixa:label() + if self.uf == "" then + return self.localidade + end + return self.localidade .. "/" .. self.uf +end + +M.Faixa = Faixa + +--- Build a `Faixa` from the `faixa` block of a 404 body, or return `nil`. +-- +-- The API **omits** the block, never sends it as null, when the CEP falls in a +-- band no municipality owns. So `nil` here is the ordinary case, not a +-- failure. +-- +-- Anything unusable — absent, not an object, without a municipality to name — +-- is also `nil`. That is deliberate and it is the whole point of this +-- function: the caller is already holding a not-found, and a malformed piece +-- of context must never be allowed to turn that clean answer into a parse +-- error. +-- +-- @tparam[opt] table block the decoded `faixa` object +-- @treturn Faixa|nil +function M.new_faixa(block) + if type(block) ~= "table" then + return nil + end + + local localidade = text(block.localidade) + if localidade == "" then + return nil + end + + return setmetatable({ + localidade = localidade, + uf = text(block.uf), + estado = text(block.estado), + regiao = text(block.regiao), + ibge = text(block.ibge), + cep_ini = text(block.cep_ini), + cep_fim = text(block.cep_fim), + ceps = integer(block.ceps), + }, Faixa) +end + --- The response of `GET /health`. -- -- Good for two things: checking that the API answers, and knowing **which |