Make routing more obviously WIP

This commit is contained in:
jaina heartles 2022-07-09 19:56:07 -07:00
parent 5fc8b99051
commit f58e0c074c
2 changed files with 75 additions and 65 deletions

View file

@ -12,7 +12,11 @@ pub const Status = std.http.Status;
pub const Request = request.Request; pub const Request = request.Request;
pub const Server = server.Server; 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 const Headers = std.HashMap([]const u8, []const u8, struct {
pub fn eql(_: @This(), a: []const u8, b: []const u8) bool { pub fn eql(_: @This(), a: []const u8, b: []const u8) bool {

View file

@ -3,13 +3,13 @@ const util = @import("util");
const http = @import("./lib.zig"); const http = @import("./lib.zig");
const Args = struct { pub const RouteArgs = struct {
const max_args = 16; const max_args = 16;
names: [max_args]?[]const u8 = [_]?[]const u8{null} ** max_args, names: [max_args]?[]const u8 = [_]?[]const u8{null} ** max_args,
values: [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| { for (self.names) |arg_name, i| {
if (arg_name == null) return null; 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 { return struct {
const Self = @This(); const Self = @This();
routes: []const Route, 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 Route = struct {
pub const Args = RouteArgs;
method: http.Method, method: http.Method,
path: []const u8, path: []const u8,
path_segments: []const RouteSegment, path_segments: []const RouteSegment,
@ -46,18 +48,18 @@ pub fn Router(comptime Context: type) type {
if (seg == .param) param_count += 1; 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; return result;
} }
fn dispatch(self: Route, ctx: Context, req_method: std.http.Method, req_path: []const u8) anyerror!void { fn dispatch(route: Route, sctx: ServerContext, rctx: RequestContext, req_method: std.http.Method, req_path: []const u8) anyerror!void {
if (req_method != self.method) return error.RouteNotApplicable; if (req_method != route.method) return error.RouteNotApplicable;
var args = Args{}; var args = RouteArgs{};
var arg_count: usize = 0; var arg_count: usize = 0;
var req_segments = util.PathIter.from(req_path); 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; const req_seg = req_segments.next() orelse return error.RouteNotApplicable;
switch (seg) { switch (seg) {
.literal => |literal| { .literal => |literal| {
@ -72,13 +74,13 @@ pub fn Router(comptime Context: type) type {
if (req_segments.next() != null) return error.RouteNotApplicable; 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| { 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, error.RouteNotApplicable => continue,
else => return err, else => return err,
}; };
@ -134,11 +136,11 @@ const _test = struct {
var args_type: type = undefined; var args_type: type = undefined;
switch (@typeInfo(@TypeOf(next))) { switch (@typeInfo(@TypeOf(next))) {
.Fn => |func| { .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.?; ctx_type = func.args[0].arg_type.?;
args_type = func.args[1].arg_type.?; args_type = func.args[2].arg_type.?;
//if (@typeInfo(Args) != .Struct) @compileError("second argument to next() must be struct"); //if (@typeInfo(RouteArgs) != .Struct) @compileError("second argument to next() must be struct");
}, },
else => @compileError("next must be function"), else => @compileError("next must be function"),
} }
@ -148,19 +150,22 @@ const _test = struct {
return struct { return struct {
var calls: u32 = 0; var calls: u32 = 0;
var last_ctx: ?Context = null; var last_rctx: ?Context = null;
var last_args: ?Args = 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; calls += 1;
last_ctx = ctx; last_sctx = sctx;
last_rctx = rctx;
last_args = args; 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(@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| { for (exp_args.names) |exp_name, i| {
if (exp_name == null) { if (exp_name == null) {
try std.testing.expectEqual(@as(?[]const u8, null), last_args.?.names[i]); try std.testing.expectEqual(@as(?[]const u8, null), last_args.?.names[i]);
@ -178,27 +183,28 @@ const _test = struct {
fn reset() void { fn reset() void {
calls = 0; calls = 0;
last_ctx = null; last_sctx = null;
last_rctx = null;
last_args = null; last_args = null;
} }
}; };
} }
const TestContext = u32; const TestContext = u32;
fn dummyHandler(_: TestContext, _: Args) anyerror!void {} fn dummyHandler(_: TestContext, _: TestContext, _: RouteArgs) anyerror!void {}
}; };
test "route(T, ...) basic" { test "route(T, ...) basic" {
const mock_a = _test.CallTracker(.{}, _test.dummyHandler); const mock_a = _test.CallTracker(.{}, _test.dummyHandler);
const MyRouter = Router(_test.TestContext); const MyRouter = Router(_test.TestContext, _test.TestContext);
const MyRoute = MyRouter.Route; const MyRoute = MyRouter.Route;
const my_router = MyRouter{ .routes = &[_]MyRoute{ const my_router = MyRouter{ .routes = &[_]MyRoute{
MyRoute.new(.GET, "/a", mock_a.func), MyRoute.new(.GET, "/a", mock_a.func),
} }; } };
_ = try my_router.dispatch(10, .GET, "/a"); _ = try my_router.dispatch(10, 100, .GET, "/a");
try mock_a.expectCalledOnceWith(10, .{}); try mock_a.expectCalledOnceWith(10, 100, .{});
mock_a.reset(); 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_a = _test.CallTracker(.{}, _test.dummyHandler);
const mock_b = _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 MyRoute = MyRouter.Route;
const my_router = MyRouter{ .routes = &[_]MyRoute{ 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), MyRoute.new(.GET, "/b", mock_b.func),
} }; } };
_ = try my_router.dispatch(10, .GET, "/a"); _ = try my_router.dispatch(10, 100, .GET, "/a");
try mock_a.expectCalledOnceWith(10, .{}); try mock_a.expectCalledOnceWith(10, 100, .{});
try mock_b.expectNotCalled(); try mock_b.expectNotCalled();
mock_a.reset(); mock_a.reset();
_ = try my_router.dispatch(10, .GET, "/b"); _ = try my_router.dispatch(10, 100, .GET, "/b");
try mock_a.expectNotCalled(); try mock_a.expectNotCalled();
try mock_b.expectCalledOnceWith(10, .{}); try mock_b.expectCalledOnceWith(10, 100, .{});
mock_b.reset(); mock_b.reset();
} }
test "Route(T) passes correct context" { test "Route(T) passes correct context" {
const mock_a = _test.CallTracker(.{}, _test.dummyHandler); const mock_a = _test.CallTracker(.{}, _test.dummyHandler);
const MyRouter = Router(_test.TestContext); const MyRouter = Router(_test.TestContext, _test.TestContext);
const MyRoute = MyRouter.Route; const MyRoute = MyRouter.Route;
const my_router = MyRouter{ .routes = &[_]MyRoute{ const my_router = MyRouter{ .routes = &[_]MyRoute{
MyRoute.new(.GET, "/a", mock_a.func), MyRoute.new(.GET, "/a", mock_a.func),
} }; } };
_ = try my_router.dispatch(10, .GET, "/a"); _ = try my_router.dispatch(10, 100, .GET, "/a");
try mock_a.expectCalledOnceWith(10, .{}); try mock_a.expectCalledOnceWith(10, 100, .{});
mock_a.reset(); mock_a.reset();
_ = try my_router.dispatch(16, .GET, "/a"); _ = try my_router.dispatch(16, 32, .GET, "/a");
try mock_a.expectCalledOnceWith(16, .{}); try mock_a.expectCalledOnceWith(16, 32, .{});
mock_a.reset(); mock_a.reset();
} }
@ -248,7 +254,7 @@ test "Route(T) errors on no matching route" {
const mock_a = _test.CallTracker(.{}, _test.dummyHandler); const mock_a = _test.CallTracker(.{}, _test.dummyHandler);
const mock_b = _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 MyRoute = MyRouter.Route;
const my_router = MyRouter{ .routes = &[_]MyRoute{ 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), 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_a.expectNotCalled();
try mock_b.expectNotCalled(); try mock_b.expectNotCalled();
} }
test "route(T) with no routes" { test "route(T) with no routes" {
const MyRouter = Router(_test.TestContext); const MyRouter = Router(_test.TestContext, _test.TestContext);
const MyRoute = MyRouter.Route; const MyRoute = MyRouter.Route;
const my_router = MyRouter{ .routes = &[_]MyRoute{} }; 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" { test "route(T, ...) same path different methods" {
const mock_get = _test.CallTracker(.{}, _test.dummyHandler); const mock_get = _test.CallTracker(.{}, _test.dummyHandler);
const mock_post = _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 MyRoute = MyRouter.Route;
const my_router = MyRouter{ .routes = &[_]MyRoute{ const my_router = MyRouter{ .routes = &[_]MyRoute{
@ -282,21 +288,21 @@ test "route(T, ...) same path different methods" {
MyRoute.new(.POST, "/a", mock_post.func), MyRoute.new(.POST, "/a", mock_post.func),
} }; } };
_ = try my_router.dispatch(10, .GET, "/a"); _ = try my_router.dispatch(10, 100, .GET, "/a");
try mock_get.expectCalledOnceWith(10, .{}); try mock_get.expectCalledOnceWith(10, 100, .{});
try mock_post.expectNotCalled(); try mock_post.expectNotCalled();
mock_get.reset(); mock_get.reset();
_ = try my_router.dispatch(10, .POST, "/a"); _ = try my_router.dispatch(10, 100, .POST, "/a");
try mock_get.expectNotCalled(); try mock_get.expectNotCalled();
try mock_post.expectCalledOnceWith(10, .{}); try mock_post.expectCalledOnceWith(10, 100, .{});
} }
test "route(T, ...) route under subpath" { test "route(T, ...) route under subpath" {
const mock_a = _test.CallTracker(.{}, _test.dummyHandler); const mock_a = _test.CallTracker(.{}, _test.dummyHandler);
const mock_b = _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 MyRoute = MyRouter.Route;
const my_router = MyRouter{ .routes = &[_]MyRoute{ const my_router = MyRouter{ .routes = &[_]MyRoute{
@ -304,56 +310,56 @@ test "route(T, ...) route under subpath" {
MyRoute.new(.GET, "/a/b", mock_b.func), MyRoute.new(.GET, "/a/b", mock_b.func),
} }; } };
_ = try my_router.dispatch(10, .GET, "/a"); _ = try my_router.dispatch(10, 100, .GET, "/a");
try mock_a.expectCalledOnceWith(10, .{}); try mock_a.expectCalledOnceWith(10, 100, .{});
try mock_b.expectNotCalled(); try mock_b.expectNotCalled();
mock_a.reset(); 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_a.expectNotCalled();
try mock_b.expectCalledOnceWith(10, .{}); try mock_b.expectCalledOnceWith(10, 100, .{});
} }
test "route(T, ...) case-insensitive route" { test "route(T, ...) case-insensitive route" {
const mock_a = _test.CallTracker(.{}, _test.dummyHandler); const mock_a = _test.CallTracker(.{}, _test.dummyHandler);
const MyRouter = Router(_test.TestContext); const MyRouter = Router(_test.TestContext, _test.TestContext);
const MyRoute = MyRouter.Route; const MyRoute = MyRouter.Route;
const my_router = MyRouter{ .routes = &[_]MyRoute{ const my_router = MyRouter{ .routes = &[_]MyRoute{
MyRoute.new(.GET, "/test/a", mock_a.func), MyRoute.new(.GET, "/test/a", mock_a.func),
} }; } };
_ = try my_router.dispatch(10, .GET, "/TEST/A"); _ = try my_router.dispatch(10, 100, .GET, "/TEST/A");
try mock_a.expectCalledOnceWith(10, .{}); try mock_a.expectCalledOnceWith(10, 100, .{});
mock_a.reset(); mock_a.reset();
_ = try my_router.dispatch(10, .GET, "/TesT/a"); _ = try my_router.dispatch(10, 100, .GET, "/TesT/a");
try mock_a.expectCalledOnceWith(10, .{}); try mock_a.expectCalledOnceWith(10, 100, .{});
} }
test "route(T, ...) redundant /" { test "route(T, ...) redundant /" {
const mock_a = _test.CallTracker(.{}, _test.dummyHandler); const mock_a = _test.CallTracker(.{}, _test.dummyHandler);
const MyRouter = Router(_test.TestContext); const MyRouter = Router(_test.TestContext, _test.TestContext);
const MyRoute = MyRouter.Route; const MyRoute = MyRouter.Route;
const my_router = MyRouter{ .routes = &[_]MyRoute{ const my_router = MyRouter{ .routes = &[_]MyRoute{
MyRoute.new(.GET, "/test/a", mock_a.func), MyRoute.new(.GET, "/test/a", mock_a.func),
} }; } };
_ = try my_router.dispatch(10, .GET, "/test//a"); _ = try my_router.dispatch(10, 100, .GET, "/test//a");
try mock_a.expectCalledOnceWith(10, .{}); try mock_a.expectCalledOnceWith(10, 100, .{});
mock_a.reset(); mock_a.reset();
_ = try my_router.dispatch(10, .GET, "//test///////////a////"); _ = try my_router.dispatch(10, 100, .GET, "//test///////////a////");
try mock_a.expectCalledOnceWith(10, .{}); try mock_a.expectCalledOnceWith(10, 100, .{});
mock_a.reset(); mock_a.reset();
_ = try my_router.dispatch(10, .GET, "test/a"); _ = try my_router.dispatch(10, 100, .GET, "test/a");
try mock_a.expectCalledOnceWith(10, .{}); try mock_a.expectCalledOnceWith(10, 100, .{});
mock_a.reset(); 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(); try mock_a.expectNotCalled();
} }