const std = @import("std"); const root = @import("root"); const builtin = @import("builtin"); const http = @import("./lib.zig"); const util = @import("util"); //const query_utils = @import("./query.zig"); //const json_utils = @import("./json.zig"); const json_utils = util; const query_utils = util; fn AddFields(comptime lhs: type, comptime rhs: type) type { const Ctx = @Type(.{ .Struct = .{ .layout = .Auto, .fields = std.meta.fields(lhs) ++ std.meta.fields(rhs), .decls = &.{}, .is_tuple = false, } }); return Ctx; } fn addFields(lhs: anytype, rhs: anytype) AddFields(@TypeOf(lhs), @TypeOf(rhs)) { var result: AddFields(@TypeOf(lhs), @TypeOf(rhs)) = undefined; inline for (comptime std.meta.fieldNames(@TypeOf(lhs))) |f| @field(result, f) = @field(lhs, f); inline for (comptime std.meta.fieldNames(@TypeOf(rhs))) |f| @field(result, f) = @field(rhs, f); return result; } test { // apply is a plumbing function that applies a tuple of middlewares in order const base = apply(.{ split_uri, mount("/abc"), }); const request = .{ .uri = "/abc/defg/hijkl?some_query=true#section" }; const response = .{}; const initial_context = .{}; try base.handle(request, response, initial_context, {}); } fn ApplyInternal(comptime fields: []const std.builtin.Type.StructField) type { if (fields.len == 0) return void; return NextHandler( fields[0].field_type, ApplyInternal(fields[1..]), ); } fn applyInternal(middlewares: anytype, comptime fields: []const std.builtin.Type.StructField) ApplyInternal(fields) { if (fields.len == 0) return {}; return .{ .first = @field(middlewares, fields[0].name), .next = applyInternal(middlewares, fields[1..]), }; } pub fn apply(middlewares: anytype) ApplyInternal(std.meta.fields(@TypeOf(middlewares))) { return applyInternal(middlewares, std.meta.fields(@TypeOf(middlewares))); } pub fn AddContext(comptime Rhs: type) type { return struct { values: Rhs, pub fn handle(self: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { return next.handle(req, res, addFields(ctx, self.values), {}); } }; } pub fn NextHandler(comptime First: type, comptime Next: type) type { return struct { first: First, next: Next, pub fn handle( self: @This(), req: anytype, res: anytype, ctx: anytype, next: void, ) !void { _ = next; return self.first.handle(req, res, ctx, self.next); } }; } pub fn CatchErrors(comptime ErrorHandler: type) type { return struct { error_handler: ErrorHandler, pub fn handle(self: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { return next.handle(req, res, ctx, {}) catch |err| { return self.error_handler.handle( req, res, addFields(ctx, .{ .err = err }), next, ); }; } }; } pub fn catchErrors(error_handler: anytype) CatchErrors(@TypeOf(error_handler)) { return .{ .error_handler = error_handler }; } pub const default_error_handler = struct { fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { _ = next; std.log.err("Error {} on uri {s}", .{ ctx.err, req.uri }); if (!res.was_opened) { if (res.open(.internal_server_error)) |stream| { defer stream.close(); stream.finish() catch {}; } } // Tell the server to close the connection after this request res.should_close = true; } }{}; pub const split_uri = struct { pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { var frag_split = std.mem.split(u8, req.uri, "#"); const without_fragment = frag_split.first(); const fragment = frag_split.rest(); var query_split = std.mem.split(u8, without_fragment, "?"); const path = query_split.first(); const query = query_split.rest(); const added_ctx = .{ .path = path, .query_string = query, .fragment_string = fragment, }; return next.handle( req, res, addFields(ctx, added_ctx), {}, ); } }{}; // helper function for doing route analysis fn routeApplies(comptime R: type, req: anytype) bool { if (R.method != req.method) return false; var path_iter = util.PathIter.from(req.path); comptime var route_iter = util.PathIter.from(R.path); inline while (comptime route_iter.next()) |route_segment| { const path_segment = path_iter.next() orelse return false; if (route_segment.len > 0 and route_segment[0] == ':') { // Route Argument } else { if (!std.ascii.eqlIgnoreCase(route_segment, path_segment)) return false; } } if (path_iter.next() != null) return false; return true; } // routes a request to the correct handler based on declared HTTP method and path pub fn Router(comptime Routes: []const type) type { return struct { routes: std.meta.Tuple(Routes), pub fn handle(self: @This(), req: anytype, res: anytype, ctx: anytype, next: void) !void { _ = next; inline for (self.routes) |r| if (routeApplies(@TypeOf(r), req, ctx)) { if (r.handle(req, res, ctx, {})) |_| { // success! return; } else |err| switch (err) { error.RouteMismatch => {}, else => return err, } }; return error.RouteMismatch; } }; } pub fn Mount(comptime route: []const u8) type { return struct { pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { var path_iter = util.PathIter.from(ctx.path); comptime var route_iter = util.PathIter.from(route); var path_unused = 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; new_ctx.path = path_unused; return next.handle(req, res, new_ctx, {}); } }; } pub fn mount(comptime route: []const u8) Mount(route) { return .{}; } pub fn HandleNotFound(comptime NotFoundHandler: type) type { return struct { not_found: NotFoundHandler, pub fn handler(self: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { return next.handler(req, res, ctx, {}) catch |err| switch (err) { error.RouteMismatch => return self.not_found.handler(req, res, ctx, {}), else => return err, }; } }; } fn parsePathArgs(comptime route: []const u8, comptime Args: type, path: []const u8) !Args { var args: Args = undefined; var path_iter = util.PathIter.from(path); comptime var route_iter = util.PathIter.from(route); inline while (comptime route_iter.next()) |route_segment| { const path_segment = path_iter.next() orelse return error.RouteMismatch; if (route_segment.len > 0 and route_segment[0] == ':') { // route segment is an argument segment const A = @TypeOf(@field(args, route_segment[1..])); @field(args, route_segment[1..]) = try parsePathArg(A, path_segment); } else { if (!std.ascii.eqlIgnoreCase(route_segment, path_segment)) return error.RouteMismatch; } } if (path_iter.next() != null) return error.RouteMismatch; return args; } fn parsePathArg(comptime T: type, segment: []const u8) !T { if (T == []const u8) return segment; if (comptime std.meta.trait.isContainer(T) and std.meta.trait.hasFn("parse")(T)) return T.parse(segment); @compileError("Unsupported Type " ++ @typeName(T)); } pub fn ParsePathArgs(comptime route: []const u8, comptime Args: type) type { return struct { pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { return next.handle( req, res, addFields(ctx, .{ .args = parsePathArgs(route, Args, req.path) }), {}, ); } }; } const BaseContentType = enum { json, url_encoded, octet_stream, other, }; fn parseBody(comptime T: type, content_type: BaseContentType, reader: anytype, alloc: std.mem.Allocator) !T { const buf = try reader.readAllAlloc(alloc, 1 << 16); defer alloc.free(buf); switch (content_type) { .octet_stream, .json => { const body = try json_utils.parse(T, buf, alloc); defer json_utils.parseFree(body, alloc); return try util.deepClone(alloc, body); }, .url_encoded => return query_utils.parseQuery(alloc, T, buf) catch |err| switch (err) { error.NoQuery => error.NoBody, else => err, }, else => return error.UnsupportedMediaType, } } fn matchContentType(hdr: ?[]const u8) ?BaseContentType { if (hdr) |h| { if (std.ascii.eqlIgnoreCase(h, "application/x-www-form-urlencoded")) return .url_encoded; if (std.ascii.eqlIgnoreCase(h, "application/json")) return .json; if (std.ascii.eqlIgnoreCase(h, "application/octet-stream")) return .octet_stream; return .other; } return null; } pub fn ParseBody(comptime Body: type) type { return struct { pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { const base_content_type = matchContentType(req.headers.get("Content-Type")); var stream = req.body orelse return error.NoBody; const body = try parseBody(Body, base_content_type orelse .json, stream.reader(), ctx.allocator); defer ctx.allocator.free(body); return next.handler( req, res, addFields(ctx, .{ .body = body }), ); } }; } pub fn ParseQueryParams(comptime Next: type, comptime QueryParams: type) type { return struct { next: Next, pub fn handler(self: @This(), req: anytype, res: anytype, ctx: anytype) !void { const query = try query_utils.parseQuery(ctx.allocator, QueryParams, ctx.query_string); defer ctx.allocator.free(query); return self.next.handler( req, res, addFields(ctx, .{ .query = query }), ); } }; }