prefect server in zig
at main 189 lines 7.1 kB view raw
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}