summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md201
1 files changed, 201 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8481710
--- /dev/null
+++ b/README.md
@@ -0,0 +1,201 @@
+# cepis
+
+Rust client for the [public CEP API at cep.is](https://cep.is). Official
+Correios data (DNE), refreshed every two weeks.
+
+```rust
+let address = cepis::lookup("01001-000")?;
+println!("{} {} {}", address.logradouro, address.localidade, address.uf);
+// Praça da Sé São Paulo SP
+```
+
+## Installation
+
+```toml
+[dependencies]
+cepis = { git = "https://git.cep.is/rust.git" }
+```
+
+Requires Rust 1.85 or newer. Two dependencies: `ureq` — the standard library
+has no HTTP client, and `ureq` is the smallest established blocking one, with
+TLS through `rustls` and no async runtime — and `serde_json`.
+
+## Usage
+
+### A single lookup
+
+`lookup` accepts the CEP masked or unmasked and returns an `Address`:
+
+```rust
+let 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.formatted_cep(); // "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 crate is updated is not lost.
+
+### Coordinates
+
+About 57% of CEPs have a coordinate. When they do not, `geo` is `None`:
+
+```rust
+if let Some(geo) = &a.geo {
+ println!("{} {}", geo.lat, geo.lon); // -23.567202 -46.649659
+ println!("{}", geo.raio_m); // 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.is_precise()` is the shorthand for `precisao == "logradouro"`:
+
+```rust
+let cost = match &a.geo {
+ Some(geo) if geo.is_precise() => shipping_from(geo.lat, geo.lon),
+ _ => shipping_from_municipality(&a.ibge),
+};
+```
+
+### Many CEPs at once
+
+Do not loop over `lookup`. Use `lookup_many`, which resolves up to 500 CEPs per
+request:
+
+```rust
+let ceps = ["01001000", "99999999", "20040020"];
+
+for (requested, address) in ceps.iter().zip(cepis::lookup_many(&ceps)?) {
+ match address {
+ Some(a) => println!("{requested}: {}", a.localidade),
+ None => println!("{requested}: not found"),
+ }
+}
+```
+
+The returned `Vec` has **the same length and the same order** as the input. A
+CEP that does not exist, or is malformed, becomes `None` at its position — it
+does not vanish, and it is not an error.
+
+Slices 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 `lookup_many` has `geo: None`,
+> 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:
+
+```rust
+let h = cepis::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, is safe to share across threads, and pools connections:
+
+```rust
+use std::time::Duration;
+
+let client = cepis::Client::builder()
+ .base_url("https://cep.is") // point at another instance
+ .timeout(Duration::from_secs(10)) // per socket operation
+ .max_attempts(3) // counting the first; 1 disables retrying
+ .backoff_base(Duration::from_millis(500)) // base of the exponential wait
+ .backoff_max(Duration::from_secs(8)) // cap on each wait
+ .total_timeout(Some(Duration::from_secs(30))) // cap on the whole call
+ .user_agent("my-app/1.0")
+ .build();
+
+let address = client.lookup("01001000")?;
+```
+
+Retries cover 429, 5xx and network failures, with exponential backoff and
+jitter; 400 and 404 are never retried.
+
+The free functions (`cepis::lookup`, and so on) share one default client, built
+on first use. Need a different configuration? Build your own.
+
+### Pointing at a local instance
+
+`base_url` is the only setting that usually changes between environments. The
+API repo brings a local instance up with `just up`:
+
+```rust
+let client = cepis::Client::builder()
+ .base_url("http://localhost:8080")
+ .build();
+```
+
+From the command line, to confirm quickly that it answers:
+
+```sh
+cargo run --bin cepis -- --base-url http://localhost:8080 01001000
+```
+
+## Errors
+
+Everything is one `Error`. Its variants split by **the decision you have to
+make**, not by the HTTP status that caused it:
+
+| variant | when | does retrying help? |
+| --- | --- | --- |
+| `InvalidCep` | not 8 digits | no |
+| `NotFound` | well-formed, not in the database | no |
+| `RateLimited` | 429; carries `retry_after` | yes, later |
+| `Server` | 5xx; carries `status` | yes, later |
+| `Network` | DNS, connection, timeout, TLS | yes, later |
+| `InvalidResponse` | unintelligible response | probably not |
+
+```rust
+match cepis::lookup(&cep_from_form) {
+ Ok(address) => render(address),
+ Err(cepis::Error::NotFound { .. }) => ask_for_another_cep(),
+ Err(err) if err.is_retriable() => reschedule(),
+ Err(err) => eprintln!("{err}"),
+}
+```
+
+`InvalidResponse` 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 `cargo`:
+
+```sh
+make # list the targets
+make test # cargo test
+make lint # clippy + rustfmt check, touches nothing
+make format # apply rustfmt (writes)
+make build # release build
+make check # lint, then test, then build
+```
+
+## Licence
+
+ISC. See [LICENSE](LICENSE).