1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
|
//! The error set, and the diagnostics that go with it.
//!
//! Everything the library can fail with is one `Error`, so a caller who only
//! cares that *something* went wrong catches one set and is done.
//!
//! Below that, the split is by **the decision the caller has to make**, not by
//! the HTTP status that caused it:
//!
//! - `error.InvalidCep` and `error.NotFound` are problems with the *request*.
//! Retrying is pointless — the same CEP will fail again.
//! - `error.NetworkError`, `error.ServerError` and `error.RateLimited` are
//! problems with *transport*. Retrying may help, and the client already
//! retried on its own before returning any of them.
//!
//! That split is why `error.NotFound` is not "the HTTP 404 error": the client
//! also returns `error.InvalidCep` without making any request at all, when the
//! CEP is malformed before it ever leaves the machine.
//!
//! 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, 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.
pub const CepisError = error{
/// The CEP is not 8 digits.
///
/// Usually returned **without** a request ever being made — the client
/// checks the format before going to the network. It is also what a 400
/// from the API becomes, in which case `Diagnostics.message` carries the
/// API's own wording.
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.
///
/// `Diagnostics.retry_after_ms` holds what the server asked for, when it
/// said. The client already honoured it across the attempts it made; it is
/// reported for callers who want to reschedule the work.
RateLimited,
/// The API answered 5xx on every attempt. `Diagnostics.status` holds the
/// status seen on the last one.
ServerError,
/// The request never completed: DNS, refused connection, timeout, TLS.
///
/// Returned only after every configured attempt has failed.
NetworkError,
/// A success status came back with a body that cannot be parsed.
///
/// In practice this means something between you and the API is answering
/// instead of it — a captive Wi-Fi portal, a corporate proxy, an HTML error
/// page. Useful as a diagnosis: the problem is not in your code.
InvalidResponse,
};
/// The error set of every fallible operation in this library.
pub const Error = CepisError || std.mem.Allocator.Error;
/// Whether trying the same call again later could plausibly succeed.
///
/// True for `RateLimited`, `ServerError` and `NetworkError`; false for a bad or
/// unknown CEP, which fails the same way forever. `OutOfMemory` is not
/// retriable in any useful sense and reports false.
///
/// The client has already retried internally by the time any of these are
/// returned, so "later" means later — not immediately.
pub fn isRetriable(err: Error) bool {
return switch (err) {
error.RateLimited, error.ServerError, error.NetworkError => true,
else => false,
};
}
/// Optional out-parameter carrying the detail a Zig error tag cannot.
///
/// Declare one on the stack, pass a pointer, and read it after the call fails:
///
/// ```zig
/// var diag: cepis.Diagnostics = .{};
/// const address = client.lookup("01001000", .{ .diagnostics = &diag }) catch |err| switch (err) {
/// error.RateLimited => return retryIn(diag.retry_after_ms orelse 60_000),
/// else => return err,
/// };
/// ```
///
/// 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
/// failure. Reusing one `Diagnostics` across calls without `reset` can show
/// stale detail.
pub const Diagnostics = struct {
/// The HTTP status of the last attempt, when there was a response at all.
status: ?u16 = null,
/// What the server asked for via `Retry-After`, in milliseconds, or null
/// if it did not say. Already capped at `Config.backoff_max_ms` by the
/// waits the client performed, but reported here uncapped.
retry_after_ms: ?u64 = null,
/// How many attempts were made before giving up.
attempts: u32 = 0,
/// Backing storage for `message`. Internal; read it through `message`.
message_buffer: [max_message_len]u8 = undefined,
/// 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;
/// A human-readable explanation of the failure, borrowed from the
/// `Diagnostics` and valid only as long as it is. Empty when none was set.
pub fn message(d: *const Diagnostics) []const u8 {
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.
pub fn setMessage(d: *Diagnostics, comptime fmt: []const u8, args: anytype) void {
var writer: std.Io.Writer = .fixed(&d.message_buffer);
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.
pub fn note(maybe_diagnostics: ?*Diagnostics, comptime fmt: []const u8, args: anytype) void {
const d = maybe_diagnostics orelse return;
d.setMessage(fmt, args);
}
test "isRetriable splits by decision" {
try std.testing.expect(isRetriable(error.NetworkError));
try std.testing.expect(isRetriable(error.ServerError));
try std.testing.expect(isRetriable(error.RateLimited));
try std.testing.expect(!isRetriable(error.NotFound));
try std.testing.expect(!isRetriable(error.InvalidCep));
try std.testing.expect(!isRetriable(error.InvalidResponse));
}
test "Diagnostics keeps a message without allocating" {
var diag: Diagnostics = .{};
try std.testing.expectEqualStrings("", diag.message());
diag.setMessage("CEP {s} rejected", .{"00000000"});
try std.testing.expectEqualStrings("CEP 00000000 rejected", diag.message());
diag.reset();
try std.testing.expectEqualStrings("", diag.message());
}
test "Diagnostics truncates instead of overflowing" {
var diag: Diagnostics = .{};
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);
}
|