const std = @import("std"); const zap = @import("zap"); const mem = std.mem; const json = std.json; const db = @import("../db/sqlite.zig"); const uuid_util = @import("../utilities/uuid.zig"); const time_util = @import("../utilities/time.zig"); const json_util = @import("../utilities/json.zig"); pub fn handle(r: zap.Request) !void { const target = r.path orelse "/"; const method = r.method orelse "GET"; // POST /variables/filter - list if (mem.eql(u8, method, "POST") and mem.endsWith(u8, target, "/filter")) { try filter(r); return; } // POST /variables/count if (mem.eql(u8, method, "POST") and mem.endsWith(u8, target, "/count")) { try countVariables(r); return; } // POST /variables/ - create if (mem.eql(u8, method, "POST") and (mem.eql(u8, target, "/variables/") or mem.eql(u8, target, "/api/variables/"))) { try createVariable(r); return; } // GET /variables/name/{name} if (mem.eql(u8, method, "GET") and mem.indexOf(u8, target, "/name/") != null) { const name = extractName(target) orelse { json_util.sendStatus(r, "{\"detail\":\"name required\"}", .bad_request); return; }; try getByName(r, name); return; } // PATCH /variables/name/{name} if (mem.eql(u8, method, "PATCH") and mem.indexOf(u8, target, "/name/") != null) { const name = extractName(target) orelse { json_util.sendStatus(r, "{\"detail\":\"name required\"}", .bad_request); return; }; try updateByName(r, name); return; } // DELETE /variables/name/{name} if (mem.eql(u8, method, "DELETE") and mem.indexOf(u8, target, "/name/") != null) { const name = extractName(target) orelse { json_util.sendStatus(r, "{\"detail\":\"name required\"}", .bad_request); return; }; try deleteByName(r, name); return; } // GET /variables/{id} if (mem.eql(u8, method, "GET")) { const id = extractId(target) orelse { json_util.sendStatus(r, "{\"detail\":\"variable id required\"}", .bad_request); return; }; try getById(r, id); return; } // PATCH /variables/{id} if (mem.eql(u8, method, "PATCH")) { const id = extractId(target) orelse { json_util.sendStatus(r, "{\"detail\":\"variable id required\"}", .bad_request); return; }; try updateById(r, id); return; } // DELETE /variables/{id} if (mem.eql(u8, method, "DELETE")) { const id = extractId(target) orelse { json_util.sendStatus(r, "{\"detail\":\"variable id required\"}", .bad_request); return; }; try deleteById(r, id); return; } json_util.sendStatus(r, "{\"detail\":\"not implemented\"}", .not_implemented); } fn extractId(target: []const u8) ?[]const u8 { const prefix = if (mem.startsWith(u8, target, "/api/variables/")) "/api/variables/" else "/variables/"; if (target.len > prefix.len) { return target[prefix.len..]; } return null; } fn extractName(target: []const u8) ?[]const u8 { const idx = mem.indexOf(u8, target, "/name/") orelse return null; const start = idx + "/name/".len; if (start < target.len) { return target[start..]; } return null; } fn createVariable(r: zap.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); const body = r.body orelse { json_util.sendStatus(r, "{\"detail\":\"failed to read body\"}", .bad_request); return; }; const parsed = json.parseFromSlice(json.Value, alloc, body, .{}) catch { json_util.sendStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); return; }; const obj = parsed.value.object; const name = switch (obj.get("name") orelse { json_util.sendStatus(r, "{\"detail\":\"name required\"}", .bad_request); return; }) { .string => |s| s, else => { json_util.sendStatus(r, "{\"detail\":\"name must be string\"}", .bad_request); return; }, }; // check for existing if (db.variables.getByName(alloc, name) catch null) |_| { const err_msg = std.fmt.allocPrint(alloc, "{{\"detail\":\"Variable with name '{s}' already exists.\"}}", .{name}) catch { json_util.sendStatus(r, "{\"detail\":\"Variable already exists\"}", .conflict); return; }; json_util.sendStatus(r, err_msg, .conflict); return; } const value_json = stringifyField(alloc, obj.get("value"), "null"); const tags_json = stringifyField(alloc, obj.get("tags"), "[]"); var new_id_buf: [36]u8 = undefined; const new_id = uuid_util.generate(&new_id_buf); var ts_buf: [32]u8 = undefined; const now = time_util.timestamp(&ts_buf); db.variables.insert(new_id, name, value_json, tags_json, now) catch { json_util.sendStatus(r, "{\"detail\":\"insert failed\"}", .internal_server_error); return; }; const variable = db.variables.VariableRow{ .id = new_id, .created = now, .updated = now, .name = name, .value = value_json, .tags = tags_json, }; const resp = writeVariable(alloc, variable) catch { json_util.sendStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); return; }; json_util.sendStatus(r, resp, .created); } fn getById(r: zap.Request, id: []const u8) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); if (db.variables.getById(alloc, id) catch null) |variable| { const resp = writeVariable(alloc, variable) catch { json_util.sendStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); return; }; json_util.send(r, resp); } else { json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); } } fn getByName(r: zap.Request, name: []const u8) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); if (db.variables.getByName(alloc, name) catch null) |variable| { const resp = writeVariable(alloc, variable) catch { json_util.sendStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); return; }; json_util.send(r, resp); } else { json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); } } fn updateById(r: zap.Request, id: []const u8) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); const body = r.body orelse { json_util.sendStatus(r, "{\"detail\":\"failed to read body\"}", .bad_request); return; }; const parsed = json.parseFromSlice(json.Value, alloc, body, .{}) catch { json_util.sendStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); return; }; const obj = parsed.value.object; const new_name = getOptionalString(obj.get("name")); const value_json = stringifyFieldOptional(alloc, obj.get("value")); const tags_json = stringifyFieldOptional(alloc, obj.get("tags")); var ts_buf: [32]u8 = undefined; const now = time_util.timestamp(&ts_buf); const did_update = db.variables.updateById(id, new_name, value_json, tags_json, now) catch { json_util.sendStatus(r, "{\"detail\":\"update failed\"}", .internal_server_error); return; }; if (!did_update) { json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); return; } r.setStatus(.no_content); r.sendBody("") catch {}; } fn updateByName(r: zap.Request, name: []const u8) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); const body = r.body orelse { json_util.sendStatus(r, "{\"detail\":\"failed to read body\"}", .bad_request); return; }; const parsed = json.parseFromSlice(json.Value, alloc, body, .{}) catch { json_util.sendStatus(r, "{\"detail\":\"invalid json\"}", .bad_request); return; }; const obj = parsed.value.object; const new_name = getOptionalString(obj.get("name")); const value_json = stringifyFieldOptional(alloc, obj.get("value")); const tags_json = stringifyFieldOptional(alloc, obj.get("tags")); var ts_buf: [32]u8 = undefined; const now = time_util.timestamp(&ts_buf); const did_update = db.variables.updateByName(name, new_name, value_json, tags_json, now) catch { json_util.sendStatus(r, "{\"detail\":\"update failed\"}", .internal_server_error); return; }; if (!did_update) { json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); return; } r.setStatus(.no_content); r.sendBody("") catch {}; } fn deleteById(r: zap.Request, id: []const u8) !void { const deleted = db.variables.deleteById(id) catch { json_util.sendStatus(r, "{\"detail\":\"delete failed\"}", .internal_server_error); return; }; if (!deleted) { json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); return; } r.setStatus(.no_content); r.sendBody("") catch {}; } fn deleteByName(r: zap.Request, name: []const u8) !void { const deleted = db.variables.deleteByName(name) catch { json_util.sendStatus(r, "{\"detail\":\"delete failed\"}", .internal_server_error); return; }; if (!deleted) { json_util.sendStatus(r, "{\"detail\":\"Variable not found\"}", .not_found); return; } r.setStatus(.no_content); r.sendBody("") catch {}; } fn filter(r: zap.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const alloc = arena.allocator(); var limit: usize = 200; var offset: usize = 0; if (r.body) |body| { if (json.parseFromSlice(json.Value, alloc, body, .{})) |parsed| { const obj = parsed.value.object; if (obj.get("limit")) |v| { if (v == .integer) limit = @intCast(v.integer); } if (obj.get("offset")) |v| { if (v == .integer) offset = @intCast(v.integer); } } else |_| {} } const variables = db.variables.list(alloc, limit, offset) catch { json_util.sendStatus(r, "{\"detail\":\"database error\"}", .internal_server_error); return; }; var output: std.io.Writer.Allocating = .init(alloc); var jw: json.Stringify = .{ .writer = &output.writer }; jw.beginArray() catch { json_util.sendStatus(r, "{\"detail\":\"serialize error\"}", .internal_server_error); return; }; for (variables) |variable| { writeVariableObject(&jw, variable) catch continue; } jw.endArray() catch {}; json_util.send(r, output.toOwnedSlice() catch "[]"); } fn countVariables(r: zap.Request) !void { const cnt = db.variables.count() catch 0; var buf: [32]u8 = undefined; const resp = std.fmt.bufPrint(&buf, "{d}", .{cnt}) catch "0"; json_util.send(r, resp); } fn stringifyField(alloc: std.mem.Allocator, val: ?json.Value, default: []const u8) []const u8 { if (val) |v| { return std.fmt.allocPrint(alloc, "{f}", .{json.fmt(v, .{})}) catch default; } return default; } fn stringifyFieldOptional(alloc: std.mem.Allocator, val: ?json.Value) ?[]const u8 { if (val) |v| { return std.fmt.allocPrint(alloc, "{f}", .{json.fmt(v, .{})}) catch null; } return null; } fn getOptionalString(val: ?json.Value) ?[]const u8 { if (val) |v| { return switch (v) { .string => |s| s, else => null, }; } return null; } fn writeVariable(alloc: std.mem.Allocator, variable: db.variables.VariableRow) ![]const u8 { var output: std.io.Writer.Allocating = .init(alloc); var jw: json.Stringify = .{ .writer = &output.writer }; try writeVariableObject(&jw, variable); return output.toOwnedSlice(); } fn writeVariableObject(jw: *json.Stringify, variable: db.variables.VariableRow) !void { try jw.beginObject(); try jw.objectField("id"); try jw.write(variable.id); try jw.objectField("created"); try jw.write(variable.created); try jw.objectField("updated"); try jw.write(variable.updated); try jw.objectField("name"); try jw.write(variable.name); try jw.objectField("value"); try jw.beginWriteRaw(); try jw.writer.writeAll(variable.value); jw.endWriteRaw(); try jw.objectField("tags"); try jw.beginWriteRaw(); try jw.writer.writeAll(variable.tags); jw.endWriteRaw(); try jw.endObject(); }