+4
CHANGELOG.md
+4
CHANGELOG.md
+1
-1
scripts/build-site.mjs
+1
-1
scripts/build-site.mjs
···
181
181
const indexMd = [
182
182
"# devlog",
183
183
"",
184
-
...devlogEntries.map((e) => `- [${e.title}](${e.path.replace("devlog/", "")})`),
184
+
...devlogEntries.map((e) => `- [${e.title}](${e.path})`),
185
185
"",
186
186
].join("\n");
187
187
await writeFile(path.join(outDocsDir, "devlog", "index.md"), indexMd, "utf8");
+2
-2
scripts/publish-docs.zig
+2
-2
scripts/publish-docs.zig
···
14
14
15
15
/// devlog entries
16
16
const devlog = [_]DocEntry{
17
-
.{ .path = "/001", .file = "devlog/001-self-publishing-docs.md" },
17
+
.{ .path = "/devlog/001", .file = "devlog/001-self-publishing-docs.md" },
18
18
};
19
19
20
20
pub fn main() !void {
···
87
87
// devlog publication (clock_id 100 to separate from docs)
88
88
const devlog_tid = zat.Tid.fromTimestamp(1704067200000000, 100);
89
89
const devlog_pub = Publication{
90
-
.url = "https://zat.dev/devlog",
90
+
.url = "https://zat.dev",
91
91
.name = "zat devlog",
92
92
.description = "building zat in public",
93
93
};
+107
-3
src/internal/json.zig
+107
-3
src/internal/json.zig
···
6
6
//! two approaches:
7
7
//! - runtime paths: getString(value, "embed.external.uri") - for dynamic paths
8
8
//! - comptime paths: extractAt(T, alloc, value, .{"embed", "external"}) - for static paths with type safety
9
+
//!
10
+
//! debug logging:
11
+
//! enable with `pub const std_options = .{ .log_scope_levels = &.{.{ .scope = .zat, .level = .debug }} };`
9
12
10
13
const std = @import("std");
14
+
const log = std.log.scoped(.zat);
11
15
12
16
/// navigate a json value by dot-separated path
13
17
/// returns null if any segment is missing or wrong type
···
92
96
/// extract a typed struct from a nested path
93
97
/// uses comptime tuple for path segments - no runtime string parsing
94
98
/// leverages std.json.parseFromValueLeaky for type-safe extraction
99
+
///
100
+
/// on failure, logs diagnostic info when debug logging is enabled for .zat scope
95
101
pub fn extractAt(
96
102
comptime T: type,
97
103
allocator: std.mem.Allocator,
···
101
107
var current = value;
102
108
inline for (path) |segment| {
103
109
current = switch (current) {
104
-
.object => |obj| obj.get(segment) orelse return error.MissingField,
105
-
else => return error.UnexpectedToken,
110
+
.object => |obj| obj.get(segment) orelse {
111
+
log.debug("extractAt: missing field \"{s}\" in path {any}, expected {s}", .{
112
+
segment,
113
+
path,
114
+
@typeName(T),
115
+
});
116
+
return error.MissingField;
117
+
},
118
+
else => {
119
+
log.debug("extractAt: expected object at \"{s}\" in path {any}, got {s}", .{
120
+
segment,
121
+
path,
122
+
@tagName(current),
123
+
});
124
+
return error.UnexpectedToken;
125
+
},
106
126
};
107
127
}
108
-
return std.json.parseFromValueLeaky(T, allocator, current, .{});
128
+
return std.json.parseFromValueLeaky(T, allocator, current, .{ .ignore_unknown_fields = true }) catch |err| {
129
+
log.debug("extractAt: parse failed for {s} at path {any}: {s} (json type: {s})", .{
130
+
@typeName(T),
131
+
path,
132
+
@errorName(err),
133
+
@tagName(current),
134
+
});
135
+
return err;
136
+
};
109
137
}
110
138
111
139
/// extract a typed value, returning null if path doesn't exist
···
278
306
const missing = extractAtOptional(Thing, arena.allocator(), parsed.value, .{"missing"});
279
307
try std.testing.expect(missing == null);
280
308
}
309
+
310
+
test "extractAt logs diagnostic on enum parse failure" {
311
+
// simulates the issue: unknown enum value from external API
312
+
const json_str =
313
+
\\{"op": {"action": "archive", "path": "app.bsky.feed.post/abc"}}
314
+
;
315
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
316
+
defer arena.deinit();
317
+
318
+
const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json_str, .{});
319
+
320
+
const Action = enum { create, update, delete };
321
+
const Op = struct {
322
+
action: Action,
323
+
path: []const u8,
324
+
};
325
+
326
+
// "archive" is not a valid Action variant - this should fail
327
+
// with debug logging enabled, you'd see:
328
+
// debug(zat): extractAt: parse failed for json.Op at path { "op" }: InvalidEnumTag (json type: object)
329
+
const result = extractAtOptional(Op, arena.allocator(), parsed.value, .{"op"});
330
+
try std.testing.expect(result == null);
331
+
}
332
+
333
+
test "extractAt logs diagnostic on missing field" {
334
+
const json_str =
335
+
\\{"data": {"name": "test"}}
336
+
;
337
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
338
+
defer arena.deinit();
339
+
340
+
const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json_str, .{});
341
+
342
+
const Thing = struct { value: i64 };
343
+
344
+
// path "data.missing" doesn't exist
345
+
// with debug logging enabled, you'd see:
346
+
// debug(zat): extractAt: missing field "missing" in path { "data", "missing" }, expected json.Thing
347
+
const result = extractAtOptional(Thing, arena.allocator(), parsed.value, .{ "data", "missing" });
348
+
try std.testing.expect(result == null);
349
+
}
350
+
351
+
test "extractAt ignores unknown fields" {
352
+
// real-world case: TAP messages have extra fields (live, rev, cid) that we don't need
353
+
const json_str =
354
+
\\{
355
+
\\ "record": {
356
+
\\ "live": true,
357
+
\\ "did": "did:plc:abc123",
358
+
\\ "rev": "3mbspmpaidl2a",
359
+
\\ "collection": "pub.leaflet.document",
360
+
\\ "rkey": "xyz789",
361
+
\\ "action": "create",
362
+
\\ "cid": "bafyreitest"
363
+
\\ }
364
+
\\}
365
+
;
366
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
367
+
defer arena.deinit();
368
+
369
+
const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json_str, .{});
370
+
371
+
// only extract the fields we care about
372
+
const Record = struct {
373
+
collection: []const u8,
374
+
action: []const u8,
375
+
did: []const u8,
376
+
rkey: []const u8,
377
+
};
378
+
379
+
const rec = try extractAt(Record, arena.allocator(), parsed.value, .{"record"});
380
+
try std.testing.expectEqualStrings("pub.leaflet.document", rec.collection);
381
+
try std.testing.expectEqualStrings("create", rec.action);
382
+
try std.testing.expectEqualStrings("did:plc:abc123", rec.did);
383
+
try std.testing.expectEqualStrings("xyz789", rec.rkey);
384
+
}