summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md222
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).