summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md208
1 files changed, 208 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9c76d3d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,208 @@
+# cepis
+
+D client for the [public CEP API at cep.is](https://cep.is). Official Correios
+data (DNE), refreshed every two weeks.
+
+```d
+import cepis;
+
+auto address = lookup("01001-000");
+writeln(address.logradouro, " ", address.localidade, " ", address.uf);
+// Praça da Sé São Paulo SP
+```
+
+## Installation
+
+```sh
+dub add cepis
+```
+
+Or in `dub.json`:
+
+```json
+"dependencies": {
+ "cepis": "~>0.1.0"
+}
+```
+
+No dub dependencies: HTTP comes from `std.net.curl`, JSON from `std.json`, CSV
+from `std.csv`. `std.net.curl` needs **libcurl at runtime** (`libcurl.so.4` on
+Linux, `libcurl.dll` on Windows) and `libcurl` at link time — most systems
+already ship it; on Debian and derivatives that is `libcurl4-openssl-dev`.
+
+## Usage
+
+### A single lookup
+
+`lookup` accepts the CEP masked or unmasked and returns an `Address`:
+
+```d
+auto 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"
+```
+
+Fields the database does not have for that CEP come back as empty strings.
+`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:
+
+```d
+if (!a.geo.isNull)
+{
+ auto geo = a.geo.get;
+ writeln(geo.lat, " ", geo.lon); // -23.567202 -46.649659
+ writeln(geo.raio_m); // 1084
+ writeln(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"`:
+
+```d
+auto cost = !a.geo.isNull && a.geo.get.isPrecise
+ ? shippingFrom(a.geo.get.lat, a.geo.get.lon)
+ : shippingFromMunicipality(a.ibge);
+```
+
+### Many CEPs at once
+
+Do not loop over `lookup`. Use `lookupMany`, which resolves up to 500 CEPs per
+request:
+
+```d
+auto ceps = ["01001000", "99999999", "20040020"];
+
+foreach (index, address; lookupMany(ceps))
+ writeln(ceps[index], ": ", address.isNull ? "not found" : address.get.localidade);
+```
+
+The returned array 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.
+
+Arrays 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:
+
+```d
+auto h = health();
+
+h.ok; // true
+h.dne; // "26071" identifier of the fortnightly database
+h.ceps; // 1596168
+h.base_gerada_em; // "2026-07-24T03:48:28Z"
+```
+
+An API reporting itself degraded is not an error — 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 copy and share across threads:
+
+```d
+ClientConfig config;
+config.baseUrl = "https://cep.is"; // point at another instance
+config.timeout = dur!"seconds"(10); // per socket operation
+config.maxAttempts = 3; // counting the first; 1 disables retrying
+config.backoffBase = dur!"msecs"(500); // base of the exponential wait
+config.backoffMax = dur!"seconds"(8); // cap on each wait
+config.totalTimeout = dur!"seconds"(30); // cap on the whole call
+config.userAgent = "my-app/1.0";
+
+auto client = Client(config);
+auto address = client.lookup("01001000");
+```
+
+Retries cover 429, 5xx and network failures, with exponential backoff and
+jitter; 400 and 404 are never retried.
+
+The module-level functions (`lookup`, `lookupMany`, `health`) share one default
+client per thread, built on first use. 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`:
+
+```d
+ClientConfig config;
+config.baseUrl = "http://localhost:8080";
+
+auto client = Client(config);
+```
+
+## Errors
+
+Everything derives from `CepisException`, so one `catch` handles any failure.
+Below that, the split is by **the decision you have to make**, not by the HTTP
+status that caused it:
+
+| exception | when | does retrying help? |
+| --- | --- | --- |
+| `InvalidCepException` | not 8 digits | no |
+| `CepNotFoundException` | well-formed, not in the database | no |
+| `RateLimitedException` | 429; carries `retryAfter` | yes, later |
+| `ServerException` | 5xx; carries `status` | yes, later |
+| `NetworkException` | DNS, connection, timeout, TLS | yes, later |
+| `InvalidResponseException` | unintelligible response | probably not |
+
+```d
+try
+ render(lookup(cepFromForm));
+catch (CepNotFoundException)
+ askForAnotherCep();
+catch (CepisException error)
+{
+ if (error.retriable)
+ reschedule();
+ else
+ stderr.writeln(error.msg);
+}
+```
+
+`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 `dub`:
+
+```sh
+make # list the targets
+make test # dub test
+make lint # dscanner + a dfmt check, touches nothing
+make format # apply dfmt (writes)
+make build # release build
+make check # lint, then test, then build
+```
+
+## Licence
+
+ISC. See [LICENSE](LICENSE).