diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:38:22 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:38:22 -0300 |
| commit | 8e719fc715eb7ae9aa1c842e633f15b5c6cd3688 (patch) | |
| tree | 4afda3d13e1b772f2c9a5cf9ceea5e84c647ebdd /lib/src/models.dart | |
| download | dart-8e719fc715eb7ae9aa1c842e633f15b5c6cd3688.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'lib/src/models.dart')
| -rw-r--r-- | lib/src/models.dart | 363 |
1 files changed, 363 insertions, 0 deletions
diff --git a/lib/src/models.dart b/lib/src/models.dart new file mode 100644 index 0000000..57a008f --- /dev/null +++ b/lib/src/models.dart @@ -0,0 +1,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; +} |