summaryrefslogtreecommitdiffstats
path: root/models.go
blob: 3d21bf809e7e6eea73a269dd1a1f59364750ad47 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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)
}