prefect server in zig

refactor: eliminate common.zig junk drawer

- move uuid functions to utilities/uuid.zig
- move timestamp functions to utilities/time.zig
- create api/routing.zig for path extraction and run name generation
- rename extractIdSimple to extractIdAfter (actually describes what it does)
- delete common.zig

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+196 -188
-158
src/api/common.zig
··· 1 - const std = @import("std"); 2 - const mem = std.mem; 3 - const uuid = @import("uuid"); 4 - 5 - pub fn normalizeUuid(alloc: std.mem.Allocator, input_uuid: []const u8) []const u8 { 6 - // remove hyphens from UUID (convert 36-char to 32-char) 7 - // ALWAYS copy because input may point to buffer that gets overwritten 8 - if (input_uuid.len == 32) { 9 - return alloc.dupe(u8, input_uuid) catch input_uuid; 10 - } 11 - if (input_uuid.len != 36) { 12 - return alloc.dupe(u8, input_uuid) catch input_uuid; 13 - } 14 - 15 - var result: [32]u8 = undefined; 16 - var j: usize = 0; 17 - for (input_uuid) |c| { 18 - if (c != '-') { 19 - if (j >= 32) return alloc.dupe(u8, input_uuid) catch input_uuid; 20 - result[j] = c; 21 - j += 1; 22 - } 23 - } 24 - 25 - return alloc.dupe(u8, &result) catch input_uuid; 26 - } 27 - 28 - pub fn extractId(target: []const u8, prefix: []const u8, suffix: []const u8) ?[]const u8 { 29 - if (!mem.startsWith(u8, target, prefix)) return null; 30 - if (!mem.endsWith(u8, target, suffix)) return null; 31 - const start = prefix.len; 32 - const end = target.len - suffix.len; 33 - if (start >= end) return null; 34 - return target[start..end]; 35 - } 36 - 37 - pub fn extractIdSimple(target: []const u8, prefix: []const u8) ?[]const u8 { 38 - if (!mem.startsWith(u8, target, prefix)) return null; 39 - const rest = target[prefix.len..]; 40 - // accept both 32-char hex and 36-char hyphenated UUIDs 41 - // strip query string if present 42 - const id_end = mem.indexOf(u8, rest, "?") orelse rest.len; 43 - if (id_end >= 36) { 44 - return rest[0..36]; 45 - } else if (id_end >= 32) { 46 - return rest[0..32]; 47 - } 48 - return null; 49 - } 50 - 51 - pub fn generateUuid(alloc: std.mem.Allocator) []const u8 { 52 - const id = uuid.v4.new(); 53 - var buf: [36]u8 = undefined; 54 - const urn = uuid.urn.serialize(id); 55 - @memcpy(&buf, &urn); 56 - 57 - var hex_buf: [32]u8 = undefined; 58 - var j: usize = 0; 59 - for (buf) |c| { 60 - if (c != '-') { 61 - hex_buf[j] = c; 62 - j += 1; 63 - } 64 - } 65 - 66 - return alloc.dupe(u8, &hex_buf) catch "00000000000000000000000000000000"; 67 - } 68 - 69 - // buffer-based version - writes to provided buffer, returns slice 70 - // returns standard 36-char dashed UUID format (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) 71 - pub fn generateUuidBuf(buf: *[36]u8) []const u8 { 72 - const id = uuid.v4.new(); 73 - const urn = uuid.urn.serialize(id); 74 - @memcpy(buf, &urn); 75 - return buf[0..36]; 76 - } 77 - 78 - // buffer-based normalize - writes to provided buffer, returns slice 79 - pub fn normalizeUuidBuf(input_uuid: []const u8, buf: *[32]u8) []const u8 { 80 - if (input_uuid.len == 32) { 81 - @memcpy(buf, input_uuid[0..32]); 82 - return buf[0..32]; 83 - } 84 - if (input_uuid.len != 36) { 85 - // invalid, return as-is (caller must handle) 86 - const len = @min(input_uuid.len, 32); 87 - @memcpy(buf[0..len], input_uuid[0..len]); 88 - return buf[0..len]; 89 - } 90 - 91 - var j: usize = 0; 92 - for (input_uuid) |c| { 93 - if (c != '-') { 94 - if (j >= 32) break; 95 - buf[j] = c; 96 - j += 1; 97 - } 98 - } 99 - return buf[0..j]; 100 - } 101 - 102 - pub fn generateRunName(alloc: std.mem.Allocator) []const u8 { 103 - const adjectives = [_][]const u8{ "happy", "quick", "brave", "calm", "eager" }; 104 - const nouns = [_][]const u8{ "panda", "tiger", "eagle", "dolphin", "falcon" }; 105 - 106 - var rng_buf: [2]u8 = undefined; 107 - std.crypto.random.bytes(&rng_buf); 108 - 109 - const adj = adjectives[rng_buf[0] % adjectives.len]; 110 - const noun = nouns[rng_buf[1] % nouns.len]; 111 - 112 - return std.fmt.allocPrint(alloc, "{s}-{s}", .{ adj, noun }) catch "unnamed-run"; 113 - } 114 - 115 - pub fn getTimestamp(buf: *[32]u8) []const u8 { 116 - const ts_us = std.time.microTimestamp(); 117 - const epoch_us: u64 = @intCast(ts_us); 118 - const epoch_secs = epoch_us / 1_000_000; 119 - const micros = epoch_us % 1_000_000; 120 - 121 - const secs_per_day: u64 = 86400; 122 - const days_since_epoch = epoch_secs / secs_per_day; 123 - const secs_today = epoch_secs % secs_per_day; 124 - 125 - const hours: u64 = secs_today / 3600; 126 - const mins: u64 = (secs_today % 3600) / 60; 127 - const secs: u64 = secs_today % 60; 128 - 129 - var days = days_since_epoch; 130 - var year: u64 = 1970; 131 - while (true) { 132 - const days_in_year: u64 = if (isLeapYear(year)) 366 else 365; 133 - if (days < days_in_year) break; 134 - days -= days_in_year; 135 - year += 1; 136 - } 137 - 138 - const month_days = if (isLeapYear(year)) 139 - [_]u64{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } 140 - else 141 - [_]u64{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; 142 - 143 - var month: u64 = 1; 144 - for (month_days) |md| { 145 - if (days < md) break; 146 - days -= md; 147 - month += 1; 148 - } 149 - const day = days + 1; 150 - 151 - return std.fmt.bufPrint(buf, "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}.{d:0>6}Z", .{ 152 - year, month, day, hours, mins, secs, micros, 153 - }) catch "2025-01-17T00:00:00.000000Z"; 154 - } 155 - 156 - fn isLeapYear(year: u64) bool { 157 - return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0); 158 - }
+15 -13
src/api/flow_runs.zig
··· 4 4 const json = std.json; 5 5 6 6 const db = @import("../db/sqlite.zig"); 7 - const common = @import("common.zig"); 7 + const routing = @import("routing.zig"); 8 + const uuid_util = @import("../utilities/uuid.zig"); 9 + const time_util = @import("../utilities/time.zig"); 8 10 const orchestration = @import("../orchestration/orchestration.zig"); 9 11 10 12 fn sendJson(r: zap.Request, body: []const u8) void { ··· 42 44 43 45 // check for /{id}/set_state 44 46 if (mem.eql(u8, method, "POST") and mem.endsWith(u8, target, "/set_state")) { 45 - const id = common.extractId(target, "/flow_runs/", "/set_state") orelse 46 - common.extractId(target, "/api/flow_runs/", "/set_state"); 47 + const id = routing.extractId(target, "/flow_runs/", "/set_state") orelse 48 + routing.extractId(target, "/api/flow_runs/", "/set_state"); 47 49 if (id) |flow_run_id| { 48 50 try setState(r, flow_run_id); 49 51 return; ··· 52 54 53 55 // GET /flow_runs/{id} - read single 54 56 if (mem.eql(u8, method, "GET")) { 55 - const id = common.extractIdSimple(target, "/flow_runs/") orelse 56 - common.extractIdSimple(target, "/api/flow_runs/"); 57 + const id = routing.extractIdAfter(target, "/flow_runs/") orelse 58 + routing.extractIdAfter(target, "/api/flow_runs/"); 57 59 if (id) |flow_run_id| { 58 60 try read(r, flow_run_id); 59 61 return; ··· 94 96 95 97 const name = if (obj.get("name")) |v| switch (v) { 96 98 .string => |s| s, 97 - .null => common.generateRunName(alloc), 98 - else => common.generateRunName(alloc), 99 - } else common.generateRunName(alloc); 99 + .null => routing.generateRunName(alloc), 100 + else => routing.generateRunName(alloc), 101 + } else routing.generateRunName(alloc); 100 102 const state = obj.get("state"); 101 103 102 104 // extract state info ··· 108 110 } 109 111 110 112 var new_id_buf: [36]u8 = undefined; 111 - const new_id = common.generateUuidBuf(&new_id_buf); 113 + const new_id = uuid_util.generate(&new_id_buf); 112 114 var ts_buf: [32]u8 = undefined; 113 - const now = common.getTimestamp(&ts_buf); 115 + const now = time_util.timestamp(&ts_buf); 114 116 115 117 db.insertFlowRun(new_id, flow_id, name, state_type, state_name, now) catch { 116 118 sendJsonStatus(r, "{\"detail\":\"insert failed\"}", .internal_server_error); ··· 119 121 120 122 // use stack buffer for response 121 123 var state_id_buf: [36]u8 = undefined; 122 - const state_id = common.generateUuidBuf(&state_id_buf); 124 + const state_id = uuid_util.generate(&state_id_buf); 123 125 var resp_buf: [2048]u8 = undefined; 124 126 const response = std.fmt.bufPrint(&resp_buf, 125 127 \\{{"id":"{s}","created":"{s}","updated":"{s}","name":"{s}","flow_id":"{s}","state_type":"{s}","state_name":"{s}","state":{{"type":"{s}","name":"{s}","timestamp":"{s}","id":"{s}"}},"parameters":{{}},"tags":[],"run_count":0,"expected_start_time":null,"start_time":null,"end_time":null,"total_run_time":0.0}} ··· 198 200 const state_type = if (state.object.get("type")) |v| v.string else "PENDING"; 199 201 const state_name = if (state.object.get("name")) |v| v.string else "Pending"; 200 202 var ts_buf: [32]u8 = undefined; 201 - const now = common.getTimestamp(&ts_buf); 203 + const now = time_util.timestamp(&ts_buf); 202 204 var state_id_buf: [36]u8 = undefined; 203 - const state_id = common.generateUuidBuf(&state_id_buf); 205 + const state_id = uuid_util.generate(&state_id_buf); 204 206 205 207 // get current run state for orchestration 206 208 const current_run = db.getFlowRun(alloc, id) catch null;
+4 -3
src/api/flows.zig
··· 4 4 const json = std.json; 5 5 6 6 const db = @import("../db/sqlite.zig"); 7 - const common = @import("common.zig"); 7 + const uuid_util = @import("../utilities/uuid.zig"); 8 + const time_util = @import("../utilities/time.zig"); 8 9 9 10 fn sendJson(r: zap.Request, body: []const u8) void { 10 11 r.setHeader("content-type", "application/json") catch {}; ··· 86 87 87 88 // create new flow 88 89 var new_id_buf: [36]u8 = undefined; 89 - const new_id = common.generateUuidBuf(&new_id_buf); 90 + const new_id = uuid_util.generate(&new_id_buf); 90 91 91 92 db.insertFlow(new_id, name_str) catch { 92 93 sendJsonStatus(r, "{\"detail\":\"insert failed\"}", .internal_server_error); ··· 94 95 }; 95 96 96 97 var ts_buf: [32]u8 = undefined; 97 - const now = common.getTimestamp(&ts_buf); 98 + const now = time_util.timestamp(&ts_buf); 98 99 99 100 var resp_buf: [512]u8 = undefined; 100 101 const response = std.fmt.bufPrint(&resp_buf,
-1
src/api/routes.zig
··· 7 7 pub const flow_runs = @import("flow_runs.zig"); 8 8 pub const task_runs = @import("task_runs.zig"); 9 9 pub const logs = @import("logs.zig"); 10 - pub const common = @import("common.zig"); 11 10 12 11 pub fn handle(r: zap.Request) !void { 13 12 const target = r.path orelse "/";
+41
src/api/routing.zig
··· 1 + const std = @import("std"); 2 + const mem = std.mem; 3 + 4 + /// Extract ID from path between prefix and suffix 5 + /// e.g., extractId("/flow_runs/abc/set_state", "/flow_runs/", "/set_state") -> "abc" 6 + pub fn extractId(target: []const u8, prefix: []const u8, suffix: []const u8) ?[]const u8 { 7 + if (!mem.startsWith(u8, target, prefix)) return null; 8 + if (!mem.endsWith(u8, target, suffix)) return null; 9 + const start = prefix.len; 10 + const end = target.len - suffix.len; 11 + if (start >= end) return null; 12 + return target[start..end]; 13 + } 14 + 15 + /// Extract ID from path after prefix (handles both 32 and 36 char UUIDs) 16 + /// e.g., extractIdAfter("/flow_runs/abc-def", "/flow_runs/") -> "abc-def" 17 + pub fn extractIdAfter(target: []const u8, prefix: []const u8) ?[]const u8 { 18 + if (!mem.startsWith(u8, target, prefix)) return null; 19 + const rest = target[prefix.len..]; 20 + const id_end = mem.indexOf(u8, rest, "?") orelse rest.len; 21 + if (id_end >= 36) { 22 + return rest[0..36]; 23 + } else if (id_end >= 32) { 24 + return rest[0..32]; 25 + } 26 + return null; 27 + } 28 + 29 + /// Generate a random run name (e.g., "happy-panda") 30 + pub fn generateRunName(alloc: std.mem.Allocator) []const u8 { 31 + const adjectives = [_][]const u8{ "happy", "quick", "brave", "calm", "eager" }; 32 + const nouns = [_][]const u8{ "panda", "tiger", "eagle", "dolphin", "falcon" }; 33 + 34 + var rng_buf: [2]u8 = undefined; 35 + std.crypto.random.bytes(&rng_buf); 36 + 37 + const adj = adjectives[rng_buf[0] % adjectives.len]; 38 + const noun = nouns[rng_buf[1] % nouns.len]; 39 + 40 + return std.fmt.allocPrint(alloc, "{s}-{s}", .{ adj, noun }) catch "unnamed-run"; 41 + }
+15 -13
src/api/task_runs.zig
··· 4 4 const json = std.json; 5 5 6 6 const db = @import("../db/sqlite.zig"); 7 - const common = @import("common.zig"); 7 + const routing = @import("routing.zig"); 8 + const uuid_util = @import("../utilities/uuid.zig"); 9 + const time_util = @import("../utilities/time.zig"); 8 10 9 11 fn sendJson(r: zap.Request, body: []const u8) void { 10 12 r.setHeader("content-type", "application/json") catch {}; ··· 41 43 42 44 // check for /{id}/set_state 43 45 if (mem.eql(u8, method, "POST") and mem.endsWith(u8, target, "/set_state")) { 44 - const id = common.extractId(target, "/task_runs/", "/set_state") orelse 45 - common.extractId(target, "/api/task_runs/", "/set_state"); 46 + const id = routing.extractId(target, "/task_runs/", "/set_state") orelse 47 + routing.extractId(target, "/api/task_runs/", "/set_state"); 46 48 if (id) |task_run_id| { 47 49 try setState(r, task_run_id); 48 50 return; ··· 51 53 52 54 // GET /task_runs/{id} - read single 53 55 if (mem.eql(u8, method, "GET")) { 54 - const id = common.extractIdSimple(target, "/task_runs/") orelse 55 - common.extractIdSimple(target, "/api/task_runs/"); 56 + const id = routing.extractIdAfter(target, "/task_runs/") orelse 57 + routing.extractIdAfter(target, "/api/task_runs/"); 56 58 if (id) |task_run_id| { 57 59 try read(r, task_run_id); 58 60 return; ··· 111 113 112 114 const name = if (obj.get("name")) |v| switch (v) { 113 115 .string => |s| s, 114 - .null => common.generateRunName(alloc), 115 - else => common.generateRunName(alloc), 116 - } else common.generateRunName(alloc); 116 + .null => routing.generateRunName(alloc), 117 + else => routing.generateRunName(alloc), 118 + } else routing.generateRunName(alloc); 117 119 118 120 const state = obj.get("state"); 119 121 ··· 143 145 } 144 146 145 147 var new_id_buf: [36]u8 = undefined; 146 - const new_id = common.generateUuidBuf(&new_id_buf); 148 + const new_id = uuid_util.generate(&new_id_buf); 147 149 var ts_buf: [32]u8 = undefined; 148 - const now = common.getTimestamp(&ts_buf); 150 + const now = time_util.timestamp(&ts_buf); 149 151 150 152 db.insertTaskRun(new_id, flow_run_id, name, task_key, dynamic_key, state_type, state_name, now) catch { 151 153 sendJsonStatus(r, "{\"detail\":\"insert failed\"}", .internal_server_error); ··· 153 155 }; 154 156 155 157 var state_id_buf: [36]u8 = undefined; 156 - const state_id = common.generateUuidBuf(&state_id_buf); 158 + const state_id = uuid_util.generate(&state_id_buf); 157 159 158 160 var resp_buf: [2048]u8 = undefined; 159 161 const response = std.fmt.bufPrint(&resp_buf, ··· 222 224 const state_type = if (state.object.get("type")) |v| v.string else "PENDING"; 223 225 const state_name = if (state.object.get("name")) |v| v.string else "Pending"; 224 226 var ts_buf: [32]u8 = undefined; 225 - const now = common.getTimestamp(&ts_buf); 227 + const now = time_util.timestamp(&ts_buf); 226 228 var state_id_buf: [36]u8 = undefined; 227 - const state_id = common.generateUuidBuf(&state_id_buf); 229 + const state_id = uuid_util.generate(&state_id_buf); 228 230 229 231 db.setTaskRunState(id, state_id, state_type, state_name, now) catch { 230 232 sendJsonStatus(r, "{\"detail\":\"update failed\"}", .internal_server_error);
+47
src/utilities/time.zig
··· 1 + const std = @import("std"); 2 + 3 + /// Generate ISO 8601 timestamp with microseconds (e.g., 2025-01-17T12:34:56.123456Z) 4 + pub fn timestamp(buf: *[32]u8) []const u8 { 5 + const ts_us = std.time.microTimestamp(); 6 + const epoch_us: u64 = @intCast(ts_us); 7 + const epoch_secs = epoch_us / 1_000_000; 8 + const micros = epoch_us % 1_000_000; 9 + 10 + const secs_per_day: u64 = 86400; 11 + const days_since_epoch = epoch_secs / secs_per_day; 12 + const secs_today = epoch_secs % secs_per_day; 13 + 14 + const hours: u64 = secs_today / 3600; 15 + const mins: u64 = (secs_today % 3600) / 60; 16 + const secs: u64 = secs_today % 60; 17 + 18 + var days = days_since_epoch; 19 + var year: u64 = 1970; 20 + while (true) { 21 + const days_in_year: u64 = if (isLeapYear(year)) 366 else 365; 22 + if (days < days_in_year) break; 23 + days -= days_in_year; 24 + year += 1; 25 + } 26 + 27 + const month_days = if (isLeapYear(year)) 28 + [_]u64{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } 29 + else 30 + [_]u64{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; 31 + 32 + var month: u64 = 1; 33 + for (month_days) |md| { 34 + if (days < md) break; 35 + days -= md; 36 + month += 1; 37 + } 38 + const day = days + 1; 39 + 40 + return std.fmt.bufPrint(buf, "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}.{d:0>6}Z", .{ 41 + year, month, day, hours, mins, secs, micros, 42 + }) catch "2025-01-17T00:00:00.000000Z"; 43 + } 44 + 45 + fn isLeapYear(year: u64) bool { 46 + return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0); 47 + }
+74
src/utilities/uuid.zig
··· 1 + const std = @import("std"); 2 + const uuid = @import("uuid"); 3 + 4 + /// Generate a new v4 UUID in standard 36-char format (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) 5 + pub fn generate(buf: *[36]u8) []const u8 { 6 + const id = uuid.v4.new(); 7 + const urn = uuid.urn.serialize(id); 8 + @memcpy(buf, &urn); 9 + return buf[0..36]; 10 + } 11 + 12 + /// Generate a new v4 UUID, allocating the result 13 + pub fn generateAlloc(alloc: std.mem.Allocator) []const u8 { 14 + const id = uuid.v4.new(); 15 + var buf: [36]u8 = undefined; 16 + const urn = uuid.urn.serialize(id); 17 + @memcpy(&buf, &urn); 18 + 19 + var hex_buf: [32]u8 = undefined; 20 + var j: usize = 0; 21 + for (buf) |c| { 22 + if (c != '-') { 23 + hex_buf[j] = c; 24 + j += 1; 25 + } 26 + } 27 + 28 + return alloc.dupe(u8, &hex_buf) catch "00000000000000000000000000000000"; 29 + } 30 + 31 + /// Normalize UUID to 32-char hex (remove dashes) 32 + pub fn normalize(input: []const u8, buf: *[32]u8) []const u8 { 33 + if (input.len == 32) { 34 + @memcpy(buf, input[0..32]); 35 + return buf[0..32]; 36 + } 37 + if (input.len != 36) { 38 + const len = @min(input.len, 32); 39 + @memcpy(buf[0..len], input[0..len]); 40 + return buf[0..len]; 41 + } 42 + 43 + var j: usize = 0; 44 + for (input) |c| { 45 + if (c != '-') { 46 + if (j >= 32) break; 47 + buf[j] = c; 48 + j += 1; 49 + } 50 + } 51 + return buf[0..j]; 52 + } 53 + 54 + /// Normalize UUID, allocating the result 55 + pub fn normalizeAlloc(alloc: std.mem.Allocator, input: []const u8) []const u8 { 56 + if (input.len == 32) { 57 + return alloc.dupe(u8, input) catch input; 58 + } 59 + if (input.len != 36) { 60 + return alloc.dupe(u8, input) catch input; 61 + } 62 + 63 + var result: [32]u8 = undefined; 64 + var j: usize = 0; 65 + for (input) |c| { 66 + if (c != '-') { 67 + if (j >= 32) return alloc.dupe(u8, input) catch input; 68 + result[j] = c; 69 + j += 1; 70 + } 71 + } 72 + 73 + return alloc.dupe(u8, &result) catch input; 74 + }