summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:48:43 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:48:43 -0300
commit87166a5299aa551517a86379a0c05eb2110b52b0 (patch)
tree3e77e7dd89cca736a22a5f2219451aa57dd99068 /README.md
downloadzig-87166a5299aa551517a86379a0c05eb2110b52b0.tar.gz
Initial importv0.1.0
Diffstat (limited to 'README.md')
-rw-r--r--README.md251
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).