diff options
Diffstat (limited to 'src/client.zig')
| -rw-r--r-- | src/client.zig | 69 |
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"}); |