fediglam/src/main/controllers.zig

209 lines
6.7 KiB
Zig

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");
const json_utils = @import("./json.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 fn routeRequest(api_source: anytype, request: http.Request, response: http.Response, alloc: std.mem.Allocator) void {
// TODO: hashmaps?
inline for (routes) |route| {
if (Context(route).matchAndHandle(api_source, request, response, alloc)) return;
}
// todo 404
}
const routes = .{sample_api};
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: util.Uuid,
};
pub const Query = struct {
arg: []const u8 = "",
};
pub fn handler(ctx: Context(Self), response: *Response, _: api.ApiSource.Conn) !void {
std.log.debug("{}", .{ctx.body.content});
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;
self.body = try json_utils.parse(Body, body, self.allocator);
}
}
fn freeBody(self: *Self) void {
if (Body != void) {
json_utils.parseFree(self.body, 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 = .{} },
};