summaryrefslogtreecommitdiffstats
path: root/lib/src/models.dart
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:38:22 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:38:22 -0300
commit8e719fc715eb7ae9aa1c842e633f15b5c6cd3688 (patch)
tree4afda3d13e1b772f2c9a5cf9ceea5e84c647ebdd /lib/src/models.dart
downloaddart-8e719fc715eb7ae9aa1c842e633f15b5c6cd3688.tar.gz
Initial importv0.1.0
Diffstat (limited to 'lib/src/models.dart')
-rw-r--r--lib/src/models.dart363
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;
+}