summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:20:42 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:20:42 -0300
commit329f679cd359bcab92adf3366cf17711836d0c79 (patch)
tree68f956deb2e0d5c3a25da20260ee3c0678a64cd6 /README.md
downloadjava-329f679cd359bcab92adf3366cf17711836d0c79.tar.gz
Initial importv0.1.0
Diffstat (limited to 'README.md')
-rw-r--r--README.md219
1 files changed, 219 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..599a25e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,219 @@
+# cepis
+
+Java client for the [public CEP API at cep.is](https://cep.is). Official
+Correios data (DNE), refreshed every two weeks.
+
+```java
+import is.cep.cepis.Address;
+import is.cep.cepis.Cepis;
+
+Address address = Cepis.lookup("01001-000");
+System.out.println(address.logradouro() + " " + address.localidade() + " "
+ + address.uf());
+// Praça da Sé São Paulo SP
+```
+
+## Installation
+
+Requires Java 17 or newer. There are no dependencies: HTTP comes from
+`java.net.http`, and the JSON and CSV readers are in the library because the
+JDK has neither and the shapes this API returns are small enough not to justify
+a jar.
+
+```sh
+git clone https://git.cep.is/java.git cepis-java
+cd cepis-java
+make build
+```
+
+That writes `build/cepis-0.1.0.jar`; put it on the classpath.
+
+## Usage
+
+### A single lookup
+
+`lookup` accepts the CEP masked or unmasked and returns an `Address`:
+
+```java
+Address a = 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"
+```
+
+The data fields keep the API's own Portuguese names, with only the casing
+adapted: what you read in the JSON is what you access on the object. Fields the
+database does not have for that CEP come back as empty strings, never `null`.
+
+### Coordinates
+
+About 57% of CEPs have a coordinate. When they do not, `geo()` is `null`:
+
+```java
+Geo geo = a.geo();
+if (geo != null) {
+ geo.lat(); // -23.567202
+ geo.lon(); // -46.649659
+ geo.raioM(); // 1084
+ 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 `"logradouro".equals(precisao)`:
+
+```java
+if (a.geo() != null && a.geo().isPrecise()) {
+ cost = shippingFrom(a.geo().lat(), a.geo().lon());
+} else {
+ cost = shippingFromMunicipality(a.ibge());
+}
+```
+
+### Many CEPs at once
+
+Do not loop over `lookup`. Use `lookupMany`, which resolves up to 500 CEPs per
+request:
+
+```java
+List<String> ceps = List.of("01001000", "99999999", "20040020");
+List<Address> result = Cepis.lookupMany(ceps);
+
+for (int i = 0; i < ceps.size(); i++) {
+ Address address = result.get(i);
+ System.out.println(address == null
+ ? ceps.get(i) + ": not found"
+ : ceps.get(i) + ": " + address.localidade());
+}
+```
+
+The returned list has **the same size 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 the coordinate? 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:
+
+```java
+Health h = 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, pools connections, and is safe to share across threads:
+
+```java
+Client client = Client.builder()
+ .baseUrl("https://cep.is") // point at another instance
+ .timeout(Duration.ofSeconds(10)) // per attempt, connection included
+ .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(Duration.ofSeconds(15)) // cap on the whole call; unset by default
+ .build();
+
+Address address = client.lookup("01001000");
+```
+
+Retries cover 429, 5xx and network failures, with exponential backoff and
+jitter; 400 and 404 are never retried. See the javadoc on `Client.Builder` for
+each setting.
+
+The static methods on `Cepis` use a shared default client. It applies
+process-wide, including to third-party libraries that also use this package;
+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`:
+
+```java
+Client client = Client.builder().baseUrl("http://localhost:8080").build();
+```
+
+From the command line, to confirm quickly that it answers:
+
+```sh
+java -jar build/cepis-0.1.0.jar --base-url http://localhost:8080 01001000
+```
+
+## Errors
+
+Everything derives from `CepisException`, which is unchecked. 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 |
+
+```java
+try {
+ Address address = Cepis.lookup(cepFromForm);
+} catch (CepNotFoundException e) {
+ // ask the user for another CEP
+} catch (CepisException e) {
+ // network, server, rate limit: reschedule
+}
+```
+
+Java allows one superclass, so these are not also `IllegalArgumentException` or
+`NoSuchElementException`: one base type that catches every failure is worth
+more.
+
+`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` and the JDK. There is nothing to install first
+and no dependency to resolve:
+
+```sh
+make # list the targets
+make test # run the suite against a local HTTP server
+make lint # javac -Werror + javadoc doclint, touches nothing
+make build # the jar into build/
+make docs # the API documentation into build/docs
+make check # lint, then test, then build
+```
+
+The JDK ships no formatter, so there is no `format` target.
+
+## Licence
+
+ISC. See [LICENSE](LICENSE).