atproto utils for zig zat.dev
atproto sdk zig
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394//! JSON path helpers//!//! simplifies navigating nested json structures.//! eliminates the verbose nested if-checks.//!//! two approaches://! - borrowed lookups: getString(value, "embed.external.uri") accepts literal or runtime paths//! - typed decoding: extractAt(T, alloc, value, .{"embed", "external"}) decodes a subtree into T//!//! getters do not allocate; keep the source JSON alive while using their results.//! extractAt validates at runtime and may allocate, including copies of string fields.//! use an arena for extraction allocations, including partial results on failure.//!//! debug logging://! enable with `pub const std_options = .{ .log_scope_levels = &.{.{ .scope = .zat, .level = .debug }} };`
const std = @import("std");const log = std.log.scoped(.zat);
/// navigate a json value by dot-separated path/// returns null if any segment is missing or wrong type/// numeric segments index arrays; literal keys containing dots require ObjectMap.get/// returned values borrow the source JSON's storagepub fn getPath(value: std.json.Value, path: []const u8) ?std.json.Value { var current = value; var it = std.mem.splitScalar(u8, path, '.');
while (it.next()) |segment| { switch (current) { .object => |obj| { current = obj.get(segment) orelse return null; }, .array => |arr| { const idx = std.fmt.parseInt(usize, segment, 10) catch return null; if (idx >= arr.items.len) return null; current = arr.items[idx]; }, else => return null, } }
return current;}
/// get a string at pathpub fn getString(value: std.json.Value, path: []const u8) ?[]const u8 { const v = getPath(value, path) orelse return null; return switch (v) { .string => |s| s, else => null, };}
/// get an integer at pathpub fn getInt(value: std.json.Value, path: []const u8) ?i64 { const v = getPath(value, path) orelse return null; return switch (v) { .integer => |i| i, else => null, };}
/// get a float at pathpub fn getFloat(value: std.json.Value, path: []const u8) ?f64 { const v = getPath(value, path) orelse return null; return switch (v) { .float => |f| f, .integer => |i| @floatFromInt(i), else => null, };}
/// get a bool at pathpub fn getBool(value: std.json.Value, path: []const u8) ?bool { const v = getPath(value, path) orelse return null; return switch (v) { .bool => |b| b, else => null, };}
/// get an array at pathpub fn getArray(value: std.json.Value, path: []const u8) ?[]std.json.Value { const v = getPath(value, path) orelse return null; return switch (v) { .array => |a| a.items, else => null, };}
/// get an object at pathpub fn getObject(value: std.json.Value, path: []const u8) ?std.json.ObjectMap { const v = getPath(value, path) orelse return null; return switch (v) { .object => |o| o, else => null, };}
// === comptime path extraction ===
/// extract a typed struct from a nested path/// the comptime tuple selects object keys; an empty tuple selects the root/// decodes at runtime with std.json.parseFromValueLeaky, ignoring unknown fields/// string and slice fields allocate; the caller manages allocations even on failure/// prefer an arena when decoding; struct defaults apply only to absent fields////// on failure, logs diagnostic info when debug logging is enabled for .zat scopepub fn extractAt( comptime T: type, allocator: std.mem.Allocator, value: std.json.Value, comptime path: anytype,) std.json.ParseFromValueError!T { var current = value; inline for (path) |segment| { current = switch (current) { .object => |obj| obj.get(segment) orelse { log.debug("extractAt: missing field \"{s}\" in path {any}, expected {s}", .{ segment, path, @typeName(T), }); return error.MissingField; }, else => { log.debug("extractAt: expected object at \"{s}\" in path {any}, got {s}", .{ segment, path, @tagName(current), }); return error.UnexpectedToken; }, }; } return std.json.parseFromValueLeaky(T, allocator, current, .{ .ignore_unknown_fields = true }) catch |err| { log.debug("extractAt: parse failed for {s} at path {any}: {s} (json type: {s})", .{ @typeName(T), path, @errorName(err), @tagName(current), }); return err; };}
/// extract a typed value, returning null on any error, including allocation failure/// has the same allocation ownership as extractAt, including on failurepub fn extractAtOptional( comptime T: type, allocator: std.mem.Allocator, value: std.json.Value, comptime path: anytype,) ?T { return extractAt(T, allocator, value, path) catch null;}
// === tests ===
test "getPath simple" { const json_str = \\{"name": "alice", "age": 30} ; const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{}); defer parsed.deinit();
try std.testing.expectEqualStrings("alice", getString(parsed.value, "name").?); try std.testing.expectEqual(@as(i64, 30), getInt(parsed.value, "age").?);}
test "getPath nested" { const json_str = \\{"embed": {"external": {"uri": "https://example.com"}}} ; const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{}); defer parsed.deinit();
try std.testing.expectEqualStrings("https://example.com", getString(parsed.value, "embed.external.uri").?);}
test "getPath array index" { const json_str = \\{"items": ["a", "b", "c"]} ; const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{}); defer parsed.deinit();
try std.testing.expectEqualStrings("b", getString(parsed.value, "items.1").?);}
test "getPath missing returns null" { const json_str = \\{"name": "alice"} ; const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{}); defer parsed.deinit();
try std.testing.expect(getString(parsed.value, "missing") == null); try std.testing.expect(getString(parsed.value, "name.nested") == null);}
test "getPath deeply nested real-world example" { // the exact painful example from user feedback const json_str = \\{ \\ "embed": { \\ "$type": "app.bsky.embed.external", \\ "external": { \\ "uri": "https://tangled.sh", \\ "title": "Tangled", \\ "description": "Git hosting on AT Protocol" \\ } \\ } \\} ; const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{}); defer parsed.deinit();
// instead of 6 nested if-checks: const uri = getString(parsed.value, "embed.external.uri"); try std.testing.expectEqualStrings("https://tangled.sh", uri.?);
const title = getString(parsed.value, "embed.external.title"); try std.testing.expectEqualStrings("Tangled", title.?);}
// === comptime extraction tests ===
test "extractAt struct" { const json_str = \\{ \\ "embed": { \\ "external": { \\ "uri": "https://tangled.sh", \\ "title": "Tangled" \\ } \\ } \\} ; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit();
const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json_str, .{});
const External = struct { uri: []const u8, title: []const u8, };
const ext = try extractAt(External, arena.allocator(), parsed.value, .{ "embed", "external" }); try std.testing.expectEqualStrings("https://tangled.sh", ext.uri); try std.testing.expectEqualStrings("Tangled", ext.title);}
test "extractAt with optional fields" { const json_str = \\{ \\ "user": { \\ "name": "alice", \\ "age": 30 \\ } \\} ; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit();
const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json_str, .{});
const User = struct { name: []const u8, age: i64, bio: ?[]const u8 = null, };
const user = try extractAt(User, arena.allocator(), parsed.value, .{"user"}); try std.testing.expectEqualStrings("alice", user.name); try std.testing.expectEqual(@as(i64, 30), user.age); try std.testing.expect(user.bio == null);}
test "extractAt empty path extracts root" { const json_str = \\{"name": "root", "value": 42} ; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit();
const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json_str, .{});
const Root = struct { name: []const u8, value: i64, };
const root = try extractAt(Root, arena.allocator(), parsed.value, .{}); try std.testing.expectEqualStrings("root", root.name); try std.testing.expectEqual(@as(i64, 42), root.value);}
test "extractAtOptional returns null on missing path" { const json_str = \\{"exists": {"value": 1}} ; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit();
const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json_str, .{});
const Thing = struct { value: i64 };
const exists = extractAtOptional(Thing, arena.allocator(), parsed.value, .{"exists"}); try std.testing.expect(exists != null); try std.testing.expectEqual(@as(i64, 1), exists.?.value);
const missing = extractAtOptional(Thing, arena.allocator(), parsed.value, .{"missing"}); try std.testing.expect(missing == null);}
test "extractAt logs diagnostic on enum parse failure" { // simulates the issue: unknown enum value from external API const json_str = \\{"op": {"action": "archive", "path": "app.bsky.feed.post/abc"}} ; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit();
const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json_str, .{});
const Action = enum { create, update, delete }; const Op = struct { action: Action, path: []const u8, };
// "archive" is not a valid Action variant - this should fail // with debug logging enabled, you'd see: // debug(zat): extractAt: parse failed for json.Op at path { "op" }: InvalidEnumTag (json type: object) const result = extractAtOptional(Op, arena.allocator(), parsed.value, .{"op"}); try std.testing.expect(result == null);}
test "extractAt logs diagnostic on missing field" { const json_str = \\{"data": {"name": "test"}} ; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit();
const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json_str, .{});
const Thing = struct { value: i64 };
// path "data.missing" doesn't exist // with debug logging enabled, you'd see: // debug(zat): extractAt: missing field "missing" in path { "data", "missing" }, expected json.Thing const result = extractAtOptional(Thing, arena.allocator(), parsed.value, .{ "data", "missing" }); try std.testing.expect(result == null);}
test "extractAt ignores unknown fields" { // real-world case: TAP messages have extra fields (live, rev, cid) that we don't need const json_str = \\{ \\ "record": { \\ "live": true, \\ "did": "did:plc:abc123", \\ "rev": "3mbspmpaidl2a", \\ "collection": "pub.leaflet.document", \\ "rkey": "xyz789", \\ "action": "create", \\ "cid": "bafyreitest" \\ } \\} ; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit();
const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json_str, .{});
// only extract the fields we care about const Record = struct { collection: []const u8, action: []const u8, did: []const u8, rkey: []const u8, };
const rec = try extractAt(Record, arena.allocator(), parsed.value, .{"record"}); try std.testing.expectEqualStrings("pub.leaflet.document", rec.collection); try std.testing.expectEqualStrings("create", rec.action); try std.testing.expectEqualStrings("did:plc:abc123", rec.did); try std.testing.expectEqualStrings("xyz789", rec.rkey);}