lib/zen/src/site/catalog/plan.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 const model = @import("model.zig");
4
5 pub const Limits = struct {
6 max_pages: usize,
7 max_text_bytes: usize,
8 };
9
10 pub const default_limits: Limits = .{
11 .max_pages = 4096,
12 .max_text_bytes = 64 * 1024 * 1024,
13 };
14
15 pub const Demand = struct {
16 relative_path_bytes: usize,
17 url_bytes: usize,
18 title_bytes: usize,
19 date_bytes: usize,
20 type_label_bytes: usize,
21
22 pub fn fromInput(input: model.EntryInput) Demand {
23 const demand = Demand{
24 .relative_path_bytes = input.relative_path.len,
25 .url_bytes = input.url.len,
26 .title_bytes = input.title.len,
27 .date_bytes = input.date.len,
28 .type_label_bytes = input.type_label.len,
29 };
30 std.debug.assert(demand.relative_path_bytes == input.relative_path.len);
31 std.debug.assert(demand.url_bytes == input.url.len);
32 std.debug.assert(demand.title_bytes == input.title.len);
33 std.debug.assert(demand.date_bytes == input.date.len);
34 std.debug.assert(demand.type_label_bytes == input.type_label.len);
35 return demand;
36 }
37
38 pub fn textBytes(self: Demand) error{CapacityOverflow}!usize {
39 var total = try added(self.relative_path_bytes, self.url_bytes);
40 std.debug.assert(total >= self.relative_path_bytes);
41 std.debug.assert(total >= self.url_bytes);
42 total = try added(total, self.title_bytes);
43 std.debug.assert(total >= self.title_bytes);
44 total = try added(total, self.date_bytes);
45 std.debug.assert(total >= self.date_bytes);
46 const result = try added(total, self.type_label_bytes);
47 std.debug.assert(result >= self.type_label_bytes);
48 return result;
49 }
50 };
51
52 pub const Plan = struct {
53 pages: usize = 0,
54 text_bytes: usize = 0,
55
56 pub fn inspect(demand: Demand, limits: Limits) model.Error!Plan {
57 var plan: Plan = .{};
58 try plan.observe(demand, limits);
59 std.debug.assert(plan.pages == 1);
60 std.debug.assert(plan.text_bytes == try demand.textBytes());
61 return plan;
62 }
63
64 pub fn observe(self: *Plan, demand: Demand, limits: Limits) model.Error!void {
65 const next_pages = try added(self.pages, 1);
66 if (next_pages > limits.max_pages) return error.SiteCatalogPageCapacityExceeded;
67 const next_text = try added(self.text_bytes, try demand.textBytes());
68 if (next_text > limits.max_text_bytes) return error.SiteCatalogTextCapacityExceeded;
69 self.pages = next_pages;
70 self.text_bytes = next_text;
71 std.debug.assert(self.pages <= limits.max_pages);
72 std.debug.assert(self.text_bytes <= limits.max_text_bytes);
73 }
74
75 pub fn exactLimits(self: Plan) Limits {
76 const limits = Limits{ .max_pages = self.pages, .max_text_bytes = self.text_bytes };
77 std.debug.assert(limits.max_pages == self.pages);
78 std.debug.assert(limits.max_text_bytes == self.text_bytes);
79 return limits;
80 }
81 };
82
83 fn added(left: usize, right: usize) error{CapacityOverflow}!usize {
84 return std.math.add(usize, left, right) catch error.CapacityOverflow;
85 }
86
87 test "site catalog plan rejects every limit before mutation" {
88 const demand = Demand{
89 .relative_path_bytes = 8,
90 .url_bytes = 9,
91 .title_bytes = 5,
92 .date_bytes = 10,
93 .type_label_bytes = 0,
94 };
95 var plan = try Plan.inspect(demand, .{ .max_pages = 2, .max_text_bytes = 64 });
96 try std.testing.expectError(error.SiteCatalogPageCapacityExceeded, plan.observe(
97 demand,
98 .{ .max_pages = 1, .max_text_bytes = 64 },
99 ));
100 try std.testing.expectEqual(
101 Limits{ .max_pages = 1, .max_text_bytes = 32 },
102 plan.exactLimits(),
103 );
104 try std.testing.expectError(error.SiteCatalogTextCapacityExceeded, plan.observe(
105 demand,
106 .{ .max_pages = 2, .max_text_bytes = 63 },
107 ));
108 try std.testing.expectEqual(
109 Limits{ .max_pages = 1, .max_text_bytes = 32 },
110 plan.exactLimits(),
111 );
112 }
113
114 test "site catalog demand rejects checked arithmetic overflow" {
115 try std.testing.expectError(error.CapacityOverflow, (Demand{
116 .relative_path_bytes = std.math.maxInt(usize),
117 .url_bytes = 1,
118 .title_bytes = 0,
119 .date_bytes = 0,
120 .type_label_bytes = 0,
121 }).textBytes());
122 }