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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
|
/// The data the API returns, as objects.
///
/// **Field names are the API's own, in Portuguese** — `logradouro`, `bairro`,
/// `localidade`. Everything else in this library is English; the fields are the
/// one deliberate exception.
///
/// The reason is fidelity: what you read in the JSON at
/// <https://cep.is/01001000> is exactly what you access on the object.
/// Translating to `street`/`city` would mean maintaining a field-by-field
/// mapping and forcing anyone reading the API docs alongside this library to
/// translate back in their head. The values are Portuguese either way — they
/// come from the Correios database.
///
/// Only the *casing* changes, where Dart demands it: the API's `raio_m` is
/// [Geo.raioM] and its `base_gerada_em` is [Health.baseGeradaEm]. Every other
/// field is a single lowercase word and is spelled exactly as the API spells
/// it.
library;
/// 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] — or the [isPrecise] shorthand — 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.
class Geo {
/// Creates a coordinate from its parts.
const Geo({
required this.lat,
required this.lon,
required this.raioM,
required this.precisao,
});
/// Latitude in decimal degrees.
final double lat;
/// Longitude in decimal degrees.
final double lon;
/// Radius in metres containing 90% of that CEP's addresses.
///
/// The API spells this field `raio_m`; Dart's naming rules make it `raioM`
/// here. It exists precisely so the difference between a street-level point
/// and a municipality centroid cannot pass unnoticed.
final int raioM;
/// Either `'logradouro'` (a stretch of street) or `'localidade'` (the
/// municipality centroid). Treat any other value as imprecise.
final String precisao;
/// Builds a coordinate from the API's `geo` block, or returns `null`.
///
/// A malformed block yields `null` rather than throwing: it is not worth
/// losing the whole address over, since the rest of the response is still
/// useful.
static Geo? fromJson(Map<String, dynamic> data) {
final lat = _toDouble(data['lat']);
final lon = _toDouble(data['lon']);
final raioM = _toInt(data['raio_m']);
final precisao = data['precisao'];
if (lat == null || lon == null || raioM == null || precisao == null) {
return null;
}
return Geo(
lat: lat,
lon: lon,
raioM: raioM,
precisao: precisao is String ? precisao : precisao.toString(),
);
}
/// `true` when the coordinate points at a street, not a municipality.
///
/// Shorthand for `precisao == 'logradouro'`, which is the check to make
/// before using the point to measure distance.
bool get isPrecise => precisao == 'logradouro';
@override
bool operator ==(Object other) =>
other is Geo &&
other.lat == lat &&
other.lon == lon &&
other.raioM == raioM &&
other.precisao == precisao;
@override
int get hashCode => Object.hash(lat, lon, raioM, precisao);
@override
String toString() =>
'Geo(lat: $lat, lon: $lon, raioM: $raioM, precisao: $precisao)';
}
/// A CEP and everything the database knows about it.
///
/// Fields the database does not have for that CEP come back as empty strings,
/// never `null` — that is what the API returns, and changing it here would
/// create a difference between reading the JSON and reading this library.
///
/// [geo] is the exception: it is `null` when there is no coordinate. There the
/// difference is real, because the API **omits** the whole field 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.
class Address {
/// Creates an address from its parts. Every string field is required, so a
/// missing value is an explicit `''` rather than an accident.
const Address({
required this.cep,
required this.logradouro,
required this.complemento,
required this.unidade,
required this.bairro,
required this.localidade,
required this.uf,
required this.estado,
required this.regiao,
required this.ibge,
required this.gia,
required this.ddd,
required this.siafi,
this.geo,
this.raw = const <String, dynamic>{},
});
/// The 8 digits, always unmasked. See [formattedCep] for `01001-000`.
final String cep;
/// Street, avenue or square.
final String logradouro;
/// Extra qualifier on the street, such as `'lado ímpar'`.
final String complemento;
/// Name of the large consumer or public body owning an exclusive CEP.
final String unidade;
/// Neighbourhood.
final String bairro;
/// Municipality.
final String localidade;
/// Two-letter state code, such as `'SP'`.
final String uf;
/// State name in full.
final String estado;
/// Region of the country, such as `'Sudeste'`.
final String regiao;
/// IBGE municipality code — the key to join against public census data.
final String ibge;
/// GIA code, São Paulo state tax administration only.
final String gia;
/// Area code for telephone numbers.
final String ddd;
/// SIAFI municipality code, federal government accounting.
final String siafi;
/// The coordinate, or `null` when the database has none for this CEP.
///
/// Always `null` for an address that came out of a batch — see
/// `Client.lookupMany`.
final Geo? geo;
/// The response exactly as it arrived, including fields this release does not
/// model. Unmodifiable.
final Map<String, dynamic> raw;
/// Builds an address from the map the API returned.
///
/// Unknown fields are ignored during construction but preserved in [raw]. A
/// new field in the API therefore cannot break this library.
///
/// A malformed `geo` block yields a `null` [geo] rather than propagating the
/// error.
factory Address.fromJson(Map<String, dynamic> data) {
final block = data['geo'];
return Address(
cep: _text(data['cep']),
logradouro: _text(data['logradouro']),
complemento: _text(data['complemento']),
unidade: _text(data['unidade']),
bairro: _text(data['bairro']),
localidade: _text(data['localidade']),
uf: _text(data['uf']),
estado: _text(data['estado']),
regiao: _text(data['regiao']),
ibge: _text(data['ibge']),
gia: _text(data['gia']),
ddd: _text(data['ddd']),
siafi: _text(data['siafi']),
geo: block is Map<String, dynamic> ? Geo.fromJson(block) : null,
raw: Map<String, dynamic>.unmodifiable(data),
);
}
/// 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 interpolation in every project
/// that uses this.
String get formattedCep =>
cep.length == 8 ? '${cep.substring(0, 5)}-${cep.substring(5)}' : cep;
/// Equality covers every modelled field, [geo] included. [raw] is excluded:
/// two responses that differ only in a field this release does not model are
/// the same address as far as this library is concerned.
@override
bool operator ==(Object other) =>
other is Address &&
other.cep == cep &&
other.logradouro == logradouro &&
other.complemento == complemento &&
other.unidade == unidade &&
other.bairro == bairro &&
other.localidade == localidade &&
other.uf == uf &&
other.estado == estado &&
other.regiao == regiao &&
other.ibge == ibge &&
other.gia == gia &&
other.ddd == ddd &&
other.siafi == siafi &&
other.geo == geo;
@override
int get hashCode => Object.hash(
cep,
logradouro,
complemento,
unidade,
bairro,
localidade,
uf,
estado,
regiao,
ibge,
gia,
ddd,
siafi,
geo,
);
@override
String toString() =>
'Address(cep: $cep, logradouro: $logradouro, bairro: $bairro, '
'localidade: $localidade, uf: $uf, geo: $geo)';
}
/// The response 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.
class Health {
/// Creates a health report from its parts.
const Health({
required this.status,
required this.dne,
required this.ceps,
required this.geo,
required this.baseGeradaEm,
this.raw = const <String, dynamic>{},
});
/// `'ok'` when the API considers itself healthy. See [ok].
final String status;
/// Identifier of the fortnightly DNE database currently loaded.
final String dne;
/// How many CEPs that database holds.
final int ceps;
/// How many of those CEPs carry a coordinate.
final int geo;
/// When the database was generated, as an ISO 8601 instant in UTC.
///
/// The API spells this field `base_gerada_em`; Dart's naming rules make it
/// `baseGeradaEm` here.
final String baseGeradaEm;
/// The response exactly as it arrived, including fields this release does not
/// model. Unmodifiable.
final Map<String, dynamic> raw;
/// Builds a health report from the map the API returned.
factory Health.fromJson(Map<String, dynamic> data) => Health(
status: _text(data['status']),
dne: _text(data['dne']),
ceps: _toInt(data['ceps']) ?? 0,
geo: _toInt(data['geo']) ?? 0,
baseGeradaEm: _text(data['base_gerada_em']),
raw: Map<String, dynamic>.unmodifiable(data),
);
/// `true` when the API reports itself healthy.
bool get ok => status == 'ok';
@override
bool operator ==(Object other) =>
other is Health &&
other.status == status &&
other.dne == dne &&
other.ceps == ceps &&
other.geo == geo &&
other.baseGeradaEm == baseGeradaEm;
@override
int get hashCode => Object.hash(status, dne, ceps, geo, baseGeradaEm);
@override
String toString() =>
'Health(status: $status, dne: $dne, ceps: $ceps, geo: $geo, '
'baseGeradaEm: $baseGeradaEm)';
}
String _text(Object? value) {
if (value == null) {
return '';
}
return value is String ? value : value.toString();
}
double? _toDouble(Object? value) {
if (value is num) {
return value.toDouble();
}
if (value is String) {
return double.tryParse(value);
}
return null;
}
int? _toInt(Object? value) {
if (value is int) {
return value;
}
if (value is num) {
return value.toInt();
}
if (value is String) {
return int.tryParse(value);
}
return null;
}
|