diff options
Diffstat (limited to 'R/api.R')
| -rw-r--r-- | R/api.R | 182 |
1 files changed, 182 insertions, 0 deletions
@@ -0,0 +1,182 @@ +#' Look up a single CEP +#' +#' @description +#' Accepts the CEP masked or unmasked and returns everything the DNE database +#' has for it, including the coordinate when there is one. +#' +#' @param cep A length-one character vector: `"01001-000"` or `"01001000"`. +#' @param client A [cepis_client()]. Defaults to a shared client built with +#' the default configuration. +#' +#' @return A [cepis_address]. +#' +#' @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_rate_limited`}{429 on every attempt.} +#' \item{`cepis_server_error`}{5xx on every attempt.} +#' \item{`cepis_network_error`}{the request never completed.} +#' \item{`cepis_invalid_response`}{a success status with an unreadable +#' body.} +#' } +#' +#' All of them inherit `cepis_error`; see [cepis_conditions]. +#' +#' @examples +#' \donttest{ +#' address <- cep_lookup("01001-000") +#' address$localidade +#' if (!is.null(address$geo) && address$geo$is_precise) { +#' c(address$geo$lat, address$geo$lon) +#' } +#' } +#' +#' @export +cep_lookup <- function(cep, client = default_client()) { + number <- normalize_cep(cep) + 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 + )) + } + if (response$status == 400L) { + stop(invalid_cep_error( + api_message(response$body, paste0("CEP ", number, " rejected")), + value = cep + )) + } + if (response$status != 200L) { + stop(server_error( + paste0("the API answered ", response$status, " for CEP ", number), + status = response$status + )) + } + + new_address(json_object(response$body)) +} + +#' Look up many CEPs in one request +#' +#' @description +#' Resolves up to 500 CEPs per request, chunking longer inputs automatically. +#' Do not loop over [cep_lookup()]: a loop of 500 calls pays 500 TLS +#' handshakes, the batch pays one. +#' +#' @param ceps A character vector of CEPs, masked or unmasked. +#' @param client A [cepis_client()]. Defaults to a shared client built with +#' the default configuration. +#' +#' @return A list of **the same length and in the same order** as `ceps`. +#' Each element is a [cepis_address], or `NULL` where the CEP does not +#' exist or is malformed — it does not vanish from the list, and it does not +#' signal a condition. That is the right difference from [cep_lookup()]: in +#' a batch of a thousand, one bad CEP must not cost the other nine hundred +#' and ninety-nine. Use [normalize_cep()] beforehand if you need to know +#' *which* input was malformed. +#' +#' A list rather than a `data.frame` because `NULL` is the only unambiguous +#' "no such CEP" in R: a row of `NA`s reads as an address with unknown +#' fields, which is a different statement, and an empty row cannot be told +#' apart from a CEP the database knows only by number. Rectangle it with +#' `do.call(rbind, lapply(result, function(a) as.data.frame(a[1:13])))` when +#' a `data.frame` is what you want. +#' +#' @section Batches carry no coordinate: +#' The endpoint behind them is CSV, and the CSV has no `geo` column. Every +#' address returned here has `geo = NULL`, including for CEPs that do have +#' one. Need `geo`? Use [cep_lookup()], one at a time. +#' +#' @examples +#' \donttest{ +#' ceps <- c("01001000", "99999999", "20040020") +#' result <- cep_lookup_many(ceps) +#' vapply(result, function(a) if (is.null(a)) NA_character_ else a$localidade, +#' character(1)) +#' } +#' +#' @export +cep_lookup_many <- function(ceps, client = default_client()) { + if (length(ceps) == 0L) { + return(list()) + } + ceps <- as.character(ceps) + + result <- list() + starts <- seq(1L, length(ceps), by = MAX_BATCH) + for (start in starts) { + chunk <- ceps[start:min(start + MAX_BATCH - 1L, length(ceps))] + result <- c(result, lookup_chunk(client, chunk)) + } + result +} + +lookup_chunk <- function(client, ceps) { + query <- utils::URLencode(paste(ceps, collapse = ","), reserved = TRUE) + response <- cepis_fetch(client, paste0("/csv?ceps=", query)) + + if (response$status == 400L) { + stop(invalid_cep_error( + api_message(response$body, "the API rejected the batch"), + value = ceps + )) + } + if (response$status != 200L) { + stop(server_error( + paste0("the API answered ", response$status, " for a batch of ", + length(ceps), " CEP(s)"), + status = response$status + )) + } + + table <- csv_rows(response$body) + rows <- vector("list", nrow(table)) + for (index in seq_len(nrow(table))) { + row <- as.list(table[index, , drop = FALSE]) + localidade <- row[["localidade"]] + if (is.null(localidade) || !nzchar(trimws(localidade))) { + next + } + rows[[index]] <- new_address(row) + } + + rows[seq_along(ceps)] +} + +#' Ask the API how it is, and which database it is serving +#' +#' @description +#' Useful for two things: checking that the API answers, and knowing which +#' version of the DNE database is live. The database changes every two weeks. +#' +#' @param client A [cepis_client()]. Defaults to a shared client built with +#' the default configuration. +#' +#' @return A [cepis_health]. +#' +#' @section Conditions: +#' An API reporting itself degraded is not a failure here — that arrives in +#' `status`, and checking health is exactly when you want the diagnosis +#' rather than an error. Network failures still signal `cepis_network_error`. +#' +#' @examples +#' \donttest{ +#' health <- cep_health() +#' health$ok +#' health$dne +#' } +#' +#' @export +cep_health <- function(client = default_client()) { + response <- cepis_fetch(client, "/health", accepted_status = c(200L, 503L)) + if (!(response$status %in% c(200L, 503L))) { + stop(server_error( + paste0("the API answered ", response$status, " at /health"), + status = response$status + )) + } + new_health(json_object(response$body)) +} |