diff --git a/src/http.zig b/src/http.zig index d016cb1..44fc7a7 100644 --- a/src/http.zig +++ b/src/http.zig @@ -1,12 +1,12 @@ const std = @import("std"); const root = @import("root"); -pub const Router = @import("./http/Router.zig"); - const ciutf8 = @import("./util.zig").ciutf8; const Reader = std.net.Stream.Reader; const Writer = std.net.Stream.Writer; -const Route = Router.Route; + +pub const Handler = fn (*Context) HttpError!void; +pub const HttpError = error{Http404}; const HeaderMap = std.HashMap([]const u8, []const u8, struct { pub fn eql(_: @This(), a: []const u8, b: []const u8) bool { @@ -127,16 +127,16 @@ fn parseHeaders(allocator: std.mem.Allocator, reader: Reader) !HeaderMap { return map; } -pub fn handleConnection(conn: std.net.StreamServer.Connection) void { +pub fn handleConnection(conn: std.net.StreamServer.Connection, handler: Handler) void { defer conn.stream.close(); const reader = conn.stream.reader(); const writer = conn.stream.writer(); - handleRequest(reader, writer) catch |err| std.log.err("unhandled error processing connection: {}", .{err}); + handleRequest(reader, writer, handler) catch |err| std.log.err("unhandled error processing connection: {}", .{err}); } -fn handleRequest(reader: Reader, writer: Writer) !void { - handleHttpRequest(reader, writer) catch |err| switch (err) { +fn handleRequest(reader: Reader, writer: Writer, handler: Handler) !void { + handleHttpRequest(reader, writer, handler) catch |err| switch (err) { error.BadRequest, error.UnknownProtocol => try handleBadRequest(writer), error.MethodNotImplemented, error.HttpVersionNotSupported => try handleNotImplemented(writer), else => { @@ -146,7 +146,7 @@ fn handleRequest(reader: Reader, writer: Writer) !void { }; } -fn handleHttpRequest(reader: Reader, writer: Writer) anyerror!void { +fn handleHttpRequest(reader: Reader, writer: Writer, handler: Handler) anyerror!void { const method = try parseHttpMethod(reader); var header_buf: [1 << 16]u8 = undefined; @@ -189,7 +189,7 @@ fn handleHttpRequest(reader: Reader, writer: Writer) anyerror!void { .allocator = allocator, }; - try (Router{ .routes = &root.routes }).routeRequest(&context); + try handler(&context); } pub const Context = struct { @@ -197,15 +197,9 @@ pub const Context = struct { method: Method, path: []const u8, - route: ?*const Route = null, - headers: HeaderMap, body: ?Reader, - - pub fn arg(self: *Request, name: []const u8) []const u8 { - return self.route.?.arg(name, self.path); - } }; const Response = struct { diff --git a/src/main.zig b/src/main.zig index fcf11bc..cbde0e8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3,25 +3,24 @@ const std = @import("std"); pub const db = @import("./db.zig"); pub const util = @import("./util.zig"); pub const http = @import("./http.zig"); +pub const routing = @import("./routing.zig"); pub const Uuid = util.Uuid; pub const ciutf8 = util.ciutf8; pub const io_mode = .evented; -const Route = http.Router.Route; - -pub const routes = [_]Route{ - Route.from(.GET, "/", staticString("Index Page")), - Route.from(.GET, "/abc", staticString("abc")), - Route.from(.GET, "/user/:id", getUser), - Route.from(.POST, "/note/", postNote), -}; +pub const router = routing.makeRouter(*http.Context, [_]routing.RouteFn(*http.Context){ + routing.makeRoute(.GET, "/", staticString("Index Page")), + routing.makeRoute(.GET, "/abc", staticString("abc")), + routing.makeRoute(.GET, "/user/:id", getUser), + routing.makeRoute(.POST, "/note/", postNote), +}); const this_scheme = "http"; const this_host = "localhost:8080"; -fn postNote(ctx: *http.Context, _: *const Route) anyerror!void { +fn postNote(ctx: *http.Context, _: struct {}) anyerror!void { const id = try db.createNote(.{ .author = Uuid.randV4(util.getRandom()), .content = "test post", @@ -33,20 +32,17 @@ fn postNote(ctx: *http.Context, _: *const Route) anyerror!void { try writer.writeAll("\"}"); } -fn getUser(ctx: *http.Context, route: *const Route) anyerror!void { - const id_str = route.arg("id", ctx.request.path); - +fn getUser(ctx: *http.Context, args: struct { id: []const u8 }) anyerror!void { const host = ctx.request.headers.get("host") orelse { + return; + }; + + const uuid = Uuid.parse(args.id) catch { try ctx.response.statusOnly(400); return; }; - const id = Uuid.parse(id_str) catch { - try ctx.response.statusOnly(400); - return; - }; - - const actor = try db.getActorById(id); + const actor = try db.getActorById(args.id); if (actor == null or !std.mem.eql(u8, actor.?.host, host)) { try ctx.response.statusOnly(404); @@ -57,20 +53,19 @@ fn getUser(ctx: *http.Context, route: *const Route) anyerror!void { var writer = try ctx.response.open(200); try writer.writeAll("{\"type\":\"Person\","); - try writer.print("\"id\":\"{s}://{s}/user/{}\",", .{ this_scheme, this_host, id }); + try writer.print("\"id\":\"{s}://{s}/user/{}\",", .{ this_scheme, this_host, uuid }); try writer.print("\"preferredUsername\":\"{s}\"", .{actor.?.handle}); try writer.writeAll("}"); } -fn staticString(comptime str: []const u8) Route.Handler { +fn staticString(comptime str: []const u8) routing.RouteFn(http.Context) { return (struct { - fn func(ctx: *http.Context, _: *const Route) anyerror!void { + fn func(ctx: *http.Context, _: struct {}) http.HttpError!void { try ctx.response.headers.put("Content-Type", "text/plain"); try ctx.response.write(200, str); } }).func; } - pub fn main() anyerror!void { var srv = std.net.StreamServer.init(.{ .reuse_address = true }); defer srv.deinit(); @@ -84,7 +79,13 @@ pub fn main() anyerror!void { const conn = try srv.accept(); // todo: keep track of connections - _ = async http.handleConnection(conn); + _ = async http.handleConnection(conn, struct { + fn func(ctx: *http.Context) http.HttpError!void { + //try router(ctx, ctx.method, ctx.path); + _ = ctx; + return; + } + }.func); } } diff --git a/src/router.zig b/src/routing.zig similarity index 95% rename from src/router.zig rename to src/routing.zig index d9473fb..74c2d2e 100644 --- a/src/router.zig +++ b/src/routing.zig @@ -70,22 +70,21 @@ const RouteSegment = union(enum) { param: []const u8, }; -// convention: return HttpError IFF a situation you can't finish the request in happens. +// convention: return http.HttpError IFF a situation you can't finish the request in happens. // If status line/headers were written, always return void -const HttpError = error{Http404}; -fn RouteFn(comptime Context: type) type { - return fn (Context, http.Method, []const u8) HttpError!void; +pub fn RouteFn(comptime Context: type) type { + return fn (Context, http.Method, []const u8) http.HttpError!void; } -/// `makeRoute` takes a route definition and a handler of the form `fn(, ) HttpError!void` +/// `makeRoute` takes a route definition and a handler of the form `fn(, ) http.HttpError!void` /// where `Params` is a struct containing one field of type `[]const u8` for each path parameter /// /// Arguments: /// method: The HTTP method to match /// path: The path spec to match against. Path segments beginning with a `:` will cause the rest of /// the segment to be treated as the name of a path parameter -/// handler: The code to execute on route match. This must be a function of form `fn(, ) HttpError!void` +/// handler: The code to execute on route match. This must be a function of form `fn(, ) http.HttpError!void` /// /// Implicit Arguments: /// Context: the type of a user-supplied Context that is passed through the route. typically `http.Context` but @@ -95,17 +94,17 @@ fn RouteFn(comptime Context: type) type { /// `[]const u8` and it must have the same name as a single path parameter. /// /// Returns: -/// A new route function of type `fn(, http.Method, []const u8) HttpError!void`. When called, +/// A new route function of type `fn(, http.Method, []const u8) http.HttpError!void`. When called, /// this function will test the provided values against its specification. If they match, then /// this function will parse path parameters and will be called with the supplied /// context and params. If they do not match, this function will return error.Http404 /// /// Example: /// route(.GET, "/user/:id/followers", struct{ -/// fn getFollowers(ctx: http.Context, params: struct{ id: []const u8 } HttpError { ... } +/// fn getFollowers(ctx: http.Context, params: struct{ id: []const u8 } http.HttpError { ... } /// ).getFollowers) /// -fn makeRoute( +pub fn makeRoute( comptime method: http.Method, comptime path: []const u8, comptime handler: anytype, @@ -115,7 +114,7 @@ fn makeRoute( break :return_type RouteFn(@typeInfo(@TypeOf(handler)).Fn.args[0].arg_type.?); } { const handler_args = @typeInfo(@TypeOf(handler)).Fn.args; - if (handler_args.len != 2) @compileError("handler function must have signature fn(Context, Params) HttpError"); + if (handler_args.len != 2) @compileError("handler function must have signature fn(Context, Params) http.HttpError"); if (@typeInfo(handler_args[1].arg_type.?) != .Struct) @compileError("Params in handler(Context, Params) must be struct"); const Context = handler_args[0].arg_type.?; @@ -143,7 +142,7 @@ fn makeRoute( } return struct { - fn func(ctx: Context, req_method: http.Method, req_path: []const u8) HttpError!void { + fn func(ctx: Context, req_method: http.Method, req_path: []const u8) http.HttpError!void { if (req_method != method) return error.Http404; var params: Params = undefined; @@ -168,8 +167,8 @@ fn makeRoute( }.func; } -fn RouterFn(comptime Context: type) type { - return fn (http.Method, path: []const u8, Context) HttpError!void; +pub fn RouterFn(comptime Context: type) type { + return fn (http.Method, path: []const u8, Context) http.HttpError!void; } pub fn makeRouter( @@ -177,7 +176,7 @@ pub fn makeRouter( comptime routes: []const RouteFn(Context), ) RouterFn(Context) { return struct { - fn dispatch(method: http.Method, path: []const u8, ctx: Context) HttpError!void { + fn dispatch(method: http.Method, path: []const u8, ctx: Context) http.HttpError!void { for (routes) |r| { return r(ctx, method, path) catch |err| switch (err) { error.Http404 => continue, @@ -303,7 +302,7 @@ const _tests = struct { fn dummyHandler(comptime Args: type) type { comptime { return struct { - fn func(_: TestContext, _: Args) HttpError!void {} + fn func(_: TestContext, _: Args) http.HttpError!void {} }; } }