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" } // Faixa is the municipality a CEP that does not exist would belong to, and the // range of CEPs that municipality actually has. It arrives on the 404, hanging // off [Error.Faixa]. // // A CEP that fails is rarely a random number. It is usually someone who knows // the city and guessed the rest, and the classic guess is the "-000": a small // town often has a single city-wide CEP ending that way, but 957 municipalities // are coded street by street and never got one. Areal/RJ answers from // 25845-001 up, never at 25845-000 — so the 404 for 25845000 names Areal. // // CEPIni and CEPFim are the smallest and the largest CEP that EXIST in that // municipality, not the bounds the Correios publish. Do not read the pair as a // list of valid CEPs: the range is sparse, which is what CEPs is for — Areal's // 118 CEPs are scattered across nearly five thousand numbers. type Faixa struct { Localidade string `json:"localidade"` UF string `json:"uf"` Estado string `json:"estado"` Regiao string `json:"regiao"` IBGE string `json:"ibge"` CEPIni string `json:"cep_ini"` CEPFim string `json:"cep_fim"` // CEPs is how many CEPs the municipality actually has inside the range. CEPs int `json:"ceps"` } // 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, } } // decodeFaixa reads the `faixa` block of a 404 body. Every failure — absent // block, malformed JSON, a `faixa` without a locality — yields nil: the field // is an extra on top of an error that is already complete without it, and it // must never turn a clean ErrCEPNotFound into a parse failure. func decodeFaixa(body []byte) *Faixa { if len(body) == 0 { return nil } var envelope struct { Faixa *Faixa `json:"faixa"` } if err := json.Unmarshal(body, &envelope); err != nil { return nil } if envelope.Faixa == nil || envelope.Faixa.Localidade == "" { return nil } return envelope.Faixa } 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) }