Add Route.bind
This commit is contained in:
parent
626a7d33b0
commit
283b85cfcc
4 changed files with 226 additions and 152 deletions
|
@ -3,7 +3,7 @@ const root = @import("root");
|
||||||
|
|
||||||
pub const Router = @import("./http/Router.zig");
|
pub const Router = @import("./http/Router.zig");
|
||||||
|
|
||||||
const ciutf8 = root.ciutf8;
|
const ciutf8 = @import("./util.zig").ciutf8;
|
||||||
const Reader = std.net.Stream.Reader;
|
const Reader = std.net.Stream.Reader;
|
||||||
const Writer = std.net.Stream.Writer;
|
const Writer = std.net.Stream.Writer;
|
||||||
const Route = Router.Route;
|
const Route = Router.Route;
|
||||||
|
|
|
@ -1,171 +1,230 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const root = @import("root");
|
|
||||||
|
|
||||||
const Context = root.http.Context;
|
const util = @import("../util.zig");
|
||||||
const Method = root.http.Method;
|
const http = @import("../http.zig");
|
||||||
const ciutf8 = root.ciutf8;
|
|
||||||
|
|
||||||
const Self = @This();
|
const Method = http.Method;
|
||||||
|
const ciutf8 = util.ciutf8;
|
||||||
|
|
||||||
routes: []const Route,
|
pub fn Router(comptime Context: type) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
routes: []const Route,
|
||||||
|
|
||||||
pub const Route = struct {
|
pub const Route = struct {
|
||||||
const Segment = union(enum) {
|
const Segment = union(enum) {
|
||||||
param: []const u8,
|
param: []const u8,
|
||||||
literal: []const u8,
|
literal: []const u8,
|
||||||
};
|
|
||||||
|
|
||||||
pub const Handler = fn (*Context, *const Route) callconv(.Async) anyerror!void;
|
|
||||||
fn normalize(comptime path: []const u8) []const u8 {
|
|
||||||
var arr: [path.len]u8 = undefined;
|
|
||||||
|
|
||||||
var i = 0;
|
|
||||||
for (path) |ch| {
|
|
||||||
if (i == 0 and ch == '/') continue;
|
|
||||||
if (i > 0 and ch == '/' and arr[i - 1] == '/') continue;
|
|
||||||
|
|
||||||
arr[i] = ch;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i > 0 and arr[i - 1] == '/') {
|
|
||||||
i -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return arr[0..i];
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseSegments(comptime path: []const u8) []const Segment {
|
|
||||||
var count = 1;
|
|
||||||
for (path) |ch| {
|
|
||||||
if (ch == '/') count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var segment_array: [count]Segment = undefined;
|
|
||||||
|
|
||||||
var segment_start = 0;
|
|
||||||
for (segment_array) |*seg| {
|
|
||||||
var index = segment_start;
|
|
||||||
while (index < path.len) : (index += 1) {
|
|
||||||
if (path[index] == '/') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const slice = path[segment_start..index];
|
|
||||||
if (slice.len > 0 and slice[0] == ':') {
|
|
||||||
// doing this kinda jankily to get around segfaults in compiler
|
|
||||||
const param = path[segment_start + 1 .. index];
|
|
||||||
seg.* = .{ .param = param };
|
|
||||||
} else {
|
|
||||||
seg.* = .{ .literal = slice };
|
|
||||||
}
|
|
||||||
|
|
||||||
segment_start = index + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return &segment_array;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from(method: Method, comptime path: []const u8, handler: Handler) Route {
|
|
||||||
const segments = parseSegments(normalize(path));
|
|
||||||
return Route{ .method = method, .path = segments, .handler = handler };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nextSegment(path: []const u8) ?[]const u8 {
|
|
||||||
var start: usize = 0;
|
|
||||||
var end: usize = start;
|
|
||||||
while (end < path.len) : (end += 1) {
|
|
||||||
// skip leading slash
|
|
||||||
if (end == start and path[start] == '/') {
|
|
||||||
start += 1;
|
|
||||||
continue;
|
|
||||||
} else if (path[end] == '/') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (start == end) return null;
|
|
||||||
|
|
||||||
return path[start..end];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn matches(self: Route, path: []const u8) bool {
|
|
||||||
var segment_start: usize = 0;
|
|
||||||
for (self.path) |seg| {
|
|
||||||
var index = segment_start;
|
|
||||||
while (index < path.len) : (index += 1) {
|
|
||||||
// skip leading slash
|
|
||||||
if (index == segment_start and path[index] == '/') {
|
|
||||||
segment_start += 1;
|
|
||||||
continue;
|
|
||||||
} else if (path[index] == '/') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const slice = path[segment_start..index];
|
|
||||||
const match = switch (seg) {
|
|
||||||
.literal => |str| ciutf8.eql(slice, str),
|
|
||||||
.param => true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!match) return false;
|
pub const Handler = fn (*Context, *const Route) callconv(.Async) anyerror!void;
|
||||||
|
fn normalize(comptime path: []const u8) []const u8 {
|
||||||
|
var arr: [path.len]u8 = undefined;
|
||||||
|
|
||||||
segment_start = index + 1;
|
comptime var i = 0;
|
||||||
}
|
for (path) |ch| {
|
||||||
|
if (i == 0 and ch == '/') continue;
|
||||||
|
if (i > 0 and ch == '/' and arr[i - 1] == '/') continue;
|
||||||
|
|
||||||
// check for trailing path
|
arr[i] = ch;
|
||||||
while (segment_start < path.len) : (segment_start += 1) {
|
i += 1;
|
||||||
if (path[segment_start] != '/') return false;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
if (i > 0 and arr[i - 1] == '/') {
|
||||||
}
|
i -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn arg(self: Route, name: []const u8, path: []const u8) []const u8 {
|
return arr[0..i];
|
||||||
var index: usize = 0;
|
|
||||||
for (self.path) |seg| {
|
|
||||||
const slice = nextSegment(path[index..]);
|
|
||||||
if (slice == null) return "";
|
|
||||||
|
|
||||||
index = @ptrToInt(slice.?.ptr) - @ptrToInt(path.ptr) + slice.?.len + 1;
|
|
||||||
|
|
||||||
switch (seg) {
|
|
||||||
.param => |param| {
|
|
||||||
if (std.mem.eql(u8, param, name)) {
|
|
||||||
return slice.?;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.literal => continue,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parseSegments(comptime path: []const u8) []const Segment {
|
||||||
|
var count = 1;
|
||||||
|
for (path) |ch| {
|
||||||
|
if (ch == '/') count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var segment_array: [count]Segment = undefined;
|
||||||
|
|
||||||
|
var segment_start = 0;
|
||||||
|
for (segment_array) |*seg| {
|
||||||
|
var index = segment_start;
|
||||||
|
while (index < path.len) : (index += 1) {
|
||||||
|
if (path[index] == '/') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const slice = path[segment_start..index];
|
||||||
|
if (slice.len > 0 and slice[0] == ':') {
|
||||||
|
// doing this kinda jankily to get around segfaults in compiler
|
||||||
|
const param = path[segment_start + 1 .. index];
|
||||||
|
seg.* = .{ .param = param };
|
||||||
|
} else {
|
||||||
|
seg.* = .{ .literal = slice };
|
||||||
|
}
|
||||||
|
|
||||||
|
segment_start = index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return &segment_array;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from(method: Method, comptime path: []const u8, handler: Handler) Route {
|
||||||
|
const segments = parseSegments(normalize(path));
|
||||||
|
return Route{ .method = method, .path = segments, .handler = handler };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nextSegment(path: []const u8) ?[]const u8 {
|
||||||
|
var start: usize = 0;
|
||||||
|
var end: usize = start;
|
||||||
|
while (end < path.len) : (end += 1) {
|
||||||
|
// skip leading slash
|
||||||
|
if (end == start and path[start] == '/') {
|
||||||
|
start += 1;
|
||||||
|
continue;
|
||||||
|
} else if (path[end] == '/') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start == end) return null;
|
||||||
|
|
||||||
|
return path[start..end];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn matches(self: Route, path: []const u8) bool {
|
||||||
|
var segment_start: usize = 0;
|
||||||
|
for (self.path) |seg| {
|
||||||
|
var index = segment_start;
|
||||||
|
while (index < path.len) : (index += 1) {
|
||||||
|
// skip leading slash
|
||||||
|
if (index == segment_start and path[index] == '/') {
|
||||||
|
segment_start += 1;
|
||||||
|
continue;
|
||||||
|
} else if (path[index] == '/') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const slice = path[segment_start..index];
|
||||||
|
const match = switch (seg) {
|
||||||
|
.literal => |str| ciutf8.eql(slice, str),
|
||||||
|
.param => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!match) return false;
|
||||||
|
|
||||||
|
segment_start = index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for trailing path
|
||||||
|
while (segment_start < path.len) : (segment_start += 1) {
|
||||||
|
if (path[segment_start] != '/') return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn arg(self: Route, name: []const u8, path: []const u8) []const u8 {
|
||||||
|
var index: usize = 0;
|
||||||
|
for (self.path) |seg| {
|
||||||
|
const slice = nextSegment(path[index..]);
|
||||||
|
if (slice == null) return "";
|
||||||
|
|
||||||
|
index = @ptrToInt(slice.?.ptr) - @ptrToInt(path.ptr) + slice.?.len + 1;
|
||||||
|
|
||||||
|
switch (seg) {
|
||||||
|
.param => |param| {
|
||||||
|
if (std.mem.eql(u8, param, name)) {
|
||||||
|
return slice.?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.literal => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std.log.err("unknown parameter {s}", .{name});
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
method: Method,
|
||||||
|
path: []const Segment,
|
||||||
|
handler: Handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn handleNotFound(ctx: *Context) !void {
|
||||||
|
try ctx.response.writer.writeAll("HTTP/1.1 404 Not Found\r\n\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
std.log.err("unknown parameter {s}", .{name});
|
pub fn routeRequest(self: Self, ctx: *Context) !void {
|
||||||
return "";
|
for (self.routes) |*route| {
|
||||||
}
|
if (route.method == ctx.request.method and route.matches(ctx.request.path)) {
|
||||||
|
std.log.info("{s} {s}", .{ @tagName(ctx.request.method), ctx.request.path });
|
||||||
|
//ctx.request.route = route;
|
||||||
|
|
||||||
method: Method,
|
var buf = try ctx.allocator.allocWithOptions(u8, @frameSize(route.handler), 8, null);
|
||||||
path: []const Segment,
|
defer ctx.allocator.free(buf);
|
||||||
handler: Handler,
|
return await @asyncCall(buf, {}, route.handler, .{ ctx, route });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std.log.info("404 {s} {s}", .{ @tagName(ctx.request.method), ctx.request.path });
|
||||||
|
try handleNotFound(ctx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestContext = struct {
|
||||||
|
request: struct {
|
||||||
|
method: Method,
|
||||||
|
path: []const u8,
|
||||||
|
},
|
||||||
|
allocator: std.mem.Allocator = std.testing.allocator,
|
||||||
};
|
};
|
||||||
|
const TestRouter = Router(TestContext);
|
||||||
|
const TestRoute = TestRouter.Route;
|
||||||
|
|
||||||
fn handleNotFound(ctx: *Context) !void {
|
fn CallTracker(comptime _uniq: anytype, comptime next: TestRoute.Handler) type {
|
||||||
try ctx.response.writer.writeAll("HTTP/1.1 404 Not Found\r\n\r\n");
|
_ = _uniq;
|
||||||
}
|
return struct {
|
||||||
|
var calls: u32 = 0;
|
||||||
pub fn routeRequest(self: Self, ctx: *Context) !void {
|
fn func(ctx: *TestContext, route: *const TestRoute) !void {
|
||||||
for (self.routes) |*route| {
|
calls += 1;
|
||||||
if (route.method == ctx.request.method and route.matches(ctx.request.path)) {
|
return next(ctx, route);
|
||||||
std.log.info("{s} {s}", .{ @tagName(ctx.request.method), ctx.request.path });
|
|
||||||
ctx.request.route = route;
|
|
||||||
|
|
||||||
var buf = try ctx.allocator.allocWithOptions(u8, @frameSize(route.handler), 8, null);
|
|
||||||
defer ctx.allocator.free(buf);
|
|
||||||
return await @asyncCall(buf, {}, route.handler, .{ ctx, route });
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std.log.info("404 {s} {s}", .{ @tagName(ctx.request.method), ctx.request.path });
|
fn expectCalled(times: u32) !void {
|
||||||
try handleNotFound(ctx);
|
return std.testing.expectEqual(times, calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset() void {
|
||||||
|
calls = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expectNotCalled(_: *TestContext, _: *const TestRoute) !void {
|
||||||
|
return error.TestWrongRouteChosen;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dummyHandler(_: *TestContext, _: *const TestRoute) !void {}
|
||||||
|
|
||||||
|
test "routeRequest" {
|
||||||
|
const call_tracker = CallTracker(.{}, dummyHandler);
|
||||||
|
const test_routes = [_]TestRoute{
|
||||||
|
TestRoute.from(.GET, "/ab", expectNotCalled),
|
||||||
|
TestRoute.from(.GET, "/abc", call_tracker.func),
|
||||||
|
TestRoute.from(.GET, "/abcdefg", expectNotCalled),
|
||||||
|
TestRoute.from(.GET, "/", expectNotCalled),
|
||||||
|
};
|
||||||
|
var context = TestContext{
|
||||||
|
.request = .{
|
||||||
|
.method = .GET,
|
||||||
|
.path = "/abc",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try (TestRouter{ .routes = &test_routes }).routeRequest(&context);
|
||||||
|
|
||||||
|
try call_tracker.expectCalled(1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,3 +87,7 @@ pub fn main() anyerror!void {
|
||||||
_ = async http.handleConnection(conn);
|
_ = async http.handleConnection(conn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
_ = http;
|
||||||
|
}
|
||||||
|
|
|
@ -42,6 +42,17 @@ test {
|
||||||
_ = _tests;
|
_ = _tests;
|
||||||
}
|
}
|
||||||
const _tests = struct {
|
const _tests = struct {
|
||||||
|
test "RouteWithContext(T).bind" {
|
||||||
|
const R = RouteWithContext(Context);
|
||||||
|
const r = R.bind(.GET, "//ab//cd", dummyHandler);
|
||||||
|
|
||||||
|
try std.testing.expectEqual(R{
|
||||||
|
.method = .GET,
|
||||||
|
.path = "/ab/cd",
|
||||||
|
.handler = dummyHandler,
|
||||||
|
}, r);
|
||||||
|
}
|
||||||
|
|
||||||
fn CallTracker(comptime _uniq: anytype, comptime next: fn (Context) void) type {
|
fn CallTracker(comptime _uniq: anytype, comptime next: fn (Context) void) type {
|
||||||
_ = _uniq;
|
_ = _uniq;
|
||||||
return struct {
|
return struct {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue