atproto utils for zig zat.dev
atproto sdk zig

add comptime json extraction (extractAt, extractAtOptional)

Changed files
+119
src
internal
+119
src/internal/json.zig
··· 2 //! 3 //! simplifies navigating nested json structures. 4 //! eliminates the verbose nested if-checks. 5 6 const std = @import("std"); 7 ··· 83 }; 84 } 85 86 // === tests === 87 88 test "getPath simple" { ··· 151 const title = getString(parsed.value, "embed.external.title"); 152 try std.testing.expectEqualStrings("Tangled", title.?); 153 }
··· 2 //! 3 //! simplifies navigating nested json structures. 4 //! eliminates the verbose nested if-checks. 5 + //! 6 + //! two approaches: 7 + //! - runtime paths: getString(value, "embed.external.uri") - for dynamic paths 8 + //! - comptime paths: extractAt(T, alloc, value, .{"embed", "external"}) - for static paths with type safety 9 10 const std = @import("std"); 11 ··· 87 }; 88 } 89 90 + // === comptime path extraction === 91 + 92 + /// extract a typed struct from a nested path 93 + /// uses comptime tuple for path segments - no runtime string parsing 94 + /// leverages std.json.parseFromValueLeaky for type-safe extraction 95 + pub fn extractAt( 96 + comptime T: type, 97 + allocator: std.mem.Allocator, 98 + value: std.json.Value, 99 + comptime path: anytype, 100 + ) std.json.ParseFromValueError!T { 101 + var current = value; 102 + inline for (path) |segment| { 103 + current = switch (current) { 104 + .object => |obj| obj.get(segment) orelse return error.MissingField, 105 + else => return error.UnexpectedToken, 106 + }; 107 + } 108 + return std.json.parseFromValueLeaky(T, allocator, current, .{}); 109 + } 110 + 111 + /// extract a typed value, returning null if path doesn't exist 112 + pub fn extractAtOptional( 113 + comptime T: type, 114 + allocator: std.mem.Allocator, 115 + value: std.json.Value, 116 + comptime path: anytype, 117 + ) ?T { 118 + return extractAt(T, allocator, value, path) catch null; 119 + } 120 + 121 // === tests === 122 123 test "getPath simple" { ··· 186 const title = getString(parsed.value, "embed.external.title"); 187 try std.testing.expectEqualStrings("Tangled", title.?); 188 } 189 + 190 + // === comptime extraction tests === 191 + 192 + test "extractAt struct" { 193 + const json_str = 194 + \\{ 195 + \\ "embed": { 196 + \\ "external": { 197 + \\ "uri": "https://tangled.sh", 198 + \\ "title": "Tangled" 199 + \\ } 200 + \\ } 201 + \\} 202 + ; 203 + const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{}); 204 + defer parsed.deinit(); 205 + 206 + const External = struct { 207 + uri: []const u8, 208 + title: []const u8, 209 + }; 210 + 211 + const ext = try extractAt(External, std.testing.allocator, parsed.value, .{ "embed", "external" }); 212 + try std.testing.expectEqualStrings("https://tangled.sh", ext.uri); 213 + try std.testing.expectEqualStrings("Tangled", ext.title); 214 + } 215 + 216 + test "extractAt with optional fields" { 217 + const json_str = 218 + \\{ 219 + \\ "user": { 220 + \\ "name": "alice", 221 + \\ "age": 30 222 + \\ } 223 + \\} 224 + ; 225 + const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{}); 226 + defer parsed.deinit(); 227 + 228 + const User = struct { 229 + name: []const u8, 230 + age: i64, 231 + bio: ?[]const u8 = null, 232 + }; 233 + 234 + const user = try extractAt(User, std.testing.allocator, parsed.value, .{"user"}); 235 + try std.testing.expectEqualStrings("alice", user.name); 236 + try std.testing.expectEqual(@as(i64, 30), user.age); 237 + try std.testing.expect(user.bio == null); 238 + } 239 + 240 + test "extractAt empty path extracts root" { 241 + const json_str = 242 + \\{"name": "root", "value": 42} 243 + ; 244 + const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{}); 245 + defer parsed.deinit(); 246 + 247 + const Root = struct { 248 + name: []const u8, 249 + value: i64, 250 + }; 251 + 252 + const root = try extractAt(Root, std.testing.allocator, parsed.value, .{}); 253 + try std.testing.expectEqualStrings("root", root.name); 254 + try std.testing.expectEqual(@as(i64, 42), root.value); 255 + } 256 + 257 + test "extractAtOptional returns null on missing path" { 258 + const json_str = 259 + \\{"exists": {"value": 1}} 260 + ; 261 + const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{}); 262 + defer parsed.deinit(); 263 + 264 + const Thing = struct { value: i64 }; 265 + 266 + const exists = extractAtOptional(Thing, std.testing.allocator, parsed.value, .{"exists"}); 267 + try std.testing.expect(exists != null); 268 + try std.testing.expectEqual(@as(i64, 1), exists.?.value); 269 + 270 + const missing = extractAtOptional(Thing, std.testing.allocator, parsed.value, .{"missing"}); 271 + try std.testing.expect(missing == null); 272 + }