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) }