summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client.lisp21
-rw-r--r--src/errors.lisp21
-rw-r--r--src/models.lisp54
-rw-r--r--src/package.lisp13
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