╭ changed ─────────╮ │src/test/event.zig│ ╰──────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────╮ │ 6 │ │ 7 const event_id_size: usize = 32; │ │ 8 │ │ 9 -const AnyEvent = struct { │ │ 9 +const EventData = union(enum) { │ │10 + add_issue: struct { │ │11 + title: []const u8, │ │12 + description: []const u8, │ │13 + tags: []const []const u8,│ │14 + }, │ │15 +}; │ │16 + │ │17 +const Event = struct { │ │18 id: [event_id_size * 2]u8, │ │11 - kind: []const u8, │ │12 - data: std.json.Value, │ │19 + data: EventData, │ │20 }; │ │21 │ │22 test "simple" { │ ╰─────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │49 // │ │50 // define test events │ │51 // │ │45 - │ │46 - const AddIssueData = struct { │ │47 - title: []const u8, │ │48 - description: []const u8, │ │49 - tags: []const []const u8, │ │50 - │ │51 - fn toJsonValue(issue: @This(), value_allocator: std.mem.Allocator) !std.json.Value { │ │52 - var tags = std.json.Array.init(value_allocator); │ │53 - for (issue.tags) |tag| { │ │54 - try tags.append(.{ .string = tag }); │ │55 - } │ │56 - │ │57 - var object: std.json.ObjectMap = .empty; │ │58 - try object.put(value_allocator, "title", .{ .string = issue.title }); │ │59 - try object.put(value_allocator, "description", .{ .string = issue.description }); │ │60 - try object.put(value_allocator, "tags", .{ .array = tags }); │ │61 - │ │62 - return .{ .object = object }; │ │63 - } │ │64 - }; │ │52 │ │66 - const issues_data = [_]AddIssueData{ │ │53 + const events_to_process = [_]EventData{ │ │54 .{ │ │68 - .title = "Login form clears password on validation error", │ │69 - .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", │ │70 - .tags = &[_][]const u8{ "bug", "priority-high", "ui" }, │ │55 + .add_issue = .{ │ │56 + .title = "Login form clears password on validation error", │ │57 + .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", │ │58 + .tags = &[_][]const u8{ "bug", "priority-high", "ui" }, │ │59 + }, │ │60 }, │ │61 .{ │ │73 - .title = "Search results ignore archived project filter", │ │74 - .description = "Filtering search results to active projects still returns issues from archived projects. Apply the archived flag before ranking results.", │ │75 - .tags = &[_][]const u8{ "bug", "search", "backend" }, │ │62 + .add_issue = .{ │ │63 + .title = "Search results ignore archived project filter", │ │64 + .description = "Filtering search results to active projects still returns issues from archived projects. Apply the archived flag before ranking results.",│ │65 + .tags = &[_][]const u8{ "bug", "search", "backend" }, │ │66 + }, │ │67 }, │ │68 .{ │ │78 - .title = "Issue list does not persist selected sort order", │ │79 - .description = "Changing the issue list sort order is lost after refresh. Store the selected sort field and direction with the user's view preferences.", │ │80 - .tags = &[_][]const u8{ "enhancement", "frontend", "preferences" }, │ │69 + .add_issue = .{ │ │70 + .title = "Issue list does not persist selected sort order", │ │71 + .description = "Changing the issue list sort order is lost after refresh. Store the selected sort field and direction with the user's view preferences.", │ │72 + .tags = &[_][]const u8{ "enhancement", "frontend", "preferences" }, │ │73 + }, │ │74 }, │ │75 .{ │ │83 - .title = "Webhook retries stop after transient timeout", │ │84 - .description = "A single gateway timeout marks webhook delivery as failed permanently. Retry transient network errors with exponential backoff.", │ │85 - .tags = &[_][]const u8{ "bug", "webhooks", "reliability" }, │ │76 + .add_issue = .{ │ │77 + .title = "Webhook retries stop after transient timeout", │ │78 + .description = "A single gateway timeout marks webhook delivery as failed permanently. Retry transient network errors with exponential backoff.", │ │79 + .tags = &[_][]const u8{ "bug", "webhooks", "reliability" }, │ │80 + }, │ │81 }, │ │82 .{ │ │88 - .title = "CSV export omits labels with commas", │ │89 - .description = "Exported issue rows drop labels that contain commas instead of escaping them. Quote CSV fields according to RFC 4180.", │ │90 - .tags = &[_][]const u8{ "bug", "export", "data-integrity" }, │ │83 + .add_issue = .{ │ │84 + .title = "CSV export omits labels with commas", │ │85 + .description = "Exported issue rows drop labels that contain commas instead of escaping them. Quote CSV fields according to RFC 4180.", │ │86 + .tags = &[_][]const u8{ "bug", "export", "data-integrity" }, │ │87 + }, │ │88 }, │ │89 }; │ │90 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────╮ │ 99 var json: std.Io.Writer.Allocating = .init(std.testing.allocator);│ │100 defer json.deinit(); │ │101 │ │105 - for (issues_data) |issue_data| { │ │102 + for (events_to_process) |event_data| { │ │103 json.clearRetainingCapacity(); │ │104 │ │105 var id_bytes: [event_id_size]u8 = undefined; │ ╰───────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │106 prng.random().bytes(&id_bytes); │ │107 │ │111 - const event = AnyEvent{ │ │108 + const event = Event{ │ │109 .id = std.fmt.bytesToHex(id_bytes, .lower), │ │113 - .kind = "add-issue", │ │114 - .data = try issue_data.toJsonValue(arena.allocator()),│ │110 + .data = event_data, │ │111 }; │ │112 │ │113 try std.json.Stringify.value(event, .{}, &json.writer); │ ╰───────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────╮ │120 // read and parse all of the events from the repo │ │121 // │ │122 │ │127 - var events: std.ArrayList(AnyEvent) = .empty; │ │123 + var events: std.ArrayList(Event) = .empty; │ │124 defer events.deinit(allocator); │ │125 │ │126 var event_strs: std.ArrayList([]const u8) = .empty;│ ╰────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │144 // add events in reverse order so the earliest event is first. │ │145 for (0..event_strs.items.len) |i| { │ │146 const event_str = event_strs.items[event_strs.items.len - i - 1]; │ │151 - const event = try std.json.parseFromSliceLeaky(AnyEvent, arena.allocator(), event_str, .{});│ │147 + const event = try std.json.parseFromSliceLeaky(Event, arena.allocator(), event_str, .{}); │ │148 try events.append(allocator, event); │ │149 } │ │150 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │153 // │ │154 │ │155 const Ctx = struct { │ │160 - events: []AnyEvent, │ │156 + events: []Event, │ │157 │ │158 pub fn run(ctx: @This(), cursor: *Repo.DB.Cursor(.read_write)) !void {│ │159 const moment = try Repo.DB.HashMap(.read_write).init(cursor.*); │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │204 { │ │205 const views = try Repo.DB.HashMap(.read_write).init(views_cursor); │ │206 │ │211 - if (std.mem.eql(u8, "add-issue", event.kind)) { │ │212 - const data = switch (event.data) { │ │213 - .object => |object| object, │ │214 - else => return error.InvalidEventData, │ │215 - }; │ │207 + switch (event.data) { │ │208 + .add_issue => |data| { │ │209 + const issues_cursor = try views.putCursor(hash.hashInt(repo_opts.hash, "issues")); │ │210 + const issues = try Repo.DB.ArrayList(.read_write).init(issues_cursor); │ │211 │ │217 - const issues_cursor = try views.putCursor(hash.hashInt(repo_opts.hash, "issues")); │ │218 - const issues = try Repo.DB.ArrayList(.read_write).init(issues_cursor); │ │212 + const issue_cursor = try issues.appendCursor(); │ │213 + const issue = try Repo.DB.HashMap(.read_write).init(issue_cursor); │ │214 │ │220 - const issue_cursor = try issues.appendCursor(); │ │221 - const issue = try Repo.DB.HashMap(.read_write).init(issue_cursor); │ │222 - │ │223 - try issue.put(hash.hashInt(repo_opts.hash, "id"), .{ .bytes = &event.id }); │ │224 - try issue.put(hash.hashInt(repo_opts.hash, "title"), .{ .bytes = try getObjectString(data, "title") }); │ │225 - try issue.put(hash.hashInt(repo_opts.hash, "description"), .{ .bytes = try getObjectString(data, "description") });│ │226 - } else { │ │227 - return error.InvalidEventKind; │ │215 + try issue.put(hash.hashInt(repo_opts.hash, "id"), .{ .bytes = &event.id }); │ │216 + try issue.put(hash.hashInt(repo_opts.hash, "title"), .{ .bytes = data.title }); │ │217 + try issue.put(hash.hashInt(repo_opts.hash, "description"), .{ .bytes = data.description }); │ │218 + }, │ │219 } │ │220 } │ │221 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │230 try haxy.put(hash.hashInt(repo_opts.hash, "last-event-id"), .{ .bytes = last_event_id });│ │231 } │ │232 } │ │242 - │ │243 - fn getObjectString(object: std.json.ObjectMap, key: []const u8) ![]const u8 { │ │244 - const value = object.get(key) orelse return error.MissingEventField; │ │245 - return switch (value) { │ │246 - .string => |string| string, │ │247 - else => error.InvalidEventField, │ │248 - }; │ │249 - } │ │233 }; │ │234 │ │235 try repo.core.db_file.lock(io, .exclusive); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────╮ │256 _ = try kv_pair_cursor.readKeyValuePair(); │ │257 count += 1; │ │258 } │ │276 - try std.testing.expectEqual(issues_data.len, count); │ │259 + try std.testing.expectEqual(events_to_process.len, count);│ │260 } │ │261 │ ╰───────────────────────────────────────────────────────────────────╯