summaryrefslogtreecommitdiffstats
path: root/src/model.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/model.zig')
-rw-r--r--src/model.zig196
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,