+119
src/internal/json.zig
+119
src/internal/json.zig
···
2
2
//!
3
3
//! simplifies navigating nested json structures.
4
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
5
9
6
10
const std = @import("std");
7
11
···
83
87
};
84
88
}
85
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
+
86
121
// === tests ===
87
122
88
123
test "getPath simple" {
···
151
186
const title = getString(parsed.value, "embed.external.title");
152
187
try std.testing.expectEqualStrings("Tangled", title.?);
153
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
+
}