const std = @import("std"); const zap = @import("zap"); /// send json response with cors headers pub fn send(r: zap.Request, body: []const u8) void { r.setHeader("content-type", "application/json") catch {}; r.setHeader("access-control-allow-origin", "*") catch {}; r.setHeader("access-control-allow-methods", "GET, POST, PATCH, DELETE, OPTIONS") catch {}; r.setHeader("access-control-allow-headers", "content-type, x-prefect-api-version") catch {}; r.sendBody(body) catch {}; } /// send json response with status code pub fn sendStatus(r: zap.Request, body: []const u8, status: zap.http.StatusCode) void { r.setStatus(status); send(r, body); } // ============================================================================ // JSON path navigation helpers // ============================================================================ /// navigate a json value by dot-separated path /// returns null if any segment is missing or wrong type pub 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 path pub 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 path pub 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 path pub 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 path pub 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 path pub 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 path pub 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 /// uses comptime tuple for path segments - no runtime string parsing /// leverages std.json.parseFromValueLeaky for type-safe extraction pub 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 return error.MissingField, else => return error.UnexpectedToken, }; } return std.json.parseFromValueLeaky(T, allocator, current, .{ .ignore_unknown_fields = true }); } /// extract a typed value, returning null if path doesn't exist pub 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 "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 "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); }