summaryrefslogtreecommitdiffstats
path: root/R/api.R
diff options
context:
space:
mode:
Diffstat (limited to 'R/api.R')
-rw-r--r--R/api.R182
1 files changed, 182 insertions, 0 deletions
diff --git a/R/api.R b/R/api.R
new file mode 100644
index 0000000..0d13c67
--- /dev/null
+++ b/R/api.R
@@ -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))
+}