diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 190 |
1 files changed, 190 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..4e1a18f --- /dev/null +++ b/README.md @@ -0,0 +1,190 @@ +# cepis + +OCaml client for the [public CEP API at cep.is](https://cep.is). Official +Correios data (DNE), refreshed every two weeks. + +```ocaml +match Cepis.lookup "01001-000" with +| Ok a -> Printf.printf "%s %s %s\n" a.Cepis.logradouro a.Cepis.localidade a.Cepis.uf +(* Praça da Sé São Paulo SP *) +| Error e -> prerr_endline (Cepis.string_of_error e) +``` + +## Installation + +```sh +opam pin add cepis git+https://git.cep.is/ocaml.git +``` + +Requires OCaml 4.14 or newer. + +Three dependencies, each because the standard library has no equivalent: +[`ezcurl`](https://github.com/c-cube/ezcurl) for HTTP, because it is +synchronous and does not force an async runtime on callers; +[`yojson`](https://github.com/ocaml-community/yojson) for JSON, the +conventional choice; and [`csv`](https://github.com/Chris00/ocaml-csv) for the +batch endpoint, whose values may be quoted and contain commas. `ezcurl` binds +libcurl, so its development headers must be present; opam installs them for +you on most systems. + +## Usage + +### A single lookup + +`lookup` accepts the CEP masked or unmasked and returns an `address`: + +```ocaml +let a = Result.get_ok (Cepis.lookup "01310-100") in +a.Cepis.logradouro (* "Avenida Paulista" *) +a.Cepis.localidade (* "São Paulo" *) +a.Cepis.uf (* "SP" *) +a.Cepis.ibge (* "3550308" *) +a.Cepis.cep (* "01310100" (always unmasked) *) +Cepis.formatted_cep a (* "01310-100" *) +``` + +Fields the database does not have for that CEP come back as empty strings. + +### Coordinates + +About 57% of CEPs have a coordinate. When they do not, `geo` is `None`: + +```ocaml +match a.Cepis.geo with +| Some g -> Printf.printf "%f %f %d %s\n" g.Cepis.lat g.Cepis.lon g.Cepis.raio_m g.Cepis.precisao +| None -> () +``` + +**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 | + +`Cepis.is_precise` is the shorthand for `precisao = "logradouro"`: + +```ocaml +match a.Cepis.geo with +| Some g when Cepis.is_precise g -> shipping_from g.Cepis.lat g.Cepis.lon +| _ -> shipping_from_municipality a.Cepis.ibge +``` + +### Many CEPs at once + +Do not loop over `lookup`. Use `lookup_many`, which resolves up to 500 CEPs +per request: + +```ocaml +let ceps = [ "01001000"; "99999999"; "20040020" ] in +match Cepis.lookup_many ceps with +| Ok rows -> + List.iter2 + (fun cep -> function + | None -> Printf.printf "%s: not found\n" cep + | Some a -> Printf.printf "%s: %s\n" cep a.Cepis.localidade) + ceps rows +| Error e -> prerr_endline (Cepis.string_of_error e) +``` + +The list returned 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 does not fail the call. + +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 `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: + +```ocaml +let h = Result.get_ok (Cepis.health ()) in +Cepis.is_ok h (* true *) +h.Cepis.dne (* "26071" identifier of the fortnightly database *) +h.Cepis.ceps (* 1596168 *) +h.Cepis.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 `Cepis.Client.t` and reuse it. It +keeps no state between calls: + +```ocaml +let client = + Cepis.Client.create + ~base_url:"https://cep.is" (* point at another instance *) + ~timeout:10.0 (* seconds for one attempt, connection included *) + ~max_attempts:3 (* counting the first; 1 disables retrying *) + ~backoff_base:0.5 (* base of the exponential wait *) + ~backoff_max:8.0 (* cap on each wait *) + ~total_timeout:15.0 (* cap on the whole call, attempts included *) + () + +let address = Cepis.Client.lookup client "01001000" +``` + +Retries cover 429, 5xx and network failures, with exponential backoff and +jitter; 400 and 404 are never retried. See `Cepis.Client.create` in the +interface for the details of each parameter. + +The module-level functions (`Cepis.lookup`, etc.) share a default client built +with the defaults. Need a different configuration? Build your own. + +### Pointing at a local instance + +`base_url` is the only parameter that usually changes between environments. +The API repo brings a local instance up with `just up`: + +```ocaml +let client = Cepis.Client.create ~base_url:"http://localhost:8080" () +``` + +## Errors + +Expected failures come back as `('a, Cepis.error) result`; only a nonsensical +configuration raises, as `Invalid_argument`. The variants split by **the +decision you have to make**, not by the HTTP status that caused it: + +| error | when | does retrying help? | +| --- | --- | --- | +| `Invalid_cep` | not 8 digits | no | +| `Cep_not_found` | well-formed, not in the database | no | +| `Rate_limited` | 429; carries `retry_after` | yes, later | +| `Server_error` | 5xx; carries `status` | yes, later | +| `Network_error` | DNS, connection, timeout, TLS | yes, later | +| `Invalid_response` | unintelligible response | probably not | + +```ocaml +match Cepis.lookup cep_from_form with +| Ok address -> use address +| Error (Cepis.Cep_not_found _) -> ask_for_another_cep () +| Error _ -> reschedule () +``` + +`Invalid_response` 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 + +```sh +make # list the targets +make test # run the suite against a local HTTP server +make lint # ocamlformat check, touches nothing +make build # build the library +make check # lint, then test, then build +make doc # odoc documentation +``` + +## Licence + +ISC. See [LICENSE](LICENSE). |