/// 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 /// 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], its `cep_ini` and `cep_fim` are [Faixa.cepIni] and /// [Faixa.cepFim], 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 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 {}, }); /// 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 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 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 ? Geo.fromJson(block) : null, raw: Map.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 municipality a CEP that does not exist *would* belong to. /// /// This is what the API sends in the `faixa` block of a **404**, and it arrives /// on `CepNotFoundException.faixa`. It is context for an error, never an /// address: the CEP asked for still does not exist, and nothing here says /// otherwise. /// /// It exists because a failing CEP is rarely random — it is usually someone who /// knows the city and guessed the number, and the classic guess is the `-000`, /// which 957 municipalities never received. Naming the municipality turns "no" /// into "no, but you were looking at Areal/RJ". /// /// [cepIni] and [cepFim] are the smallest and the largest CEP that **actually /// exist** in that municipality — not the bounds the Correios publish. Areal's /// published range starts at `25845000`, which is exactly the CEP that just /// failed; echoing it back would say nothing. /// /// **The range is sparse.** Areal's [ceps] is 118, spread over the ~9000 /// numbers between [cepIni] and [cepFim]. The pair is not a list of valid CEPs /// and iterating it would be wrong; [ceps] is there so the emptiness is /// visible. class Faixa { /// Creates a municipality band from its parts. const Faixa({ required this.localidade, required this.uf, required this.estado, required this.regiao, required this.ibge, required this.cepIni, required this.cepFim, required this.ceps, }); /// Municipality that owns the band the CEP fell in. final String localidade; /// Two-letter state code, such as `'RJ'`. 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; /// Smallest CEP that exists in the municipality, 8 digits. /// /// The API spells this field `cep_ini`; Dart's naming rules make it `cepIni` /// here. It is an observed minimum, not the Correios' published start. final String cepIni; /// Largest CEP that exists in the municipality, 8 digits. /// /// The API spells this field `cep_fim`; Dart's naming rules make it `cepFim` /// here. It is an observed maximum, not the Correios' published end. final String cepFim; /// How many CEPs exist between [cepIni] and [cepFim], inclusive. /// /// Almost always far fewer than the arithmetic distance between the two: the /// band is sparse, and this is the number that says how sparse. final int ceps; /// Builds a band from the API's `faixa` block, or returns `null`. /// /// A missing or malformed block yields `null` rather than throwing. The 404 /// is the answer; losing it to a parse error over the hint attached to it /// would be a bad trade. /// /// `localidade` is the gate: without a municipality name there is nothing to /// tell the caller, so an empty one is the same as no block at all. static Faixa? fromJson(Map data) { final localidade = _text(data['localidade']); if (localidade.isEmpty) { return null; } return Faixa( localidade: localidade, uf: _text(data['uf']), estado: _text(data['estado']), regiao: _text(data['regiao']), ibge: _text(data['ibge']), cepIni: _text(data['cep_ini']), cepFim: _text(data['cep_fim']), ceps: _toInt(data['ceps']) ?? 0, ); } @override bool operator ==(Object other) => other is Faixa && other.localidade == localidade && other.uf == uf && other.estado == estado && other.regiao == regiao && other.ibge == ibge && other.cepIni == cepIni && other.cepFim == cepFim && other.ceps == ceps; @override int get hashCode => Object.hash( localidade, uf, estado, regiao, ibge, cepIni, cepFim, ceps, ); @override String toString() => 'Faixa(localidade: $localidade, uf: $uf, cepIni: $cepIni, ' 'cepFim: $cepFim, ceps: $ceps)'; } /// 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 {}, }); /// `'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 raw; /// Builds a health report from the map the API returned. factory Health.fromJson(Map 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.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; }