diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:19:13 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:19:13 -0300 |
| commit | 0757164507a1f9be8b9f61292a9069759f1da034 (patch) | |
| tree | e958ba14c7f6a6ae3cdabe1ed0cd5f69b530edb7 /README.md | |
| download | kotlin-0757164507a1f9be8b9f61292a9069759f1da034.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 222 |
1 files changed, 222 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..4278f8a --- /dev/null +++ b/README.md @@ -0,0 +1,222 @@ +# cepis + +Kotlin client for the [public CEP API at cep.is](https://cep.is). Official +Correios data (DNE), refreshed every two weeks. + +```kotlin +import org.cepis.lookup + +val address = lookup("01001-000") +println("${address.logradouro} ${address.localidade} ${address.uf}") +// Praça da Sé São Paulo SP +``` + +No runtime dependencies: HTTP is `java.net.http.HttpClient` from the JDK, and +the JSON and CSV readers are ~200 lines in this library — the API's payloads +are small and fixed, which is cheaper than adding `kotlinx.serialization` and +its compiler plugin to every consumer's build. + +## Installation + +Requires Java 17 or newer. + +```kotlin +repositories { + mavenCentral() + // plus wherever you host it +} + +dependencies { + implementation("org.cepis:cepis:0.1.0") +} +``` + +## Usage + +### A single lookup + +`lookup` accepts the CEP masked or unmasked and returns an `Address`: + +```kotlin +val a = 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" +``` + +Data fields keep the API's own Portuguese names, so what you read in the JSON +is what you access here. Only the casing is adapted where Kotlin demands it: +the API's `raio_m` is `raioM` and its `base_gerada_em` is `baseGeradaEm`. + +Fields the database does not have for that CEP come back as empty strings, +never `null`. `a.raw` holds the response exactly as it arrived, so a field the +API grows before this library is updated is not lost. + +### Coordinates + +About 57% of CEPs have a coordinate. When they do not, `geo` is `null`: + +```kotlin +a.geo?.let { geo -> + println("${geo.lat} ${geo.lon}") // -23.567202 -46.649659 + println(geo.raioM) // 1084 + println(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"`: + +```kotlin +val geo = a.geo +val cost = + if (geo != null && geo.isPrecise) { + shippingFrom(geo.lat, geo.lon) + } else { + shippingFromMunicipality(a.ibge) + } +``` + +### Many CEPs at once + +Do not loop over `lookup`. Use `lookupMany`, which resolves up to 500 CEPs per +request: + +```kotlin +val ceps = listOf("01001000", "99999999", "20040020") + +ceps.zip(lookupMany(ceps)).forEach { (requested, address) -> + if (address == null) println("$requested: not found") + else println("$requested: ${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, becomes `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 `geo == null`, +> 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: + +```kotlin +val h = 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 and is safe to share across threads: + +```kotlin +import org.cepis.Client +import java.time.Duration + +val client = + Client( + baseUrl = "https://cep.is", // point at another instance + timeout = Duration.ofSeconds(10), // per attempt: connect, then response + maxAttempts = 3, // counting the first; 1 disables retrying + backoffBase = Duration.ofMillis(500), // base of the exponential wait + backoffMax = Duration.ofSeconds(8), // cap on each wait + totalTimeout = null, // cap on the whole call, attempts included + userAgent = null, // defaults to cepis-kotlin/0.1.0 + ) + +val address = client.lookup("01001000") +``` + +Retries cover 429, 5xx and network failures, with exponential backoff and +jitter; 400 and 404 are never retried. + +The top-level functions (`lookup`, `lookupMany`, `health`) use a shared default +client. It applies process-wide, including to third-party libraries that also +use this module. Need a different configuration? Build your own. + +### Pointing at a local instance + +`baseUrl` is the only parameter that usually changes between environments. The +API repo brings a local instance up with `just up`: + +```kotlin +val client = Client(baseUrl = "http://localhost:8080") +``` + +## Errors + +Everything derives from `CepisException`, a `sealed` class, so one `catch` +handles any failure and a `when` over it is exhaustive. 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; carries `retryAfter` | yes, later | +| `ServerErrorException` | 5xx; carries `status` | yes, later | +| `NetworkException` | DNS, connection, timeout, TLS | yes, later | +| `InvalidResponseException` | unintelligible response | probably not | + +```kotlin +try { + val address = lookup(cepFromForm) +} catch (e: CepNotFoundException) { + // ask the user for another CEP +} catch (e: CepisException) { + // transient failure: reschedule +} +``` + +A nonsensical `Client` configuration throws a plain `IllegalArgumentException` +instead: that is a programming error, not a lookup failure. + +`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 the Gradle wrapper — there +is nothing to install first beyond a JDK 17: + +```sh +make # list the targets +make test # run the test suite +make lint # ktlint, touches nothing +make format # apply ktlint (writes) +make build # jar + sources jar into build/libs/ +make check # lint, then test, then build +``` + +The tests stand up a real `com.sun.net.httpserver.HttpServer` on a loopback +port and point a real `Client` at it through `baseUrl`. Nothing is mocked and +nothing touches the network. + +## Licence + +ISC. See [LICENSE](LICENSE). |