diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-27 20:38:27 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-27 20:38:27 -0300 |
| commit | 399eac351b1bc29d5047dbac96486c281be1b05c (patch) | |
| tree | 81151d5bf139f07096a8f38313d64482250163c2 /R | |
| parent | 11d49bc25eb1643f1b742f9527212d8781f2e0de (diff) | |
| download | r-0.2.0.tar.gz | |
A CEP that fails a lookup is rarely a random number: it is usually someone
who knows the city and guessed, and the classic guess is the -000 that 957
municipalities never received. The API now answers those 404s with a faixa
naming the municipality that owns the band the number fell into, which is
the difference between telling a user "invalid CEP" and "no such CEP in
Areal/RJ".
It is context for an error and never an address, so it rides on the
cepis_not_found condition instead of turning the failure into a success,
and cep_ini/cep_fim are the endpoints that exist rather than the range the
Correios publish - the interval between them is sparse, which is what ceps
counts. Parsing is deliberately unable to signal: a faixa that is null, an
array, a string or half a body leaves a clean not-found behind, because the
caller asked for the CEP, not for the decoration on its absence.
Diffstat (limited to 'R')
| -rw-r--r-- | R/api.R | 16 | ||||
| -rw-r--r-- | R/conditions.R | 30 | ||||
| -rw-r--r-- | R/http.R | 14 | ||||
| -rw-r--r-- | R/models.R | 102 |
4 files changed, 147 insertions, 15 deletions
@@ -13,7 +13,10 @@ #' @section Conditions: #' \describe{ #' \item{`cepis_invalid_cep`}{the CEP is not 8 digits. No request is made.} -#' \item{`cepis_not_found`}{well-formed, but not in the database.} +#' \item{`cepis_not_found`}{well-formed, but not in the database. Carries +#' `faixa`, a [cepis_faixa] naming the municipality whose range the CEP +#' fell into, when it fell into one. Still a not-found: the CEP does not +#' exist, and a `faixa` is not an address.} #' \item{`cepis_rate_limited`}{429 on every attempt.} #' \item{`cepis_server_error`}{5xx on every attempt.} #' \item{`cepis_network_error`}{the request never completed.} @@ -38,10 +41,13 @@ cep_lookup <- function(cep, client = default_client()) { response <- cepis_fetch(client, paste0("/", number)) if (response$status == 404L) { - stop(not_found_error( - paste0("CEP ", number, " does not exist in the DNE database"), - cep = number - )) + faixa <- new_faixa(json_field(response$body, "faixa")) + message <- paste0("CEP ", number, " does not exist in the DNE database") + if (!is.null(faixa)) { + message <- paste0(message, "; the range it falls in belongs to ", + faixa$localidade, "/", faixa$uf) + } + stop(not_found_error(message, cep = number, faixa = faixa)) } if (response$status == 400L) { stop(invalid_cep_error( diff --git a/R/conditions.R b/R/conditions.R index 1eebe5b..7cdb45b 100644 --- a/R/conditions.R +++ b/R/conditions.R @@ -38,6 +38,32 @@ #' proxy, an HTML error page. Useful as a diagnosis: the problem is not in #' your code. #' +#' @section What cepis_not_found carries: +#' `cep`, the 8 digits as they went to the API, and `faixa`: a [cepis_faixa] +#' naming the municipality that owns the band of numbers the CEP fell into, +#' or `NULL` when it fell in a band nobody owns. The API omits it, it never +#' sends null, so `NULL` here is the same statement. +#' +#' It changes nothing about the answer — the CEP still does not exist, and a +#' `faixa` is not an address — but it is usually the difference between +#' telling someone "invalid CEP" and telling them "no such CEP in Areal/RJ": +#' +#' ```r +#' tryCatch( +#' cep_lookup("25845000"), +#' cepis_not_found = function(e) { +#' if (is.null(e$faixa)) { +#' "no such CEP" +#' } else { +#' paste0("no such CEP in ", e$faixa$localidade, "/", e$faixa$uf) +#' } +#' } +#' ) +#' ``` +#' +#' The CEP that failed above is the DNE's own first number for Areal, and it +#' is the shape of guess this field is for. +#' #' @name cepis_conditions NULL @@ -52,8 +78,8 @@ invalid_cep_error <- function(message, value = NULL) { cepis_condition("cepis_invalid_cep", message, value = value) } -not_found_error <- function(message, cep = NULL) { - cepis_condition("cepis_not_found", message, cep = cep) +not_found_error <- function(message, cep = NULL, faixa = NULL) { + cepis_condition("cepis_not_found", message, cep = cep, faixa = faixa) } rate_limited_error <- function(message, retry_after = NULL) { @@ -173,12 +173,22 @@ json_object <- function(body) { data } -api_message <- function(body, fallback) { +# The counterpart of json_object() for a body that is already an error: it +# never signals, because a broken error body must not replace the error the +# caller is being told about. Missing, malformed, empty: all of them NULL. +json_field <- function(body, name) { data <- tryCatch( jsonlite::parse_json(decode_body(body), simplifyVector = FALSE), error = function(e) NULL ) - message <- if (is.list(data)) data[["mensagem"]] else NULL + if (!is.list(data) || is.null(names(data))) { + return(NULL) + } + data[[name]] +} + +api_message <- function(body, fallback) { + message <- json_field(body, "mensagem") if (is.character(message) && length(message) == 1L && nzchar(message)) { message } else { @@ -62,6 +62,53 @@ NULL #' @name cepis_geo NULL +#' The municipality a CEP that does not exist would have belonged to +#' +#' @description +#' The `faixa` field of a [cepis_not_found][cepis_conditions] condition, a +#' plain list of class `cepis_faixa`. It is **context for an error, not an +#' address**: the CEP asked for does not exist, and nothing here says +#' otherwise. What it does say is which municipality owns the band of numbers +#' the CEP fell into. +#' +#' It exists because a CEP that fails is rarely random — it is usually +#' someone who knows the city and guessed the number, and the classic guess is +#' the `-000` that 957 municipalities never received. +#' +#' @section Fields: +#' \describe{ +#' \item{`localidade`, `uf`, `estado`, `regiao`}{municipality, state code, +#' state name, region — the same names, and the same meaning, as in a +#' [cepis_address].} +#' \item{`ibge`}{the municipality's IBGE code, so the answer can be joined +#' to other data without matching on the name.} +#' \item{`cep_ini`, `cep_fim`}{the smallest and the largest CEP that +#' **actually exists** in that municipality. Not the range the Correios +#' publish: the DNE's own start for Areal/RJ is `25845000`, which is +#' precisely the CEP that just failed.} +#' \item{`ceps`}{how many CEPs the municipality really has.} +#' } +#' +#' @section The range is sparse: +#' Areal's 118 CEPs are scattered over the ~4900 numbers between `25845001` +#' and `25849899`. `cep_ini` and `cep_fim` are two endpoints that exist, not a +#' promise about anything in between — never present the pair as a list of +#' valid CEPs, and never iterate from one to the other. `ceps` is there so the +#' emptiness of the interval is visible: 118 out of 4899. +#' +#' @section When there is no faixa: +#' `NULL`, and not a list of empty strings, when the CEP falls in a band no +#' municipality owns. The API **omits** the field rather than sending null, +#' and this package keeps that distinction intact: `is.null(x$faixa)` means +#' the number is close to nothing, not that the fields came back blank. +#' +#' Only [cep_lookup()] reports it. In [cep_lookup_many()] a CEP that does not +#' exist is still `NULL` at its position: the endpoint behind a batch is CSV +#' and says nothing about ranges. +#' +#' @name cepis_faixa +NULL + #' The answer of `GET /health` #' #' @description @@ -96,6 +143,15 @@ text_field <- function(data, name) { as.character(value) } +count_field <- function(data, name) { + value <- data[[name]] + if (!is.numeric(value) && !is.character(value)) { + return(0L) + } + value <- suppressWarnings(as.numeric(value)) + if (length(value) != 1L || is.na(value)) 0L else as.integer(value) +} + new_geo <- function(block) { lat <- suppressWarnings(as.numeric(block[["lat"]])) lon <- suppressWarnings(as.numeric(block[["lon"]])) @@ -121,6 +177,35 @@ new_geo <- function(block) { ) } +# Never signals. A faixa is decoration on a 404, and a malformed one must not +# turn a clean "this CEP does not exist" into a parse failure: anything that +# is not an object naming a municipality is simply no faixa at all. +new_faixa <- function(block) { + if (!is.list(block) || is.null(names(block))) { + return(NULL) + } + + localidade <- text_field(block, "localidade") + uf <- text_field(block, "uf") + if (!nzchar(localidade) || !nzchar(uf)) { + return(NULL) + } + + structure( + list( + localidade = localidade, + uf = uf, + estado = text_field(block, "estado"), + regiao = text_field(block, "regiao"), + ibge = text_field(block, "ibge"), + cep_ini = text_field(block, "cep_ini"), + cep_fim = text_field(block, "cep_fim"), + ceps = count_field(block, "ceps") + ), + class = "cepis_faixa" + ) +} + new_address <- function(data) { geo <- NULL block <- data[["geo"]] @@ -135,10 +220,6 @@ new_address <- function(data) { } new_health <- function(data) { - count <- function(name) { - value <- suppressWarnings(as.numeric(data[[name]])) - if (length(value) != 1L || is.na(value)) 0L else as.integer(value) - } status <- text_field(data, "status") structure( @@ -146,8 +227,8 @@ new_health <- function(data) { status = status, ok = identical(status, "ok"), dne = text_field(data, "dne"), - ceps = count("ceps"), - geo = count("geo"), + ceps = count_field(data, "ceps"), + geo = count_field(data, "geo"), base_gerada_em = text_field(data, "base_gerada_em") ), class = "cepis_health" @@ -204,6 +285,15 @@ print.cepis_geo <- function(x, ...) { } #' @export +print.cepis_faixa <- function(x, ...) { + cat("<cepis_faixa> ", x$localidade, "/", x$uf, " (IBGE ", x$ibge, ")\n", + sep = "") + cat(" ", x$ceps, " CEP(s), the first ", formatted_cep(x$cep_ini), + " and the last ", formatted_cep(x$cep_fim), "\n", sep = "") + invisible(x) +} + +#' @export print.cepis_health <- function(x, ...) { cat("<cepis_health> ", x$status, "\n", sep = "") cat(" dne: ", x$dne, " (generated ", x$base_gerada_em, ")\n", sep = "") |