--- 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