diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 251 |
1 files changed, 251 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..26e957d --- /dev/null +++ b/README.md @@ -0,0 +1,251 @@ +# cepis + +Zig client for the [public CEP API at cep.is](https://cep.is). Official Correios +data (DNE), refreshed every two weeks. + +```zig +var client: cepis.Client = try .init(gpa, io, .{}); +defer client.deinit(); + +var address = try client.lookup("01001-000", .{}); +defer address.deinit(gpa); + +std.debug.print("{s} {s} {s}\n", .{ address.logradouro, address.localidade, address.uf }); +// Praça da Sé São Paulo SP +``` + +## Installation + +```sh +zig fetch --save git+https://git.cep.is/zig.git +``` + +Then, in `build.zig`: + +```zig +const cepis = b.dependency("cepis", .{ .target = target, .optimize = optimize }); +exe.root_module.addImport("cepis", cepis.module("cepis")); +``` + +Requires Zig 0.16.0. No dependencies: HTTP, TLS, JSON and the CSV reader all +come from the standard library or from this package. + +## Usage + +Everything goes through a `Client`, built with an allocator and an `std.Io`. +Zig has no ambient allocator and, since 0.16, no ambient I/O, so there is no +module-level `lookup` — one extra line, and it is the honest one: + +```zig +const std = @import("std"); +const cepis = @import("cepis"); + +pub fn main(init: std.process.Init) !void { + var client: cepis.Client = try .init(init.gpa, init.io, .{}); + defer client.deinit(); + + var address = try client.lookup("01310-100", .{}); + defer address.deinit(init.gpa); +} +``` + +### A single lookup + +`lookup` accepts the CEP masked or unmasked and returns an `Address`: + +```zig +var a = try client.lookup("01310-100", .{}); +defer a.deinit(gpa); + +a.logradouro; // "Avenida Paulista" +a.localidade; // "São Paulo" +a.uf; // "SP" +a.ibge; // "3550308" +a.cep; // "01310100" (always unmasked) + +var buffer: [16]u8 = undefined; +a.formattedCep(&buffer); // "01310-100" +``` + +Fields the database does not have for that CEP come back as empty strings. +`a.raw` holds the response body exactly as it arrived, so a field the API grows +before this package is updated is not lost. + +Every returned value owns its memory and has a `deinit` taking the allocator the +client was built with. `client.gpa` is that allocator, if it is easier to reach. + +### Coordinates + +About 57% of CEPs have a coordinate. When they do not, `geo` is null: + +```zig +if (a.geo) |geo| { + geo.lat; // -23.567202 + geo.lon; // -46.649659 + geo.raio_m; // 1084 + 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.isPrecise()` is the shorthand for `precisao == "logradouro"`: + +```zig +const cost = if (a.geo != null and a.geo.?.isPrecise()) + shippingFrom(a.geo.?.lat, a.geo.?.lon) +else + shippingFromMunicipality(a.ibge); +``` + +### Many CEPs at once + +Do not loop over `lookup`. Use `lookupMany`, which resolves up to 500 CEPs per +request: + +```zig +const ceps = [_][]const u8{ "01001000", "99999999", "20040020" }; + +const found = try client.lookupMany(&ceps, .{}); +defer cepis.freeAddresses(gpa, found); + +for (ceps, found) |requested, maybe_address| { + if (maybe_address) |a| { + std.debug.print("{s}: {s}\n", .{ requested, a.localidade }); + } else { + std.debug.print("{s}: not found\n", .{requested}); + } +} +``` + +The returned slice has **the same length and the same order** as the input. A +CEP that does not exist, or is malformed, becomes null at its position — it does +not vanish, and it is not an error. + +Inputs 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 `lookupMany` has `geo == null`, +> 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: + +```zig +var h = try client.health(.{}); +defer h.deinit(gpa); + +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 + +Everything beyond the defaults goes in the `Config` passed to `init`. Build one +client and reuse it: it keeps no state between calls, pools connections, and is +safe to share across threads. + +```zig +var client: cepis.Client = try .init(gpa, io, .{ + .base_url = "https://cep.is", // point at another instance + .timeout_ms = 10_000, // connect and TLS handshake + .max_attempts = 3, // counting the first; 1 disables retrying + .backoff_base_ms = 500, // base of the exponential wait + .backoff_max_ms = 8_000, // cap on each wait + .total_timeout_ms = 30_000, // cap on the whole call + .user_agent = "my-app/1.0", +}); +defer client.deinit(); +``` + +Retries cover 429, 5xx and network failures, with exponential backoff and +jitter; 400 and 404 are never retried. + +`timeout_ms` bounds the connection handshake. Zig 0.16's `std.http.Client` +exposes no per-read timeout, so a server that accepts and then stalls is bounded +only by `total_timeout_ms` — set that one in code serving a web request. + +### 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`: + +```zig +var client: cepis.Client = try .init(gpa, io, .{ .base_url = "http://localhost:8080" }); +``` + +From the command line, to confirm quickly that it answers: + +```sh +make run ARGS="--base-url http://localhost:8080 01001000" +``` + +## Errors + +Everything fails with one error set. The tags split by **the decision you have +to make**, not by the HTTP status that caused it: + +| error | when | does retrying help? | +| --- | --- | --- | +| `error.InvalidCep` | not 8 digits | no | +| `error.NotFound` | well-formed, not in the database | no | +| `error.RateLimited` | 429; `retry_after_ms` in the diagnostics | yes, later | +| `error.ServerError` | 5xx; `status` in the diagnostics | yes, later | +| `error.NetworkError` | DNS, connection, timeout, TLS | yes, later | +| `error.InvalidResponse` | unintelligible response | probably not | +| `error.OutOfMemory` | allocation failed | no | + +A Zig error carries no payload, so the detail travels in an optional +`Diagnostics` you pass in. It allocates nothing and needs no `deinit`: + +```zig +var diag: cepis.Diagnostics = .{}; + +const address = client.lookup(cep_from_form, .{ .diagnostics = &diag }) catch |err| switch (err) { + error.NotFound => return askForAnotherCep(), + error.RateLimited => return rescheduleIn(diag.retry_after_ms orelse 60_000), + else => { + if (cepis.isRetriable(err)) return reschedule(); + std.log.err("{t}: {s}", .{ err, diag.message() }); + return err; + }, +}; +``` + +`error.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 `zig`: + +```sh +make # list the targets +make test # unit and integration tests +make lint # zig fmt --check + zig build check, touches nothing +make format # apply zig fmt (writes) +make build # release build of the library and the CLI +make check # lint, then test, then build +make docs # API documentation into zig-out/docs +``` + +The integration tests stand up a real HTTP server on loopback and point the +client at it via `base_url`. They allocate through `std.testing.allocator`, so a +leak on either side fails the test. + +## Licence + +ISC. See [LICENSE](LICENSE). |