# 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).