An HTTP/1.1 server for zig
0

Configure Feed

Select the types of activity you want to include in your feed.

http.zig / src / router.zig
37 kB 885 lines
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], &params).?.action); 537 try t.expectEqual(&testRoute2, router.route(.PUT, "", urls[0], &params).?.action); 538 try t.expectEqual(&testRoute3, router.route(.OTHER, "TEA", urls[0], &params).?.action); 539 540 try t.expectEqual(null, router.route(.GET, "", urls[1], &params)); 541 try t.expectEqual(null, router.route(.DELETE, "", urls[0], &params)); 542 try t.expectEqual(null, router.route(.OTHER, "TEA", urls[1], &params)); 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], &params)); 549 } else { 550 try t.expectEqual(&testRoute4, router.route(m, "", urls[2], &params).?.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], &params).?.action); 568 try t.expectEqual(&testRoute1, router.route(.GET, "", urls[1], &params).?.action); 569 try t.expectEqual(&testRoute1, router.route(.GET, "", urls[2], &params).?.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, &params).?.action); 577 578 // different method 579 try t.expectEqual(null, router.route(.PUT, "", url, &params)); 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, &params)); 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", &params).?.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", &params).?.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", &params).?.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", &params).?.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", &params).?.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", &params).?.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", &params)); 652 try t.expectEqual(0, params.len); 653 654 try t.expectEqual(null, router.route(.GET, "", "/users/u1/favss/blue", &params)); 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, &params).?.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, &params).?.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, &params).?.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", &params).?.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, &params, "/1", &.{}); 722 try assertMiddlewares(&router, &params, "/2", &.{1}); 723 try assertMiddlewares(&router, &params, "/3", &.{1}); 724 try assertMiddlewares(&router, &params, "/4", &.{ 1, 2 }); 725 try assertMiddlewares(&router, &params, "/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, &params, "/g1/1", &.{}); 737 try assertMiddlewares(&router, &params, "/g1/2", &.{1}); 738 try assertMiddlewares(&router, &params, "/g1/3", &.{1}); 739 try assertMiddlewares(&router, &params, "/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, &params, "/g2/1", &.{1}); 751 try assertMiddlewares(&router, &params, "/g2/2", &.{ 1, 2 }); 752 try assertMiddlewares(&router, &params, "/g2/3", &.{ 1, 2, 3 }); 753 try assertMiddlewares(&router, &params, "/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, &params, "/1", &.{ 4, 5 }); 778 try assertMiddlewares(&router, &params, "/2", &.{ 4, 5, 1 }); 779 try assertMiddlewares(&router, &params, "/3", &.{1}); 780 try assertMiddlewares(&router, &params, "/4", &.{ 4, 5, 1, 2 }); 781 try assertMiddlewares(&router, &params, "/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, &params, "/g1/1", &.{ 4, 5 }); 793 try assertMiddlewares(&router, &params, "/g1/2", &.{ 4, 5, 1 }); 794 try assertMiddlewares(&router, &params, "/g1/3", &.{ 4, 5, 1, 2 }); 795 try assertMiddlewares(&router, &params, "/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, &params, "/g2/1", &.{ 4, 5, 1 }); 807 try assertMiddlewares(&router, &params, "/g2/2", &.{ 4, 5, 1, 2 }); 808 try assertMiddlewares(&router, &params, "/g2/3", &.{ 4, 5, 1, 2, 3 }); 809 try assertMiddlewares(&router, &params, "/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, &params, "/g2/1", &.{1}); 821 try assertMiddlewares(&router, &params, "/g2/2", &.{ 1, 2 }); 822 try assertMiddlewares(&router, &params, "/g2/3", &.{ 4, 5, 1, 2, 3 }); 823 try assertMiddlewares(&router, &params, "/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", &params)); 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", &params)); 870// try t.expectEqual(0, params.len); 871 872// params.reset(); 873// try t.expectEqual(1, router.route(.GET, "/hello/users", &params)); 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 {}