summaryrefslogtreecommitdiffstats
path: root/models.go
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:13:27 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:13:27 -0300
commit625cab052606cb3c2a7c4e008c43cf536b6404ce (patch)
tree80d126ce42750d29eec42d9fef98a68341bb2dfa /models.go
downloadgo-625cab052606cb3c2a7c4e008c43cf536b6404ce.tar.gz
Initial import
Diffstat (limited to 'models.go')
-rw-r--r--models.go214
1 files changed, 214 insertions, 0 deletions
diff --git a/models.go b/models.go
new file mode 100644
index 0000000..3d21bf8
--- /dev/null
+++ b/models.go
@@ -0,0 +1,214 @@
+package cepis
+
+import (
+ "encoding/json"
+ "strconv"
+)
+
+// Geo is 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 Precisao before using Lat/Lon for distance:
+//
+// - "logradouro": the point is a stretch of street. RaioM 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. RaioM runs from 7 km to 40 km.
+// Computing shipping from this is wrong by tens of kilometres.
+//
+// RaioM is the radius, in metres, containing 90% of that CEP's addresses. It
+// exists precisely so that error cannot pass unnoticed.
+type Geo struct {
+ Lat float64 `json:"lat"`
+ Lon float64 `json:"lon"`
+ RaioM int `json:"raio_m"`
+ Precisao string `json:"precisao"`
+}
+
+// IsPrecise reports whether the coordinate points at a street rather than at a
+// whole municipality. Shorthand for Precisao == "logradouro", which is the
+// check to make before using the point to measure distance.
+func (g Geo) IsPrecise() bool {
+ return g.Precisao == "logradouro"
+}
+
+// Address is a CEP and everything the database knows about it.
+//
+// The field names are the API's own, in Portuguese, with only the casing
+// adapted to Go: what you read in the JSON at https://cep.is/01001000 is what
+// you access here. The values are Portuguese either way — they come from the
+// Correios database.
+//
+// Fields the database does not have for that CEP come back as empty strings,
+// never null. Geo is the exception: it is nil when there is no coordinate,
+// because the API omits the whole object in that case (about 43% of CEPs)
+// rather than sending null.
+//
+// Raw holds the response exactly as it arrived, uninterpreted, so a field the
+// API grows before this library is updated is not lost.
+type Address struct {
+ CEP string `json:"cep"`
+ Logradouro string `json:"logradouro"`
+ Complemento string `json:"complemento"`
+ Unidade string `json:"unidade"`
+ Bairro string `json:"bairro"`
+ Localidade string `json:"localidade"`
+ UF string `json:"uf"`
+ Estado string `json:"estado"`
+ Regiao string `json:"regiao"`
+ IBGE string `json:"ibge"`
+ GIA string `json:"gia"`
+ DDD string `json:"ddd"`
+ Siafi string `json:"siafi"`
+ Geo *Geo `json:"geo,omitempty"`
+
+ // Raw is the response as it arrived, field by field, including fields
+ // this version of the library does not know about.
+ Raw map[string]json.RawMessage `json:"-"`
+}
+
+// FormattedCEP returns the CEP with its mask: "01001-000".
+//
+// The API always returns it unmasked; formatting is presentation, and it lives
+// here so it does not become a repeated Sprintf in every project.
+func (a *Address) FormattedCEP() string {
+ if len(a.CEP) != 8 {
+ return a.CEP
+ }
+ return a.CEP[:5] + "-" + a.CEP[5:]
+}
+
+// Field returns a field of the raw response as a string, "" when the API did
+// not send it. Use it to read a field the API grew after this library was
+// released, without waiting for a new version.
+func (a *Address) Field(name string) string {
+ if a.Raw == nil {
+ return ""
+ }
+ return rawString(a.Raw[name])
+}
+
+// UnmarshalJSON decodes the API's JSON leniently.
+//
+// Unknown fields are ignored during decoding but preserved in Raw, so a new
+// field in the API cannot break this library. A malformed geo block yields a
+// nil Geo rather than failing the whole document: the rest of the address is
+// still useful and it is not worth losing over a coordinate.
+func (a *Address) UnmarshalJSON(data []byte) error {
+ var raw map[string]json.RawMessage
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ *a = Address{
+ CEP: rawString(raw["cep"]),
+ Logradouro: rawString(raw["logradouro"]),
+ Complemento: rawString(raw["complemento"]),
+ Unidade: rawString(raw["unidade"]),
+ Bairro: rawString(raw["bairro"]),
+ Localidade: rawString(raw["localidade"]),
+ UF: rawString(raw["uf"]),
+ Estado: rawString(raw["estado"]),
+ Regiao: rawString(raw["regiao"]),
+ IBGE: rawString(raw["ibge"]),
+ GIA: rawString(raw["gia"]),
+ DDD: rawString(raw["ddd"]),
+ Siafi: rawString(raw["siafi"]),
+ Geo: decodeGeo(raw["geo"]),
+ Raw: raw,
+ }
+ return nil
+}
+
+func decodeGeo(block json.RawMessage) *Geo {
+ if len(block) == 0 {
+ return nil
+ }
+ var probe struct {
+ Lat *float64 `json:"lat"`
+ Lon *float64 `json:"lon"`
+ RaioM *int `json:"raio_m"`
+ Precisao *string `json:"precisao"`
+ }
+ if err := json.Unmarshal(block, &probe); err != nil {
+ return nil
+ }
+ if probe.Lat == nil || probe.Lon == nil || probe.RaioM == nil || probe.Precisao == nil {
+ return nil
+ }
+ return &Geo{
+ Lat: *probe.Lat,
+ Lon: *probe.Lon,
+ RaioM: *probe.RaioM,
+ Precisao: *probe.Precisao,
+ }
+}
+
+func rawString(value json.RawMessage) string {
+ if len(value) == 0 {
+ return ""
+ }
+ var text string
+ if err := json.Unmarshal(value, &text); err == nil {
+ return text
+ }
+ var number json.Number
+ if err := json.Unmarshal(value, &number); err == nil {
+ return number.String()
+ }
+ return ""
+}
+
+// Health is the answer 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;
+// DNE identifies it.
+type Health struct {
+ Status string `json:"status"`
+ DNE string `json:"dne"`
+ CEPs int64 `json:"ceps"`
+ Geo int64 `json:"geo"`
+ BaseGeradaEm string `json:"base_gerada_em"`
+
+ // Raw is the response as it arrived, field by field.
+ Raw map[string]json.RawMessage `json:"-"`
+}
+
+// OK reports whether the API describes itself as healthy.
+func (h *Health) OK() bool {
+ return h.Status == "ok"
+}
+
+// UnmarshalJSON decodes the health payload leniently, keeping the response in
+// Raw and tolerating fields this version does not know about.
+func (h *Health) UnmarshalJSON(data []byte) error {
+ var raw map[string]json.RawMessage
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+ *h = Health{
+ Status: rawString(raw["status"]),
+ DNE: rawString(raw["dne"]),
+ CEPs: rawInt(raw["ceps"]),
+ Geo: rawInt(raw["geo"]),
+ BaseGeradaEm: rawString(raw["base_gerada_em"]),
+ Raw: raw,
+ }
+ return nil
+}
+
+func rawInt(value json.RawMessage) int64 {
+ text := rawString(value)
+ if text == "" {
+ return 0
+ }
+ number, err := strconv.ParseFloat(text, 64)
+ if err != nil {
+ return 0
+ }
+ return int64(number)
+}