diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 233 |
1 files changed, 233 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..ffb4cac --- /dev/null +++ b/README.md @@ -0,0 +1,233 @@ +# cepis + +Dart client for the [public CEP API at cep.is](https://cep.is). Official +Correios data (DNE), refreshed every two weeks. Pure Dart, so the same code runs +on the VM, in AOT binaries and everywhere Flutter runs — Android, iOS, desktop +and Web. + +```dart +import 'package:cepis/cepis.dart' as cepis; + +final address = await cepis.lookup('01001-000'); +print('${address.logradouro} ${address.localidade} ${address.uf}'); +// Praça da Sé São Paulo SP +``` + +## Installation + +```sh +dart pub add cepis +# or, in a Flutter app: flutter pub add cepis +``` + +Requires Dart 3.6 or newer. One dependency, `package:http`: the SDK's own +`HttpClient` does not exist on Flutter Web, and a client that breaks on one of +Flutter's platforms is not a Flutter client. + +## Usage + +### A single lookup + +`lookup` accepts the CEP masked or unmasked and resolves to an `Address`: + +```dart +final a = await cepis.lookup('01310-100'); + +a.logradouro; // 'Avenida Paulista' +a.localidade; // 'São Paulo' +a.uf; // 'SP' +a.ibge; // '3550308' +a.cep; // '01310100' (always unmasked) +a.formattedCep; // '01310-100' +``` + +Fields the database does not have for that CEP come back as empty strings, never +`null`. `a.raw` reads the response as it arrived, including a field the API grew +after this release. + +The data fields keep the API's own Portuguese names; everything else — methods, +types, errors — is English. Only the casing changes, where Dart demands it: the +API's `raio_m` is `geo.raioM` and its `base_gerada_em` is `health.baseGeradaEm`. + +### Coordinates + +About 57% of CEPs have a coordinate. When they do not, `geo` is `null`: + +```dart +final a = await cepis.lookup('01310100'); + +final geo = a.geo; +if (geo != null) { + geo.lat; // -23.567202 + geo.lon; // -46.649659 + geo.raioM; // 1084 (the API's `raio_m`) + geo.precisao; // 'logradouro' +} +``` + +**Check `precisao` before computing distance.** There are two kinds of point, +and the difference between them is tens of kilometres: + +| `precisao` | typical radius | what the point is | +| --- | --- | --- | +| `logradouro` | 110–140 m | the stretch of street — good for maps and distance | +| `localidade` | 7–40 km | the municipality centroid: the CEP *is* the whole city | + +`geo.isPrecise` is the shorthand for `precisao == 'logradouro'`: + +```dart +final geo = a.geo; +final cost = geo != null && geo.isPrecise + ? shippingFrom(geo.lat, geo.lon) + : shippingFromMunicipality(a.ibge); +``` + +### Many CEPs at once + +Do not loop over `lookup`. Use `lookupMany`, which resolves up to 500 CEPs per +request: + +```dart +final ceps = <String>['01001000', '99999999', '20040020']; +final result = await cepis.lookupMany(ceps); + +for (var i = 0; i < ceps.length; i++) { + final address = result[i]; + print('${ceps[i]}: ${address == null ? 'not found' : address.localidade}'); +} +``` + +The returned list has **the same length and the same order** as the input. A CEP +that does not exist, or is malformed, is `null` at its position — it does not +vanish, and it does not throw. + +Lists longer than 500 are chunked automatically. + +> **Batches carry no coordinate.** The endpoint behind them is CSV, and the CSV +> has no `geo` column. Every `Address` from `lookupMany` has a `null` `geo`, +> including for CEPs that do have one. Need `geo`? Use `lookup`, one at a time. + +### API health + +Useful for two things: checking the API answers, and knowing **which version of +the database** is live: + +```dart +final h = await cepis.health(); + +h.ok; // true +h.dne; // '26071' identifier of the fortnightly database +h.ceps; // 1596168 +h.baseGeradaEm; // '2026-07-24T03:48:28Z' +``` + +An API reporting itself degraded does not throw — that arrives in `h.status`. + +## Configuration + +For anything beyond the defaults, build a `Client` and reuse it. It keeps no +state between calls: + +```dart +final client = cepis.Client( + baseUrl: 'https://cep.is', // point at another instance + timeout: const Duration(seconds: 10), // per attempt, body read included + maxAttempts: 3, // counting the first; 1 disables retrying + backoffBase: const Duration(milliseconds: 500), + backoffMax: const Duration(seconds: 8), // cap on each wait + totalTimeout: const Duration(seconds: 15), // cap on the whole call +); + +final address = await client.lookup('01001000'); +client.close(); +``` + +`timeout` bounds a single attempt — request and body read together. The cap on +all the attempts and the waits between them is `totalTimeout`. + +Retries cover 429, 5xx and network failures, with exponential backoff and full +jitter; 400 and 404 are never retried. + +Every snippet here assumes the `import ... as cepis;` above; a plain +`import 'package:cepis/cepis.dart';` puts `Client`, `Address` and the rest in +scope unprefixed. + +`Client` also takes an `httpClient`, for sharing a connection pool or for a +platform-native transport such as `cupertino_http` or `cronet_http`. A client +passed that way is yours to close; `close()` leaves it alone. + +The top-level functions (`cepis.lookup`, `cepis.lookupMany`, `cepis.health`) use +a shared default client, reachable as `cepis.defaultClient`. It applies +process-wide, third-party packages included. Need a different configuration? +Build your own. + +### Pointing at a local instance + +`baseUrl` is the only setting that usually changes between environments. The API +repo brings a local instance up with `just up`: + +```dart +final client = cepis.Client(baseUrl: 'http://localhost:8080'); +``` + +From the command line, to confirm quickly that it answers: + +```sh +dart run cepis --base-url http://localhost:8080 01001000 +``` + +## Errors + +Everything derives from `CepisException`, which is `sealed` — a `switch` over it +is checked for exhaustiveness. Below that, the split is by **the decision you +have to make**, not by the HTTP status that caused it: + +| error | when | does retrying help? | +| --- | --- | --- | +| `InvalidCepException` | not 8 digits | no | +| `CepNotFoundException` | well-formed, not in the database | no | +| `RateLimitedException` | 429 | yes, later | +| `ServerException` | 5xx | yes, later | +| `NetworkException` | DNS, connection, timeout, TLS | yes, later | +| `InvalidResponseException` | unintelligible response | probably not | + +```dart +try { + final address = await cepis.lookup(cepFromForm); +} on cepis.CepNotFoundException { + // ask the user for another CEP +} on cepis.CepisException { + // transient failure: reschedule +} +``` + +`RateLimitedException` exposes `retryAfter`, `ServerException` exposes `status`, +and `NetworkException` exposes `cause`. + +`InvalidCepException` also implements `FormatException`, so code already +handling malformed input during validation keeps working without knowing this +library exists. + +`InvalidResponseException` almost always means something between you and the API +is answering instead of it — a captive Wi-Fi portal, a corporate proxy, an HTML +error page. Useful as a diagnosis: the problem is not in your code. + +## Development + +Everything runs through `make`, which shells out to `dart`: + +```sh +make # list the targets +make test # dart test +make lint # dart format --set-exit-if-changed + dart analyze, touches nothing +make format # apply dart format +make build # compile the CLI to build/cepis +make check # lint, then test, then build +``` + +The suite stands a real `HttpServer` up on a loopback port and points `baseUrl` +at it: no network, and nothing about the HTTP layer is stubbed. + +## Licence + +ISC. See [LICENSE](LICENSE). |