diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:42:08 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:42:08 -0300 |
| commit | 7fd555fcaf4304cb0dff3e8f5661565c3618299e (patch) | |
| tree | 61199793c3046d73b97e9822895ede2a33034d41 /cepis/models.lua | |
| download | lua-7fd555fcaf4304cb0dff3e8f5661565c3618299e.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'cepis/models.lua')
| -rw-r--r-- | cepis/models.lua | 226 |
1 files changed, 226 insertions, 0 deletions
diff --git a/cepis/models.lua b/cepis/models.lua new file mode 100644 index 0000000..61ad678 --- /dev/null +++ b/cepis/models.lua @@ -0,0 +1,226 @@ +--- The data the API returns, as tables. +-- +-- **Field names are the API's own, in Portuguese** — `logradouro`, `bairro`, +-- `localidade`. Everything else in this library is English; the fields are the +-- one deliberate exception. +-- +-- The reason is fidelity: what you read in the JSON at https://cep.is/01001000 +-- is exactly what you index on the table. Translating to `street`/`city` would +-- mean maintaining a field-by-field mapping and forcing anyone reading the API +-- docs alongside this library to translate back in their head. The values are +-- Portuguese either way — they come from the Correios database. +-- +-- @module cepis.models + +local M = {} + +local function text(value) + if value == nil then + return "" + end + if type(value) == "string" then + return value + end + if type(value) == "table" then + return "" + end + return tostring(value) +end + +local function integer(value) + local number = tonumber(value) + if not number then + return 0 + end + if number < 0 then + return -math.floor(-number) + end + return math.floor(number) +end + +--- The CEP's coordinate, when the database has one. +-- +-- The point is the **median** of the coordinates of that CEP's addresses, not +-- the mean: one badly geocoded address 40 km away shifts a mean and does not +-- move a median. +-- +-- Always check `precisao` before using `lat`/`lon` for distance: +-- +-- - `"logradouro"`: the point is a stretch of street. `raio_m` lands around +-- 100–150 m. Good for maps and for distance. +-- - `"localidade"`: the CEP *is* the whole municipality (a city-wide general +-- CEP), and the point is its centroid. `raio_m` runs from 7 km to 40 km. +-- Computing shipping from this is wrong by tens of kilometres. +-- +-- `raio_m` is the radius containing 90% of that CEP's addresses. It exists +-- precisely so that error cannot pass unnoticed. +-- +-- @type Geo +-- @field lat number +-- @field lon number +-- @field raio_m number +-- @field precisao string `"logradouro"` or `"localidade"` +local Geo = {} +Geo.__index = Geo +Geo.__name = "cepis.Geo" + +--- True when the coordinate points at a street, not a municipality. +-- +-- Shorthand for `precisao == "logradouro"`, which is the check to make before +-- using the point to measure distance. +-- @treturn boolean +function Geo:is_precise() + return self.precisao == "logradouro" +end + +M.Geo = Geo + +local function build_geo(block) + if type(block) ~= "table" then + return nil + end + + local lat = tonumber(block.lat) + local lon = tonumber(block.lon) + local raio = tonumber(block.raio_m) + local precisao = block.precisao + + if not lat or not lon or not raio then + return nil + end + if precisao == nil or type(precisao) == "table" then + return nil + end + + return setmetatable({ + lat = lat + 0.0, + lon = lon + 0.0, + raio_m = integer(raio), + precisao = tostring(precisao), + }, Geo) +end + +--- A CEP and everything the database knows about it. +-- +-- Fields the database does not have for that CEP come back as empty strings, +-- never `nil` — that is what the API returns, and changing it here would +-- create a difference between reading the JSON and reading this library. +-- +-- `geo` is the exception: it is `nil` when there is no coordinate. There the +-- difference is real, because the API **omits** the whole field in that case +-- (about 43% of CEPs) rather than sending null. +-- +-- `raw` holds the response exactly as it arrived, uninterpreted, so a field +-- the API grows before this library is updated is not lost. +-- +-- @type Address +-- @field cep string always the 8 digits, unmasked +-- @field logradouro string +-- @field complemento string +-- @field unidade string +-- @field bairro string +-- @field localidade string +-- @field uf string +-- @field estado string +-- @field regiao string +-- @field ibge string +-- @field gia string +-- @field ddd string +-- @field siafi string +-- @field geo Geo|nil +-- @field raw table the decoded response, untouched +local Address = {} +Address.__index = Address +Address.__name = "cepis.Address" + +--- The CEP with its mask: `01001-000`. +-- +-- The native contract always returns it unmasked; formatting is presentation, +-- and it lives here so it does not become a repeated `string.format` in every +-- project that uses this. +-- @treturn string +function Address:formatted_cep() + if #self.cep ~= 8 then + return self.cep + end + return self.cep:sub(1, 5) .. "-" .. self.cep:sub(6) +end + +M.Address = Address + +local FIELDS = { + "cep", + "logradouro", + "complemento", + "unidade", + "bairro", + "localidade", + "uf", + "estado", + "regiao", + "ibge", + "gia", + "ddd", + "siafi", +} + +--- Build an `Address` from the decoded response. +-- +-- Unknown fields are ignored during construction but preserved in `raw`. A new +-- field in the API therefore cannot break this library. +-- +-- A malformed `geo` block yields `geo = nil` rather than failing the whole +-- call: it is not worth losing the address over, since the rest of the +-- response is still useful. +-- +-- @tparam table data decoded JSON object, or a CSV row keyed by column name +-- @treturn Address +function M.new_address(data) + local address = setmetatable({}, Address) + for _, name in ipairs(FIELDS) do + address[name] = text(data[name]) + end + address.geo = build_geo(data.geo) + address.raw = data + return address +end + +--- The response of `GET /health`. +-- +-- Good for two things: checking that the API answers, and knowing **which +-- version of the database** is live. The DNE database changes every two weeks; +-- `dne` identifies it. +-- +-- @type Health +-- @field status string `"ok"` when the API reports itself healthy +-- @field dne string identifier of the fortnightly database +-- @field ceps number how many CEPs the base holds +-- @field geo number how many of them carry a coordinate +-- @field base_gerada_em string when the base was generated, RFC 3339 +local Health = {} +Health.__index = Health +Health.__name = "cepis.Health" + +--- True when the API reports itself healthy. +-- @treturn boolean +function Health:ok() + return self.status == "ok" +end + +M.Health = Health + +--- Build a `Health` from the decoded response. +-- @tparam table data decoded JSON object +-- @treturn Health +function M.new_health(data) + return setmetatable({ + status = text(data.status), + dne = text(data.dne), + ceps = integer(data.ceps), + geo = integer(data.geo), + base_gerada_em = text(data.base_gerada_em), + raw = data, + }, Health) +end + +return M |