#' Conditions signalled by cepis #' #' @description #' Every failure raised by this package is a classed condition inheriting #' from `cepis_error`, so one handler catches all of them: #' #' ```r #' tryCatch( #' cep_lookup(cep_from_form), #' cepis_not_found = function(e) NULL, #' cepis_error = function(e) reschedule() #' ) #' ``` #' #' Below `cepis_error` the split is by **the decision you have to make**, not #' by the HTTP status that caused it: #' #' | class | when | does retrying help? | #' | --- | --- | --- | #' | `cepis_invalid_cep` | not 8 digits | no | #' | `cepis_not_found` | well-formed, not in the database | no | #' | `cepis_rate_limited` | 429 | yes, later | #' | `cepis_server_error` | 5xx | yes, later | #' | `cepis_network_error` | DNS, connection, timeout, TLS | yes, later | #' | `cepis_invalid_response` | unintelligible response | probably not | #' #' That split is why `cepis_not_found` is not "the HTTP 404 condition": it is #' also signalled with no request made at all, when the CEP is malformed #' before it ever leaves the machine. #' #' Each condition carries the fields a caller needs to act on it: #' `cepis_rate_limited` has `retry_after` (seconds the server asked for, or #' `NULL`), `cepis_server_error` has `status`, `cepis_network_error` has #' `url`, and `cepis_invalid_cep` has `value`, the input as it arrived. #' #' `cepis_invalid_response` almost always means something between you and the #' API is answering instead of it — a captive Wi-Fi portal, a corporate #' 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 cepis_condition <- function(class, message, ...) { structure( list(message = message, call = NULL, ...), class = c(class, "cepis_error", "error", "condition") ) } invalid_cep_error <- function(message, value = NULL) { cepis_condition("cepis_invalid_cep", message, value = value) } 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) { cepis_condition("cepis_rate_limited", message, retry_after = retry_after) } server_error <- function(message, status) { cepis_condition("cepis_server_error", message, status = status) } network_error <- function(message, url) { cepis_condition("cepis_network_error", message, url = url) } invalid_response_error <- function(message) { cepis_condition("cepis_invalid_response", message) }