diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-27 20:39:06 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-27 20:39:06 -0300 |
| commit | 497b1c1b01f6c9a584e5b40e02aff4763447529c (patch) | |
| tree | b50feb4252ba93cf045be3aaac14a96eeb04dff5 /src/model.zig | |
| parent | 87166a5299aa551517a86379a0c05eb2110b52b0 (diff) | |
| download | zig-497b1c1b01f6c9a584e5b40e02aff4763447529c.tar.gz | |
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/model.zig')
| -rw-r--r-- | src/model.zig | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/src/model.zig b/src/model.zig index 10ecbd3..8fa1673 100644 --- a/src/model.zig +++ b/src/model.zig @@ -16,6 +16,9 @@ //! fields points into memory the value allocated, and `deinit` releases it. //! Pass the same allocator the `Client` was built with, and treat every slice //! as invalid afterwards. +//! +//! `Faixa` is the exception: it allocates nothing and owns nothing. It travels +//! inside a `Diagnostics`, which is a stack value, and borrows its storage. const std = @import("std"); const Allocator = std.mem.Allocator; @@ -264,6 +267,119 @@ pub const Address = struct { } }; +/// The municipality a CEP that does **not** exist would have belonged to. +/// +/// `GET /{cep}` still answers **404** for a CEP the DNE does not have. When the +/// number falls inside a band a municipality owns, that 404 now carries this +/// block as well. It is **context for an error, never an address**: the CEP +/// asked for does not exist, and nothing here says otherwise. Treating a +/// non-null `faixa` as a successful lookup turns a clean failure into wrong +/// data. +/// +/// It is worth reporting because a failing CEP is rarely random — it is usually +/// someone who knows the city and guessed the number, and the classic guess is +/// the `-000` that 957 municipalities never received. +/// +/// ## `cep_ini` and `cep_fim` are not the Correios' bounds +/// +/// They are the smallest and the largest CEP that **actually exist** in that +/// municipality, not the range the Correios publish. For Areal (RJ) the DNE's +/// own start is `25845000` — precisely the CEP that just failed — while +/// `cep_ini` is `25845001`. +/// +/// ## The range is sparse +/// +/// Areal's 118 CEPs are scattered across the ~4,900 numbers between `25845001` +/// and `25849899`. Never present the pair as a list of valid CEPs, and never +/// count `cep_fim - cep_ini`: `ceps` is the count, and it is here so that +/// mistake cannot be made by accident. +/// +/// ## Ownership +/// +/// Owns nothing and has no `deinit`. Its strings borrow the buffer they were +/// packed into — the `Diagnostics` that read them — and are valid for exactly +/// as long as that value is. Copy them if you need them to outlive it. +pub const Faixa = struct { + /// Municipality: `"Areal"`. + localidade: []const u8 = "", + /// Federative unit, two letters: `"RJ"`. + uf: []const u8 = "", + /// Federative unit spelled out: `"Rio de Janeiro"`. + estado: []const u8 = "", + /// Region: `"Sudeste"`. + regiao: []const u8 = "", + /// IBGE municipality code — the key to join with public datasets. + ibge: []const u8 = "", + /// The **smallest CEP that exists** in the municipality, not the start of + /// the band the Correios published for it. + cep_ini: []const u8 = "", + /// The **largest CEP that exists** in the municipality. + cep_fim: []const u8 = "", + /// How many CEPs the municipality actually has between `cep_ini` and + /// `cep_fim`. Almost always far fewer than the numbers those two span. + ceps: u64 = 0, + + /// The string fields, in the order they are packed into the buffer. Used to + /// keep the field list and the JSON keys in step; `localidade` must stay + /// first, since it is the one that decides the block says anything at all. + pub const text_fields = [_][]const u8{ + "localidade", "uf", "estado", "regiao", + "ibge", "cep_ini", "cep_fim", + }; + + /// Bytes of storage a `Faixa` can need. The longest municipality name the + /// DNE holds is 72 bytes and every other field is short, so this has room + /// to spare — see `fromJson` for what happens if it somehow does not. + pub const max_bytes = 256; + + /// Read the `faixa` block off the JSON body of a 404, packing its strings + /// into `buffer` in `text_fields` order. + /// + /// Returns null — never an error — when the block is missing, null, not an + /// object, carries no `localidade`, or would not fit in `buffer`. The + /// answer is already `error.NotFound`, and no shape of this field may turn + /// that into a failure to parse. The API **omits** `faixa` rather than + /// sending null when the CEP falls in a band no municipality owns, so + /// absent is the common case, not a malformed one. + /// + /// Nothing is allocated. The result borrows `buffer`, which must be at + /// least `max_bytes` long to be sure of holding any real range. + pub fn fromJson(value: std.json.Value, buffer: []u8) ?Faixa { + const object = switch (value) { + .object => |o| o, + else => return null, + }; + const block = switch (object.get("faixa") orelse return null) { + .object => |o| o, + else => return null, + }; + const wrapped: std.json.Value = .{ .object = block }; + + var scratch: [text_fields.len][32]u8 = undefined; + var values: [text_fields.len][]const u8 = undefined; + var total: usize = 0; + inline for (text_fields, 0..) |name, i| { + values[i] = jsonText(wrapped, name, &scratch[i]); + total += values[i].len; + } + + // Without the municipality the block names nothing, which is the same + // as not being there. + if (values[0].len == 0) return null; + if (total > buffer.len) return null; + + var faixa: Faixa = .{ .ceps = jsonCount(block.get("ceps")) }; + var cursor: usize = 0; + inline for (text_fields, 0..) |name, i| { + const v = values[i]; + @memcpy(buffer[cursor..][0..v.len], v); + @field(faixa, name) = buffer[cursor..][0..v.len]; + cursor += v.len; + } + return faixa; + } +}; + /// The response of `GET /health`. /// /// Good for two things: checking that the API answers, and knowing **which @@ -510,6 +626,86 @@ test "formattedCep passes a malformed CEP through" { try testing.expectEqualStrings("123", address.formattedCep(&buffer)); } +const areal_404 = + \\{"erro":"cep_nao_encontrado","mensagem":"CEP não existe na base do DNE", + \\ "faixa":{"localidade":"Areal","uf":"RJ","estado":"Rio de Janeiro", + \\ "regiao":"Sudeste","ibge":"3300225","cep_ini":"25845001", + \\ "cep_fim":"25849899","ceps":118}} +; + +test "Faixa.fromJson names the municipality of a CEP that does not exist" { + const parsed = try parseValue(areal_404); + defer parsed.deinit(); + + var buffer: [Faixa.max_bytes]u8 = undefined; + const faixa = Faixa.fromJson(parsed.value, &buffer).?; + + try testing.expectEqualStrings("Areal", faixa.localidade); + try testing.expectEqualStrings("RJ", faixa.uf); + try testing.expectEqualStrings("Rio de Janeiro", faixa.estado); + try testing.expectEqualStrings("Sudeste", faixa.regiao); + try testing.expectEqualStrings("3300225", faixa.ibge); + // The DNE's own start for Areal is 25845000; cep_ini is the smallest CEP + // that exists, which is not the same number. + try testing.expectEqualStrings("25845001", faixa.cep_ini); + try testing.expectEqualStrings("25849899", faixa.cep_fim); + // 118 CEPs over the ~4,900 numbers the pair spans: the range is sparse. + try testing.expectEqual(@as(u64, 118), faixa.ceps); +} + +test "a faixa the API omitted is null, not an error" { + const bodies = [_][]const u8{ + \\{"erro":"cep_nao_encontrado","mensagem":"CEP não existe na base do DNE"} + , + \\{"erro":"cep_nao_encontrado","faixa":null} + , + \\{"erro":"cep_nao_encontrado","faixa":{}} + , + \\{"erro":"cep_nao_encontrado","faixa":"Areal"} + , + \\{"erro":"cep_nao_encontrado","faixa":{"uf":"RJ","ceps":118}} + , + \\{"erro":"cep_nao_encontrado","faixa":{"localidade":"","uf":"RJ"}} + , + \\[] + , + }; + for (bodies) |body| { + const parsed = try parseValue(body); + defer parsed.deinit(); + + var buffer: [Faixa.max_bytes]u8 = undefined; + try testing.expect(Faixa.fromJson(parsed.value, &buffer) == null); + } +} + +test "a partial faixa keeps what it has" { + const body = + \\{"faixa":{"localidade":"Areal","ceps":"118"}} + ; + const parsed = try parseValue(body); + defer parsed.deinit(); + + var buffer: [Faixa.max_bytes]u8 = undefined; + const faixa = Faixa.fromJson(parsed.value, &buffer).?; + + try testing.expectEqualStrings("Areal", faixa.localidade); + try testing.expectEqualStrings("", faixa.uf); + try testing.expectEqualStrings("", faixa.cep_ini); + try testing.expectEqual(@as(u64, 118), faixa.ceps); +} + +test "a faixa that does not fit is reported as absent, not truncated" { + const body = + \\{"faixa":{"localidade":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","uf":"RJ"}} + ; + const parsed = try parseValue(body); + defer parsed.deinit(); + + var small: [8]u8 = undefined; + try testing.expect(Faixa.fromJson(parsed.value, &small) == null); +} + test "Health.fromJson reads counts and status" { const body = \\{"status":"ok","dne":"26071","ceps":1596168,"geo":894489, |