diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/client.lisp | 21 | ||||
| -rw-r--r-- | src/errors.lisp | 21 | ||||
| -rw-r--r-- | src/models.lisp | 54 | ||||
| -rw-r--r-- | src/package.lisp | 13 |
4 files changed, 106 insertions, 3 deletions
diff --git a/src/client.lisp b/src/client.lisp index 2965c86..9dd5338 100644 --- a/src/client.lisp +++ b/src/client.lisp @@ -1,6 +1,6 @@ (in-package #:cepis) -(defparameter +version+ "0.1.0" +(defparameter +version+ "0.2.0" "Version of this library, as it appears in the default User-Agent.") (defparameter +default-url+ "https://cep.is" @@ -306,6 +306,19 @@ between you and the API is answering instead of it: ~s" fallback)) (error () fallback))) +(defun error-faixa (body) + "The \"faixa\" block of an error body as a `faixa', or NIL when there is none. + +Every failure to read it is NIL: a body that is not JSON, a `faixa' that is not +an object, an API too old to send the field. A not-found is a definitive answer +about the request, and a field that only says which municipality the CEP would +have belonged to must never be able to turn it into an `invalid-response'." + (handler-case + (let ((data (com.inuoe.jzon:parse (body-text body)))) + (when (hash-table-p data) + (faixa-from-json (gethash "faixa" data)))) + (error () nil))) + (defun lookup (cep &key (client *client*)) "Look up a single CEP and return an `address'. @@ -315,6 +328,11 @@ A 400 from the API is surfaced as `invalid-cep' carrying the API's own message: the format already passed local validation, so the server knows something this client does not. +A `cep-not-found' may carry a `faixa' in `cep-not-found-faixa': the +municipality that owns the band the CEP falls in, when the API knows one. It is +context for the error and nothing more — the answer is still a 404, and the +band is sparse, so it says which city to ask about, never which CEPs are valid. + Signals `invalid-cep' when the CEP is not 8 digits, without making a request; `cep-not-found' when it is well-formed but not in the database; `rate-limited' on 429 and `server-error' on 5xx after every attempt; `network-error' when the @@ -325,6 +343,7 @@ request never completed; `invalid-response' when the body cannot be read." (case status (404 (error 'cep-not-found :name number + :faixa (error-faixa body) :message (format nil "CEP ~a does not exist in the DNE database" number))) (400 (error 'invalid-cep diff --git a/src/errors.lisp b/src/errors.lisp index 525ac67..ea6f83f 100644 --- a/src/errors.lisp +++ b/src/errors.lisp @@ -30,12 +30,29 @@ Usually signalled without a request ever being made — the client checks the format before going to the network.")) (define-condition cep-not-found (cepis-error cell-error) - () + ((faixa :initarg :faixa + :initform nil + :reader cep-not-found-faixa + :documentation + "The municipality owning the band this CEP falls in, or NIL.")) + (:report (lambda (condition stream) + (write-string (error-message condition) stream) + (let ((faixa (cep-not-found-faixa condition))) + (when faixa + (format stream " — it falls in the band of ~a/~a" + (faixa-localidade faixa) + (faixa-uf faixa)))))) (:documentation "The CEP is well-formed but does not exist in the DNE database. Also a `cell-error', which is the closest thing the standard has to a failed -key lookup: `cell-error-name' returns the CEP that was not found.")) +key lookup: `cell-error-name' returns the CEP that was not found. + +`cep-not-found-faixa' is a `faixa' naming the municipality that owns the band +the CEP falls in, when the API knows one, and NIL otherwise — either because +no municipality owns that band or because an older API is answering. It is +context for the error, not an address: this is still a 404, and a caller must +not read a municipality here as a CEP that resolved.")) (define-condition network-error (cepis-error) () diff --git a/src/models.lisp b/src/models.lisp index 8905ae1..9ca0eaf 100644 --- a/src/models.lisp +++ b/src/models.lisp @@ -30,6 +30,36 @@ Shorthand for (string= (geo-precisao geo) \"logradouro\"), which is the check to make before using the point to measure distance." (string= (geo-precisao geo) "logradouro")) +(defstruct (faixa (:copier nil)) + "The municipality a CEP that does not exist would have belonged to. + +Context for an error, never an address. It arrives on the 404 of a CEP the DNE +does not have, and names the municipality that owns the band the CEP falls in. +A CEP that fails is rarely random: it is usually someone who knows the city and +guessed the number — the classic guess being the `-000' that 957 municipalities +never received. + +`faixa-cep-ini' and `faixa-cep-fim' are the smallest and the largest CEP that +*actually exist* in that municipality, not the bounds the Correios publish. For +Areal/RJ the DNE's own start is 25845000 — exactly the CEP that just failed — +while `faixa-cep-ini' is 25845001. + +They are not the ends of a list, either: the range is sparse. Areal's 118 CEPs +are scattered over the ~4,900 numbers between its two ends, which is what +`faixa-ceps' counts. Offer the municipality to whoever typed the CEP; never +present the pair as a range of valid CEPs. + +Both ends are strings, like every CEP in this library, because a leading zero +is part of the number." + (localidade "" :type string :read-only t) + (uf "" :type string :read-only t) + (estado "" :type string :read-only t) + (regiao "" :type string :read-only t) + (ibge "" :type string :read-only t) + (cep-ini "" :type string :read-only t) + (cep-fim "" :type string :read-only t) + (ceps 0 :type integer :read-only t)) + (defstruct (address (:copier nil)) "A CEP and everything the database knows about it. @@ -117,6 +147,30 @@ useful." :raio-m (round raio-m) :precisao precisao))))) +(defun faixa-from-json (value) + "Build a `faixa' from the parsed \"faixa\" block, or NIL when it is absent or +malformed. + +The API omits the field entirely — it never sends null — when the CEP falls in +a band no municipality owns, so an absent block is the ordinary case and not an +anomaly. + +Anything that is not a block naming a municipality also yields NIL. That is the +whole point of parsing it here and not at the call site: the response is a +definitive not-found, and a field that only adds context must never be able to +turn it into a parse failure." + (when (hash-table-p value) + (let ((localidade (json-string value "localidade"))) + (when (plusp (length localidade)) + (make-faixa :localidade localidade + :uf (json-string value "uf") + :estado (json-string value "estado") + :regiao (json-string value "regiao") + :ibge (json-string value "ibge") + :cep-ini (json-string value "cep_ini") + :cep-fim (json-string value "cep_fim") + :ceps (json-integer value "ceps")))))) + (defun address-from-json (data) "Build an `address' from the hash table the JSON parser produced. diff --git a/src/package.lisp b/src/package.lisp index 20d6f32..274c654 100644 --- a/src/package.lisp +++ b/src/package.lisp @@ -44,6 +44,19 @@ Lisp's hyphenation, so the JSON `raio_m' is the slot `raio-m', read with #:error-message #:server-error-status #:rate-limited-retry-after + #:cep-not-found-faixa + + #:faixa + #:faixa-p + #:make-faixa + #:faixa-localidade + #:faixa-uf + #:faixa-estado + #:faixa-regiao + #:faixa-ibge + #:faixa-cep-ini + #:faixa-cep-fim + #:faixa-ceps #:geo #:geo-p |