summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md190
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).