summaryrefslogtreecommitdiffstats
path: root/src/errors.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/errors.zig')
-rw-r--r--src/errors.zig128
1 files changed, 124 insertions, 4 deletions
diff --git a/src/errors.zig b/src/errors.zig
index c46e266..5ce474a 100644
--- a/src/errors.zig
+++ b/src/errors.zig
@@ -18,10 +18,13 @@
//!
//! A Zig error carries no payload, so anything a caller may need beyond the
//! tag — the HTTP status, the `Retry-After` the server asked for, the API's own
-//! message — travels in an optional `Diagnostics` the caller passes in.
+//! message, the municipality a missing CEP would have belonged to — travels in
+//! an optional `Diagnostics` the caller passes in.
const std = @import("std");
+const Faixa = @import("model.zig").Faixa;
+
/// Everything that can go wrong when talking to the API, minus allocation
/// failure. Use `Error` in signatures; use this when you want to match
/// exhaustively without `error.OutOfMemory` in the way.
@@ -35,6 +38,10 @@ pub const CepisError = error{
InvalidCep,
/// The CEP is well-formed but does not exist in the DNE database.
+ ///
+ /// `Diagnostics.faixa` may name the municipality the CEP would have
+ /// belonged to. That is context for the failure, not an address — the
+ /// lookup found nothing either way.
NotFound,
/// The API (or the edge in front of it) answered 429 on every attempt.
@@ -91,9 +98,9 @@ pub fn isRetriable(err: Error) bool {
/// };
/// ```
///
-/// It owns no memory and needs no `deinit`: the message lives in a fixed
-/// buffer inside the struct, and is only valid as long as the `Diagnostics`
-/// itself. Copy it if you need it to outlive the value.
+/// It owns no memory and needs no `deinit`: the message and the range live in
+/// fixed buffers inside the struct, and are only valid as long as the
+/// `Diagnostics` itself. Copy them if you need them to outlive the value.
///
/// The fields describe the last response the call saw, so `status` is also
/// filled after a call that succeeded. They are only *meaningful* after a
@@ -117,6 +124,17 @@ pub const Diagnostics = struct {
/// Length of the text in `message_buffer`. Internal.
message_len: usize = 0,
+ /// Backing storage for `faixa`. Internal; read it through `faixa`.
+ faixa_buffer: [Faixa.max_bytes]u8 = undefined,
+
+ /// Length of each of `faixa`'s strings inside `faixa_buffer`, in
+ /// `Faixa.text_fields` order, or null when no range was reported.
+ /// Internal.
+ faixa_lengths: ?[Faixa.text_fields.len]u16 = null,
+
+ /// The `ceps` count of the reported range. Internal. Read through `faixa`.
+ faixa_ceps: u64 = 0,
+
/// The longest explanation a `Diagnostics` will keep. Longer text is
/// truncated rather than allocated.
pub const max_message_len = 512;
@@ -127,12 +145,47 @@ pub const Diagnostics = struct {
return d.message_buffer[0..d.message_len];
}
+ /// After `error.NotFound`, the municipality the CEP would have belonged to.
+ ///
+ /// **The lookup still failed.** A non-null `Faixa` explains *where* the
+ /// number would have fallen; it is not the address, and there is no address
+ /// to be had. Use it to answer better — "no such CEP in Areal/RJ, which has
+ /// 118 of them" — not to carry on as though the CEP existed.
+ ///
+ /// Null whenever the API said nothing: the CEP fell in a band no
+ /// municipality owns (the API **omits** the field there, it never sends
+ /// null), the response predates the field, or the body could not be read.
+ /// Null is therefore ordinary, not a sign of trouble.
+ ///
+ /// Like `message`, the value borrows this `Diagnostics` and is valid only
+ /// as long as it is.
+ ///
+ /// ```zig
+ /// client.lookup(cep, .{ .diagnostics = &diag }) catch |err| switch (err) {
+ /// error.NotFound => if (diag.faixa()) |f| {
+ /// std.log.info("no such CEP in {s}/{s} ({d} CEPs)", .{ f.localidade, f.uf, f.ceps });
+ /// },
+ /// else => return err,
+ /// };
+ /// ```
+ pub fn faixa(d: *const Diagnostics) ?Faixa {
+ const lengths = d.faixa_lengths orelse return null;
+ var found: Faixa = .{ .ceps = d.faixa_ceps };
+ var cursor: usize = 0;
+ inline for (Faixa.text_fields, 0..) |name, i| {
+ @field(found, name) = d.faixa_buffer[cursor..][0..lengths[i]];
+ cursor += lengths[i];
+ }
+ return found;
+ }
+
/// Clear every field, so the value can be reused for another call.
pub fn reset(d: *Diagnostics) void {
d.status = null;
d.retry_after_ms = null;
d.attempts = 0;
d.message_len = 0;
+ d.clearFaixa();
}
/// Set the message, truncating at `max_message_len`. Internal.
@@ -141,6 +194,26 @@ pub const Diagnostics = struct {
writer.print(fmt, args) catch {};
d.message_len = writer.end;
}
+
+ /// Record the `faixa` block of a 404 body, or clear it when the body has
+ /// none this can use. Never fails. Internal.
+ pub fn setFaixa(d: *Diagnostics, value: std.json.Value) void {
+ d.clearFaixa();
+ const found = Faixa.fromJson(value, &d.faixa_buffer) orelse return;
+
+ var lengths: [Faixa.text_fields.len]u16 = undefined;
+ inline for (Faixa.text_fields, 0..) |name, i| {
+ lengths[i] = @intCast(@field(found, name).len);
+ }
+ d.faixa_lengths = lengths;
+ d.faixa_ceps = found.ceps;
+ }
+
+ /// Forget any range previously recorded. Internal.
+ pub fn clearFaixa(d: *Diagnostics) void {
+ d.faixa_lengths = null;
+ d.faixa_ceps = 0;
+ }
};
/// Write to `maybe_diagnostics` when the caller supplied one. Internal.
@@ -174,3 +247,50 @@ test "Diagnostics truncates instead of overflowing" {
diag.setMessage("{s}", .{"x" ** (Diagnostics.max_message_len * 2)});
try std.testing.expectEqual(Diagnostics.max_message_len, diag.message().len);
}
+
+test "Diagnostics keeps a faixa without allocating" {
+ const body =
+ \\{"erro":"cep_nao_encontrado","faixa":{"localidade":"Areal","uf":"RJ",
+ \\ "estado":"Rio de Janeiro","regiao":"Sudeste","ibge":"3300225",
+ \\ "cep_ini":"25845001","cep_fim":"25849899","ceps":118}}
+ ;
+ const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, body, .{});
+ defer parsed.deinit();
+
+ var diag: Diagnostics = .{};
+ try std.testing.expect(diag.faixa() == null);
+
+ diag.setFaixa(parsed.value);
+ const found = diag.faixa().?;
+ try std.testing.expectEqualStrings("Areal", found.localidade);
+ try std.testing.expectEqualStrings("RJ", found.uf);
+ try std.testing.expectEqualStrings("Rio de Janeiro", found.estado);
+ try std.testing.expectEqualStrings("25845001", found.cep_ini);
+ try std.testing.expectEqualStrings("25849899", found.cep_fim);
+ try std.testing.expectEqual(@as(u64, 118), found.ceps);
+
+ diag.reset();
+ try std.testing.expect(diag.faixa() == null);
+}
+
+test "a 404 without a faixa clears the one before it" {
+ const with =
+ \\{"faixa":{"localidade":"Areal","uf":"RJ","ceps":118}}
+ ;
+ const without =
+ \\{"erro":"cep_nao_encontrado","mensagem":"CEP não existe na base do DNE"}
+ ;
+
+ var diag: Diagnostics = .{};
+ inline for (.{ with, without }) |body| {
+ const parsed = try std.json.parseFromSlice(
+ std.json.Value,
+ std.testing.allocator,
+ body,
+ .{},
+ );
+ defer parsed.deinit();
+ diag.setFaixa(parsed.value);
+ }
+ try std.testing.expect(diag.faixa() == null);
+}