const std = @import("std"); const zap = @import("zap.zig"); const Allocator = std.mem.Allocator; /// Errors returnable by init() const RouterError = error{ AlreadyExists, EmptyPath, }; const Self = @This(); /// This is a singleton var _instance: *Self = undefined; /// Options to pass to init() pub const Options = struct { /// an optional zap request function for 404 not found case not_found: ?zap.HttpRequestFn = null, }; const CallbackTag = enum { bound, unbound }; const BoundHandler = *fn (*const anyopaque, zap.Request) void; const Callback = union(CallbackTag) { bound: struct { instance: usize, handler: usize }, unbound: zap.HttpRequestFn, }; routes: std.StringHashMap(Callback), not_found: ?zap.HttpRequestFn, /// Create a new Router pub fn init(allocator: Allocator, options: Options) Self { return .{ .routes = std.StringHashMap(Callback).init(allocator), .not_found = options.not_found, }; } /// Deinit the router pub fn deinit(self: *Self) void { self.routes.deinit(); } /// Call this to add a route with an unbound handler: a handler that is not member of a struct. pub fn handle_func_unbound(self: *Self, path: []const u8, h: zap.HttpRequestFn) !void { if (path.len == 0) { return RouterError.EmptyPath; } if (self.routes.contains(path)) { return RouterError.AlreadyExists; } try self.routes.put(path, Callback{ .unbound = h }); } /// Call this to add a route with a handler that is bound to an instance of a struct. /// Example: /// /// ```zig /// const HandlerType = struct { /// pub fn getA(self: *HandlerType, r: zap.Request) void { /// _ = self; /// r.sendBody("hello\n\n") catch return; /// } /// } /// var handler_instance = HandlerType{}; /// /// my_router.handle_func("/getA", &handler_instance, HandlerType.getA); /// ``` pub fn handle_func(self: *Self, path: []const u8, instance: *anyopaque, handler: anytype) !void { // TODO: assert type of instance has handler if (path.len == 0) { return RouterError.EmptyPath; } if (self.routes.contains(path)) { return RouterError.AlreadyExists; } try self.routes.put(path, Callback{ .bound = .{ .instance = @intFromPtr(instance), .handler = @intFromPtr(handler), } }); } /// Get the zap request handler function needed for a listener pub fn on_request_handler(self: *Self) zap.HttpRequestFn { _instance = self; return zap_on_request; } fn zap_on_request(r: zap.Request) void { return serve(_instance, r); } fn serve(self: *Self, r: zap.Request) void { const path = r.path orelse "/"; if (self.routes.get(path)) |routeInfo| { switch (routeInfo) { .bound => |b| @call(.auto, @as(BoundHandler, @ptrFromInt(b.handler)), .{ @as(*anyopaque, @ptrFromInt(b.instance)), r }), .unbound => |h| h(r), } } else if (self.not_found) |handler| { // not found handler handler(r); } else { // default 404 output r.setStatus(.not_found); r.sendBody("404 Not Found") catch return; } }