const std = @import("std"); const root = @import("root"); const builtin = @import("builtin"); const http = @import("http"); const api = @import("api"); const util = @import("util"); const query_utils = @import("./query.zig"); pub const auth = @import("./controllers/auth.zig"); pub const communities = @import("./controllers/communities.zig"); pub const invites = @import("./controllers/invites.zig"); pub const users = @import("./controllers/users.zig"); pub const notes = @import("./controllers/notes.zig"); pub const sample_api = struct { const Self = @This(); pub const method = .POST; pub const path = "/notes/:id/reacts"; pub const content_type = "application/json"; pub const Args = struct { id: []const u8, }; pub const Body = struct { content: []const u8, }; pub const Query = struct { arg: []const u8 = "", }; pub fn handler(ctx: Context(Self), response: *Response, _: api.ApiSource.Conn) !void { try response.writeJson(.created, ctx.query); } }; pub fn Context(comptime Route: type) type { return struct { const Self = @This(); pub const Args = if (@hasDecl(Route, "Args")) Route.Args else void; pub const Body = if (@hasDecl(Route, "Body")) Route.Body else void; pub const Query = if (@hasDecl(Route, "Query")) Route.Query else void; allocator: std.mem.Allocator, method: http.Method, request_line: []const u8, headers: http.Headers, args: Args, body: Body, query: Query, fn parseArgs(path: []const u8) ?Args { var args: Route.Args = undefined; var path_iter = util.PathIter.from(path); comptime var route_iter = util.PathIter.from(Route.path); inline while (comptime route_iter.next()) |route_segment| { const path_segment = path_iter.next() orelse return null; if (route_segment[0] == ':') { @field(args, route_segment[1..]) = path_segment; } else { if (!std.ascii.eqlIgnoreCase(route_segment, path_segment)) return null; } } return args; } pub fn matchAndHandle(api_source: *api.ApiSource, ctx: http.server.Context, alloc: std.mem.Allocator) bool { const req = ctx.request; if (req.method != Route.method) return false; var path = std.mem.sliceTo(std.mem.sliceTo(req.path, '#'), '?'); var args: Route.Args = parseArgs(path) orelse return false; var response = Response{ .headers = http.Headers.init(alloc), .ctx = ctx }; defer response.headers.deinit(); var self = Self{ .allocator = alloc, .method = req.method, .request_line = req.path, .headers = req.headers, .args = args, .body = undefined, .query = undefined, }; self.prepareAndHandle(api_source, req, &response); return true; } fn errorHandler(response: *Response, status: http.Status) void { response.writeStatus(status) catch unreachable; } fn prepareAndHandle(self: *Self, api_source: anytype, req: http.Request, response: *Response) void { self.parseBody(req) catch return errorHandler(response, .bad_request); defer self.freeBody(); self.parseQuery() catch return errorHandler(response, .bad_request); var api_conn = self.getApiConn(api_source) catch return errorHandler(response, .internal_server_error); // TODO defer api_conn.close(); self.handle(response, api_conn); } fn parseBody(self: *Self, req: http.Request) !void { if (Body != void) { const body = req.body orelse return error.NoBody; var tokens = std.json.TokenStream.init(body); self.body = try std.json.parse(Body, &tokens, .{ .allocator = self.allocator }); } } fn freeBody(self: *Self) void { if (Body != void) { std.json.parseFree(Body, self.body, .{ .allocator = self.allocator }); } } fn parseQuery(self: *Self) !void { if (Query != void) { const path = std.mem.sliceTo(self.request_line, '?'); const q = std.mem.sliceTo(self.request_line[path.len..], '#'); self.query = try query_utils.parseQuery(Query, q); } } fn handle(self: Self, response: *Response, api_conn: anytype) void { Route.handler(self, response, api_conn) catch |err| std.log.err("{}", .{err}); } fn getApiConn(self: *Self, api_source: anytype) !api.ApiSource.Conn { const host = self.headers.get("Host") orelse return error.NoHost; const auth_header = self.headers.get("Authorization"); const token = if (auth_header) |header| blk: { const prefix = "bearer "; if (header.len < prefix.len) break :blk null; if (!std.ascii.eqlIgnoreCase(prefix, header[0..prefix.len])) break :blk null; break :blk header[prefix.len..]; } else null; if (token) |t| return try api_source.connectToken(host, t, self.allocator); return try api_source.connectUnauthorized(host, self.allocator); } }; } pub const Response = struct { const Self = @This(); headers: http.Headers, ctx: http.server.Context, pub fn writeStatus(self: *Self, status: http.Status) !void { var stream = try self.ctx.openResponse(&self.headers, status); defer stream.close(); try stream.finish(); } pub fn writeJson(self: *Self, status: http.Status, response_body: anytype) !void { try self.headers.put("Content-Type", "application/json"); var stream = try self.ctx.openResponse(&self.headers, status); defer stream.close(); const writer = stream.writer(); try std.json.stringify(response_body, json_options, writer); try stream.finish(); } }; const json_options = if (builtin.mode == .Debug) .{ .whitespace = .{ .indent = .{ .Space = 2 }, .separator = true, }, .string = .{ .String = .{} }, } else .{ .whitespace = .{ .indent = .None, .separator = false, }, .string = .{ .String = .{} }, };