prefect server in zig
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}