diff options
Diffstat (limited to 'models.go')
| -rw-r--r-- | models.go | 214 |
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) +} |