prefect server in zig
1const std = @import("std");
2const zap = @import("zap");
3
4/// send json response with cors headers
5pub fn send(r: zap.Request, body: []const u8) void {
6 r.setHeader("content-type", "application/json") catch {};
7 r.setHeader("access-control-allow-origin", "*") catch {};
8 r.setHeader("access-control-allow-methods", "GET, POST, PATCH, DELETE, OPTIONS") catch {};
9 r.setHeader("access-control-allow-headers", "content-type, x-prefect-api-version") catch {};
10 r.sendBody(body) catch {};
11}
12
13/// send json response with status code
14pub fn sendStatus(r: zap.Request, body: []const u8, status: zap.http.StatusCode) void {
15 r.setStatus(status);
16 send(r, body);
17}
18
19// ============================================================================
20// JSON path navigation helpers
21// ============================================================================
22
23/// navigate a json value by dot-separated path
24/// returns null if any segment is missing or wrong type
25pub fn getPath(value: std.json.Value, path: []const u8) ?std.json.Value {
26 var current = value;
27 var it = std.mem.splitScalar(u8, path, '.');
28
29 while (it.next()) |segment| {
30 switch (current) {
31 .object => |obj| {
32 current = obj.get(segment) orelse return null;
33 },
34 .array => |arr| {
35 const idx = std.fmt.parseInt(usize, segment, 10) catch return null;
36 if (idx >= arr.items.len) return null;
37 current = arr.items[idx];
38 },
39 else => return null,
40 }
41 }
42 return current;
43}
44
45/// get a string at path
46pub fn getString(value: std.json.Value, path: []const u8) ?[]const u8 {
47 const v = getPath(value, path) orelse return null;
48 return switch (v) {
49 .string => |s| s,
50 else => null,
51 };
52}
53
54/// get an integer at path
55pub fn getInt(value: std.json.Value, path: []const u8) ?i64 {
56 const v = getPath(value, path) orelse return null;
57 return switch (v) {
58 .integer => |i| i,
59 else => null,
60 };
61}
62
63/// get a float at path
64pub fn getFloat(value: std.json.Value, path: []const u8) ?f64 {
65 const v = getPath(value, path) orelse return null;
66 return switch (v) {
67 .float => |f| f,
68 .integer => |i| @floatFromInt(i),
69 else => null,
70 };
71}
72
73/// get a bool at path
74pub fn getBool(value: std.json.Value, path: []const u8) ?bool {
75 const v = getPath(value, path) orelse return null;
76 return switch (v) {
77 .bool => |b| b,
78 else => null,
79 };
80}
81
82/// get an array at path
83pub fn getArray(value: std.json.Value, path: []const u8) ?[]std.json.Value {
84 const v = getPath(value, path) orelse return null;
85 return switch (v) {
86 .array => |a| a.items,
87 else => null,
88 };
89}
90
91/// get an object at path
92pub fn getObject(value: std.json.Value, path: []const u8) ?std.json.ObjectMap {
93 const v = getPath(value, path) orelse return null;
94 return switch (v) {
95 .object => |o| o,
96 else => null,
97 };
98}
99
100// ============================================================================
101// comptime path extraction
102// ============================================================================
103
104/// extract a typed struct from a nested path
105/// uses comptime tuple for path segments - no runtime string parsing
106/// leverages std.json.parseFromValueLeaky for type-safe extraction
107pub fn extractAt(
108 comptime T: type,
109 allocator: std.mem.Allocator,
110 value: std.json.Value,
111 comptime path: anytype,
112) std.json.ParseFromValueError!T {
113 var current = value;
114 inline for (path) |segment| {
115 current = switch (current) {
116 .object => |obj| obj.get(segment) orelse return error.MissingField,
117 else => return error.UnexpectedToken,
118 };
119 }
120 return std.json.parseFromValueLeaky(T, allocator, current, .{ .ignore_unknown_fields = true });
121}
122
123/// extract a typed value, returning null if path doesn't exist
124pub fn extractAtOptional(
125 comptime T: type,
126 allocator: std.mem.Allocator,
127 value: std.json.Value,
128 comptime path: anytype,
129) ?T {
130 return extractAt(T, allocator, value, path) catch null;
131}
132
133// ============================================================================
134// tests
135// ============================================================================
136
137test "getPath simple" {
138 const json_str = "{\"name\": \"alice\", \"age\": 30}";
139 const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{});
140 defer parsed.deinit();
141 try std.testing.expectEqualStrings("alice", getString(parsed.value, "name").?);
142 try std.testing.expectEqual(@as(i64, 30), getInt(parsed.value, "age").?);
143}
144
145test "getPath nested" {
146 const json_str = "{\"embed\": {\"external\": {\"uri\": \"https://example.com\"}}}";
147 const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{});
148 defer parsed.deinit();
149 try std.testing.expectEqualStrings("https://example.com", getString(parsed.value, "embed.external.uri").?);
150}
151
152test "getPath array index" {
153 const json_str = "{\"items\": [\"a\", \"b\", \"c\"]}";
154 const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{});
155 defer parsed.deinit();
156 try std.testing.expectEqualStrings("b", getString(parsed.value, "items.1").?);
157}
158
159test "getPath missing returns null" {
160 const json_str = "{\"name\": \"alice\"}";
161 const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json_str, .{});
162 defer parsed.deinit();
163 try std.testing.expect(getString(parsed.value, "missing") == null);
164 try std.testing.expect(getString(parsed.value, "name.nested") == null);
165}
166
167test "extractAt struct" {
168 const json_str = "{\"embed\": {\"external\": {\"uri\": \"https://tangled.sh\", \"title\": \"Tangled\"}}}";
169 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
170 defer arena.deinit();
171 const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json_str, .{});
172 const External = struct { uri: []const u8, title: []const u8 };
173 const ext = try extractAt(External, arena.allocator(), parsed.value, .{ "embed", "external" });
174 try std.testing.expectEqualStrings("https://tangled.sh", ext.uri);
175 try std.testing.expectEqualStrings("Tangled", ext.title);
176}
177
178test "extractAtOptional returns null on missing path" {
179 const json_str = "{\"exists\": {\"value\": 1}}";
180 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
181 defer arena.deinit();
182 const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json_str, .{});
183 const Thing = struct { value: i64 };
184 const exists = extractAtOptional(Thing, arena.allocator(), parsed.value, .{"exists"});
185 try std.testing.expect(exists != null);
186 try std.testing.expectEqual(@as(i64, 1), exists.?.value);
187 const missing = extractAtOptional(Thing, arena.allocator(), parsed.value, .{"missing"});
188 try std.testing.expect(missing == null);
189}