Add star segment support in routes
This commit is contained in:
parent
2206cd6ac9
commit
2bcef49e5e
1 changed files with 50 additions and 23 deletions
|
@ -407,10 +407,14 @@ fn pathMatches(route: []const u8, path: []const u8) bool {
|
||||||
var path_iter = PathIter.from(path);
|
var path_iter = PathIter.from(path);
|
||||||
var route_iter = PathIter.from(route);
|
var route_iter = PathIter.from(route);
|
||||||
while (route_iter.next()) |route_segment| {
|
while (route_iter.next()) |route_segment| {
|
||||||
const path_segment = path_iter.next() orelse return false;
|
const path_segment = path_iter.next() orelse "";
|
||||||
|
|
||||||
if (route_segment.len > 0 and route_segment[0] == ':') {
|
if (route_segment.len > 0 and route_segment[0] == ':') {
|
||||||
// Route Argument
|
// Route Argument
|
||||||
if (path_segment.len == 0) return false;
|
if (route_segment[route_segment.len - 1] == '*') {
|
||||||
|
// consume rest of path segments
|
||||||
|
while (path_iter.next()) |_| {}
|
||||||
|
} else if (path_segment.len == 0) return false;
|
||||||
} else {
|
} else {
|
||||||
if (!std.ascii.eqlIgnoreCase(route_segment, path_segment)) return false;
|
if (!std.ascii.eqlIgnoreCase(route_segment, path_segment)) return false;
|
||||||
}
|
}
|
||||||
|
@ -481,6 +485,10 @@ test "route" {
|
||||||
try testCase(true, .{ .method = .POST, .path = "/abcd/efgh" }, .POST, "abcd/efgh");
|
try testCase(true, .{ .method = .POST, .path = "/abcd/efgh" }, .POST, "abcd/efgh");
|
||||||
try testCase(true, .{ .method = .GET, .path = "/abcd/:arg" }, .GET, "abcd/efgh");
|
try testCase(true, .{ .method = .GET, .path = "/abcd/:arg" }, .GET, "abcd/efgh");
|
||||||
try testCase(true, .{ .method = .GET, .path = "/abcd/:arg/xyz" }, .GET, "abcd/efgh/xyz");
|
try testCase(true, .{ .method = .GET, .path = "/abcd/:arg/xyz" }, .GET, "abcd/efgh/xyz");
|
||||||
|
try testCase(true, .{ .method = .GET, .path = "/abcd/:arg*" }, .GET, "abcd/efgh/xyz");
|
||||||
|
try testCase(true, .{ .method = .GET, .path = "/abcd/:arg*" }, .GET, "abcd/efgh");
|
||||||
|
try testCase(true, .{ .method = .GET, .path = "/abcd/:arg*" }, .GET, "abcd/");
|
||||||
|
try testCase(true, .{ .method = .GET, .path = "/abcd/:arg*" }, .GET, "abcd");
|
||||||
|
|
||||||
try testCase(false, .{ .method = .POST, .path = "/" }, .GET, "/");
|
try testCase(false, .{ .method = .POST, .path = "/" }, .GET, "/");
|
||||||
try testCase(false, .{ .method = .GET, .path = "/abcd" }, .GET, "");
|
try testCase(false, .{ .method = .GET, .path = "/abcd" }, .GET, "");
|
||||||
|
@ -489,32 +497,21 @@ test "route" {
|
||||||
try testCase(false, .{ .method = .GET, .path = "/abcd/:arg" }, .GET, "/abcd/");
|
try testCase(false, .{ .method = .GET, .path = "/abcd/:arg" }, .GET, "/abcd/");
|
||||||
try testCase(false, .{ .method = .GET, .path = "/abcd/:arg/xyz" }, .GET, "abcd/efgh/");
|
try testCase(false, .{ .method = .GET, .path = "/abcd/:arg/xyz" }, .GET, "abcd/efgh/");
|
||||||
try testCase(false, .{ .method = .GET, .path = "/abcd/:arg/xyz" }, .GET, "abcd/efgh/xyz/foo");
|
try testCase(false, .{ .method = .GET, .path = "/abcd/:arg/xyz" }, .GET, "abcd/efgh/xyz/foo");
|
||||||
|
try testCase(false, .{ .method = .GET, .path = "/abcd/:arg*" }, .GET, "defg/abcd");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mounts a router subtree under a given path. Middlewares further down on the list
|
/// Mounts a router subtree under a given path. Middlewares further down on the list
|
||||||
/// are called with the path prefix specified by `route` removed from the path.
|
/// are called with the path prefix specified by `route` removed from the path.
|
||||||
/// Must be below `split_uri` on the middleware list.
|
/// Must be below `split_uri` on the middleware list.
|
||||||
pub fn Mount(comptime route: []const u8) type {
|
pub fn Mount(comptime route: []const u8) type {
|
||||||
|
if (std.mem.indexOfScalar(u8, route, ':') != null) @compileError("Route args cannot be mounted");
|
||||||
return struct {
|
return struct {
|
||||||
pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void {
|
pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void {
|
||||||
var path_iter = PathIter.from(ctx.path);
|
const args = try parseArgsFromPath(route ++ "/:path*", struct { path: []const u8 }, ctx.path);
|
||||||
comptime var route_iter = PathIter.from(route);
|
|
||||||
var path_unused: []const u8 = ctx.path;
|
|
||||||
|
|
||||||
inline while (comptime route_iter.next()) |route_segment| {
|
|
||||||
if (comptime route_segment.len == 0) continue;
|
|
||||||
const path_segment = path_iter.next() orelse return error.RouteMismatch;
|
|
||||||
path_unused = path_iter.rest();
|
|
||||||
if (comptime route_segment[0] == ':') {
|
|
||||||
@compileLog("Argument segments cannot be mounted");
|
|
||||||
// Route Argument
|
|
||||||
} else {
|
|
||||||
if (!std.ascii.eqlIgnoreCase(route_segment, path_segment)) return error.RouteMismatch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var new_ctx = ctx;
|
var new_ctx = ctx;
|
||||||
new_ctx.path = path_unused;
|
new_ctx.path = args.path;
|
||||||
|
|
||||||
return next.handle(req, res, new_ctx, {});
|
return next.handle(req, res, new_ctx, {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -546,16 +543,31 @@ fn parseArgsFromPath(comptime route: []const u8, comptime Args: type, path: []co
|
||||||
var args: Args = undefined;
|
var args: Args = undefined;
|
||||||
var path_iter = PathIter.from(path);
|
var path_iter = PathIter.from(path);
|
||||||
comptime var route_iter = PathIter.from(route);
|
comptime var route_iter = PathIter.from(route);
|
||||||
|
var path_unused: []const u8 = path;
|
||||||
|
|
||||||
inline while (comptime route_iter.next()) |route_segment| {
|
inline while (comptime route_iter.next()) |route_segment| {
|
||||||
const path_segment = path_iter.next() orelse return error.RouteMismatch;
|
const path_segment = path_iter.next() orelse "";
|
||||||
if (route_segment.len > 0 and route_segment[0] == ':') {
|
if (route_segment[0] == ':') {
|
||||||
|
comptime var name: []const u8 = route_segment[1..];
|
||||||
|
var value: []const u8 = path_segment;
|
||||||
|
|
||||||
// route segment is an argument segment
|
// route segment is an argument segment
|
||||||
if (path_segment.len == 0) return error.RouteMismatch;
|
if (comptime route_segment[route_segment.len - 1] == '*') {
|
||||||
const A = @TypeOf(@field(args, route_segment[1..]));
|
// waste remaining args
|
||||||
@field(args, route_segment[1..]) = try parseArgFromPath(A, path_segment);
|
while (path_iter.next()) |_| {}
|
||||||
|
name = route_segment[1 .. route_segment.len - 1];
|
||||||
|
value = path_unused;
|
||||||
|
} else {
|
||||||
|
if (path_segment.len == 0) return error.RouteMismatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
const A = @TypeOf(@field(args, name));
|
||||||
|
@field(args, name) = try parseArgFromPath(A, value);
|
||||||
} else {
|
} else {
|
||||||
|
// route segment is a literal segment
|
||||||
if (!std.ascii.eqlIgnoreCase(route_segment, path_segment)) return error.RouteMismatch;
|
if (!std.ascii.eqlIgnoreCase(route_segment, path_segment)) return error.RouteMismatch;
|
||||||
}
|
}
|
||||||
|
path_unused = path_iter.rest();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path_iter.next() != null) return error.RouteMismatch;
|
if (path_iter.next() != null) return error.RouteMismatch;
|
||||||
|
@ -630,6 +642,21 @@ test "ParsePathArgs" {
|
||||||
try testCase("/:id/xyz/:str", struct { id: usize, str: []const u8 }, "/3/xyz/abcd", .{ .id = 3, .str = "abcd" });
|
try testCase("/:id/xyz/:str", struct { id: usize, str: []const u8 }, "/3/xyz/abcd", .{ .id = 3, .str = "abcd" });
|
||||||
try testCase("/:id", struct { id: util.Uuid }, "/" ++ util.Uuid.nil.toCharArray(), .{ .id = util.Uuid.nil });
|
try testCase("/:id", struct { id: util.Uuid }, "/" ++ util.Uuid.nil.toCharArray(), .{ .id = util.Uuid.nil });
|
||||||
|
|
||||||
|
try testCase("/xyz/:arg*", struct { arg: []const u8 }, "/xyz/abc", .{ .arg = "abc" });
|
||||||
|
try testCase("/xyz/:arg*", struct { arg: []const u8 }, "/xyz/abc/def", .{ .arg = "abc/def" });
|
||||||
|
try testCase("/xyz/:arg*", struct { arg: []const u8 }, "/xyz/", .{ .arg = "" });
|
||||||
|
|
||||||
|
// Compiler crashes if i keep the args named the same as above.
|
||||||
|
// TODO: Debug this and try to fix it
|
||||||
|
try testCase("/xyz/:bar*", struct { bar: []const u8 }, "/xyz", .{ .bar = "" });
|
||||||
|
|
||||||
|
// It's a quirk that the initial / is left in for these cases. However, it results in a path
|
||||||
|
// that's semantically equivalent so i didn't bother fixing it
|
||||||
|
try testCase("/:foo*", struct { foo: []const u8 }, "/abc", .{ .foo = "/abc" });
|
||||||
|
try testCase("/:foo*", struct { foo: []const u8 }, "/abc/def", .{ .foo = "/abc/def" });
|
||||||
|
try testCase("/:foo*", struct { foo: []const u8 }, "/", .{ .foo = "/" });
|
||||||
|
try testCase("/:foo*", struct { foo: []const u8 }, "", .{ .foo = "" });
|
||||||
|
|
||||||
try std.testing.expectError(error.RouteMismatch, testCase("/:id", struct { id: usize }, "/", .{}));
|
try std.testing.expectError(error.RouteMismatch, testCase("/:id", struct { id: usize }, "/", .{}));
|
||||||
try std.testing.expectError(error.RouteMismatch, testCase("/abcd/:id", struct { id: usize }, "/123", .{}));
|
try std.testing.expectError(error.RouteMismatch, testCase("/abcd/:id", struct { id: usize }, "/123", .{}));
|
||||||
try std.testing.expectError(error.RouteMismatch, testCase("/:id", struct { id: usize }, "/3/id/blahblah", .{ .id = 3 }));
|
try std.testing.expectError(error.RouteMismatch, testCase("/:id", struct { id: usize }, "/3/id/blahblah", .{ .id = 3 }));
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue