prefect server in zig
at f511403a2b901063559cd17995b45527418e76c6 423 lines 13 kB view raw
1const std = @import("std"); 2const zap = @import("zap"); 3const mem = std.mem; 4const json = std.json; 5 6const db = @import("../db/sqlite.zig"); 7const uuid_util = @import("../utilities/uuid.zig"); 8const time_util = @import("../utilities/time.zig"); 9const json_util = @import("../utilities/json.zig"); 10 11pub fn handle(r: zap.Request) !void { 12 const target = r.path orelse "/"; 13 const method = r.method orelse "GET"; 14 15 // POST /variables/filter - list 16 if (mem.eql(u8, method, "POST") and mem.endsWith(u8, target, "/filter")) { 17 try filter(r); 18 return; 19 } 20 21 // POST /variables/count 22 if (mem.eql(u8, method, "POST") and mem.endsWith(u8, target, "/count")) { 23 try countVariables(r); 24 return; 25 } 26 27 // POST /variables/ - create 28 if (mem.eql(u8, method, "POST") and (mem.eql(u8, target, "/variables/") or mem.eql(u8, target, "/api/variables/"))) { 29 try createVariable(r); 30 return; 31 } 32 33 // GET /variables/name/{name} 34 if (mem.eql(u8, method, "GET") and mem.indexOf(u8, target, "/name/") != null) { 35 const name = extractName(target) orelse { 36 json_util.sendStatus(r, "{\"detail\":\"name required\"}", .bad_request); 37 return; 38 }; 39 try getByName(r, name); 40 return; 41 } 42 43 // PATCH /variables/name/{name} 44 if (mem.eql(u8, method, "PATCH") and mem.indexOf(u8, target, "/name/") != null) { 45 const name = extractName(target) orelse { 46 json_util.sendStatus(r, "{\"detail\":\"name required\"}", .bad_request); 47 return; 48 }; 49 try updateByName(r, name); 50 return; 51 } 52 53 // DELETE /variables/name/{name} 54 if (mem.eql(u8, method, "DELETE") and mem.indexOf(u8, target, "/name/") != null) { 55 const name = extractName(target) orelse { 56 json_util.sendStatus(r, "{\"detail\":\"name required\"}", .bad_request); 57 return; 58 }; 59 try deleteByName(r, name); 60 return; 61 } 62 63 // GET /variables/{id} 64 if (mem.eql(u8, method, "GET")) { 65 const id = extractId(target) orelse { 66 json_util.sendStatus(r, "{\"detail\":\"variable id required\"}", .bad_request); 67 return; 68 }; 69 try getById(r, id); 70 return; 71 } 72 73 // PATCH /variables/{id} 74 if (mem.eql(u8, method, "PATCH")) { 75 const id = extractId(target) orelse { 76 json_util.sendStatus(r, "{\"detail\":\"variable id required\"}", .bad_request); 77 return; 78 }; 79 try updateById(r, id); 80 return; 81 } 82 83 // DELETE /variables/{id} 84 if (mem.eql(u8, method, "DELETE")) { 85 const id = extractId(target) orelse { 86 json_util.sendStatus(r, "{\"detail\":\"variable id required\"}", .bad_request); 87 return; 88 }; 89 try deleteById(r, id); 90 return; 91 } 92 93 json_util.sendStatus(r, "{\"detail\":\"not implemented\"}", .not_implemented); 94} 95 96fn extractId(target: []const u8) ?[]const u8 { 97 const prefix = if (mem.startsWith(u8, target, "/api/variables/")) "/api/variables/" else "/variables/"; 98 if (target.len > prefix.len) { 99 return target[prefix.len..]; 100 } 101 return null; 102} 103 104fn extractName(target: []const u8) ?[]const u8 { 105 const idx = mem.indexOf(u8, target, "/name/") orelse return null; 106 const start = idx + "/name/".len; 107 if (start < target.len) { 108 return target[start..]; 109 } 110 return null; 111} 112 113fn createVariable(r: zap.Request) !void { 114 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 115 defer arena.deinit(); 116 const alloc = arena.allocator(); 117 118 const body = r.body orelse { 119 json_util.sendStatus(r, "{\"detail\":\"failed to read body\"}", .bad_request); 120 return; 121 }; 122 123 const parsed = json.parseFromSlice(json.Value, alloc, body, .{}) catch { 124 json_util.sendStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); 125 return; 126 }; 127 128 const obj = parsed.value.object; 129 130 const name = switch (obj.get("name") orelse { 131 json_util.sendStatus(r, "{\"detail\":\"name required\"}", .bad_request); 132 return; 133 }) { 134 .string => |s| s, 135 else => { 136 json_util.sendStatus(r, "{\"detail\":\"name must be string\"}", .bad_request); 137 return; 138 }, 139 }; 140 141 // check for existing 142 if (db.variables.getByName(alloc, name) catch null) |_| { 143 const err_msg = std.fmt.allocPrint(alloc, "{{\"detail\":\"Variable with name '{s}' already exists.\"}}", .{name}) catch { 144 json_util.sendStatus(r, "{\"detail\":\"Variable already exists\"}", .conflict); 145 return; 146 }; 147 json_util.sendStatus(r, err_msg, .conflict); 148 return; 149 } 150 151 const value_json = stringifyField(alloc, obj.get("value"), "null"); 152 const tags_json = stringifyField(alloc, obj.get("tags"), "[]"); 153 154 var new_id_buf: [36]u8 = undefined; 155 const new_id = uuid_util.generate(&new_id_buf); 156 157 var ts_buf: [32]u8 = undefined; 158 const now = time_util.timestamp(&ts_buf); 159 160 db.variables.insert(new_id, name, value_json, tags_json, now) catch { 161 json_util.sendStatus(r, "{\"detail\":\"insert failed\"}", .internal_server_error); 162 return; 163 }; 164 165 const variable = db.variables.VariableRow{ 166 .id = new_id, 167 .created = now, 168 .updated = now, 169 .name = name, 170 .value = value_json, 171 .tags = tags_json, 172 }; 173 174 const resp = writeVariable(alloc, variable) catch { 175 json_util.sendStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 176 return; 177 }; 178 json_util.sendStatus(r, resp, .created); 179} 180 181fn getById(r: zap.Request, id: []const u8) !void { 182 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 183 defer arena.deinit(); 184 const alloc = arena.allocator(); 185 186 if (db.variables.getById(alloc, id) catch null) |variable| { 187 const resp = writeVariable(alloc, variable) catch { 188 json_util.sendStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 189 return; 190 }; 191 json_util.send(r, resp); 192 } else { 193 json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); 194 } 195} 196 197fn getByName(r: zap.Request, name: []const u8) !void { 198 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 199 defer arena.deinit(); 200 const alloc = arena.allocator(); 201 202 if (db.variables.getByName(alloc, name) catch null) |variable| { 203 const resp = writeVariable(alloc, variable) catch { 204 json_util.sendStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 205 return; 206 }; 207 json_util.send(r, resp); 208 } else { 209 json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); 210 } 211} 212 213fn updateById(r: zap.Request, id: []const u8) !void { 214 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 215 defer arena.deinit(); 216 const alloc = arena.allocator(); 217 218 const body = r.body orelse { 219 json_util.sendStatus(r, "{\"detail\":\"failed to read body\"}", .bad_request); 220 return; 221 }; 222 223 const parsed = json.parseFromSlice(json.Value, alloc, body, .{}) catch { 224 json_util.sendStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); 225 return; 226 }; 227 228 const obj = parsed.value.object; 229 const new_name = getOptionalString(obj.get("name")); 230 const value_json = stringifyFieldOptional(alloc, obj.get("value")); 231 const tags_json = stringifyFieldOptional(alloc, obj.get("tags")); 232 233 var ts_buf: [32]u8 = undefined; 234 const now = time_util.timestamp(&ts_buf); 235 236 const did_update = db.variables.updateById(id, new_name, value_json, tags_json, now) catch { 237 json_util.sendStatus(r, "{\"detail\":\"update failed\"}", .internal_server_error); 238 return; 239 }; 240 241 if (!did_update) { 242 json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); 243 return; 244 } 245 246 r.setStatus(.no_content); 247 r.sendBody("") catch {}; 248} 249 250fn updateByName(r: zap.Request, name: []const u8) !void { 251 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 252 defer arena.deinit(); 253 const alloc = arena.allocator(); 254 255 const body = r.body orelse { 256 json_util.sendStatus(r, "{\"detail\":\"failed to read body\"}", .bad_request); 257 return; 258 }; 259 260 const parsed = json.parseFromSlice(json.Value, alloc, body, .{}) catch { 261 json_util.sendStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); 262 return; 263 }; 264 265 const obj = parsed.value.object; 266 const new_name = getOptionalString(obj.get("name")); 267 const value_json = stringifyFieldOptional(alloc, obj.get("value")); 268 const tags_json = stringifyFieldOptional(alloc, obj.get("tags")); 269 270 var ts_buf: [32]u8 = undefined; 271 const now = time_util.timestamp(&ts_buf); 272 273 const did_update = db.variables.updateByName(name, new_name, value_json, tags_json, now) catch { 274 json_util.sendStatus(r, "{\"detail\":\"update failed\"}", .internal_server_error); 275 return; 276 }; 277 278 if (!did_update) { 279 json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); 280 return; 281 } 282 283 r.setStatus(.no_content); 284 r.sendBody("") catch {}; 285} 286 287fn deleteById(r: zap.Request, id: []const u8) !void { 288 const deleted = db.variables.deleteById(id) catch { 289 json_util.sendStatus(r, "{\"detail\":\"delete failed\"}", .internal_server_error); 290 return; 291 }; 292 293 if (!deleted) { 294 json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); 295 return; 296 } 297 298 r.setStatus(.no_content); 299 r.sendBody("") catch {}; 300} 301 302fn deleteByName(r: zap.Request, name: []const u8) !void { 303 const deleted = db.variables.deleteByName(name) catch { 304 json_util.sendStatus(r, "{\"detail\":\"delete failed\"}", .internal_server_error); 305 return; 306 }; 307 308 if (!deleted) { 309 json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); 310 return; 311 } 312 313 r.setStatus(.no_content); 314 r.sendBody("") catch {}; 315} 316 317fn filter(r: zap.Request) !void { 318 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 319 defer arena.deinit(); 320 const alloc = arena.allocator(); 321 322 var limit: usize = 200; 323 var offset: usize = 0; 324 325 if (r.body) |body| { 326 if (json.parseFromSlice(json.Value, alloc, body, .{})) |parsed| { 327 const obj = parsed.value.object; 328 if (obj.get("limit")) |v| { 329 if (v == .integer) limit = @intCast(v.integer); 330 } 331 if (obj.get("offset")) |v| { 332 if (v == .integer) offset = @intCast(v.integer); 333 } 334 } else |_| {} 335 } 336 337 const variables = db.variables.list(alloc, limit, offset) catch { 338 json_util.sendStatus(r, "{\"detail\":\"database error\"}", .internal_server_error); 339 return; 340 }; 341 342 var output: std.io.Writer.Allocating = .init(alloc); 343 var jw: json.Stringify = .{ .writer = &output.writer }; 344 345 jw.beginArray() catch { 346 json_util.sendStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); 347 return; 348 }; 349 350 for (variables) |variable| { 351 writeVariableObject(&jw, variable) catch continue; 352 } 353 354 jw.endArray() catch {}; 355 356 json_util.send(r, output.toOwnedSlice() catch "[]"); 357} 358 359fn countVariables(r: zap.Request) !void { 360 const cnt = db.variables.count() catch 0; 361 var buf: [32]u8 = undefined; 362 const resp = std.fmt.bufPrint(&buf, "{d}", .{cnt}) catch "0"; 363 json_util.send(r, resp); 364} 365 366fn stringifyField(alloc: std.mem.Allocator, val: ?json.Value, default: []const u8) []const u8 { 367 if (val) |v| { 368 return std.fmt.allocPrint(alloc, "{f}", .{json.fmt(v, .{})}) catch default; 369 } 370 return default; 371} 372 373fn stringifyFieldOptional(alloc: std.mem.Allocator, val: ?json.Value) ?[]const u8 { 374 if (val) |v| { 375 return std.fmt.allocPrint(alloc, "{f}", .{json.fmt(v, .{})}) catch null; 376 } 377 return null; 378} 379 380fn getOptionalString(val: ?json.Value) ?[]const u8 { 381 if (val) |v| { 382 return switch (v) { 383 .string => |s| s, 384 else => null, 385 }; 386 } 387 return null; 388} 389 390fn writeVariable(alloc: std.mem.Allocator, variable: db.variables.VariableRow) ![]const u8 { 391 var output: std.io.Writer.Allocating = .init(alloc); 392 var jw: json.Stringify = .{ .writer = &output.writer }; 393 try writeVariableObject(&jw, variable); 394 return output.toOwnedSlice(); 395} 396 397fn writeVariableObject(jw: *json.Stringify, variable: db.variables.VariableRow) !void { 398 try jw.beginObject(); 399 400 try jw.objectField("id"); 401 try jw.write(variable.id); 402 403 try jw.objectField("created"); 404 try jw.write(variable.created); 405 406 try jw.objectField("updated"); 407 try jw.write(variable.updated); 408 409 try jw.objectField("name"); 410 try jw.write(variable.name); 411 412 try jw.objectField("value"); 413 try jw.beginWriteRaw(); 414 try jw.writer.writeAll(variable.value); 415 jw.endWriteRaw(); 416 417 try jw.objectField("tags"); 418 try jw.beginWriteRaw(); 419 try jw.writer.writeAll(variable.tags); 420 jw.endWriteRaw(); 421 422 try jw.endObject(); 423}