forked from
karlseguin.tngl.sh/http.zig
An HTTP/1.1 server for zig
1const std = @import("std");
2
3const httpz = @import("httpz.zig");
4const Params = @import("params.zig").Params;
5const Request = httpz.Request;
6const Response = httpz.Response;
7
8const Allocator = std.mem.Allocator;
9const StringHashMap = std.StringHashMap;
10
11fn RouteConfig(comptime Handler: type, comptime Action: type) type {
12 const Dispatcher = httpz.Dispatcher(Handler, Action);
13 return struct {
14 data: ?*const anyopaque = null,
15 handler: ?Handler = null,
16 dispatcher: ?Dispatcher = null,
17 middlewares: ?[]const httpz.Middleware(Handler) = null,
18 middleware_strategy: ?MiddlewareStrategy = null,
19 };
20}
21
22pub const MiddlewareStrategy = enum {
23 append,
24 replace,
25};
26
27pub fn Router(comptime Handler: type, comptime Action: type) type {
28 const Dispatcher = httpz.Dispatcher(Handler, Action);
29 const DispatchableAction = httpz.DispatchableAction(Handler, Action);
30
31 const P = Part(DispatchableAction);
32 const RC = RouteConfig(Handler, Action);
33
34 return struct {
35 _get: P,
36 _put: P,
37 _post: P,
38 _head: P,
39 _patch: P,
40 _trace: P,
41 _delete: P,
42 _options: P,
43 _connect: P,
44 _other: std.StringHashMapUnmanaged(P),
45 _allocator: Allocator,
46 handler: Handler,
47 dispatcher: Dispatcher,
48 middlewares: []const httpz.Middleware(Handler),
49
50 const Self = @This();
51
52 // We expect allocator to be an Arena
53 pub fn init(allocator: Allocator, dispatcher: Dispatcher, handler: Handler) !Self {
54 return .{
55 .handler = handler,
56 ._allocator = allocator,
57 .dispatcher = dispatcher,
58 .middlewares = &.{},
59 ._get = try P.init(allocator),
60 ._head = try P.init(allocator),
61 ._post = try P.init(allocator),
62 ._put = try P.init(allocator),
63 ._patch = try P.init(allocator),
64 ._trace = try P.init(allocator),
65 ._delete = try P.init(allocator),
66 ._options = try P.init(allocator),
67 ._connect = try P.init(allocator),
68 ._other = .{},
69 };
70 }
71
72 pub fn group(self: *Self, prefix: []const u8, config: RC) Group(Handler, Action) {
73 return Group(Handler, Action).init(self, prefix, config);
74 }
75
76 pub fn route(self: *Self, m: httpz.Method, method_string: []const u8, url: []const u8, params: *Params) ?*const DispatchableAction {
77 return switch (m) {
78 .GET => getRoute(DispatchableAction, &self._get, url, params),
79 .POST => getRoute(DispatchableAction, &self._post, url, params),
80 .PUT => getRoute(DispatchableAction, &self._put, url, params),
81 .DELETE => getRoute(DispatchableAction, &self._delete, url, params),
82 .PATCH => getRoute(DispatchableAction, &self._patch, url, params),
83 .HEAD => getRoute(DispatchableAction, &self._head, url, params),
84 .OPTIONS => getRoute(DispatchableAction, &self._options, url, params),
85 .CONNECT => getRoute(DispatchableAction, &self._connect, url, params),
86 .OTHER => {
87 const p = self._other.getPtr(method_string) orelse return null;
88 return getRoute(DispatchableAction, p, url, params);
89 },
90 };
91 }
92
93 /// routeConfig allows the user to pass an anon struct, and have it coerced into the correct RC type for this router
94 pub fn routeConfig(_: Self, config: RC) RC {
95 return config;
96 }
97
98 pub fn get(self: *Self, path: []const u8, action: Action, config: RC) void {
99 self.tryGet(path, action, config) catch @panic("failed to create route");
100 }
101 pub fn tryGet(self: *Self, path: []const u8, action: Action, config: RC) !void {
102 return self.addRoute(&self._get, path, action, config);
103 }
104
105 pub fn put(self: *Self, path: []const u8, action: Action, config: RC) void {
106 self.tryPut(path, action, config) catch @panic("failed to create route");
107 }
108 pub fn tryPut(self: *Self, path: []const u8, action: Action, config: RC) !void {
109 return self.addRoute(&self._put, path, action, config);
110 }
111
112 pub fn post(self: *Self, path: []const u8, action: Action, config: RC) void {
113 self.tryPost(path, action, config) catch @panic("failed to create route");
114 }
115 pub fn tryPost(self: *Self, path: []const u8, action: Action, config: RC) !void {
116 return self.addRoute(&self._post, path, action, config);
117 }
118
119 pub fn head(self: *Self, path: []const u8, action: Action, config: RC) void {
120 self.tryHead(path, action, config) catch @panic("failed to create route");
121 }
122 pub fn tryHead(self: *Self, path: []const u8, action: Action, config: RC) !void {
123 return self.addRoute(&self._head, path, action, config);
124 }
125
126 pub fn patch(self: *Self, path: []const u8, action: Action, config: RC) void {
127 self.tryPatch(path, action, config) catch @panic("failed to create route");
128 }
129 pub fn tryPatch(self: *Self, path: []const u8, action: Action, config: RC) !void {
130 return self.addRoute(&self._patch, path, action, config);
131 }
132
133 pub fn trace(self: *Self, path: []const u8, action: Action, config: RC) void {
134 self.tryTrace(path, action, config) catch @panic("failed to create route");
135 }
136 pub fn tryTrace(self: *Self, path: []const u8, action: Action, config: RC) !void {
137 return self.addRoute(&self._trace, path, action, config);
138 }
139
140 pub fn delete(self: *Self, path: []const u8, action: Action, config: RC) void {
141 self.tryDelete(path, action, config) catch @panic("failed to create route");
142 }
143 pub fn tryDelete(self: *Self, path: []const u8, action: Action, config: RC) !void {
144 return self.addRoute(&self._delete, path, action, config);
145 }
146
147 pub fn options(self: *Self, path: []const u8, action: Action, config: RC) void {
148 self.tryOptions(path, action, config) catch @panic("failed to create route");
149 }
150 pub fn tryOptions(self: *Self, path: []const u8, action: Action, config: RC) !void {
151 return self.addRoute(&self._options, path, action, config);
152 }
153
154 pub fn connect(self: *Self, path: []const u8, action: Action, config: RC) void {
155 self.tryConnect(path, action, config) catch @panic("failed to create route");
156 }
157 pub fn tryConnect(self: *Self, path: []const u8, action: Action, config: RC) !void {
158 return self.addRoute(&self._connect, path, action, config);
159 }
160
161 pub fn method(self: *Self, m: []const u8, path: []const u8, action: Action, config: RC) void {
162 self.tryMethod(m, path, action, config) catch @panic("failed to create route");
163 }
164 pub fn tryMethod(self: *Self, m: []const u8, path: []const u8, action: Action, config: RC) !void {
165 const allocator = self._allocator;
166 const gop = try self._other.getOrPut(allocator, m);
167 if (gop.found_existing == false) {
168 gop.key_ptr.* = try allocator.dupe(u8, m);
169 gop.value_ptr.* = try P.init(allocator);
170 }
171 return self.addRoute(gop.value_ptr, path, action, config);
172 }
173
174 pub fn all(self: *Self, path: []const u8, action: Action, config: RC) void {
175 self.tryAll(path, action, config) catch @panic("failed to create route");
176 }
177 pub fn tryAll(self: *Self, path: []const u8, action: Action, config: RC) !void {
178 try self.tryGet(path, action, config);
179 try self.tryPut(path, action, config);
180 try self.tryPost(path, action, config);
181 try self.tryHead(path, action, config);
182 try self.tryPatch(path, action, config);
183 try self.tryTrace(path, action, config);
184 try self.tryDelete(path, action, config);
185 try self.tryOptions(path, action, config);
186 try self.tryConnect(path, action, config);
187 }
188
189 fn addRoute(self: *Self, root: *P, path: []const u8, action: Action, config: RC) !void {
190 const da = DispatchableAction{
191 .action = action,
192 .data = config.data,
193 .handler = config.handler orelse self.handler,
194 .dispatcher = config.dispatcher orelse self.dispatcher,
195 .middlewares = try self.mergeMiddleware(self.middlewares, config),
196 };
197
198 if (path.len == 0 or (path.len == 1 and path[0] == '/')) {
199 root.action = da;
200 return;
201 }
202
203 const allocator = self._allocator;
204
205 var normalized = path;
206 if (normalized[0] == '/') {
207 normalized = normalized[1..];
208 }
209 if (normalized[normalized.len - 1] == '/') {
210 normalized = normalized[0 .. normalized.len - 1];
211 }
212
213 var param_name_collector: std.ArrayList([]const u8) = .empty;
214 defer param_name_collector.deinit(allocator);
215
216 var route_part = root;
217 var it = std.mem.splitScalar(u8, normalized, '/');
218 while (it.next()) |part| {
219 if (part[0] == ':') {
220 try param_name_collector.append(allocator, part[1..]);
221 if (route_part.param_part) |child| {
222 route_part = child;
223 } else {
224 const child = try allocator.create(P);
225 child.clear(allocator);
226 route_part.param_part = child;
227 route_part = child;
228 }
229 } else if (part.len == 1 and part[0] == '*') {
230 // if this route_part didn't already have an action, then this glob also
231 // includes it
232 if (route_part.action == null) {
233 route_part.action = da;
234 }
235
236 if (route_part.glob) |child| {
237 route_part = child;
238 } else {
239 const child = try allocator.create(P);
240 child.clear(allocator);
241 route_part.glob = child;
242 route_part = child;
243 }
244 } else {
245 const gop = try route_part.parts.getOrPut(part);
246 if (gop.found_existing) {
247 route_part = gop.value_ptr;
248 } else {
249 route_part = gop.value_ptr;
250 route_part.clear(allocator);
251 }
252 }
253 }
254
255 const param_name_count = param_name_collector.items.len;
256 if (param_name_count > 0) {
257 const param_names = try allocator.alloc([]const u8, param_name_count);
258 for (param_name_collector.items, 0..) |name, i| {
259 param_names[i] = name;
260 }
261 route_part.param_names = param_names;
262 }
263
264 // if the route ended with a '*' (importantly, as opposed to a '*/') then
265 // this is a "glob all" route will. Important, use "url" and not "normalized"
266 // since normalized stripped out the trailing / (if any), which is important
267 // here
268 route_part.glob_all = path[path.len - 1] == '*';
269
270 route_part.action = da;
271 }
272
273 fn mergeMiddleware(self: *Self, parent_middlewares: []const httpz.Middleware(Handler), config: RC) ![]const httpz.Middleware(Handler) {
274 const route_middlewares = config.middlewares orelse return parent_middlewares;
275
276 const strategy = config.middleware_strategy orelse .append;
277 if (strategy == .replace or parent_middlewares.len == 0) {
278 return route_middlewares;
279 }
280
281 // allocator is an arena
282 const merged = try self._allocator.alloc(httpz.Middleware(Handler), route_middlewares.len + parent_middlewares.len);
283 @memcpy(merged[0..parent_middlewares.len], parent_middlewares);
284 @memcpy(merged[parent_middlewares.len..], route_middlewares);
285 return merged;
286 }
287 };
288}
289
290pub fn Group(comptime Handler: type, comptime Action: type) type {
291 const RC = RouteConfig(Handler, Action);
292 return struct {
293 _allocator: Allocator,
294 _prefix: []const u8,
295 _router: *Router(Handler, Action),
296 _config: RouteConfig(Handler, Action),
297
298 const Self = @This();
299
300 fn init(router: *Router(Handler, Action), prefix: []const u8, config: RC) Self {
301 return .{
302 ._prefix = prefix,
303 ._router = router,
304 ._config = config,
305 ._allocator = router._allocator,
306 };
307 }
308
309 pub fn get(self: *Self, path: []const u8, action: Action, override: RC) void {
310 self._router.get(self.createPath(path), action, self.mergeConfig(override));
311 }
312 pub fn tryGet(self: *Self, path: []const u8, action: Action, override: RC) !void {
313 return self._router.tryGet(self.tryCreatePath(path), action, self.tryMergeConfig(override));
314 }
315
316 pub fn put(self: *Self, path: []const u8, action: Action, override: RC) void {
317 self._router.put(self.createPath(path), action, self.mergeConfig(override));
318 }
319 pub fn tryPut(self: *Self, path: []const u8, action: Action, override: RC) !void {
320 return self._router.tryPut(self.tryCreatePath(path), action, self.tryMergeConfig(override));
321 }
322
323 pub fn post(self: *Self, path: []const u8, action: Action, override: RC) void {
324 self._router.post(self.createPath(path), action, self.mergeConfig(override));
325 }
326 pub fn tryPost(self: *Self, path: []const u8, action: Action, override: RC) !void {
327 return self._router.tryPost(self.tryCreatePath(path), action, self.tryMergeConfig(override));
328 }
329
330 pub fn head(self: *Self, path: []const u8, action: Action, override: RC) void {
331 self._router.head(self.createPath(path), action, self.mergeConfig(override));
332 }
333 pub fn tryHead(self: *Self, path: []const u8, action: Action, override: RC) !void {
334 return self._router.tryHead(self.tryCreatePath(path), action, self.tryMergeConfig(override));
335 }
336
337 pub fn patch(self: *Self, path: []const u8, action: Action, override: RC) void {
338 self._router.patch(self.createPath(path), action, self.mergeConfig(override));
339 }
340 pub fn tryPatch(self: *Self, path: []const u8, action: Action, override: RC) !void {
341 return self._router.tryPatch(self.tryCreatePath(path), action, self.tryMergeConfig(override));
342 }
343
344 pub fn trace(self: *Self, path: []const u8, action: Action, override: RC) void {
345 self._router.trace(self.createPath(path), action, self.mergeConfig(override));
346 }
347 pub fn tryTrace(self: *Self, path: []const u8, action: Action, override: RC) !void {
348 return self._router.tryTrace(self.tryCreatePath(path), action, self.tryMergeConfig(override));
349 }
350
351 pub fn delete(self: *Self, path: []const u8, action: Action, override: RC) void {
352 self._router.delete(self.createPath(path), action, self.mergeConfig(override));
353 }
354 pub fn tryDelete(self: *Self, path: []const u8, action: Action, override: RC) !void {
355 return self._router.tryDelete(self.tryCreatePath(path), action, self.tryMergeConfig(override));
356 }
357
358 pub fn options(self: *Self, path: []const u8, action: Action, override: RC) void {
359 self._router.options(self.createPath(path), action, self.mergeConfig(override));
360 }
361 pub fn tryOptions(self: *Self, path: []const u8, action: Action, override: RC) !void {
362 return self._router.tryOptions(self.tryCreatePath(path), action, self.tryMergeConfig(override));
363 }
364
365 pub fn connect(self: *Self, path: []const u8, action: Action, override: RC) void {
366 self._router.connect(self.createPath(path), action, self.mergeConfig(override));
367 }
368 pub fn tryConnect(self: *Self, path: []const u8, action: Action, override: RC) !void {
369 return self._router.tryConnect(self.tryCreatePath(path), action, self.tryMergeConfig(override));
370 }
371
372 pub fn all(self: *Self, path: []const u8, action: Action, override: RC) void {
373 self._router.all(self.createPath(path), action, self.mergeConfig(override));
374 }
375 pub fn tryAll(self: *Self, path: []const u8, action: Action, override: RC) !void {
376 return self._router.tryAll(self.tryCreatePath(path), action, self.tryMergeConfig(override));
377 }
378
379 pub fn method(self: *Self, m: []const u8, path: []const u8, action: Action, override: RC) void {
380 self._router.method(m, self.createPath(path), action, self.mergeConfig(override));
381 }
382 pub fn tryMethod(self: *Self, m: []const u8, path: []const u8, action: Action, override: RC) !void {
383 return self._router.tryMethod(m, self.tryCreatePath(path), action, self.tryMergeConfig(override));
384 }
385
386 fn createPath(self: *Self, path: []const u8) []const u8 {
387 return self.tryCreatePath(path) catch unreachable;
388 }
389
390 fn tryCreatePath(self: *Self, path: []const u8) ![]const u8 {
391 var prefix = self._prefix;
392 if (prefix.len == 0) {
393 return path;
394 }
395 if (path.len == 0) {
396 return prefix;
397 }
398
399 // prefix = /admin/
400 // path = /users/
401 // result ==> /admin/users/ NOT /admin//users/
402 if (prefix[prefix.len - 1] == '/' and path[0] == '/') {
403 prefix = prefix[0 .. prefix.len - 1];
404 }
405
406 const joined = try self._allocator.alloc(u8, prefix.len + path.len);
407 @memcpy(joined[0..prefix.len], prefix);
408 @memcpy(joined[prefix.len..], path);
409 return joined;
410 }
411
412 fn mergeConfig(self: *Self, override: RC) RC {
413 return self.tryMergeConfig(override) catch unreachable;
414 }
415
416 fn tryMergeConfig(self: *Self, override: RC) !RC {
417 return .{
418 .data = override.data orelse self._config.data,
419 .handler = override.handler orelse self._config.handler,
420 .dispatcher = override.dispatcher orelse self._config.dispatcher,
421 .middlewares = try self._router.mergeMiddleware(self._config.middlewares orelse &.{}, override),
422 .middleware_strategy = override.middleware_strategy orelse self._config.middleware_strategy,
423 };
424 }
425 };
426}
427
428pub fn Part(comptime A: type) type {
429 return struct {
430 action: ?A,
431 glob: ?*Part(A),
432 glob_all: bool,
433 param_part: ?*Part(A),
434 param_names: ?[][]const u8,
435 parts: StringHashMap(Part(A)),
436
437 const Self = @This();
438
439 pub fn init(allocator: Allocator) !Self {
440 return Self{
441 .glob = null,
442 .glob_all = false,
443 .action = null,
444 .param_part = null,
445 .param_names = null,
446 .parts = StringHashMap(Part(A)).init(allocator),
447 };
448 }
449
450 pub fn clear(self: *Self, allocator: Allocator) void {
451 self.glob = null;
452 self.glob_all = false;
453 self.action = null;
454 self.param_part = null;
455 self.param_names = null;
456 self.parts = StringHashMap(Part(A)).init(allocator);
457 }
458 };
459}
460
461fn getRoute(comptime A: type, root: *const Part(A), url: []const u8, params: *Params) ?*const A {
462 std.debug.assert(url.len != 0);
463 if (url.len == 1 and url[0] == '/') {
464 return if (root.action) |*a| a else null;
465 }
466
467 var normalized = url;
468 if (normalized[0] == '/') {
469 normalized = normalized[1..];
470 }
471 if (normalized[normalized.len - 1] == '/') {
472 normalized = normalized[0 .. normalized.len - 1];
473 }
474
475 var route_part = root;
476
477 var glob_all: ?*const Part(A) = null;
478 var pos: usize = 0;
479 while (pos < normalized.len) {
480 const index = std.mem.indexOfScalarPos(u8, normalized, pos, '/') orelse normalized.len;
481 const part = normalized[pos..index];
482
483 // the most specific "glob_all" route we find, which is the one most deeply
484 // nested, is the one we'll use in case there are no other matches.
485 if (route_part.glob_all) {
486 glob_all = route_part;
487 }
488
489 if (route_part.parts.getPtr(part)) |child| {
490 route_part = child;
491 } else if (route_part.param_part) |child| {
492 params.addValue(part);
493 route_part = child;
494 } else if (route_part.glob) |child| {
495 route_part = child;
496 } else {
497 params.len = 0;
498 if (glob_all) |fallback| {
499 return if (fallback.action) |*a| a else null;
500 }
501 return null;
502 }
503 pos = index + 1; // +1 tos skip the slash on the next iteration
504 }
505
506 if (route_part.action) |*action| {
507 if (route_part.param_names) |names| {
508 params.addNames(names);
509 } else {
510 params.len = 0;
511 }
512 return action;
513 }
514
515 params.len = 0;
516 if (glob_all) |fallback| {
517 return if (fallback.action) |*a| a else null;
518 }
519 return null;
520}
521
522const t = @import("t.zig");
523test "route: root" {
524 defer t.reset();
525
526 var params = try Params.init(t.arena.allocator(), 5);
527 var router = Router(void, httpz.Action(void)).init(t.arena.allocator(), testDispatcher1, {}) catch unreachable;
528
529 router.get("/", testRoute1, .{});
530 router.put("/", testRoute2, .{});
531 router.method("TEA", "/", testRoute3, .{});
532 router.all("/all", testRoute4, .{});
533
534 const urls = .{ "/", "/other", "/all" };
535
536 try t.expectEqual(&testRoute1, router.route(.GET, "", urls[0], ¶ms).?.action);
537 try t.expectEqual(&testRoute2, router.route(.PUT, "", urls[0], ¶ms).?.action);
538 try t.expectEqual(&testRoute3, router.route(.OTHER, "TEA", urls[0], ¶ms).?.action);
539
540 try t.expectEqual(null, router.route(.GET, "", urls[1], ¶ms));
541 try t.expectEqual(null, router.route(.DELETE, "", urls[0], ¶ms));
542 try t.expectEqual(null, router.route(.OTHER, "TEA", urls[1], ¶ms));
543
544 // test "all" route
545 inline for (@typeInfo(httpz.Method).@"enum".fields) |field| {
546 const m = @as(httpz.Method, @enumFromInt(field.value));
547 if (m == .OTHER) {
548 try t.expectEqual(null, router.route(m, "", urls[2], ¶ms));
549 } else {
550 try t.expectEqual(&testRoute4, router.route(m, "", urls[2], ¶ms).?.action);
551 }
552 }
553}
554
555test "route: static" {
556 defer t.reset();
557
558 var params = try Params.init(t.arena.allocator(), 5);
559 var router = Router(void, httpz.Action(void)).init(t.arena.allocator(), testDispatcher1, {}) catch unreachable;
560
561 router.get("hello/world", testRoute1, .{});
562 router.get("/over/9000/", testRoute2, .{});
563
564 {
565 const urls = .{ "hello/world", "/hello/world", "hello/world/", "/hello/world/" };
566 // all trailing/leading slash combinations
567 try t.expectEqual(&testRoute1, router.route(.GET, "", urls[0], ¶ms).?.action);
568 try t.expectEqual(&testRoute1, router.route(.GET, "", urls[1], ¶ms).?.action);
569 try t.expectEqual(&testRoute1, router.route(.GET, "", urls[2], ¶ms).?.action);
570 }
571
572 {
573 const urls = .{ "over/9000", "/over/9000", "over/9000/", "/over/9000/" };
574 // all trailing/leading slash combinations
575 inline for (urls) |url| {
576 try t.expectEqual(&testRoute2, router.route(.GET, "", url, ¶ms).?.action);
577
578 // different method
579 try t.expectEqual(null, router.route(.PUT, "", url, ¶ms));
580 }
581 }
582
583 {
584 // random not found
585 const urls = .{ "over/9000!", "over/ 9000" };
586 inline for (urls) |url| {
587 try t.expectEqual(null, router.route(.GET, "", url, ¶ms));
588 }
589 }
590}
591
592test "route: params" {
593 defer t.reset();
594
595 var params = try Params.init(t.arena.allocator(), 5);
596 var router = Router(void, httpz.Action(void)).init(t.arena.allocator(), testDispatcher1, {}) catch unreachable;
597
598 router.get("/:p1", testRoute1, .{});
599 router.get("/users/:p2", testRoute2, .{});
600 router.get("/users/:p2/fav", testRoute3, .{});
601 router.get("/users/:p2/like", testRoute4, .{});
602 router.get("/users/:p2/fav/:p3", testRoute5, .{});
603 router.get("/users/:p2/like/:p3", testRoute6, .{});
604
605 {
606 // root param
607 try t.expectEqual(&testRoute1, router.route(.GET, "", "info", ¶ms).?.action);
608 try t.expectEqual(1, params.len);
609 try t.expectString("info", params.get("p1").?);
610 }
611
612 {
613 // nested param
614 params.reset();
615 try t.expectEqual(&testRoute2, router.route(.GET, "", "/users/33", ¶ms).?.action);
616 try t.expectEqual(1, params.len);
617 try t.expectString("33", params.get("p2").?);
618 }
619
620 {
621 // nested param with statix suffix
622 params.reset();
623 try t.expectEqual(&testRoute3, router.route(.GET, "", "/users/9/fav", ¶ms).?.action);
624 try t.expectEqual(1, params.len);
625 try t.expectString("9", params.get("p2").?);
626
627 params.reset();
628 try t.expectEqual(&testRoute4, router.route(.GET, "", "/users/9/like", ¶ms).?.action);
629 try t.expectEqual(1, params.len);
630 try t.expectString("9", params.get("p2").?);
631 }
632
633 {
634 // nested params
635 params.reset();
636 try t.expectEqual(&testRoute5, router.route(.GET, "", "/users/u1/fav/blue", ¶ms).?.action);
637 try t.expectEqual(2, params.len);
638 try t.expectString("u1", params.get("p2").?);
639 try t.expectString("blue", params.get("p3").?);
640
641 params.reset();
642 try t.expectEqual(&testRoute6, router.route(.GET, "", "/users/u3/like/tea", ¶ms).?.action);
643 try t.expectEqual(2, params.len);
644 try t.expectString("u3", params.get("p2").?);
645 try t.expectString("tea", params.get("p3").?);
646 }
647
648 {
649 // not_found
650 params.reset();
651 try t.expectEqual(null, router.route(.GET, "", "/users/u1/other", ¶ms));
652 try t.expectEqual(0, params.len);
653
654 try t.expectEqual(null, router.route(.GET, "", "/users/u1/favss/blue", ¶ms));
655 try t.expectEqual(0, params.len);
656 }
657}
658
659test "route: glob" {
660 defer t.reset();
661
662 var params = try Params.init(t.arena.allocator(), 5);
663 var router = Router(void, httpz.Action(void)).init(t.arena.allocator(), testDispatcher1, {}) catch unreachable;
664
665 router.get("/*", testRoute1, .{});
666 router.get("/users/*", testRoute2, .{});
667 router.get("/users/*/test", testRoute3, .{});
668 router.get("/users/other/test", testRoute4, .{});
669
670 {
671 // root glob
672 const urls = .{ "/anything", "/this/could/be/anything", "/" };
673 inline for (urls) |url| {
674 try t.expectEqual(&testRoute1, router.route(.GET, "", url, ¶ms).?.action);
675 try t.expectEqual(0, params.len);
676 }
677 }
678
679 {
680 // nest glob
681 const urls = .{ "/users/", "/users", "/users/hello", "/users/could/be/anything" };
682 inline for (urls) |url| {
683 try t.expectEqual(&testRoute2, router.route(.GET, "", url, ¶ms).?.action);
684 try t.expectEqual(0, params.len);
685 }
686 }
687
688 {
689 // nest glob specific
690 const urls = .{ "/users/hello/test", "/users/x/test" };
691 inline for (urls) |url| {
692 try t.expectEqual(&testRoute3, router.route(.GET, "", url, ¶ms).?.action);
693 try t.expectEqual(0, params.len);
694 }
695 }
696
697 {
698 // nest glob specific
699 try t.expectEqual(&testRoute4, router.route(.GET, "", "/users/other/test", ¶ms).?.action);
700 try t.expectEqual(0, params.len);
701 }
702}
703
704test "route: middlewares no global" {
705 defer t.reset();
706
707 const m1 = fakeMiddleware(&.{ .id = 1 });
708 const m2 = fakeMiddleware(&.{ .id = 2 });
709 const m3 = fakeMiddleware(&.{ .id = 3 });
710
711 var params = try Params.init(t.arena.allocator(), 5);
712 var router = Router(void, httpz.Action(void)).init(t.arena.allocator(), testDispatcher1, {}) catch unreachable;
713
714 {
715 router.get("/1", testRoute1, .{});
716 router.get("/2", testRoute1, .{ .middlewares = &.{m1} });
717 router.get("/3", testRoute1, .{ .middlewares = &.{m1}, .middleware_strategy = .replace });
718 router.get("/4", testRoute1, .{ .middlewares = &.{ m1, m2 } });
719 router.get("/5", testRoute1, .{ .middlewares = &.{ m1, m2 }, .middleware_strategy = .replace });
720
721 try assertMiddlewares(&router, ¶ms, "/1", &.{});
722 try assertMiddlewares(&router, ¶ms, "/2", &.{1});
723 try assertMiddlewares(&router, ¶ms, "/3", &.{1});
724 try assertMiddlewares(&router, ¶ms, "/4", &.{ 1, 2 });
725 try assertMiddlewares(&router, ¶ms, "/5", &.{ 1, 2 });
726 }
727
728 {
729 // group with no group-level middleware
730 var group = router.group("/g1", .{});
731 group.get("/1", testRoute1, .{});
732 group.get("/2", testRoute1, .{ .middlewares = &.{m1} });
733 group.get("/3", testRoute1, .{ .middlewares = &.{m1}, .middleware_strategy = .append });
734 group.get("/4", testRoute1, .{ .middlewares = &.{ m1, m2 }, .middleware_strategy = .replace });
735
736 try assertMiddlewares(&router, ¶ms, "/g1/1", &.{});
737 try assertMiddlewares(&router, ¶ms, "/g1/2", &.{1});
738 try assertMiddlewares(&router, ¶ms, "/g1/3", &.{1});
739 try assertMiddlewares(&router, ¶ms, "/g1/4", &.{ 1, 2 });
740 }
741
742 {
743 // group with group-level middleware
744 var group = router.group("/g2", .{ .middlewares = &.{m1} });
745 group.get("/1", testRoute1, .{});
746 group.get("/2", testRoute1, .{ .middlewares = &.{m2} });
747 group.get("/3", testRoute1, .{ .middlewares = &.{ m2, m3 }, .middleware_strategy = .append });
748 group.get("/4", testRoute1, .{ .middlewares = &.{ m2, m3 }, .middleware_strategy = .replace });
749
750 try assertMiddlewares(&router, ¶ms, "/g2/1", &.{1});
751 try assertMiddlewares(&router, ¶ms, "/g2/2", &.{ 1, 2 });
752 try assertMiddlewares(&router, ¶ms, "/g2/3", &.{ 1, 2, 3 });
753 try assertMiddlewares(&router, ¶ms, "/g2/4", &.{ 2, 3 });
754 }
755}
756
757test "route: middlewares with global" {
758 defer t.reset();
759
760 const m1 = fakeMiddleware(&.{ .id = 1 });
761 const m2 = fakeMiddleware(&.{ .id = 2 });
762 const m3 = fakeMiddleware(&.{ .id = 3 });
763 const m4 = fakeMiddleware(&.{ .id = 4 });
764 const m5 = fakeMiddleware(&.{ .id = 5 });
765
766 var params = try Params.init(t.arena.allocator(), 5);
767 var router = Router(void, httpz.Action(void)).init(t.arena.allocator(), testDispatcher1, {}) catch unreachable;
768 router.middlewares = &.{ m4, m5 };
769
770 {
771 router.get("/1", testRoute1, .{});
772 router.get("/2", testRoute1, .{ .middlewares = &.{m1} });
773 router.get("/3", testRoute1, .{ .middlewares = &.{m1}, .middleware_strategy = .replace });
774 router.get("/4", testRoute1, .{ .middlewares = &.{ m1, m2 } });
775 router.get("/5", testRoute1, .{ .middlewares = &.{ m1, m2 }, .middleware_strategy = .replace });
776
777 try assertMiddlewares(&router, ¶ms, "/1", &.{ 4, 5 });
778 try assertMiddlewares(&router, ¶ms, "/2", &.{ 4, 5, 1 });
779 try assertMiddlewares(&router, ¶ms, "/3", &.{1});
780 try assertMiddlewares(&router, ¶ms, "/4", &.{ 4, 5, 1, 2 });
781 try assertMiddlewares(&router, ¶ms, "/5", &.{ 1, 2 });
782 }
783
784 {
785 // group with no group-level middleware
786 var group = router.group("/g1", .{});
787 group.get("/1", testRoute1, .{});
788 group.get("/2", testRoute1, .{ .middlewares = &.{m1} });
789 group.get("/3", testRoute1, .{ .middlewares = &.{ m1, m2 }, .middleware_strategy = .append });
790 group.get("/4", testRoute1, .{ .middlewares = &.{ m1, m2 }, .middleware_strategy = .replace });
791
792 try assertMiddlewares(&router, ¶ms, "/g1/1", &.{ 4, 5 });
793 try assertMiddlewares(&router, ¶ms, "/g1/2", &.{ 4, 5, 1 });
794 try assertMiddlewares(&router, ¶ms, "/g1/3", &.{ 4, 5, 1, 2 });
795 try assertMiddlewares(&router, ¶ms, "/g1/4", &.{ 1, 2 });
796 }
797
798 {
799 // group with appended group-level middleware
800 var group = router.group("/g2", .{ .middlewares = &.{m1}, .middleware_strategy = .append });
801 group.get("/1", testRoute1, .{});
802 group.get("/2", testRoute1, .{ .middlewares = &.{m2} });
803 group.get("/3", testRoute1, .{ .middlewares = &.{ m2, m3 }, .middleware_strategy = .append });
804 group.get("/4", testRoute1, .{ .middlewares = &.{ m2, m3 }, .middleware_strategy = .replace });
805
806 try assertMiddlewares(&router, ¶ms, "/g2/1", &.{ 4, 5, 1 });
807 try assertMiddlewares(&router, ¶ms, "/g2/2", &.{ 4, 5, 1, 2 });
808 try assertMiddlewares(&router, ¶ms, "/g2/3", &.{ 4, 5, 1, 2, 3 });
809 try assertMiddlewares(&router, ¶ms, "/g2/4", &.{ 2, 3 });
810 }
811
812 {
813 // group with replace group-level middleware
814 var group = router.group("/g2", .{ .middlewares = &.{m1}, .middleware_strategy = .replace });
815 group.get("/1", testRoute1, .{});
816 group.get("/2", testRoute1, .{ .middlewares = &.{m2} });
817 group.get("/3", testRoute1, .{ .middlewares = &.{ m2, m3 }, .middleware_strategy = .append });
818 group.get("/4", testRoute1, .{ .middlewares = &.{ m2, m3 }, .middleware_strategy = .replace });
819
820 try assertMiddlewares(&router, ¶ms, "/g2/1", &.{1});
821 try assertMiddlewares(&router, ¶ms, "/g2/2", &.{ 1, 2 });
822 try assertMiddlewares(&router, ¶ms, "/g2/3", &.{ 4, 5, 1, 2, 3 });
823 try assertMiddlewares(&router, ¶ms, "/g2/4", &.{ 2, 3 });
824 }
825}
826
827fn assertMiddlewares(router: anytype, params: *Params, path: []const u8, expected: []const u32) !void {
828 const middlewares = router.route(.GET, "", path, params).?.middlewares;
829 try t.expectEqual(expected.len, middlewares.len);
830 for (expected, middlewares) |e, m| {
831 const impl: *const FakeMiddlewareImpl = @ptrCast(@alignCast(m.ptr));
832 try t.expectEqual(e, impl.id);
833 }
834}
835
836fn fakeMiddleware(impl: *const FakeMiddlewareImpl) httpz.Middleware(void) {
837 return .{
838 .ptr = @constCast(impl),
839 .deinitFn = undefined,
840 .executeFn = undefined,
841 };
842}
843
844const FakeMiddlewareImpl = struct {
845 id: u32,
846};
847
848// TODO: this functionality isn't implemented because I can't think of a way
849// to do it which isn't relatively expensive (e.g. recursively or keeping a
850// stack of (a) parts and (b) url segments and trying to rematch every possible
851// combination of param + part)...and I don't know that this use-case is really
852// common.
853
854// test "route: ambiguous params" {
855// var params = try Params.init(t.allocator, 5);
856// defer params.deinit();
857
858// var router = Router(u32).init(t.allocator, 9999999) catch unreachable;
859// defer router.deinit();
860// router.get("/:any/users", 1, .{});
861// router.get("/hello/users/test", 2, .{});
862
863// {
864// try t.expectEqual(1, router.route(.GET, "/x/users", ¶ms));
865// try t.expectEqual(1, params.len);
866// try t.expectString("x", params.get("any"));
867
868// params.reset();
869// try t.expectEqual(2, router.route(.GET, "/hello/users/test", ¶ms));
870// try t.expectEqual(0, params.len);
871
872// params.reset();
873// try t.expectEqual(1, router.route(.GET, "/hello/users", ¶ms));
874// try t.expectEqual(1, params.len);
875// try t.expectString("hello", params.get("any"));
876// }
877// }
878
879fn testDispatcher1(_: httpz.Action(void), _: *Request, _: *Response) anyerror!void {}
880fn testRoute1(_: *Request, _: *Response) anyerror!void {}
881fn testRoute2(_: *Request, _: *Response) anyerror!void {}
882fn testRoute3(_: *Request, _: *Response) anyerror!void {}
883fn testRoute4(_: *Request, _: *Response) anyerror!void {}
884fn testRoute5(_: *Request, _: *Response) anyerror!void {}
885fn testRoute6(_: *Request, _: *Response) anyerror!void {}