summaryrefslogtreecommitdiffstats
path: root/src/models.lisp
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:42:28 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:42:28 -0300
commitae9ad8a4c61b793f6859c84dac80b1c1b04b7b68 (patch)
tree62271e7672dfc58f59dda2ea4dc539c89a1ea5ca /src/models.lisp
downloadlisp-ae9ad8a4c61b793f6859c84dac80b1c1b04b7b68.tar.gz
Initial importv0.1.0
Diffstat (limited to 'src/models.lisp')
-rw-r--r--src/models.lisp147
1 files changed, 147 insertions, 0 deletions
diff --git a/src/models.lisp b/src/models.lisp
new file mode 100644
index 0000000..8905ae1
--- /dev/null
+++ b/src/models.lisp
@@ -0,0 +1,147 @@
+(in-package #:cepis)
+
+(defstruct (geo (:copier nil))
+ "The CEP's coordinate, when the database has one.
+
+The point is the median of the coordinates of that CEP's addresses, not the
+mean: one badly geocoded address 40 km away shifts a mean and does not move a
+median.
+
+Always check `geo-precisao' before using the point for distance:
+
+- \"logradouro\": the point is a stretch of street. `geo-raio-m' lands around
+ 100-150 m. Good for maps and for distance.
+- \"localidade\": the CEP is the whole municipality (a city-wide general CEP),
+ and the point is its centroid. `geo-raio-m' runs from 7 km to 40 km.
+ Computing shipping from this is wrong by tens of kilometres.
+
+`geo-raio-m' is the radius containing 90% of that CEP's addresses. It exists
+precisely so that error cannot pass unnoticed. The slot is the JSON's `raio_m'
+with Lisp's hyphenation."
+ (lat 0.0d0 :type double-float :read-only t)
+ (lon 0.0d0 :type double-float :read-only t)
+ (raio-m 0 :type integer :read-only t)
+ (precisao "" :type string :read-only t))
+
+(defun geo-precise-p (geo)
+ "True when the coordinate points at a street, not at a municipality.
+
+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 (address (:copier nil))
+ "A CEP and everything the database knows about it.
+
+Fields the database does not have for that CEP come back as empty strings,
+never NIL — that is what the API returns, and changing it here would create a
+difference between reading the JSON and reading this library.
+
+`address-geo' is the exception: it is NIL when there is no coordinate. There
+the difference is real, because the API omits the whole field in that case
+(about 43% of CEPs) rather than sending null.
+
+`address-raw' holds the response exactly as it arrived, as the hash table the
+JSON parser produced, so a field the API grows before this library is updated
+is not lost."
+ (cep "" :type string :read-only t)
+ (logradouro "" :type string :read-only t)
+ (complemento "" :type string :read-only t)
+ (unidade "" :type string :read-only t)
+ (bairro "" :type string :read-only t)
+ (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)
+ (gia "" :type string :read-only t)
+ (ddd "" :type string :read-only t)
+ (siafi "" :type string :read-only t)
+ (geo nil :type (or null geo) :read-only t)
+ (raw nil :type (or null hash-table) :read-only t))
+
+(defun address-formatted-cep (address)
+ "The CEP with its mask: \"01001-000\".
+
+The native contract always returns it unmasked; formatting is presentation,
+and it lives here so it does not become a repeated format string in every
+project that uses this."
+ (let ((cep (address-cep address)))
+ (if (= (length cep) 8)
+ (format nil "~a-~a" (subseq cep 0 5) (subseq cep 5))
+ cep)))
+
+(defstruct (health (:copier nil))
+ "The response of GET /health.
+
+Good for two things: checking that the API answers, and knowing which version
+of the database is live. The DNE database changes every two weeks;
+`health-dne' identifies it."
+ (status "" :type string :read-only t)
+ (dne "" :type string :read-only t)
+ (ceps 0 :type integer :read-only t)
+ (geo 0 :type integer :read-only t)
+ (base-gerada-em "" :type string :read-only t))
+
+(defun health-ok-p (health)
+ "True when the API reports itself healthy."
+ (string= (health-status health) "ok"))
+
+(defun json-string (table key)
+ "The value of KEY in TABLE as a string, or \"\" when it is absent or not a scalar."
+ (let ((value (gethash key table)))
+ (cond ((stringp value) value)
+ ((integerp value) (format nil "~d" value))
+ ((realp value) (princ-to-string value))
+ (t ""))))
+
+(defun json-integer (table key)
+ "The value of KEY in TABLE as an integer, or 0 when it is absent or not a number."
+ (let ((value (gethash key table)))
+ (if (realp value) (round value) 0)))
+
+(defun geo-from-json (value)
+ "Build a `geo' from the parsed \"geo\" block, or NIL when it is absent or malformed.
+
+A malformed block yields NIL rather than propagating the error: it is not
+worth losing the whole address over, since the rest of the response is still
+useful."
+ (when (hash-table-p value)
+ (let ((lat (gethash "lat" value))
+ (lon (gethash "lon" value))
+ (raio-m (gethash "raio_m" value))
+ (precisao (gethash "precisao" value)))
+ (when (and (realp lat) (realp lon) (realp raio-m) (stringp precisao))
+ (make-geo :lat (coerce lat 'double-float)
+ :lon (coerce lon 'double-float)
+ :raio-m (round raio-m)
+ :precisao precisao)))))
+
+(defun address-from-json (data)
+ "Build an `address' from the hash table the JSON parser produced.
+
+Unknown fields are ignored during construction but preserved in
+`address-raw'. A new field in the API therefore cannot break this library."
+ (make-address :cep (json-string data "cep")
+ :logradouro (json-string data "logradouro")
+ :complemento (json-string data "complemento")
+ :unidade (json-string data "unidade")
+ :bairro (json-string data "bairro")
+ :localidade (json-string data "localidade")
+ :uf (json-string data "uf")
+ :estado (json-string data "estado")
+ :regiao (json-string data "regiao")
+ :ibge (json-string data "ibge")
+ :gia (json-string data "gia")
+ :ddd (json-string data "ddd")
+ :siafi (json-string data "siafi")
+ :geo (geo-from-json (gethash "geo" data))
+ :raw data))
+
+(defun health-from-json (data)
+ "Build a `health' from the hash table the JSON parser produced."
+ (make-health :status (json-string data "status")
+ :dne (json-string data "dne")
+ :ceps (json-integer data "ceps")
+ :geo (json-integer data "geo")
+ :base-gerada-em (json-string data "base_gerada_em")))