diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:17:46 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:17:46 -0300 |
| commit | 4a8c3b675df4b1a6b29904fc2af60605002a7f9e (patch) | |
| tree | 5c3dcc6594603d242b6a284c1601fd1ffbfcc8f4 /README.md | |
| download | rust-4a8c3b675df4b1a6b29904fc2af60605002a7f9e.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 201 |
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). |