summaryrefslogtreecommitdiffstats
path: root/src/client.zig
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-27 20:39:06 -0300
committerMurilo Ijanc <[email protected]>2026-07-27 20:39:06 -0300
commit497b1c1b01f6c9a584e5b40e02aff4763447529c (patch)
treeb50feb4252ba93cf045be3aaac14a96eeb04dff5 /src/client.zig
parent87166a5299aa551517a86379a0c05eb2110b52b0 (diff)
downloadzig-0.2.0.tar.gz
Report the municipality behind a CEP that does not existHEADv0.2.0main
A CEP that fails to resolve is rarely a random number. It is almost always someone who knows the city and guessed the digits, and the classic guess is the -000 that 957 municipalities never received. The API now says so: a 404 carries an optional faixa block naming the municipality the number would have belonged to, and answering "no such CEP in Areal/RJ, which has 118 of them" is a great deal more useful than "not found". It arrives through Diagnostics, which is where every other detail a Zig error tag cannot carry already travels, and it is read with diag.faixa(). Like message(), it lives in a fixed buffer inside the value and allocates nothing, so no caller has to learn a new deinit for it. The lookup still fails. faixa is context for the error, never an address, and cep_ini/cep_fim are the smallest and largest CEP that actually exist there rather than the bounds the Correios publish — for Areal the DNE's own start is 25845000, which is exactly the CEP that failed. The range is also sparse, 118 CEPs over ~4,900 numbers, so ceps is the count and the pair must never be presented as a list. The doc comments and the README say all three, because this field is easy to misread as a success. Parsing is deliberately inert: a faixa that is missing, null, not an object, nameless or oversized yields null, and an unreadable body is skipped. Nothing about this field may promote a clean not-found into a parse failure.
Diffstat (limited to 'src/client.zig')
-rw-r--r--src/client.zig69
1 files changed, 68 insertions, 1 deletions
diff --git a/src/client.zig b/src/client.zig
index 3a726e5..2c71e16 100644
--- a/src/client.zig
+++ b/src/client.zig
@@ -22,7 +22,7 @@ const Health = model.Health;
const note = errors.note;
/// The version of this library, as sent in the `User-Agent`.
-pub const version = "0.1.0";
+pub const version = "0.2.0";
/// Where the public API lives.
pub const default_url = "https://cep.is";
@@ -238,6 +238,11 @@ pub const Client = struct {
/// in `Diagnostics`: the format already passed local validation, so the
/// server knows something this client does not.
///
+ /// A 404 becomes `error.NotFound`, and the 404 body may name the
+ /// municipality the CEP would have belonged to — read it with
+ /// `Diagnostics.faixa`. It stays an error: the CEP does not exist, and the
+ /// range is context for saying so, not an address.
+ ///
/// Errors: `error.InvalidCep` (not 8 digits — no request is made),
/// `error.NotFound`, `error.RateLimited`, `error.ServerError`,
/// `error.NetworkError`, `error.InvalidResponse`, `error.OutOfMemory`.
@@ -259,6 +264,7 @@ pub const Client = struct {
defer c.gpa.free(response.body);
if (response.status == 404) {
+ noteFaixa(options.diagnostics, response.body);
note(options.diagnostics, "CEP {s} does not exist in the DNE database", .{number});
return error.NotFound;
}
@@ -746,6 +752,35 @@ fn noteApiMessage(
d.setMessage(fallback, args);
}
+/// Record the `faixa` a 404 body may carry — the municipality the CEP would
+/// have belonged to — into `diagnostics`.
+///
+/// Every failure here is silent and leaves `Diagnostics.faixa` null. The answer
+/// is already `error.NotFound`, and no body, however mangled, may promote a
+/// clean not-found into `error.InvalidResponse`. The parse runs against a fixed
+/// stack buffer, so a body too large for it is simply skipped: nothing is
+/// allocated from `gpa` on this path and nothing can leak.
+///
+/// The *leaky* parse is the point of the fixed buffer: the arena
+/// `parseFromSlice` builds asks its backing allocator for a node far larger
+/// than the document, which a stack buffer this size cannot serve. Here the
+/// buffer *is* the arena, and it is discarded whole on return.
+fn noteFaixa(diagnostics: ?*Diagnostics, body: []const u8) void {
+ const d = diagnostics orelse return;
+ d.clearFaixa();
+
+ var buffer: [4096]u8 = undefined;
+ var fba: std.heap.FixedBufferAllocator = .init(&buffer);
+ const value = std.json.parseFromSliceLeaky(
+ std.json.Value,
+ fba.allocator(),
+ body,
+ .{},
+ ) catch return;
+
+ d.setFaixa(value);
+}
+
fn preview(body: []const u8) []const u8 {
return body[0..@min(body.len, 120)];
}
@@ -793,6 +828,38 @@ test "only transient statuses are retriable" {
}
}
+test "noteFaixa reads the range a 404 carries" {
+ var diag: Diagnostics = .{};
+ noteFaixa(&diag,
+ \\{"erro":"cep_nao_encontrado","mensagem":"CEP não existe na base do DNE",
+ \\ "faixa":{"localidade":"Areal","uf":"RJ","ibge":"3300225",
+ \\ "cep_ini":"25845001","cep_fim":"25849899","ceps":118}}
+ );
+ const faixa = diag.faixa().?;
+ try testing.expectEqualStrings("Areal", faixa.localidade);
+ try testing.expectEqual(@as(u64, 118), faixa.ceps);
+}
+
+test "noteFaixa reports nothing rather than failing" {
+ var diag: Diagnostics = .{};
+ for ([_][]const u8{
+ "<html>",
+ "",
+ \\{"erro":"cep_nao_encontrado"}
+ ,
+ \\{"faixa":[]}
+ ,
+ \\{"faixa":{"uf":"RJ"}}
+ ,
+ }) |body| {
+ noteFaixa(&diag, body);
+ try testing.expect(diag.faixa() == null);
+ }
+
+ // Null works the same way with no `Diagnostics` at all.
+ noteFaixa(null, "{}");
+}
+
test "noteApiMessage prefers the API wording" {
var diag: Diagnostics = .{};
noteApiMessage(&diag, "{\"erro\":\"cep_invalido\",\"mensagem\":\"rejected\"}", "fallback {s}", .{"x"});