From f58e0c074c032d0c17af00e929735acdbf8db2ad Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sat, 9 Jul 2022 19:56:07 -0700 Subject: [PATCH] Make routing more obviously WIP --- src/http/lib.zig | 6 +- src/http/routing.zig | 134 ++++++++++++++++++++++--------------------- 2 files changed, 75 insertions(+), 65 deletions(-) diff --git a/src/http/lib.zig b/src/http/lib.zig index dee4c69..c64db18 100644 --- a/src/http/lib.zig +++ b/src/http/lib.zig @@ -12,7 +12,11 @@ pub const Status = std.http.Status; pub const Request = request.Request; pub const Server = server.Server; -//pub const Router = routing.Router(Context); +// TODO: rework routing +pub fn Router(comptime ServerContext: type) type { + return routing.Router(ServerContext, *server.Context); +} +pub const RouteArgs = routing.RouteArgs; pub const Headers = std.HashMap([]const u8, []const u8, struct { pub fn eql(_: @This(), a: []const u8, b: []const u8) bool { diff --git a/src/http/routing.zig b/src/http/routing.zig index c6824b9..3b7a84e 100644 --- a/src/http/routing.zig +++ b/src/http/routing.zig @@ -3,13 +3,13 @@ const util = @import("util"); const http = @import("./lib.zig"); -const Args = struct { +pub const RouteArgs = struct { const max_args = 16; names: [max_args]?[]const u8 = [_]?[]const u8{null} ** max_args, values: [max_args]?[]const u8 = [_]?[]const u8{null} ** max_args, - pub fn get(self: Args, name: []const u8) ?[]const u8 { + pub fn get(self: RouteArgs, name: []const u8) ?[]const u8 { for (self.names) |arg_name, i| { if (arg_name == null) return null; @@ -20,14 +20,16 @@ const Args = struct { } }; -pub fn Router(comptime Context: type) type { +pub fn Router(comptime ServerContext: type, comptime RequestContext: type) type { return struct { const Self = @This(); routes: []const Route, - pub const Handler = fn (Context, Args) anyerror!void; + pub const Handler = fn (ServerContext, RequestContext, RouteArgs) anyerror!void; pub const Route = struct { + pub const Args = RouteArgs; + method: http.Method, path: []const u8, path_segments: []const RouteSegment, @@ -46,18 +48,18 @@ pub fn Router(comptime Context: type) type { if (seg == .param) param_count += 1; } - if (param_count > Args.max_args) @panic("Too many params"); + if (param_count > RouteArgs.max_args) @panic("Too many params"); return result; } - fn dispatch(self: Route, ctx: Context, req_method: std.http.Method, req_path: []const u8) anyerror!void { - if (req_method != self.method) return error.RouteNotApplicable; + fn dispatch(route: Route, sctx: ServerContext, rctx: RequestContext, req_method: std.http.Method, req_path: []const u8) anyerror!void { + if (req_method != route.method) return error.RouteNotApplicable; - var args = Args{}; + var args = RouteArgs{}; var arg_count: usize = 0; var req_segments = util.PathIter.from(req_path); - for (self.path_segments) |seg| { + for (route.path_segments) |seg| { const req_seg = req_segments.next() orelse return error.RouteNotApplicable; switch (seg) { .literal => |literal| { @@ -72,13 +74,13 @@ pub fn Router(comptime Context: type) type { if (req_segments.next() != null) return error.RouteNotApplicable; - return self.handler(ctx, args); + return route.handler(sctx, rctx, args); } }; - pub fn dispatch(self: Self, ctx: Context, method: std.http.Method, path: []const u8) anyerror!void { + pub fn dispatch(self: Self, sctx: ServerContext, rctx: RequestContext, method: std.http.Method, path: []const u8) anyerror!void { for (self.routes) |r| { - r.dispatch(ctx, method, path) catch |err| switch (err) { + r.dispatch(sctx, rctx, method, path) catch |err| switch (err) { error.RouteNotApplicable => continue, else => return err, }; @@ -134,11 +136,11 @@ const _test = struct { var args_type: type = undefined; switch (@typeInfo(@TypeOf(next))) { .Fn => |func| { - if (func.args.len != 2) @compileError("next() must take 2 arguments"); + if (func.args.len != 3) @compileError("next() must take 3 arguments"); ctx_type = func.args[0].arg_type.?; - args_type = func.args[1].arg_type.?; - //if (@typeInfo(Args) != .Struct) @compileError("second argument to next() must be struct"); + args_type = func.args[2].arg_type.?; + //if (@typeInfo(RouteArgs) != .Struct) @compileError("second argument to next() must be struct"); }, else => @compileError("next must be function"), } @@ -148,19 +150,22 @@ const _test = struct { return struct { var calls: u32 = 0; - var last_ctx: ?Context = null; - var last_args: ?Args = null; + var last_rctx: ?Context = null; + var last_sctx: ?Context = null; + var last_args: ?RouteArgs = null; - fn func(ctx: Context, args: Args) !void { + fn func(sctx: Context, rctx: Context, args: RouteArgs) !void { calls += 1; - last_ctx = ctx; + last_sctx = sctx; + last_rctx = rctx; last_args = args; - return next(ctx, args); + return next(sctx, rctx, args); } - fn expectCalledOnceWith(exp_ctx: Context, exp_args: Args) !void { + fn expectCalledOnceWith(exp_sctx: Context, exp_rctx: Context, exp_args: RouteArgs) !void { try std.testing.expectEqual(@as(u32, 1), calls); - try std.testing.expectEqual(exp_ctx, last_ctx.?); + try std.testing.expectEqual(exp_sctx, last_sctx.?); + try std.testing.expectEqual(exp_rctx, last_rctx.?); for (exp_args.names) |exp_name, i| { if (exp_name == null) { try std.testing.expectEqual(@as(?[]const u8, null), last_args.?.names[i]); @@ -178,27 +183,28 @@ const _test = struct { fn reset() void { calls = 0; - last_ctx = null; + last_sctx = null; + last_rctx = null; last_args = null; } }; } const TestContext = u32; - fn dummyHandler(_: TestContext, _: Args) anyerror!void {} + fn dummyHandler(_: TestContext, _: TestContext, _: RouteArgs) anyerror!void {} }; test "route(T, ...) basic" { const mock_a = _test.CallTracker(.{}, _test.dummyHandler); - const MyRouter = Router(_test.TestContext); + const MyRouter = Router(_test.TestContext, _test.TestContext); const MyRoute = MyRouter.Route; const my_router = MyRouter{ .routes = &[_]MyRoute{ MyRoute.new(.GET, "/a", mock_a.func), } }; - _ = try my_router.dispatch(10, .GET, "/a"); - try mock_a.expectCalledOnceWith(10, .{}); + _ = try my_router.dispatch(10, 100, .GET, "/a"); + try mock_a.expectCalledOnceWith(10, 100, .{}); mock_a.reset(); } @@ -206,7 +212,7 @@ test "Route(T) matches correct route from multiple routes" { const mock_a = _test.CallTracker(.{}, _test.dummyHandler); const mock_b = _test.CallTracker(.{}, _test.dummyHandler); - const MyRouter = Router(_test.TestContext); + const MyRouter = Router(_test.TestContext, _test.TestContext); const MyRoute = MyRouter.Route; const my_router = MyRouter{ .routes = &[_]MyRoute{ @@ -214,33 +220,33 @@ test "Route(T) matches correct route from multiple routes" { MyRoute.new(.GET, "/b", mock_b.func), } }; - _ = try my_router.dispatch(10, .GET, "/a"); - try mock_a.expectCalledOnceWith(10, .{}); + _ = try my_router.dispatch(10, 100, .GET, "/a"); + try mock_a.expectCalledOnceWith(10, 100, .{}); try mock_b.expectNotCalled(); mock_a.reset(); - _ = try my_router.dispatch(10, .GET, "/b"); + _ = try my_router.dispatch(10, 100, .GET, "/b"); try mock_a.expectNotCalled(); - try mock_b.expectCalledOnceWith(10, .{}); + try mock_b.expectCalledOnceWith(10, 100, .{}); mock_b.reset(); } test "Route(T) passes correct context" { const mock_a = _test.CallTracker(.{}, _test.dummyHandler); - const MyRouter = Router(_test.TestContext); + const MyRouter = Router(_test.TestContext, _test.TestContext); const MyRoute = MyRouter.Route; const my_router = MyRouter{ .routes = &[_]MyRoute{ MyRoute.new(.GET, "/a", mock_a.func), } }; - _ = try my_router.dispatch(10, .GET, "/a"); - try mock_a.expectCalledOnceWith(10, .{}); + _ = try my_router.dispatch(10, 100, .GET, "/a"); + try mock_a.expectCalledOnceWith(10, 100, .{}); mock_a.reset(); - _ = try my_router.dispatch(16, .GET, "/a"); - try mock_a.expectCalledOnceWith(16, .{}); + _ = try my_router.dispatch(16, 32, .GET, "/a"); + try mock_a.expectCalledOnceWith(16, 32, .{}); mock_a.reset(); } @@ -248,7 +254,7 @@ test "Route(T) errors on no matching route" { const mock_a = _test.CallTracker(.{}, _test.dummyHandler); const mock_b = _test.CallTracker(.{}, _test.dummyHandler); - const MyRouter = Router(_test.TestContext); + const MyRouter = Router(_test.TestContext, _test.TestContext); const MyRoute = MyRouter.Route; const my_router = MyRouter{ .routes = &[_]MyRoute{ @@ -256,25 +262,25 @@ test "Route(T) errors on no matching route" { MyRoute.new(.GET, "/b", mock_b.func), } }; - try std.testing.expectError(error.RouteNotApplicable, my_router.dispatch(0, .GET, "/c")); + try std.testing.expectError(error.RouteNotApplicable, my_router.dispatch(10, 100, .GET, "/c")); try mock_a.expectNotCalled(); try mock_b.expectNotCalled(); } test "route(T) with no routes" { - const MyRouter = Router(_test.TestContext); + const MyRouter = Router(_test.TestContext, _test.TestContext); const MyRoute = MyRouter.Route; const my_router = MyRouter{ .routes = &[_]MyRoute{} }; - try std.testing.expectError(error.RouteNotApplicable, my_router.dispatch(0, .GET, "/c")); + try std.testing.expectError(error.RouteNotApplicable, my_router.dispatch(10, 100, .GET, "/c")); } test "route(T, ...) same path different methods" { const mock_get = _test.CallTracker(.{}, _test.dummyHandler); const mock_post = _test.CallTracker(.{}, _test.dummyHandler); - const MyRouter = Router(_test.TestContext); + const MyRouter = Router(_test.TestContext, _test.TestContext); const MyRoute = MyRouter.Route; const my_router = MyRouter{ .routes = &[_]MyRoute{ @@ -282,21 +288,21 @@ test "route(T, ...) same path different methods" { MyRoute.new(.POST, "/a", mock_post.func), } }; - _ = try my_router.dispatch(10, .GET, "/a"); - try mock_get.expectCalledOnceWith(10, .{}); + _ = try my_router.dispatch(10, 100, .GET, "/a"); + try mock_get.expectCalledOnceWith(10, 100, .{}); try mock_post.expectNotCalled(); mock_get.reset(); - _ = try my_router.dispatch(10, .POST, "/a"); + _ = try my_router.dispatch(10, 100, .POST, "/a"); try mock_get.expectNotCalled(); - try mock_post.expectCalledOnceWith(10, .{}); + try mock_post.expectCalledOnceWith(10, 100, .{}); } test "route(T, ...) route under subpath" { const mock_a = _test.CallTracker(.{}, _test.dummyHandler); const mock_b = _test.CallTracker(.{}, _test.dummyHandler); - const MyRouter = Router(_test.TestContext); + const MyRouter = Router(_test.TestContext, _test.TestContext); const MyRoute = MyRouter.Route; const my_router = MyRouter{ .routes = &[_]MyRoute{ @@ -304,56 +310,56 @@ test "route(T, ...) route under subpath" { MyRoute.new(.GET, "/a/b", mock_b.func), } }; - _ = try my_router.dispatch(10, .GET, "/a"); - try mock_a.expectCalledOnceWith(10, .{}); + _ = try my_router.dispatch(10, 100, .GET, "/a"); + try mock_a.expectCalledOnceWith(10, 100, .{}); try mock_b.expectNotCalled(); mock_a.reset(); - _ = try my_router.dispatch(10, .GET, "/a/b"); + _ = try my_router.dispatch(10, 100, .GET, "/a/b"); try mock_a.expectNotCalled(); - try mock_b.expectCalledOnceWith(10, .{}); + try mock_b.expectCalledOnceWith(10, 100, .{}); } test "route(T, ...) case-insensitive route" { const mock_a = _test.CallTracker(.{}, _test.dummyHandler); - const MyRouter = Router(_test.TestContext); + const MyRouter = Router(_test.TestContext, _test.TestContext); const MyRoute = MyRouter.Route; const my_router = MyRouter{ .routes = &[_]MyRoute{ MyRoute.new(.GET, "/test/a", mock_a.func), } }; - _ = try my_router.dispatch(10, .GET, "/TEST/A"); - try mock_a.expectCalledOnceWith(10, .{}); + _ = try my_router.dispatch(10, 100, .GET, "/TEST/A"); + try mock_a.expectCalledOnceWith(10, 100, .{}); mock_a.reset(); - _ = try my_router.dispatch(10, .GET, "/TesT/a"); - try mock_a.expectCalledOnceWith(10, .{}); + _ = try my_router.dispatch(10, 100, .GET, "/TesT/a"); + try mock_a.expectCalledOnceWith(10, 100, .{}); } test "route(T, ...) redundant /" { const mock_a = _test.CallTracker(.{}, _test.dummyHandler); - const MyRouter = Router(_test.TestContext); + const MyRouter = Router(_test.TestContext, _test.TestContext); const MyRoute = MyRouter.Route; const my_router = MyRouter{ .routes = &[_]MyRoute{ MyRoute.new(.GET, "/test/a", mock_a.func), } }; - _ = try my_router.dispatch(10, .GET, "/test//a"); - try mock_a.expectCalledOnceWith(10, .{}); + _ = try my_router.dispatch(10, 100, .GET, "/test//a"); + try mock_a.expectCalledOnceWith(10, 100, .{}); mock_a.reset(); - _ = try my_router.dispatch(10, .GET, "//test///////////a////"); - try mock_a.expectCalledOnceWith(10, .{}); + _ = try my_router.dispatch(10, 100, .GET, "//test///////////a////"); + try mock_a.expectCalledOnceWith(10, 100, .{}); mock_a.reset(); - _ = try my_router.dispatch(10, .GET, "test/a"); - try mock_a.expectCalledOnceWith(10, .{}); + _ = try my_router.dispatch(10, 100, .GET, "test/a"); + try mock_a.expectCalledOnceWith(10, 100, .{}); mock_a.reset(); - try std.testing.expectError(error.RouteNotApplicable, my_router.dispatch(10, .GET, "/te/st/a")); + try std.testing.expectError(error.RouteNotApplicable, my_router.dispatch(10, 100, .GET, "/te/st/a")); try mock_a.expectNotCalled(); }