From f457b7a0d6a958f24aaf1ee83eddd6219743c8ec Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Mon, 21 Nov 2022 03:58:54 -0800 Subject: [PATCH 01/11] Add drive files to db --- src/api/services/files.zig | 69 ++++++++++++++++++++++++++++++++++++++ src/main/migrations.zig | 21 ++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 src/api/services/files.zig diff --git a/src/api/services/files.zig b/src/api/services/files.zig new file mode 100644 index 0000000..18c0e9d --- /dev/null +++ b/src/api/services/files.zig @@ -0,0 +1,69 @@ +const std = @import("std"); +const util = @import("util"); + +const Uuid = util.Uuid; +const DateTime = util.DateTime; + +pub const FileOwner = union(enum) { + user_id: Uuid, + community_id: Uuid, +}; + +pub const DriveFile = struct { + id: Uuid, + filename: []const u8, + owner: FileOwner, + size: usize, + created_at: DateTime, +}; + +pub const files = struct { + pub fn create(db: anytype, owner: FileOwner, filename: []const u8, data: []const u8, alloc: std.mem.Allocator) !void { + const id = Uuid.randV4(util.getThreadPrng()); + const now = DateTime.now(); + + // TODO: assert we're not in a transaction + db.insert("drive_file", .{ + .id = id, + .filename = filename, + .owner = owner, + .created_at = now, + }, alloc) catch return error.DatabaseFailure; + // Assume the previous statement succeeded and is not stuck in a transaction + errdefer { + db.exec("DELETE FROM drive_file WHERE ID = $1", .{id}, alloc) catch |err| { + std.log.err("Unable to remove file record in DB: {}", .{err}); + }; + } + + try saveFile(id, data); + } + + const data_root = "./files"; + fn saveFile(id: Uuid, data: []const u8) !void { + var dir = try std.fs.cwd().openDir(data_root); + defer dir.close(); + + var file = try dir.createFile(id.toCharArray(), .{ .exclusive = true }); + defer file.close(); + + try file.writer().writeAll(data); + try file.sync(); + } + + pub fn deref(alloc: std.mem.Allocator, id: Uuid) ![]const u8 { + var dir = try std.fs.cwd().openDir(data_root); + defer dir.close(); + + return dir.readFileAlloc(alloc, id.toCharArray(), 1 << 32); + } + + pub fn delete(db: anytype, alloc: std.mem.Allocator, id: Uuid) !void { + var dir = try std.fs.cwd().openDir(data_root); + defer dir.close(); + + try dir.deleteFile(id.toCharArray()); + + db.exec("DELETE FROM drive_file WHERE ID = $1", .{id}, alloc) catch return error.DatabaseFailure; + } +}; diff --git a/src/main/migrations.zig b/src/main/migrations.zig index 89a63d3..dc97b44 100644 --- a/src/main/migrations.zig +++ b/src/main/migrations.zig @@ -205,4 +205,25 @@ const migrations: []const Migration = &.{ , .down = "DROP TABLE follow", }, + .{ + .name = "files", + .up = + \\CREATE TABLE drive_file( + \\ id UUID NOT NULL PRIMARY KEY, + \\ + \\ filename TEXT NOT NULL, + \\ account_owner_id UUID REFERENCES account(id), + \\ community_owner_id UUID REFERENCES community(id), + \\ size INTEGER NOT NULL, + \\ + \\ created_at TIMESTAMPTZ NOT NULL, + \\ + \\ CHECK( + \\ (account_owner_id IS NULL AND community_owner_id IS NOT NULL) + \\ OR (account_owner_id IS NOT NULL AND community_owner_id IS NULL) + \\ ) + \\); + , + .down = "DROP TABLE drive_file", + }, }; From 73f7022d3602c512284e75d35b6e8dc549ca79c3 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Wed, 23 Nov 2022 20:51:30 -0800 Subject: [PATCH 02/11] Start work on middleware api --- src/http/lib.zig | 2 + src/http/middleware.zig | 345 +++++++++++++++++++++++++++++++++++++++ src/main/controllers.zig | 30 +++- src/util/iters.zig | 25 ++- 4 files changed, 391 insertions(+), 11 deletions(-) create mode 100644 src/http/middleware.zig diff --git a/src/http/lib.zig b/src/http/lib.zig index 26f7756..9a4d8c9 100644 --- a/src/http/lib.zig +++ b/src/http/lib.zig @@ -15,6 +15,8 @@ pub const Response = server.Response; pub const Handler = server.Handler; pub const Server = server.Server; +pub const middleware = @import("./middleware.zig"); + pub const Fields = @import("./headers.zig").Fields; pub const Protocol = enum { diff --git a/src/http/middleware.zig b/src/http/middleware.zig new file mode 100644 index 0000000..428d52f --- /dev/null +++ b/src/http/middleware.zig @@ -0,0 +1,345 @@ +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 }), + ); + } + }; +} diff --git a/src/main/controllers.zig b/src/main/controllers.zig index 007e909..3c22e66 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -18,14 +18,36 @@ pub const timelines = @import("./controllers/api/timelines.zig"); const web = @import("./controllers/web.zig"); +const mdw = http.middleware; + +const router = Router(&.{}); + +const not_found = struct { + pub fn handler(self: @This(), _: anytype, res: anytype, ctx: anytype) !void { + var headers = http.Fields.init(ctx.allocator); + defer headers.deinit(); + + var stream = try res.open(.not_found, &headers); + defer stream.close(); + try stream.finish(); + } +}; + +const base_handler = mdw.SplitUri(mdw.CatchErrors(not_found, mdw.DefaultErrorHandler)); + +fn ApiCall(comptime Route: type) type { + return mdw. +} + pub fn routeRequest(api_source: anytype, req: *http.Request, res: *http.Response, alloc: std.mem.Allocator) void { // TODO: hashmaps? - var response = Response{ .headers = http.Fields.init(alloc), .res = res }; - defer response.headers.deinit(); + base_handler + //var response = Response{ .headers = http.Fields.init(alloc), .res = res }; + //defer response.headers.deinit(); - const found = routeRequestInternal(api_source, req, &response, alloc); + //const found = routeRequestInternal(api_source, req, &response, alloc); - if (!found) response.status(.not_found) catch {}; + //if (!found) response.status(.not_found) catch {}; } fn routeRequestInternal(api_source: anytype, req: *http.Request, res: *Response, alloc: std.mem.Allocator) bool { diff --git a/src/util/iters.zig b/src/util/iters.zig index b19c5bd..5ad2258 100644 --- a/src/util/iters.zig +++ b/src/util/iters.zig @@ -49,19 +49,30 @@ pub const QueryIter = struct { pub const PathIter = struct { is_first: bool, - iter: Separator('/'), + iter: std.mem.SplitIterator(u8), pub fn from(path: []const u8) PathIter { - return .{ .is_first = true, .iter = Separator('/').from(path) }; + return .{ .is_first = true, .iter = std.mem.split(u8, path, "/") }; } pub fn next(self: *PathIter) ?[]const u8 { - if (self.is_first) { - self.is_first = false; - return self.iter.next() orelse ""; - } + defer self.is_first = false; + while (self.iter.next()) |it| if (it.len != 0) { + return it; + }; - return self.iter.next(); + if (self.is_first) return self.iter.rest(); + + return null; + } + + pub fn first(self: *PathIter) []const u8 { + std.debug.assert(self.is_first); + return self.next().?; + } + + pub fn rest(self: *PathIter) []const u8 { + return self.iter.rest(); } }; From 03338da307711d67fef6eda79922d7801d6e9279 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Thu, 24 Nov 2022 03:30:49 -0800 Subject: [PATCH 03/11] Update server.zig to use new handlers --- src/http/middleware.zig | 105 +++++++++++++++++++++++++++------------- src/http/server.zig | 36 +++++++------- 2 files changed, 89 insertions(+), 52 deletions(-) diff --git a/src/http/middleware.zig b/src/http/middleware.zig index 428d52f..9e8ce1b 100644 --- a/src/http/middleware.zig +++ b/src/http/middleware.zig @@ -55,19 +55,27 @@ fn applyInternal(middlewares: anytype, comptime fields: []const std.builtin.Type }; } -pub fn apply(middlewares: anytype) ApplyInternal(std.meta.fields(@TypeOf(middlewares))) { +pub fn apply(middlewares: anytype) Apply(@TypeOf(middlewares)) { return applyInternal(middlewares, std.meta.fields(@TypeOf(middlewares))); } -pub fn AddContext(comptime Rhs: type) type { +pub fn Apply(comptime Middlewares: type) type { + return ApplyInternal(std.meta.fields(Middlewares)); +} + +pub fn InjectContext(comptime Values: type) type { return struct { - values: Rhs, + values: Values, 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 injectContext(values: anytype) InjectContext(@TypeOf(values)) { + return .{ .values = values }; +} + pub fn NextHandler(comptime First: type, comptime Next: type) type { return struct { first: First, @@ -146,13 +154,34 @@ pub const split_uri = struct { } }{}; -// helper function for doing route analysis -fn routeApplies(comptime R: type, req: anytype) bool { - if (R.method != req.method) return false; +// 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), - 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| { + pub fn handle(self: @This(), req: anytype, res: anytype, ctx: anytype, next: void) !void { + _ = next; + + inline for (self.routes) |r| { + if (r.handle(req, res, ctx, {})) |_| + // success + return + else |err| switch (err) { + error.RouteMismatch => {}, + else => return err, + } + } + + return error.RouteMismatch; + } + }; +} + +// helper function for doing route analysis +fn pathMatches(route: []const u8, path: []const u8) bool { + var path_iter = util.PathIter.from(path); + var route_iter = util.PathIter.from(route); + while (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 @@ -164,26 +193,37 @@ fn routeApplies(comptime R: type, req: anytype) bool { return true; } +pub const Route = struct { + pub const Desc = struct { + path: []const u8, + method: http.Method, + }; -// routes a request to the correct handler based on declared HTTP method and path -pub fn Router(comptime Routes: []const type) type { + desc: Desc, + + fn applies(self: @This(), req: anytype, ctx: anytype) bool { + if (self.desc.method != req.method) return false; + + const eff_path = if (@hasDecl(ctx, "path")) + ctx.path + else + std.mem.sliceTo(req.uri, '?'); + + return pathMatches(self.desc.path, eff_path); + } + + pub fn handle(self: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { + return if (self.applies(req, ctx)) + next.handle(req, res, ctx, {}) + else + error.RouteMismatch; + } +}; +pub fn ComptimeRoute(comptime desc: Route.Desc) 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; + const route = Route{ .desc = desc }; + pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { + return route.handle(req, res, ctx, next); } }; } @@ -327,18 +367,17 @@ pub fn ParseBody(comptime Body: type) type { }; } -pub fn ParseQueryParams(comptime Next: type, comptime QueryParams: type) type { +pub fn ParseQueryParams(comptime QueryParams: type) type { return struct { - next: Next, - - pub fn handler(self: @This(), req: anytype, res: anytype, ctx: anytype) !void { + pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { const query = try query_utils.parseQuery(ctx.allocator, QueryParams, ctx.query_string); defer ctx.allocator.free(query); - return self.next.handler( + return next.handle( req, res, - addFields(ctx, .{ .query = query }), + addFields(ctx, .{ .query_params = query }), + {}, ); } }; diff --git a/src/http/server.zig b/src/http/server.zig index b24ae75..bfb97a9 100644 --- a/src/http/server.zig +++ b/src/http/server.zig @@ -92,7 +92,6 @@ pub const Server = struct { pub fn handleLoop( self: *Server, allocator: std.mem.Allocator, - ctx: anytype, handler: anytype, ) void { while (true) { @@ -109,7 +108,6 @@ pub const Server = struct { .stream = Stream{ .kind = .tcp, .socket = conn.stream.handle }, .address = conn.address, }, - ctx, handler, ); } @@ -118,12 +116,23 @@ pub const Server = struct { fn serveConn( allocator: std.mem.Allocator, conn: Connection, - ctx: anytype, handler: anytype, ) void { while (true) { var req = request.parse(allocator, conn.stream.reader()) catch |err| { - return handleError(conn.stream.writer(), err) catch {}; + const status: http.Status = switch (err) { + error.EndOfStream => return, // Do nothing, the client closed the connection + error.BadRequest => .bad_request, + error.UnsupportedMediaType => .unsupported_media_type, + error.HttpVersionNotSupported => .http_version_not_supported, + + else => blk: { + std.log.err("Unknown error parsing request: {}\n{?s}", .{ err, @errorReturnTrace() }); + break :blk .internal_server_error; + }, + }; + + try conn.stream.writer().print("HTTP/1.1 {} {?s}\r\nConnection: close\r\n\r\n", .{ @enumToInt(status), status.phrase() }); }; var res = Response{ @@ -131,7 +140,10 @@ pub const Server = struct { .stream = conn.stream, }; - handler(ctx, &req, &res); + handler.handle(&req, &res, .{}, {}) catch |err| { + std.log.err("Unhandled error serving request: {}\n{?s}", .{ err, @errorReturnTrace() }); + return; + }; if (req.headers.get("Connection")) |hdr| { if (std.ascii.indexOfIgnoreCase(hdr, "close")) |_| return; @@ -143,17 +155,3 @@ pub const Server = struct { } } }; - -/// Writes an error response message and requests closure of the connection -fn handleError(writer: anytype, err: anyerror) !void { - const status: http.Status = switch (err) { - error.EndOfStream => return, // Do nothing, the client closed the connection - error.BadRequest => .bad_request, - error.UnsupportedMediaType => .unsupported_media_type, - error.HttpVersionNotSupported => .http_version_not_supported, - - else => .internal_server_error, - }; - - try writer.print("HTTP/1.1 {} {?s}\r\nConnection: close\r\n\r\n", .{ @enumToInt(status), status.phrase() }); -} From 051217cdaf97b52a87d42a707773b0924e03cadf Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Thu, 24 Nov 2022 03:31:24 -0800 Subject: [PATCH 04/11] Use middlewares in controller endpoint helper --- src/main/controllers.zig | 343 +++++++++++++++------------------------ 1 file changed, 132 insertions(+), 211 deletions(-) diff --git a/src/main/controllers.zig b/src/main/controllers.zig index 3c22e66..ede27c3 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -20,10 +20,15 @@ const web = @import("./controllers/web.zig"); const mdw = http.middleware; -const router = Router(&.{}); +const router = mdw.Router(&.{}); const not_found = struct { - pub fn handler(self: @This(), _: anytype, res: anytype, ctx: anytype) !void { + pub fn handler( + _: @This(), + _: anytype, + res: anytype, + ctx: anytype, + ) !void { var headers = http.Fields.init(ctx.allocator); defer headers.deinit(); @@ -35,13 +40,135 @@ const not_found = struct { const base_handler = mdw.SplitUri(mdw.CatchErrors(not_found, mdw.DefaultErrorHandler)); -fn ApiCall(comptime Route: type) type { - return mdw. +fn InjectApiConn(comptime ApiSource: type) type { + return struct { + api_source: ApiSource, + fn getApiConn(self: @This(), alloc: std.mem.Allocator, req: anytype) !ApiSource.Conn { + const host = req.headers.get("Host") orelse return error.NoHost; + const auth_header = req.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 self.api_source.connectToken(host, t, alloc); + + if (req.headers.getCookie("active_account") catch return error.BadRequest) |account| { + if (account.len + ("token.").len <= 64) { + var buf: [64]u8 = undefined; + const cookie_name = std.fmt.bufPrint(&buf, "token.{s}", .{account}) catch unreachable; + if (try req.headers.getCookie(cookie_name)) |token_hdr| { + return try self.api_source.connectToken(host, token_hdr, alloc); + } + } else return error.InvalidCookie; + } + + return try self.api_source.connectUnauthorized(host, alloc); + } + + fn handle(self: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { + var api_conn = try self.getApiConn(ctx.allocator, req); + defer api_conn.close(); + + return next.handle( + req, + res, + mdw.injectContext(.{ .api_conn = &api_conn }), + {}, + ); + } + }; +} + +pub fn EndpointRequest(comptime Endpoint: type) type { + return struct { + pub const Args = if (@hasDecl(Endpoint, "Args")) Endpoint.Args else void; + pub const Body = if (@hasDecl(Endpoint, "Body")) Endpoint.Body else void; + pub const Query = if (@hasDecl(Endpoint, "Query")) Endpoint.Query else void; + + allocator: std.mem.Allocator, + + method: http.Method, + uri: []const u8, + headers: http.Fields, + + args: Args, + body: Body, + query: Query, + + const args_middleware = if (Args == void) + mdw.injectContext(.{ .args = {} }) + else + mdw.ParsePathArgs(Args){}; + + const body_middleware = if (Body == void) + mdw.injectContext(.{ .body = {} }) + else + mdw.ParseBody(Body){}; + + const query_middleware = if (Query == void) + mdw.injectContext(.{ .query = {} }) + else + mdw.ParseQueryParams(Query){}; + }; +} + +fn CallApiEndpoint(comptime Endpoint: type) type { + return struct { + fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, _: void) !void { + const request = EndpointRequest(Endpoint){ + .allocator = ctx.allocator, + + .method = req.method, + .uri = req.uri, + .headers = req.headers, + + .args = ctx.args, + .body = ctx.body, + .query = ctx.query, + }; + + var response = Response{ .headers = http.Fields.init(ctx.allocator), .res = res }; + defer response.headers.deinit(); + + return Endpoint.handler(request, &response, ctx.api_conn); + } + }; +} + +pub fn apiEndpoint( + comptime Endpoint: type, + api_source: anytype, +) return_type: { + const RequestType = EndpointRequest(Endpoint); + break :return_type mdw.Apply(std.meta.Tuple(.{ + mdw.Route, + @TypeOf(RequestType.args_middleware), + @TypeOf(RequestType.query_middleware), + @TypeOf(RequestType.body_middleware), + // TODO: allocation strategy + InjectApiConn(@TypeOf(api_source)), + CallApiEndpoint(Endpoint), + })); +} { + const RequestType = EndpointRequest(Endpoint); + return mdw.apply(.{ + mdw.Route{ .desc = .{ .path = Endpoint.path, .method = Endpoint.method } }, + RequestType.args_middleware, + RequestType.query_middleware, + RequestType.body_middleware, + // TODO: allocation strategy + InjectApiConn(@TypeOf(api_source)){ .api_source = api_source }, + CallApiEndpoint(Endpoint){}, + }); } pub fn routeRequest(api_source: anytype, req: *http.Request, res: *http.Response, alloc: std.mem.Allocator) void { // TODO: hashmaps? - base_handler + _ = .{ api_source, req, res, alloc }; + unreachable; //var response = Response{ .headers = http.Fields.init(alloc), .res = res }; //defer response.headers.deinit(); @@ -50,14 +177,6 @@ pub fn routeRequest(api_source: anytype, req: *http.Request, res: *http.Response //if (!found) response.status(.not_found) catch {}; } -fn routeRequestInternal(api_source: anytype, req: *http.Request, res: *Response, alloc: std.mem.Allocator) bool { - inline for (routes) |route| { - if (Context(route).matchAndHandle(api_source, req, res, alloc)) return true; - } - - return false; -} - const routes = .{ auth.login, auth.verify_login, @@ -77,209 +196,11 @@ const routes = .{ follows.query_following, } ++ web.routes; -fn parseRouteArgs(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] == ':') { - const A = @TypeOf(@field(args, route_segment[1..])); - @field(args, route_segment[1..]) = try parseRouteArg(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 parseRouteArg(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)); -} - -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 const AllocationStrategy = enum { arena, normal, }; -pub fn Context(comptime Route: type) type { - return struct { - const Self = @This(); - - pub const Args = if (@hasDecl(Route, "Args")) Route.Args else void; - - // TODO: if controller does not provide a body type, maybe we should - // leave it as a simple reader instead of void - pub const Body = if (@hasDecl(Route, "Body")) Route.Body else void; - - // TODO: if controller does not provide a query type, maybe we should - // leave it as a simple string instead of void - pub const Query = if (@hasDecl(Route, "Query")) Route.Query else void; - - const allocation_strategy: AllocationStrategy = if (@hasDecl(Route, "allocation_strategy")) - Route.AllocationStrategy - else - .arena; - - base_request: *http.Request, - - allocator: std.mem.Allocator, - - method: http.Method, - uri: []const u8, - headers: http.Fields, - - args: Args, - body: Body, - query: Query, - - // TODO - body_buf: ?[]const u8 = null, - - pub fn matchAndHandle(api_source: *api.ApiSource, req: *http.Request, res: *Response, alloc: std.mem.Allocator) bool { - if (req.method != Route.method) return false; - var path = std.mem.sliceTo(std.mem.sliceTo(req.uri, '#'), '?'); - var args = parseRouteArgs(Route.path, Args, path) catch return false; - - std.log.debug("Matched route {s}", .{Route.path}); - - handle(api_source, req, res, alloc, args) catch |err| { - std.log.err("{}", .{err}); - if (!res.opened) res.err(.internal_server_error, "", {}) catch {}; - }; - - return true; - } - - fn handle( - api_source: *api.ApiSource, - req: *http.Request, - res: *Response, - base_allocator: std.mem.Allocator, - args: Args, - ) !void { - const base_content_type = matchContentType(req.headers.get("Content-Type")); - - var arena = if (allocation_strategy == .arena) - std.heap.ArenaAllocator.init(base_allocator) - else {}; - const alloc = if (allocation_strategy == .arena) arena.allocator() else base_allocator; - - const body = if (Body != void) blk: { - var stream = req.body orelse return error.NoBody; - break :blk try parseBody(Body, base_content_type orelse .json, stream.reader(), alloc); - } else {}; - defer if (Body != void) util.deepFree(alloc, body); - - const query = if (Query != void) blk: { - const path = std.mem.sliceTo(req.uri, '?'); - const q = req.uri[path.len..]; - - break :blk try query_utils.parseQuery(alloc, Query, q); - }; - defer if (Query != void) util.deepFree(alloc, query); - - var api_conn = conn: { - const host = req.headers.get("Host") orelse return error.NoHost; - const auth_header = req.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| break :conn try api_source.connectToken(host, t, alloc); - - if (req.headers.getCookie("active_account") catch return error.BadRequest) |account| { - if (account.len + ("token.").len <= 64) { - var buf: [64]u8 = undefined; - const cookie_name = std.fmt.bufPrint(&buf, "token.{s}", .{account}) catch unreachable; - if (try req.headers.getCookie(cookie_name)) |token_hdr| { - break :conn try api_source.connectToken(host, token_hdr, alloc); - } - } else return error.InvalidCookie; - } - - break :conn try api_source.connectUnauthorized(host, alloc); - }; - defer api_conn.close(); - - const self = Self{ - .allocator = alloc, - .base_request = req, - - .method = req.method, - .uri = req.uri, - .headers = req.headers, - - .args = args, - .body = body, - .query = query, - }; - - try Route.handler(self, res, &api_conn); - } - - fn errorHandler(response: *Response, status: http.Status, err: anytype) void { - std.log.err("Error occured on handler {s} {s}", .{ @tagName(Route.method), Route.path }); - std.log.err("{}", .{err}); - const result = if (builtin.mode == .Debug) - response.err(status, @errorName(err), {}) - else - response.status(status); - _ = result catch |err2| { - std.log.err("Error printing response: {}", .{err2}); - }; - } - }; -} - pub const Response = struct { const Self = @This(); headers: http.Fields, From 503ab62607c6f50ebd0ff03cec8fb893b597b23c Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Thu, 24 Nov 2022 03:50:25 -0800 Subject: [PATCH 05/11] Get api source from context --- src/main/controllers.zig | 105 +++++++++++++---------------------- src/main/controllers/api.zig | 29 ++++++++++ src/main/controllers/web.zig | 11 ++-- 3 files changed, 73 insertions(+), 72 deletions(-) create mode 100644 src/main/controllers/api.zig diff --git a/src/main/controllers.zig b/src/main/controllers.zig index ede27c3..d965a07 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -7,16 +7,10 @@ const util = @import("util"); const query_utils = @import("./query.zig"); const json_utils = @import("./json.zig"); -pub const auth = @import("./controllers/api/auth.zig"); -pub const communities = @import("./controllers/api/communities.zig"); -pub const invites = @import("./controllers/api/invites.zig"); -pub const users = @import("./controllers/api/users.zig"); -pub const follows = @import("./controllers/api/users/follows.zig"); -pub const notes = @import("./controllers/api/notes.zig"); -pub const streaming = @import("./controllers/api/streaming.zig"); -pub const timelines = @import("./controllers/api/timelines.zig"); +const web_controllers = @import("./controllers/web.zig"); +const api_controllers = @import("./controllers/api.zig"); -const web = @import("./controllers/web.zig"); +const routes = api_controllers ++ web_controllers.routes; const mdw = http.middleware; @@ -40,47 +34,44 @@ const not_found = struct { const base_handler = mdw.SplitUri(mdw.CatchErrors(not_found, mdw.DefaultErrorHandler)); -fn InjectApiConn(comptime ApiSource: type) type { - return struct { - api_source: ApiSource, - fn getApiConn(self: @This(), alloc: std.mem.Allocator, req: anytype) !ApiSource.Conn { - const host = req.headers.get("Host") orelse return error.NoHost; - const auth_header = req.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; +const inject_api_conn = struct { + fn getApiConn(alloc: std.mem.Allocator, api_source: anytype, req: anytype) !@TypeOf(api_source).Conn { + const host = req.headers.get("Host") orelse return error.NoHost; + const auth_header = req.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 self.api_source.connectToken(host, t, alloc); + if (token) |t| return try api_source.connectToken(host, t, alloc); - if (req.headers.getCookie("active_account") catch return error.BadRequest) |account| { - if (account.len + ("token.").len <= 64) { - var buf: [64]u8 = undefined; - const cookie_name = std.fmt.bufPrint(&buf, "token.{s}", .{account}) catch unreachable; - if (try req.headers.getCookie(cookie_name)) |token_hdr| { - return try self.api_source.connectToken(host, token_hdr, alloc); - } - } else return error.InvalidCookie; - } - - return try self.api_source.connectUnauthorized(host, alloc); + if (req.headers.getCookie("active_account") catch return error.BadRequest) |account| { + if (account.len + ("token.").len <= 64) { + var buf: [64]u8 = undefined; + const cookie_name = std.fmt.bufPrint(&buf, "token.{s}", .{account}) catch unreachable; + if (try req.headers.getCookie(cookie_name)) |token_hdr| { + return try api_source.connectToken(host, token_hdr, alloc); + } + } else return error.InvalidCookie; } - fn handle(self: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { - var api_conn = try self.getApiConn(ctx.allocator, req); - defer api_conn.close(); + return try api_source.connectUnauthorized(host, alloc); + } - return next.handle( - req, - res, - mdw.injectContext(.{ .api_conn = &api_conn }), - {}, - ); - } - }; -} + pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { + var api_conn = try getApiConn(ctx.allocator, ctx.api_source, req); + defer api_conn.close(); + + return next.handle( + req, + res, + mdw.injectContext(.{ .api_conn = &api_conn }), + {}, + ); + } +}; pub fn EndpointRequest(comptime Endpoint: type) type { return struct { @@ -140,7 +131,6 @@ fn CallApiEndpoint(comptime Endpoint: type) type { pub fn apiEndpoint( comptime Endpoint: type, - api_source: anytype, ) return_type: { const RequestType = EndpointRequest(Endpoint); break :return_type mdw.Apply(std.meta.Tuple(.{ @@ -149,7 +139,7 @@ pub fn apiEndpoint( @TypeOf(RequestType.query_middleware), @TypeOf(RequestType.body_middleware), // TODO: allocation strategy - InjectApiConn(@TypeOf(api_source)), + @TypeOf(inject_api_conn), CallApiEndpoint(Endpoint), })); } { @@ -160,7 +150,7 @@ pub fn apiEndpoint( RequestType.query_middleware, RequestType.body_middleware, // TODO: allocation strategy - InjectApiConn(@TypeOf(api_source)){ .api_source = api_source }, + inject_api_conn, CallApiEndpoint(Endpoint){}, }); } @@ -177,25 +167,6 @@ pub fn routeRequest(api_source: anytype, req: *http.Request, res: *http.Response //if (!found) response.status(.not_found) catch {}; } -const routes = .{ - auth.login, - auth.verify_login, - communities.create, - communities.query, - invites.create, - users.create, - notes.create, - notes.get, - streaming.streaming, - timelines.global, - timelines.local, - timelines.home, - follows.create, - follows.delete, - follows.query_followers, - follows.query_following, -} ++ web.routes; - pub const AllocationStrategy = enum { arena, normal, diff --git a/src/main/controllers/api.zig b/src/main/controllers/api.zig new file mode 100644 index 0000000..38de71e --- /dev/null +++ b/src/main/controllers/api.zig @@ -0,0 +1,29 @@ +const controllers = @import("../controllers.zig"); + +const auth = @import("./api/auth.zig"); +const communities = @import("./api/communities.zig"); +const invites = @import("./api/invites.zig"); +const users = @import("./api/users.zig"); +const follows = @import("./api/users/follows.zig"); +const notes = @import("./api/notes.zig"); +const streaming = @import("./api/streaming.zig"); +const timelines = @import("./api/timelines.zig"); + +pub const routes = .{ + controllers.apiEndpoint(auth.login), + controllers.apiEndpoint(auth.verify_login), + controllers.apiEndpoint(communities.create), + controllers.apiEndpoint(communities.query), + controllers.apiEndpoint(invites.create), + controllers.apiEndpoint(users.create), + controllers.apiEndpoint(notes.create), + controllers.apiEndpoint(notes.get), + controllers.apiEndpoint(streaming.streaming), + controllers.apiEndpoint(timelines.global), + controllers.apiEndpoint(timelines.local), + controllers.apiEndpoint(timelines.home), + controllers.apiEndpoint(follows.create), + controllers.apiEndpoint(follows.delete), + controllers.apiEndpoint(follows.query_followers), + controllers.apiEndpoint(follows.query_following), +}; diff --git a/src/main/controllers/web.zig b/src/main/controllers/web.zig index 9ee8ac4..430e405 100644 --- a/src/main/controllers/web.zig +++ b/src/main/controllers/web.zig @@ -1,11 +1,12 @@ const std = @import("std"); +const controllers = @import("../controllers.zig"); pub const routes = .{ - index, - about, - login, - global_timeline, - cluster.overview, + controllers.apiEndpoint(index), + controllers.apiEndpoint(about), + controllers.apiEndpoint(login), + controllers.apiEndpoint(global_timeline), + controllers.apiEndpoint(cluster.overview), }; const index = struct { From 039377f16843e72bd20e96d99ba9978e3659c2f1 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Thu, 24 Nov 2022 03:55:47 -0800 Subject: [PATCH 06/11] Add router helper fn --- src/http/middleware.zig | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/http/middleware.zig b/src/http/middleware.zig index 9e8ce1b..7f4e5f6 100644 --- a/src/http/middleware.zig +++ b/src/http/middleware.zig @@ -176,6 +176,14 @@ pub fn Router(comptime Routes: []const type) type { } }; } +fn fieldTypes(comptime Tuple: type) []const type { + var types: [Tuple.len]type = undefined; + for (std.meta.fields(Tuple)) |f, i| types[i] = f.field_type; + return &types; +} +pub fn router(routes: anytype) Router(fieldTypes(@TypeOf(routes))) { + return Router(fieldTypes(@TypeOf(routes))){ .routes = routes }; +} // helper function for doing route analysis fn pathMatches(route: []const u8, path: []const u8) bool { @@ -219,14 +227,6 @@ pub const Route = struct { error.RouteMismatch; } }; -pub fn ComptimeRoute(comptime desc: Route.Desc) type { - return struct { - const route = Route{ .desc = desc }; - pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { - return route.handle(req, res, ctx, next); - } - }; -} pub fn Mount(comptime route: []const u8) type { return struct { From a28af9e1fe5f42552f5702c9e4b187073547f549 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Fri, 25 Nov 2022 17:42:58 -0800 Subject: [PATCH 07/11] Move json and query helpers --- src/http/json.zig | 677 +++++++++++++++++++++++++++++++++++++++++++++ src/http/query.zig | 380 +++++++++++++++++++++++++ 2 files changed, 1057 insertions(+) create mode 100644 src/http/json.zig create mode 100644 src/http/query.zig diff --git a/src/http/json.zig b/src/http/json.zig new file mode 100644 index 0000000..21474cc --- /dev/null +++ b/src/http/json.zig @@ -0,0 +1,677 @@ +const std = @import("std"); +const mem = std.mem; +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; + +// This file is largely a copy of std.json + +const StreamingParser = std.json.StreamingParser; +const Token = std.json.Token; +const unescapeValidString = std.json.unescapeValidString; +const UnescapeValidStringError = std.json.UnescapeValidStringError; + +pub fn parse(comptime T: type, body: []const u8, alloc: std.mem.Allocator) !T { + var tokens = TokenStream.init(body); + + const options = ParseOptions{ .allocator = alloc }; + + const token = (try tokens.next()) orelse return error.UnexpectedEndOfJson; + const r = try parseInternal(T, token, &tokens, options); + errdefer parseFreeInternal(T, r, options); + if (!options.allow_trailing_data) { + if ((try tokens.next()) != null) unreachable; + assert(tokens.i >= tokens.slice.len); + } + return r; +} + +pub fn parseFree(value: anytype, alloc: std.mem.Allocator) void { + parseFreeInternal(@TypeOf(value), value, .{ .allocator = alloc }); +} + +// WARNING: the objects "parse" method must not contain a reference to the original value +fn hasCustomParse(comptime T: type) bool { + if (!std.meta.trait.hasFn("parse")(T)) return false; + if (!@hasDecl(T, "JsonParseAs")) return false; + + return true; +} + +///// The rest is (modified) from std.json + +/// A small wrapper over a StreamingParser for full slices. Returns a stream of json Tokens. +pub const TokenStream = struct { + i: usize, + slice: []const u8, + parser: StreamingParser, + token: ?Token, + + pub const Error = StreamingParser.Error || error{UnexpectedEndOfJson}; + + pub fn init(slice: []const u8) TokenStream { + return TokenStream{ + .i = 0, + .slice = slice, + .parser = StreamingParser.init(), + .token = null, + }; + } + + fn stackUsed(self: *TokenStream) usize { + return self.parser.stack.len + if (self.token != null) @as(usize, 1) else 0; + } + + pub fn next(self: *TokenStream) Error!?Token { + if (self.token) |token| { + self.token = null; + return token; + } + + var t1: ?Token = undefined; + var t2: ?Token = undefined; + + while (self.i < self.slice.len) { + try self.parser.feed(self.slice[self.i], &t1, &t2); + self.i += 1; + + if (t1) |token| { + self.token = t2; + return token; + } + } + + // Without this a bare number fails, the streaming parser doesn't know the input ended + try self.parser.feed(' ', &t1, &t2); + self.i += 1; + + if (t1) |token| { + return token; + } else if (self.parser.complete) { + return null; + } else { + return error.UnexpectedEndOfJson; + } + } +}; + +/// Checks to see if a string matches what it would be as a json-encoded string +/// Assumes that `encoded` is a well-formed json string +fn encodesTo(decoded: []const u8, encoded: []const u8) bool { + var i: usize = 0; + var j: usize = 0; + while (i < decoded.len) { + if (j >= encoded.len) return false; + if (encoded[j] != '\\') { + if (decoded[i] != encoded[j]) return false; + j += 1; + i += 1; + } else { + const escape_type = encoded[j + 1]; + if (escape_type != 'u') { + const t: u8 = switch (escape_type) { + '\\' => '\\', + '/' => '/', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + 'f' => 12, + 'b' => 8, + '"' => '"', + else => unreachable, + }; + if (decoded[i] != t) return false; + j += 2; + i += 1; + } else { + var codepoint = std.fmt.parseInt(u21, encoded[j + 2 .. j + 6], 16) catch unreachable; + j += 6; + if (codepoint >= 0xD800 and codepoint < 0xDC00) { + // surrogate pair + assert(encoded[j] == '\\'); + assert(encoded[j + 1] == 'u'); + const low_surrogate = std.fmt.parseInt(u21, encoded[j + 2 .. j + 6], 16) catch unreachable; + codepoint = 0x10000 + (((codepoint & 0x03ff) << 10) | (low_surrogate & 0x03ff)); + j += 6; + } + var buf: [4]u8 = undefined; + const len = std.unicode.utf8Encode(codepoint, &buf) catch unreachable; + if (i + len > decoded.len) return false; + if (!mem.eql(u8, decoded[i .. i + len], buf[0..len])) return false; + i += len; + } + } + } + assert(i == decoded.len); + assert(j == encoded.len); + return true; +} + +/// parse tokens from a stream, returning `false` if they do not decode to `value` +fn parsesTo(comptime T: type, value: T, tokens: *TokenStream, options: ParseOptions) !bool { + // TODO: should be able to write this function to not require an allocator + const tmp = try parse(T, tokens, options); + defer parseFree(T, tmp, options); + + return parsedEqual(tmp, value); +} + +/// Returns if a value returned by `parse` is deep-equal to another value +fn parsedEqual(a: anytype, b: @TypeOf(a)) bool { + switch (@typeInfo(@TypeOf(a))) { + .Optional => { + if (a == null and b == null) return true; + if (a == null or b == null) return false; + return parsedEqual(a.?, b.?); + }, + .Union => |info| { + if (info.tag_type) |UnionTag| { + const tag_a = std.meta.activeTag(a); + const tag_b = std.meta.activeTag(b); + if (tag_a != tag_b) return false; + + inline for (info.fields) |field_info| { + if (@field(UnionTag, field_info.name) == tag_a) { + return parsedEqual(@field(a, field_info.name), @field(b, field_info.name)); + } + } + return false; + } else { + unreachable; + } + }, + .Array => { + for (a) |e, i| + if (!parsedEqual(e, b[i])) return false; + return true; + }, + .Struct => |info| { + inline for (info.fields) |field_info| { + if (!parsedEqual(@field(a, field_info.name), @field(b, field_info.name))) return false; + } + return true; + }, + .Pointer => |ptrInfo| switch (ptrInfo.size) { + .One => return parsedEqual(a.*, b.*), + .Slice => { + if (a.len != b.len) return false; + for (a) |e, i| + if (!parsedEqual(e, b[i])) return false; + return true; + }, + .Many, .C => unreachable, + }, + else => return a == b, + } + unreachable; +} + +const ParseOptions = struct { + allocator: ?Allocator = null, + + /// Behaviour when a duplicate field is encountered. + duplicate_field_behavior: enum { + UseFirst, + Error, + UseLast, + } = .Error, + + /// If false, finding an unknown field returns an error. + ignore_unknown_fields: bool = false, + + allow_trailing_data: bool = false, +}; + +const SkipValueError = error{UnexpectedJsonDepth} || TokenStream.Error; + +fn skipValue(tokens: *TokenStream) SkipValueError!void { + const original_depth = tokens.stackUsed(); + + // Return an error if no value is found + _ = try tokens.next(); + if (tokens.stackUsed() < original_depth) return error.UnexpectedJsonDepth; + if (tokens.stackUsed() == original_depth) return; + + while (try tokens.next()) |_| { + if (tokens.stackUsed() == original_depth) return; + } +} + +fn ParseInternalError(comptime T: type) type { + // `inferred_types` is used to avoid infinite recursion for recursive type definitions. + const inferred_types = [_]type{}; + return ParseInternalErrorImpl(T, &inferred_types); +} + +fn ParseInternalErrorImpl(comptime T: type, comptime inferred_types: []const type) type { + if (hasCustomParse(T)) { + return ParseInternalError(T.JsonParseAs) || T.ParseError; + } + for (inferred_types) |ty| { + if (T == ty) return error{}; + } + + switch (@typeInfo(T)) { + .Bool => return error{UnexpectedToken}, + .Float, .ComptimeFloat => return error{UnexpectedToken} || std.fmt.ParseFloatError, + .Int, .ComptimeInt => { + return error{ UnexpectedToken, InvalidNumber, Overflow } || + std.fmt.ParseIntError || std.fmt.ParseFloatError; + }, + .Optional => |optionalInfo| { + return ParseInternalErrorImpl(optionalInfo.child, inferred_types ++ [_]type{T}); + }, + .Enum => return error{ UnexpectedToken, InvalidEnumTag } || std.fmt.ParseIntError || + std.meta.IntToEnumError || std.meta.IntToEnumError, + .Union => |unionInfo| { + if (unionInfo.tag_type) |_| { + var errors = error{NoUnionMembersMatched}; + for (unionInfo.fields) |u_field| { + errors = errors || ParseInternalErrorImpl(u_field.field_type, inferred_types ++ [_]type{T}); + } + return errors; + } else { + @compileError("Unable to parse into untagged union '" ++ @typeName(T) ++ "'"); + } + }, + .Struct => |structInfo| { + var errors = error{ + DuplicateJSONField, + UnexpectedEndOfJson, + UnexpectedToken, + UnexpectedValue, + UnknownField, + MissingField, + } || SkipValueError || TokenStream.Error; + for (structInfo.fields) |field| { + errors = errors || ParseInternalErrorImpl(field.field_type, inferred_types ++ [_]type{T}); + } + return errors; + }, + .Array => |arrayInfo| { + return error{ UnexpectedEndOfJson, UnexpectedToken } || TokenStream.Error || + UnescapeValidStringError || + ParseInternalErrorImpl(arrayInfo.child, inferred_types ++ [_]type{T}); + }, + .Pointer => |ptrInfo| { + var errors = error{AllocatorRequired} || std.mem.Allocator.Error; + switch (ptrInfo.size) { + .One => { + return errors || ParseInternalErrorImpl(ptrInfo.child, inferred_types ++ [_]type{T}); + }, + .Slice => { + return errors || error{ UnexpectedEndOfJson, UnexpectedToken } || + ParseInternalErrorImpl(ptrInfo.child, inferred_types ++ [_]type{T}) || + UnescapeValidStringError || TokenStream.Error; + }, + else => @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"), + } + }, + else => return error{}, + } + unreachable; +} + +fn parseInternal( + comptime T: type, + token: Token, + tokens: *TokenStream, + options: ParseOptions, +) ParseInternalError(T)!T { + if (comptime hasCustomParse(T)) { + const val = try parseInternal(T.JsonParseAs, token, tokens, options); + defer parseFreeInternal(T.JsonParseAs, val, options); + return try T.parse(val); + } + + switch (@typeInfo(T)) { + .Bool => { + return switch (token) { + .True => true, + .False => false, + else => error.UnexpectedToken, + }; + }, + .Float, .ComptimeFloat => { + switch (token) { + .Number => |numberToken| return try std.fmt.parseFloat(T, numberToken.slice(tokens.slice, tokens.i - 1)), + .String => |stringToken| return try std.fmt.parseFloat(T, stringToken.slice(tokens.slice, tokens.i - 1)), + else => return error.UnexpectedToken, + } + }, + .Int, .ComptimeInt => { + switch (token) { + .Number => |numberToken| { + if (numberToken.is_integer) + return try std.fmt.parseInt(T, numberToken.slice(tokens.slice, tokens.i - 1), 10); + const float = try std.fmt.parseFloat(f128, numberToken.slice(tokens.slice, tokens.i - 1)); + if (@round(float) != float) return error.InvalidNumber; + if (float > std.math.maxInt(T) or float < std.math.minInt(T)) return error.Overflow; + return @floatToInt(T, float); + }, + .String => |stringToken| { + return std.fmt.parseInt(T, stringToken.slice(tokens.slice, tokens.i - 1), 10) catch |err| { + switch (err) { + error.Overflow => return err, + error.InvalidCharacter => { + const float = try std.fmt.parseFloat(f128, stringToken.slice(tokens.slice, tokens.i - 1)); + if (@round(float) != float) return error.InvalidNumber; + if (float > std.math.maxInt(T) or float < std.math.minInt(T)) return error.Overflow; + return @floatToInt(T, float); + }, + } + }; + }, + else => return error.UnexpectedToken, + } + }, + .Optional => |optionalInfo| { + if (token == .Null) { + return null; + } else { + return try parseInternal(optionalInfo.child, token, tokens, options); + } + }, + .Enum => |enumInfo| { + switch (token) { + .Number => |numberToken| { + if (!numberToken.is_integer) return error.UnexpectedToken; + const n = try std.fmt.parseInt(enumInfo.tag_type, numberToken.slice(tokens.slice, tokens.i - 1), 10); + return try std.meta.intToEnum(T, n); + }, + .String => |stringToken| { + const source_slice = stringToken.slice(tokens.slice, tokens.i - 1); + switch (stringToken.escapes) { + .None => return std.meta.stringToEnum(T, source_slice) orelse return error.InvalidEnumTag, + .Some => { + inline for (enumInfo.fields) |field| { + if (field.name.len == stringToken.decodedLength() and encodesTo(field.name, source_slice)) { + return @field(T, field.name); + } + } + return error.InvalidEnumTag; + }, + } + }, + else => return error.UnexpectedToken, + } + }, + .Union => |unionInfo| { + if (unionInfo.tag_type) |_| { + // try each of the union fields until we find one that matches + inline for (unionInfo.fields) |u_field| { + // take a copy of tokens so we can withhold mutations until success + var tokens_copy = tokens.*; + if (parseInternal(u_field.field_type, token, &tokens_copy, options)) |value| { + tokens.* = tokens_copy; + return @unionInit(T, u_field.name, value); + } else |err| { + // Bubble up error.OutOfMemory + // Parsing some types won't have OutOfMemory in their + // error-sets, for the condition to be valid, merge it in. + if (@as(@TypeOf(err) || error{OutOfMemory}, err) == error.OutOfMemory) return err; + // Bubble up AllocatorRequired, as it indicates missing option + if (@as(@TypeOf(err) || error{AllocatorRequired}, err) == error.AllocatorRequired) return err; + // otherwise continue through the `inline for` + } + } + return error.NoUnionMembersMatched; + } else { + @compileError("Unable to parse into untagged union '" ++ @typeName(T) ++ "'"); + } + }, + .Struct => |structInfo| { + switch (token) { + .ObjectBegin => {}, + else => return error.UnexpectedToken, + } + var r: T = undefined; + var fields_seen = [_]bool{false} ** structInfo.fields.len; + errdefer { + inline for (structInfo.fields) |field, i| { + if (fields_seen[i] and !field.is_comptime) { + parseFreeInternal(field.field_type, @field(r, field.name), options); + } + } + } + + while (true) { + switch ((try tokens.next()) orelse return error.UnexpectedEndOfJson) { + .ObjectEnd => break, + .String => |stringToken| { + const key_source_slice = stringToken.slice(tokens.slice, tokens.i - 1); + var child_options = options; + child_options.allow_trailing_data = true; + var found = false; + inline for (structInfo.fields) |field, i| { + // TODO: using switches here segfault the compiler (#2727?) + if ((stringToken.escapes == .None and mem.eql(u8, field.name, key_source_slice)) or (stringToken.escapes == .Some and (field.name.len == stringToken.decodedLength() and encodesTo(field.name, key_source_slice)))) { + // if (switch (stringToken.escapes) { + // .None => mem.eql(u8, field.name, key_source_slice), + // .Some => (field.name.len == stringToken.decodedLength() and encodesTo(field.name, key_source_slice)), + // }) { + if (fields_seen[i]) { + // switch (options.duplicate_field_behavior) { + // .UseFirst => {}, + // .Error => {}, + // .UseLast => {}, + // } + if (options.duplicate_field_behavior == .UseFirst) { + // unconditonally ignore value. for comptime fields, this skips check against default_value + const next_token = (try tokens.next()) orelse return error.UnexpectedEndOfJson; + parseFreeInternal(field.field_type, try parseInternal(field.field_type, next_token, tokens, child_options), child_options); + found = true; + break; + } else if (options.duplicate_field_behavior == .Error) { + return error.DuplicateJSONField; + } else if (options.duplicate_field_behavior == .UseLast) { + if (!field.is_comptime) { + parseFreeInternal(field.field_type, @field(r, field.name), child_options); + } + fields_seen[i] = false; + } + } + if (field.is_comptime) { + if (!try parsesTo(field.field_type, @ptrCast(*const field.field_type, field.default_value.?).*, tokens, child_options)) { + return error.UnexpectedValue; + } + } else { + const next_token = (try tokens.next()) orelse return error.UnexpectedEndOfJson; + @field(r, field.name) = try parseInternal(field.field_type, next_token, tokens, child_options); + } + fields_seen[i] = true; + found = true; + break; + } + } + if (!found) { + if (options.ignore_unknown_fields) { + try skipValue(tokens); + continue; + } else { + return error.UnknownField; + } + } + }, + else => return error.UnexpectedToken, + } + } + inline for (structInfo.fields) |field, i| { + if (!fields_seen[i]) { + if (field.default_value) |default_ptr| { + if (!field.is_comptime) { + const default = @ptrCast(*align(1) const field.field_type, default_ptr).*; + @field(r, field.name) = default; + } + } else { + return error.MissingField; + } + } + } + return r; + }, + .Array => |arrayInfo| { + switch (token) { + .ArrayBegin => { + var r: T = undefined; + var i: usize = 0; + var child_options = options; + child_options.allow_trailing_data = true; + errdefer { + // Without the r.len check `r[i]` is not allowed + if (r.len > 0) while (true) : (i -= 1) { + parseFreeInternal(arrayInfo.child, r[i], options); + if (i == 0) break; + }; + } + while (i < r.len) : (i += 1) { + const next_token = (try tokens.next()) orelse return error.UnexpectedEndOfJson; + r[i] = try parseInternal(arrayInfo.child, next_token, tokens, child_options); + } + const tok = (try tokens.next()) orelse return error.UnexpectedEndOfJson; + switch (tok) { + .ArrayEnd => {}, + else => return error.UnexpectedToken, + } + return r; + }, + .String => |stringToken| { + if (arrayInfo.child != u8) return error.UnexpectedToken; + var r: T = undefined; + const source_slice = stringToken.slice(tokens.slice, tokens.i - 1); + switch (stringToken.escapes) { + .None => mem.copy(u8, &r, source_slice), + .Some => try unescapeValidString(&r, source_slice), + } + return r; + }, + else => return error.UnexpectedToken, + } + }, + .Pointer => |ptrInfo| { + const allocator = options.allocator orelse return error.AllocatorRequired; + switch (ptrInfo.size) { + .One => { + const r: T = try allocator.create(ptrInfo.child); + errdefer allocator.destroy(r); + r.* = try parseInternal(ptrInfo.child, token, tokens, options); + return r; + }, + .Slice => { + switch (token) { + .ArrayBegin => { + var arraylist = std.ArrayList(ptrInfo.child).init(allocator); + errdefer { + while (arraylist.popOrNull()) |v| { + parseFreeInternal(ptrInfo.child, v, options); + } + arraylist.deinit(); + } + + while (true) { + const tok = (try tokens.next()) orelse return error.UnexpectedEndOfJson; + switch (tok) { + .ArrayEnd => break, + else => {}, + } + + try arraylist.ensureUnusedCapacity(1); + const v = try parseInternal(ptrInfo.child, tok, tokens, options); + arraylist.appendAssumeCapacity(v); + } + + if (ptrInfo.sentinel) |some| { + const sentinel_value = @ptrCast(*const ptrInfo.child, some).*; + try arraylist.append(sentinel_value); + const output = arraylist.toOwnedSlice(); + return output[0 .. output.len - 1 :sentinel_value]; + } + + return arraylist.toOwnedSlice(); + }, + .String => |stringToken| { + if (ptrInfo.child != u8) return error.UnexpectedToken; + const source_slice = stringToken.slice(tokens.slice, tokens.i - 1); + const len = stringToken.decodedLength(); + const output = try allocator.alloc(u8, len + @boolToInt(ptrInfo.sentinel != null)); + errdefer allocator.free(output); + switch (stringToken.escapes) { + .None => mem.copy(u8, output, source_slice), + .Some => try unescapeValidString(output, source_slice), + } + + if (ptrInfo.sentinel) |some| { + const char = @ptrCast(*const u8, some).*; + output[len] = char; + return output[0..len :char]; + } + + return output; + }, + else => return error.UnexpectedToken, + } + }, + else => @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"), + } + }, + else => @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"), + } + unreachable; +} + +fn ParseError(comptime T: type) type { + return ParseInternalError(T) || error{UnexpectedEndOfJson} || TokenStream.Error; +} + +/// Releases resources created by `parse`. +/// Should be called with the same type and `ParseOptions` that were passed to `parse` +fn parseFreeInternal(comptime T: type, value: T, options: ParseOptions) void { + switch (@typeInfo(T)) { + .Bool, .Float, .ComptimeFloat, .Int, .ComptimeInt, .Enum => {}, + .Optional => { + if (value) |v| { + return parseFreeInternal(@TypeOf(v), v, options); + } + }, + .Union => |unionInfo| { + if (unionInfo.tag_type) |UnionTagType| { + inline for (unionInfo.fields) |u_field| { + if (value == @field(UnionTagType, u_field.name)) { + parseFreeInternal(u_field.field_type, @field(value, u_field.name), options); + break; + } + } + } else { + unreachable; + } + }, + .Struct => |structInfo| { + inline for (structInfo.fields) |field| { + if (!field.is_comptime) { + parseFreeInternal(field.field_type, @field(value, field.name), options); + } + } + }, + .Array => |arrayInfo| { + for (value) |v| { + parseFreeInternal(arrayInfo.child, v, options); + } + }, + .Pointer => |ptrInfo| { + const allocator = options.allocator orelse unreachable; + switch (ptrInfo.size) { + .One => { + parseFreeInternal(ptrInfo.child, value.*, options); + allocator.destroy(value); + }, + .Slice => { + for (value) |v| { + parseFreeInternal(ptrInfo.child, v, options); + } + allocator.free(value); + }, + else => unreachable, + } + }, + else => unreachable, + } +} diff --git a/src/http/query.zig b/src/http/query.zig new file mode 100644 index 0000000..1933429 --- /dev/null +++ b/src/http/query.zig @@ -0,0 +1,380 @@ +const std = @import("std"); + +const QueryIter = @import("util").QueryIter; + +/// Parses a set of query parameters described by the struct `T`. +/// +/// To specify query parameters, provide a struct similar to the following: +/// ``` +/// struct { +/// foo: bool = false, +/// bar: ?[]const u8 = null, +/// baz: usize = 10, +/// qux: enum { quux, snap } = .quux, +/// } +/// ``` +/// +/// This will allow it to parse a query string like the following: +/// `?foo&bar=abc&qux=snap` +/// +/// Every parameter must have a default value that will be used when the +/// parameter is not provided, and parameter keys. +/// Numbers are parsed from their string representations, and a parameter +/// provided in the query string without a value is parsed either as a bool +/// `true` flag or as `null` depending on the type of its param. +/// +/// Parameter types supported: +/// - []const u8 +/// - numbers (both integer and float) +/// + Numbers are parsed in base 10 +/// - bool +/// + See below for detals +/// - exhaustive enums +/// + Enums are treated as strings with values equal to the enum fields +/// - ?F (where isScalar(F) and F != bool) +/// - Any type that implements: +/// + pub fn parse([]const u8) !F +/// +/// Boolean Parameters: +/// The following query strings will all parse a `true` value for the +/// parameter `foo: bool = false`: +/// - `?foo` +/// - `?foo=true` +/// - `?foo=t` +/// - `?foo=yes` +/// - `?foo=y` +/// - `?foo=1` +/// And the following query strings all parse a `false` value: +/// - `?` +/// - `?foo=false` +/// - `?foo=f` +/// - `?foo=no` +/// - `?foo=n` +/// - `?foo=0` +/// +/// Compound Types: +/// Compound (struct) types are also supported, with the parameter key +/// for its parameters consisting of the struct's field + '.' + parameter +/// field. For example: +/// ``` +/// struct { +/// foo: struct { +/// baz: usize = 0, +/// } = .{}, +/// } +/// ``` +/// Would be used to parse a query string like +/// `?foo.baz=12345` +/// +/// Compound types cannot currently be nullable, and must be structs. +/// +/// TODO: values are currently case-sensitive, and are not url-decoded properly. +/// This should be fixed. +pub fn parseQuery(alloc: std.mem.Allocator, comptime T: type, query: []const u8) !T { + if (comptime !std.meta.trait.isContainer(T)) @compileError("T must be a struct"); + var iter = QueryIter.from(query); + + var fields = Intermediary(T){}; + while (iter.next()) |pair| { + // TODO: Hash map + inline for (std.meta.fields(Intermediary(T))) |field| { + if (std.ascii.eqlIgnoreCase(field.name[2..], pair.key)) { + @field(fields, field.name) = if (pair.value) |v| .{ .value = v } else .{ .no_value = {} }; + break; + } + } else std.log.debug("unknown param {s}", .{pair.key}); + } + + return (try parse(alloc, T, "", "", fields)) orelse error.NoQuery; +} + +fn decodeString(alloc: std.mem.Allocator, val: []const u8) ![]const u8 { + var list = try std.ArrayList(u8).initCapacity(alloc, val.len); + errdefer list.deinit(); + + var idx: usize = 0; + while (idx < val.len) : (idx += 1) { + if (val[idx] != '%') { + try list.append(val[idx]); + } else { + if (val.len < idx + 2) return error.InvalidEscape; + const buf = [2]u8{ val[idx + 1], val[idx + 2] }; + idx += 2; + + const ch = try std.fmt.parseInt(u8, &buf, 16); + try list.append(ch); + } + } + + return list.toOwnedSlice(); +} + +fn parseScalar(alloc: std.mem.Allocator, comptime T: type, comptime name: []const u8, fields: anytype) !?T { + const param = @field(fields, name); + return switch (param) { + .not_specified => null, + .no_value => try parseQueryValue(alloc, T, null), + .value => |v| try parseQueryValue(alloc, T, v), + }; +} + +fn parse( + alloc: std.mem.Allocator, + comptime T: type, + comptime prefix: []const u8, + comptime name: []const u8, + fields: anytype, +) !?T { + if (comptime isScalar(T)) return parseScalar(alloc, T, prefix ++ "." ++ name, fields); + switch (@typeInfo(T)) { + .Union => |info| { + var result: ?T = null; + inline for (info.fields) |field| { + const F = field.field_type; + + const maybe_value = try parse(alloc, F, prefix, field.name, fields); + if (maybe_value) |value| { + if (result != null) return error.DuplicateUnionField; + + result = @unionInit(T, field.name, value); + } + } + std.log.debug("{any}", .{result}); + return result; + }, + + .Struct => |info| { + var result: T = undefined; + var fields_specified: usize = 0; + + inline for (info.fields) |field| { + const F = field.field_type; + + var maybe_value: ?F = null; + if (try parse(alloc, F, prefix ++ "." ++ name, field.name, fields)) |v| { + maybe_value = v; + } else if (field.default_value) |default| { + if (comptime @sizeOf(F) != 0) { + maybe_value = @ptrCast(*const F, @alignCast(@alignOf(F), default)).*; + } else { + maybe_value = std.mem.zeroes(F); + } + } + + if (maybe_value) |v| { + fields_specified += 1; + @field(result, field.name) = v; + } + } + + if (fields_specified == 0) { + return null; + } else if (fields_specified != info.fields.len) { + std.log.debug("{} {s} {s}", .{ T, prefix, name }); + return error.PartiallySpecifiedStruct; + } else { + return result; + } + }, + + // Only applies to non-scalar optionals + .Optional => |info| return try parse(alloc, info.child, prefix, name, fields), + + else => @compileError("tmp"), + } +} + +fn recursiveFieldPaths(comptime T: type, comptime prefix: []const u8) []const []const u8 { + comptime { + if (std.meta.trait.is(.Optional)(T)) return recursiveFieldPaths(std.meta.Child(T), prefix); + + var fields: []const []const u8 = &.{}; + + for (std.meta.fields(T)) |f| { + const full_name = prefix ++ f.name; + + if (isScalar(f.field_type)) { + fields = fields ++ @as([]const []const u8, &.{full_name}); + } else { + const field_prefix = if (@typeInfo(f.field_type) == .Union) prefix else full_name ++ "."; + fields = fields ++ recursiveFieldPaths(f.field_type, field_prefix); + } + } + + return fields; + } +} + +const QueryParam = union(enum) { + not_specified: void, + no_value: void, + value: []const u8, +}; + +fn Intermediary(comptime T: type) type { + const field_names = recursiveFieldPaths(T, ".."); + + var fields: [field_names.len]std.builtin.Type.StructField = undefined; + for (field_names) |name, i| fields[i] = .{ + .name = name, + .field_type = QueryParam, + .default_value = &QueryParam{ .not_specified = {} }, + .is_comptime = false, + .alignment = @alignOf(QueryParam), + }; + + return @Type(.{ .Struct = .{ + .layout = .Auto, + .fields = &fields, + .decls = &.{}, + .is_tuple = false, + } }); +} + +fn parseQueryValue(alloc: std.mem.Allocator, comptime T: type, value: ?[]const u8) !T { + const is_optional = comptime std.meta.trait.is(.Optional)(T); + // If param is present, but without an associated value + if (value == null) { + return if (is_optional) + null + else if (T == bool) + true + else + error.InvalidValue; + } + + return try parseQueryValueNotNull(alloc, if (is_optional) std.meta.Child(T) else T, value.?); +} + +const bool_map = std.ComptimeStringMap(bool, .{ + .{ "true", true }, + .{ "t", true }, + .{ "yes", true }, + .{ "y", true }, + .{ "1", true }, + + .{ "false", false }, + .{ "f", false }, + .{ "no", false }, + .{ "n", false }, + .{ "0", false }, +}); + +fn parseQueryValueNotNull(alloc: std.mem.Allocator, comptime T: type, value: []const u8) !T { + const decoded = try decodeString(alloc, value); + errdefer alloc.free(decoded); + + if (comptime std.meta.trait.isZigString(T)) return decoded; + + const result = if (comptime std.meta.trait.isIntegral(T)) + try std.fmt.parseInt(T, decoded, 0) + else if (comptime std.meta.trait.isFloat(T)) + try std.fmt.parseFloat(T, decoded) + else if (comptime std.meta.trait.is(.Enum)(T)) + std.meta.stringToEnum(T, decoded) orelse return error.InvalidEnumValue + else if (T == bool) + bool_map.get(value) orelse return error.InvalidBool + else if (comptime std.meta.trait.hasFn("parse")(T)) + try T.parse(value) + else + @compileError("Invalid type " ++ @typeName(T)); + + alloc.free(decoded); + return result; +} + +fn isScalar(comptime T: type) bool { + if (comptime std.meta.trait.isZigString(T)) return true; + if (comptime std.meta.trait.isIntegral(T)) return true; + if (comptime std.meta.trait.isFloat(T)) return true; + if (comptime std.meta.trait.is(.Enum)(T)) return true; + if (T == bool) return true; + if (comptime std.meta.trait.hasFn("parse")(T)) return true; + + if (comptime std.meta.trait.is(.Optional)(T) and isScalar(std.meta.Child(T))) return true; + + return false; +} + +pub fn formatQuery(params: anytype, writer: anytype) !void { + try format("", "", params, writer); +} + +fn urlFormatString(writer: anytype, val: []const u8) !void { + for (val) |ch| { + const printable = switch (ch) { + '0'...'9', 'a'...'z', 'A'...'Z' => true, + '-', '.', '_', '~', ':', '@', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true, + else => false, + }; + + try if (printable) writer.writeByte(ch) else std.fmt.format(writer, "%{x:0>2}", .{ch}); + } +} + +fn formatScalar(comptime name: []const u8, val: anytype, writer: anytype) !void { + const T = @TypeOf(val); + if (comptime std.meta.trait.is(.Optional)(T)) { + return if (val) |v| formatScalar(name, v, writer) else {}; + } + + try urlFormatString(writer, name); + try writer.writeByte('='); + if (comptime std.meta.trait.isZigString(T)) { + try urlFormatString(writer, val); + } else try switch (@typeInfo(T)) { + .Enum => urlFormatString(writer, @tagName(val)), + else => std.fmt.format(writer, "{}", .{val}), + }; + + try writer.writeByte('&'); +} + +fn format(comptime prefix: []const u8, comptime name: []const u8, params: anytype, writer: anytype) !void { + const T = @TypeOf(params); + const eff_prefix = if (prefix.len == 0) "" else prefix ++ "."; + if (comptime isScalar(T)) return formatScalar(eff_prefix ++ name, params, writer); + + switch (@typeInfo(T)) { + .Struct => { + inline for (std.meta.fields(T)) |field| { + const val = @field(params, field.name); + try format(eff_prefix ++ name, field.name, val, writer); + } + }, + .Union => { + inline for (std.meta.fields(T)) |field| { + const tag = @field(std.meta.Tag(T), field.name); + const tag_name = field.name; + if (@as(std.meta.Tag(T), params) == tag) { + const val = @field(params, tag_name); + try format(prefix, tag_name, val, writer); + } + } + }, + .Optional => { + if (params) |p| try format(prefix, name, p, writer); + }, + else => @compileError("Unsupported query type"), + } +} + +test { + const TestQuery = struct { + int: usize = 3, + boolean: bool = false, + str_enum: ?enum { foo, bar } = null, + }; + + try std.testing.expectEqual(TestQuery{ + .int = 3, + .boolean = false, + .str_enum = null, + }, try parseQuery(TestQuery, "")); + + try std.testing.expectEqual(TestQuery{ + .int = 5, + .boolean = true, + .str_enum = .foo, + }, try parseQuery(TestQuery, "?int=5&boolean=yes&str_enum=foo")); +} From 43ddc7534b0bb6c9027d724008744c79e5fb2a63 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Fri, 25 Nov 2022 17:43:16 -0800 Subject: [PATCH 08/11] debugging changes for middleware --- src/http/middleware.zig | 71 ++++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/src/http/middleware.zig b/src/http/middleware.zig index 7f4e5f6..32cceca 100644 --- a/src/http/middleware.zig +++ b/src/http/middleware.zig @@ -3,19 +3,41 @@ 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; +const query_utils = @import("./query.zig"); +const json_utils = @import("./json.zig"); +//const json_utils = util; +//const query_utils = util; + +fn printFields(comptime fields: []const std.builtin.Type.StructField) void { + comptime { + inline for (fields) |f| @compileLog(f.name.ptr); + } +} fn AddFields(comptime lhs: type, comptime rhs: type) type { + const fields = std.meta.fields(lhs) ++ std.meta.fields(rhs); + comptime if (false) { + @compileLog("New Type"); + @compileLog("lhs"); + printFields(std.meta.fields(lhs)); + @compileLog("rhs"); + printFields(std.meta.fields(rhs)); + @compileLog("all"); + printFields(fields); + }; const Ctx = @Type(.{ .Struct = .{ .layout = .Auto, - .fields = std.meta.fields(lhs) ++ std.meta.fields(rhs), + .fields = fields, .decls = &.{}, .is_tuple = false, } }); return Ctx; + // return struct { + // path: []const u8, + // fragment: []const u8, + // query_string: []const u8, + // api_source: *anyopaque, + // }; } fn addFields(lhs: anytype, rhs: anytype) AddFields(@TypeOf(lhs), @TypeOf(rhs)) { @@ -117,15 +139,18 @@ 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; + + var buf: [1024]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buf); + var headers = http.Fields.init(fba.allocator()); + if (!res.was_opened) { + var stream = res.open(.internal_server_error, &headers) catch return; + defer stream.close(); + stream.finish() catch {}; + } } }{}; @@ -155,9 +180,9 @@ pub const split_uri = struct { }{}; // routes a request to the correct handler based on declared HTTP method and path -pub fn Router(comptime Routes: []const type) type { +pub fn Router(comptime Routes: type) type { return struct { - routes: std.meta.Tuple(Routes), + routes: Routes, pub fn handle(self: @This(), req: anytype, res: anytype, ctx: anytype, next: void) !void { _ = next; @@ -176,13 +201,8 @@ pub fn Router(comptime Routes: []const type) type { } }; } -fn fieldTypes(comptime Tuple: type) []const type { - var types: [Tuple.len]type = undefined; - for (std.meta.fields(Tuple)) |f, i| types[i] = f.field_type; - return &types; -} -pub fn router(routes: anytype) Router(fieldTypes(@TypeOf(routes))) { - return Router(fieldTypes(@TypeOf(routes))){ .routes = routes }; +pub fn router(routes: anytype) Router(@TypeOf(routes)) { + return Router(@TypeOf(routes)){ .routes = routes }; } // helper function for doing route analysis @@ -212,7 +232,7 @@ pub const Route = struct { fn applies(self: @This(), req: anytype, ctx: anytype) bool { if (self.desc.method != req.method) return false; - const eff_path = if (@hasDecl(ctx, "path")) + const eff_path = if (@hasField(@TypeOf(ctx), "path")) ctx.path else std.mem.sliceTo(req.uri, '?'); @@ -303,7 +323,7 @@ pub fn ParsePathArgs(comptime route: []const u8, comptime Args: type) type { return next.handle( req, res, - addFields(ctx, .{ .args = parsePathArgs(route, Args, req.path) }), + addFields(ctx, .{ .args = try parsePathArgs(route, Args, ctx.path) }), {}, ); } @@ -356,12 +376,13 @@ pub fn ParseBody(comptime Body: type) 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); + defer util.deepFree(ctx.allocator, body); - return next.handler( + return next.handle( req, res, addFields(ctx, .{ .body = body }), + {}, ); } }; @@ -371,7 +392,7 @@ pub fn ParseQueryParams(comptime QueryParams: type) type { return struct { pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { const query = try query_utils.parseQuery(ctx.allocator, QueryParams, ctx.query_string); - defer ctx.allocator.free(query); + defer util.deepFree(ctx.allocator, query); return next.handle( req, From 04a95a280b675adb52c85f1ba3bdf357f5ef6711 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Fri, 25 Nov 2022 17:43:30 -0800 Subject: [PATCH 09/11] use new middleware system --- src/main/controllers.zig | 56 +++++++++++++++++------------------- src/main/controllers/api.zig | 32 ++++++++++----------- src/main/controllers/web.zig | 10 +++---- 3 files changed, 47 insertions(+), 51 deletions(-) diff --git a/src/main/controllers.zig b/src/main/controllers.zig index d965a07..79be22b 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -7,15 +7,11 @@ const util = @import("util"); const query_utils = @import("./query.zig"); const json_utils = @import("./json.zig"); -const web_controllers = @import("./controllers/web.zig"); -const api_controllers = @import("./controllers/api.zig"); - -const routes = api_controllers ++ web_controllers.routes; +const web_endpoints = @import("./controllers/web.zig").routes; +const api_endpoints = @import("./controllers/api.zig").routes; const mdw = http.middleware; -const router = mdw.Router(&.{}); - const not_found = struct { pub fn handler( _: @This(), @@ -35,7 +31,7 @@ const not_found = struct { const base_handler = mdw.SplitUri(mdw.CatchErrors(not_found, mdw.DefaultErrorHandler)); const inject_api_conn = struct { - fn getApiConn(alloc: std.mem.Allocator, api_source: anytype, req: anytype) !@TypeOf(api_source).Conn { + fn getApiConn(alloc: std.mem.Allocator, api_source: anytype, req: anytype) !@TypeOf(api_source.*).Conn { const host = req.headers.get("Host") orelse return error.NoHost; const auth_header = req.headers.get("Authorization"); const token = if (auth_header) |header| blk: { @@ -64,20 +60,20 @@ const inject_api_conn = struct { var api_conn = try getApiConn(ctx.allocator, ctx.api_source, req); defer api_conn.close(); - return next.handle( + return mdw.injectContext(.{ .api_conn = &api_conn }).handle( req, res, - mdw.injectContext(.{ .api_conn = &api_conn }), - {}, + ctx, + next, ); } -}; +}{}; pub fn EndpointRequest(comptime Endpoint: type) type { return struct { - pub const Args = if (@hasDecl(Endpoint, "Args")) Endpoint.Args else void; - pub const Body = if (@hasDecl(Endpoint, "Body")) Endpoint.Body else void; - pub const Query = if (@hasDecl(Endpoint, "Query")) Endpoint.Query else void; + const Args = if (@hasDecl(Endpoint, "Args")) Endpoint.Args else void; + const Body = if (@hasDecl(Endpoint, "Body")) Endpoint.Body else void; + const Query = if (@hasDecl(Endpoint, "Query")) Endpoint.Query else void; allocator: std.mem.Allocator, @@ -92,7 +88,7 @@ pub fn EndpointRequest(comptime Endpoint: type) type { const args_middleware = if (Args == void) mdw.injectContext(.{ .args = {} }) else - mdw.ParsePathArgs(Args){}; + mdw.ParsePathArgs(Endpoint.path, Args){}; const body_middleware = if (Body == void) mdw.injectContext(.{ .body = {} }) @@ -100,7 +96,7 @@ pub fn EndpointRequest(comptime Endpoint: type) type { mdw.ParseBody(Body){}; const query_middleware = if (Query == void) - mdw.injectContext(.{ .query = {} }) + mdw.injectContext(.{ .query_params = {} }) else mdw.ParseQueryParams(Query){}; }; @@ -108,7 +104,7 @@ pub fn EndpointRequest(comptime Endpoint: type) type { fn CallApiEndpoint(comptime Endpoint: type) type { return struct { - fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, _: void) !void { + pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, _: void) !void { const request = EndpointRequest(Endpoint){ .allocator = ctx.allocator, @@ -118,12 +114,11 @@ fn CallApiEndpoint(comptime Endpoint: type) type { .args = ctx.args, .body = ctx.body, - .query = ctx.query, + .query = ctx.query_params, }; var response = Response{ .headers = http.Fields.init(ctx.allocator), .res = res }; defer response.headers.deinit(); - return Endpoint.handler(request, &response, ctx.api_conn); } }; @@ -133,7 +128,7 @@ pub fn apiEndpoint( comptime Endpoint: type, ) return_type: { const RequestType = EndpointRequest(Endpoint); - break :return_type mdw.Apply(std.meta.Tuple(.{ + break :return_type mdw.Apply(std.meta.Tuple(&.{ mdw.Route, @TypeOf(RequestType.args_middleware), @TypeOf(RequestType.query_middleware), @@ -155,17 +150,18 @@ pub fn apiEndpoint( }); } -pub fn routeRequest(api_source: anytype, req: *http.Request, res: *http.Response, alloc: std.mem.Allocator) void { - // TODO: hashmaps? - _ = .{ api_source, req, res, alloc }; - unreachable; - //var response = Response{ .headers = http.Fields.init(alloc), .res = res }; - //defer response.headers.deinit(); +const api_router = mdw.apply(.{ + mdw.mount("/api/v0/"), + mdw.router(api_endpoints), +}); - //const found = routeRequestInternal(api_source, req, &response, alloc); - - //if (!found) response.status(.not_found) catch {}; -} +pub const router = mdw.apply(.{ + mdw.split_uri, + mdw.catchErrors(mdw.default_error_handler), + //mdw.router(.{api_router} ++ web_endpoints), + mdw.router(web_endpoints), + //api_router, +}); pub const AllocationStrategy = enum { arena, diff --git a/src/main/controllers/api.zig b/src/main/controllers/api.zig index 38de71e..f3b6d43 100644 --- a/src/main/controllers/api.zig +++ b/src/main/controllers/api.zig @@ -10,20 +10,20 @@ const streaming = @import("./api/streaming.zig"); const timelines = @import("./api/timelines.zig"); pub const routes = .{ - controllers.apiEndpoint(auth.login), - controllers.apiEndpoint(auth.verify_login), - controllers.apiEndpoint(communities.create), - controllers.apiEndpoint(communities.query), - controllers.apiEndpoint(invites.create), - controllers.apiEndpoint(users.create), - controllers.apiEndpoint(notes.create), - controllers.apiEndpoint(notes.get), - controllers.apiEndpoint(streaming.streaming), - controllers.apiEndpoint(timelines.global), - controllers.apiEndpoint(timelines.local), - controllers.apiEndpoint(timelines.home), - controllers.apiEndpoint(follows.create), - controllers.apiEndpoint(follows.delete), - controllers.apiEndpoint(follows.query_followers), - controllers.apiEndpoint(follows.query_following), + //controllers.apiEndpoint(auth.login), + //controllers.apiEndpoint(auth.verify_login), + //controllers.apiEndpoint(communities.create), + //controllers.apiEndpoint(communities.query), + //controllers.apiEndpoint(invites.create), + //controllers.apiEndpoint(users.create), + //controllers.apiEndpoint(notes.create), + //controllers.apiEndpoint(notes.get), + //controllers.apiEndpoint(streaming.streaming), + //controllers.apiEndpoint(timelines.global), + //controllers.apiEndpoint(timelines.local), + //controllers.apiEndpoint(timelines.home), + //controllers.apiEndpoint(follows.create), + //controllers.apiEndpoint(follows.delete), + //controllers.apiEndpoint(follows.query_followers), + //controllers.apiEndpoint(follows.query_following), }; diff --git a/src/main/controllers/web.zig b/src/main/controllers/web.zig index 430e405..6d0e67d 100644 --- a/src/main/controllers/web.zig +++ b/src/main/controllers/web.zig @@ -2,11 +2,11 @@ const std = @import("std"); const controllers = @import("../controllers.zig"); pub const routes = .{ - controllers.apiEndpoint(index), - controllers.apiEndpoint(about), - controllers.apiEndpoint(login), - controllers.apiEndpoint(global_timeline), - controllers.apiEndpoint(cluster.overview), + //controllers.apiEndpoint(index), + //controllers.apiEndpoint(about), + //controllers.apiEndpoint(login), + //controllers.apiEndpoint(global_timeline), + //controllers.apiEndpoint(cluster.overview), }; const index = struct { From 9c5e46ec5a034c8d89458bf2f250f30bdffce3e3 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sat, 26 Nov 2022 17:33:46 -0800 Subject: [PATCH 10/11] Fix middleware compilation??? --- build.zig | 3 +- src/http/middleware.zig | 62 +++++++++++++++++++++++++++++++++--- src/http/server.zig | 20 +++++++++--- src/http/socket.zig | 10 +++--- src/main/controllers.zig | 26 +++++++-------- src/main/controllers/api.zig | 30 ++++++++--------- src/main/controllers/web.zig | 10 +++--- src/main/main.zig | 6 +--- src/sql/engines/sqlite.zig | 2 +- 9 files changed, 115 insertions(+), 54 deletions(-) diff --git a/build.zig b/build.zig index 2017afc..f68bbd5 100644 --- a/build.zig +++ b/build.zig @@ -82,7 +82,7 @@ pub fn build(b: *std.build.Builder) !void { const pkgs = makePkgs(b, options.getPackage("build_options")); - const exe = b.addExecutable("apub", "src/main/main.zig"); + const exe = b.addExecutable("fediglam", "src/main/main.zig"); exe.setTarget(target); exe.setBuildMode(mode); @@ -96,6 +96,7 @@ pub fn build(b: *std.build.Builder) !void { if (enable_sqlite) exe.linkSystemLibrary("sqlite3"); if (enable_postgres) exe.linkSystemLibrary("pq"); exe.linkLibC(); + exe.addSystemIncludePath("/usr/include/"); //const util_tests = b.addTest("src/util/lib.zig"); const http_tests = b.addTest("src/http/test.zig"); diff --git a/src/http/middleware.zig b/src/http/middleware.zig index 32cceca..4ab6f6d 100644 --- a/src/http/middleware.zig +++ b/src/http/middleware.zig @@ -10,7 +10,7 @@ const json_utils = @import("./json.zig"); fn printFields(comptime fields: []const std.builtin.Type.StructField) void { comptime { - inline for (fields) |f| @compileLog(f.name.ptr); + inline for (fields) |f| @compileLog(f.name.ptr, f.field_type); } } @@ -47,6 +47,35 @@ fn addFields(lhs: anytype, rhs: anytype) AddFields(@TypeOf(lhs), @TypeOf(rhs)) { return result; } +fn AddUniqueField(comptime Lhs: type, comptime N: usize, comptime name: [N]u8, comptime Val: type) type { + const Ctx = @Type(.{ .Struct = .{ + .layout = .Auto, + .fields = std.meta.fields(Lhs) ++ &[_]std.builtin.Type.StructField{ + .{ + .name = &name, + .field_type = Val, + .alignment = if (@sizeOf(Val) != 0) @alignOf(Val) else 0, + .default_value = null, + .is_comptime = false, + }, + }, + .decls = &.{}, + .is_tuple = false, + } }); + return Ctx; +} + +fn AddField(comptime Lhs: type, comptime name: []const u8, comptime Val: type) type { + return AddUniqueField(Lhs, name.len, name[0..].*, Val); +} + +fn addField(lhs: anytype, comptime name: []const u8, val: anytype) AddField(@TypeOf(lhs), name, @TypeOf(val)) { + var result: AddField(@TypeOf(lhs), name, @TypeOf(val)) = undefined; + inline for (std.meta.fields(@TypeOf(lhs))) |f| @field(result, f.name) = @field(lhs, f.name); + @field(result, name) = val; + return result; +} + test { // apply is a plumbing function that applies a tuple of middlewares in order const base = apply(.{ @@ -94,10 +123,22 @@ pub fn InjectContext(comptime Values: type) type { }; } +pub fn InjectContextValue(comptime name: []const u8, comptime V: type) type { + return struct { + val: V, + pub fn handle(self: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { + return next.handle(req, res, addField(ctx, name, self.val), {}); + } + }; +} pub fn injectContext(values: anytype) InjectContext(@TypeOf(values)) { return .{ .values = values }; } +pub fn injectContextValue(comptime name: []const u8, val: anytype) InjectContextValue(name, @TypeOf(val)) { + return .{ .val = val }; +} + pub fn NextHandler(comptime First: type, comptime Next: type) type { return struct { first: First, @@ -320,10 +361,11 @@ fn parsePathArg(comptime T: type, segment: []const u8) !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 { + if (Args == void) return next.handle(req, res, addField(ctx, "args", {}), {}); return next.handle( req, res, - addFields(ctx, .{ .args = try parsePathArgs(route, Args, ctx.path) }), + addField(ctx, "args", try parsePathArgs(route, Args, ctx.path)), {}, ); } @@ -339,6 +381,7 @@ const BaseContentType = enum { }; fn parseBody(comptime T: type, content_type: BaseContentType, reader: anytype, alloc: std.mem.Allocator) !T { + //@compileLog(T); const buf = try reader.readAllAlloc(alloc, 1 << 16); defer alloc.free(buf); @@ -372,7 +415,15 @@ fn matchContentType(hdr: ?[]const u8) ?BaseContentType { 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")); + const content_type = req.headers.get("Content-Type"); + if (Body == void) { + if (content_type != null) return error.UnexpectedBody; + const new_ctx = addField(ctx, "body", {}); + //if (true) @compileError("bug"); + return next.handle(req, res, new_ctx, {}); + } + + const base_content_type = matchContentType(content_type); var stream = req.body orelse return error.NoBody; const body = try parseBody(Body, base_content_type orelse .json, stream.reader(), ctx.allocator); @@ -381,7 +432,7 @@ pub fn ParseBody(comptime Body: type) type { return next.handle( req, res, - addFields(ctx, .{ .body = body }), + addField(ctx, "body", body), {}, ); } @@ -391,13 +442,14 @@ pub fn ParseBody(comptime Body: type) type { pub fn ParseQueryParams(comptime QueryParams: type) type { return struct { pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { + if (QueryParams == void) return next.handle(req, res, addField(ctx, "query_params", {}), {}); const query = try query_utils.parseQuery(ctx.allocator, QueryParams, ctx.query_string); defer util.deepFree(ctx.allocator, query); return next.handle( req, res, - addFields(ctx, .{ .query_params = query }), + addField(ctx, "query_params", query), {}, ); } diff --git a/src/http/server.zig b/src/http/server.zig index bfb97a9..4763f19 100644 --- a/src/http/server.zig +++ b/src/http/server.zig @@ -10,9 +10,12 @@ pub const Response = struct { alloc: std.mem.Allocator, stream: Stream, should_close: bool = false, + was_opened: bool = true, pub const ResponseStream = response.ResponseStream(Stream.Writer); pub fn open(self: *Response, status: http.Status, headers: *const http.Fields) !ResponseStream { + std.debug.assert(!self.was_opened); + self.was_opened = true; if (headers.get("Connection")) |hdr| { if (std.ascii.indexOfIgnoreCase(hdr, "close")) |_| self.should_close = true; } @@ -21,6 +24,8 @@ pub const Response = struct { } pub fn upgrade(self: *Response, status: http.Status, headers: *const http.Fields) !Stream { + std.debug.assert(!self.was_opened); + self.was_opened = true; try response.writeRequestHeader(self.stream.writer(), headers, status); return self.stream; } @@ -92,6 +97,7 @@ pub const Server = struct { pub fn handleLoop( self: *Server, allocator: std.mem.Allocator, + initial_context: anytype, handler: anytype, ) void { while (true) { @@ -108,6 +114,7 @@ pub const Server = struct { .stream = Stream{ .kind = .tcp, .socket = conn.stream.handle }, .address = conn.address, }, + initial_context, handler, ); } @@ -116,6 +123,7 @@ pub const Server = struct { fn serveConn( allocator: std.mem.Allocator, conn: Connection, + initial_context: anytype, handler: anytype, ) void { while (true) { @@ -127,12 +135,16 @@ pub const Server = struct { error.HttpVersionNotSupported => .http_version_not_supported, else => blk: { - std.log.err("Unknown error parsing request: {}\n{?s}", .{ err, @errorReturnTrace() }); + std.log.err("Unknown error parsing request: {}\n{?}", .{ err, @errorReturnTrace() }); break :blk .internal_server_error; }, }; - try conn.stream.writer().print("HTTP/1.1 {} {?s}\r\nConnection: close\r\n\r\n", .{ @enumToInt(status), status.phrase() }); + conn.stream.writer().print( + "HTTP/1.1 {} {?s}\r\nConnection: close\r\n\r\n", + .{ @enumToInt(status), status.phrase() }, + ) catch {}; + return; }; var res = Response{ @@ -140,8 +152,8 @@ pub const Server = struct { .stream = conn.stream, }; - handler.handle(&req, &res, .{}, {}) catch |err| { - std.log.err("Unhandled error serving request: {}\n{?s}", .{ err, @errorReturnTrace() }); + handler.handle(&req, &res, initial_context, {}) catch |err| { + std.log.err("Unhandled error serving request: {}\n{?}", .{ err, @errorReturnTrace() }); return; }; diff --git a/src/http/socket.zig b/src/http/socket.zig index ab885bb..f481fff 100644 --- a/src/http/socket.zig +++ b/src/http/socket.zig @@ -23,18 +23,18 @@ const Opcode = enum(u4) { } }; -pub fn handshake(alloc: std.mem.Allocator, req: *http.Request, res: *http.Response) !Socket { - const upgrade = req.headers.get("Upgrade") orelse return error.BadHandshake; - const connection = req.headers.get("Connection") orelse return error.BadHandshake; +pub fn handshake(alloc: std.mem.Allocator, req_headers: *const http.Fields, res: *http.Response) !Socket { + const upgrade = req_headers.get("Upgrade") orelse return error.BadHandshake; + const connection = req_headers.get("Connection") orelse return error.BadHandshake; if (std.ascii.indexOfIgnoreCase(upgrade, "websocket") == null) return error.BadHandshake; if (std.ascii.indexOfIgnoreCase(connection, "Upgrade") == null) return error.BadHandshake; - const key_hdr = req.headers.get("Sec-WebSocket-Key") orelse return error.BadHandshake; + const key_hdr = req_headers.get("Sec-WebSocket-Key") orelse return error.BadHandshake; if ((try std.base64.standard.Decoder.calcSizeForSlice(key_hdr)) != 16) return error.BadHandshake; var key: [16]u8 = undefined; std.base64.standard.Decoder.decode(&key, key_hdr) catch return error.BadHandshake; - const version = req.headers.get("Sec-WebSocket-version") orelse return error.BadHandshake; + const version = req_headers.get("Sec-WebSocket-version") orelse return error.BadHandshake; if (!std.mem.eql(u8, "13", version)) return error.BadHandshake; var headers = http.Fields.init(alloc); diff --git a/src/main/controllers.zig b/src/main/controllers.zig index 79be22b..08344f8 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -60,7 +60,7 @@ const inject_api_conn = struct { var api_conn = try getApiConn(ctx.allocator, ctx.api_source, req); defer api_conn.close(); - return mdw.injectContext(.{ .api_conn = &api_conn }).handle( + return mdw.injectContextValue("api_conn", &api_conn).handle( req, res, ctx, @@ -85,19 +85,19 @@ pub fn EndpointRequest(comptime Endpoint: type) type { body: Body, query: Query, - const args_middleware = if (Args == void) - mdw.injectContext(.{ .args = {} }) - else + const args_middleware = //if (Args == void) + //mdw.injectContext(.{ .args = {} }) + //else mdw.ParsePathArgs(Endpoint.path, Args){}; - const body_middleware = if (Body == void) - mdw.injectContext(.{ .body = {} }) - else + const body_middleware = //if (Body == void) + //mdw.injectContext(.{ .body = {} }) + //else mdw.ParseBody(Body){}; - const query_middleware = if (Query == void) - mdw.injectContext(.{ .query_params = {} }) - else + const query_middleware = //if (Query == void) + //mdw.injectContext(.{ .query_params = {} }) + //else mdw.ParseQueryParams(Query){}; }; } @@ -157,9 +157,9 @@ const api_router = mdw.apply(.{ pub const router = mdw.apply(.{ mdw.split_uri, - mdw.catchErrors(mdw.default_error_handler), - //mdw.router(.{api_router} ++ web_endpoints), - mdw.router(web_endpoints), + //mdw.catchErrors(mdw.default_error_handler), + mdw.router(.{api_router} ++ web_endpoints), + //mdw.router(web_endpoints), //api_router, }); diff --git a/src/main/controllers/api.zig b/src/main/controllers/api.zig index f3b6d43..12f3a1f 100644 --- a/src/main/controllers/api.zig +++ b/src/main/controllers/api.zig @@ -10,20 +10,20 @@ const streaming = @import("./api/streaming.zig"); const timelines = @import("./api/timelines.zig"); pub const routes = .{ - //controllers.apiEndpoint(auth.login), - //controllers.apiEndpoint(auth.verify_login), - //controllers.apiEndpoint(communities.create), - //controllers.apiEndpoint(communities.query), - //controllers.apiEndpoint(invites.create), - //controllers.apiEndpoint(users.create), - //controllers.apiEndpoint(notes.create), - //controllers.apiEndpoint(notes.get), + controllers.apiEndpoint(auth.login), + controllers.apiEndpoint(auth.verify_login), + controllers.apiEndpoint(communities.create), + controllers.apiEndpoint(communities.query), + controllers.apiEndpoint(invites.create), + controllers.apiEndpoint(users.create), + controllers.apiEndpoint(notes.create), + controllers.apiEndpoint(notes.get), //controllers.apiEndpoint(streaming.streaming), - //controllers.apiEndpoint(timelines.global), - //controllers.apiEndpoint(timelines.local), - //controllers.apiEndpoint(timelines.home), - //controllers.apiEndpoint(follows.create), - //controllers.apiEndpoint(follows.delete), - //controllers.apiEndpoint(follows.query_followers), - //controllers.apiEndpoint(follows.query_following), + controllers.apiEndpoint(timelines.global), + controllers.apiEndpoint(timelines.local), + controllers.apiEndpoint(timelines.home), + controllers.apiEndpoint(follows.create), + controllers.apiEndpoint(follows.delete), + controllers.apiEndpoint(follows.query_followers), + controllers.apiEndpoint(follows.query_following), }; diff --git a/src/main/controllers/web.zig b/src/main/controllers/web.zig index 6d0e67d..430e405 100644 --- a/src/main/controllers/web.zig +++ b/src/main/controllers/web.zig @@ -2,11 +2,11 @@ const std = @import("std"); const controllers = @import("../controllers.zig"); pub const routes = .{ - //controllers.apiEndpoint(index), - //controllers.apiEndpoint(about), - //controllers.apiEndpoint(login), - //controllers.apiEndpoint(global_timeline), - //controllers.apiEndpoint(cluster.overview), + controllers.apiEndpoint(index), + controllers.apiEndpoint(about), + controllers.apiEndpoint(login), + controllers.apiEndpoint(global_timeline), + controllers.apiEndpoint(cluster.overview), }; const index = struct { diff --git a/src/main/main.zig b/src/main/main.zig index c512d3c..18f644a 100644 --- a/src/main/main.zig +++ b/src/main/main.zig @@ -70,11 +70,7 @@ fn thread_main(src: *api.ApiSource, srv: *http.Server) void { util.seedThreadPrng() catch unreachable; var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); - srv.handleLoop(gpa.allocator(), .{ .src = src, .allocator = gpa.allocator() }, handle); -} - -fn handle(ctx: anytype, req: *http.Request, res: *http.Response) void { - c.routeRequest(ctx.src, req, res, ctx.allocator); + srv.handleLoop(gpa.allocator(), .{ .api_source = src, .allocator = gpa.allocator() }, c.router); } pub fn main() !void { diff --git a/src/sql/engines/sqlite.zig b/src/sql/engines/sqlite.zig index e2bb697..5b59910 100644 --- a/src/sql/engines/sqlite.zig +++ b/src/sql/engines/sqlite.zig @@ -40,7 +40,7 @@ fn handleUnexpectedError(db: *c.sqlite3, code: c_int, sql_text: ?[]const u8) err std.log.debug("Failed at char ({}:{}) of SQL:\n{s}", .{ pos.row, pos.col, sql }); } } - std.log.debug("{?s}", .{@errorReturnTrace()}); + std.log.debug("{?}", .{@errorReturnTrace()}); return error.Unexpected; } From 856619b286ba91c24152e3b8916999be0a0cda50 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sat, 26 Nov 2022 17:52:30 -0800 Subject: [PATCH 11/11] Finish using middleware impl --- src/http/middleware.zig | 73 ++++------------------ src/http/server.zig | 3 +- src/main/controllers.zig | 4 +- src/main/controllers/api/auth.zig | 4 +- src/main/controllers/api/communities.zig | 4 +- src/main/controllers/api/invites.zig | 2 +- src/main/controllers/api/notes.zig | 4 +- src/main/controllers/api/streaming.zig | 2 +- src/main/controllers/api/timelines.zig | 6 +- src/main/controllers/api/users.zig | 2 +- src/main/controllers/api/users/follows.zig | 8 +-- 11 files changed, 31 insertions(+), 81 deletions(-) diff --git a/src/http/middleware.zig b/src/http/middleware.zig index 4ab6f6d..12ddcf3 100644 --- a/src/http/middleware.zig +++ b/src/http/middleware.zig @@ -5,48 +5,6 @@ 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 printFields(comptime fields: []const std.builtin.Type.StructField) void { - comptime { - inline for (fields) |f| @compileLog(f.name.ptr, f.field_type); - } -} - -fn AddFields(comptime lhs: type, comptime rhs: type) type { - const fields = std.meta.fields(lhs) ++ std.meta.fields(rhs); - comptime if (false) { - @compileLog("New Type"); - @compileLog("lhs"); - printFields(std.meta.fields(lhs)); - @compileLog("rhs"); - printFields(std.meta.fields(rhs)); - @compileLog("all"); - printFields(fields); - }; - const Ctx = @Type(.{ .Struct = .{ - .layout = .Auto, - .fields = fields, - .decls = &.{}, - .is_tuple = false, - } }); - return Ctx; - // return struct { - // path: []const u8, - // fragment: []const u8, - // query_string: []const u8, - // api_source: *anyopaque, - // }; -} - -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; -} - fn AddUniqueField(comptime Lhs: type, comptime N: usize, comptime name: [N]u8, comptime Val: type) type { const Ctx = @Type(.{ .Struct = .{ .layout = .Auto, @@ -114,15 +72,6 @@ pub fn Apply(comptime Middlewares: type) type { return ApplyInternal(std.meta.fields(Middlewares)); } -pub fn InjectContext(comptime Values: type) type { - return struct { - values: Values, - 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 InjectContextValue(comptime name: []const u8, comptime V: type) type { return struct { val: V, @@ -131,9 +80,6 @@ pub fn InjectContextValue(comptime name: []const u8, comptime V: type) type { } }; } -pub fn injectContext(values: anytype) InjectContext(@TypeOf(values)) { - return .{ .values = values }; -} pub fn injectContextValue(comptime name: []const u8, val: anytype) InjectContextValue(name, @TypeOf(val)) { return .{ .val = val }; @@ -165,7 +111,7 @@ pub fn CatchErrors(comptime ErrorHandler: type) type { return self.error_handler.handle( req, res, - addFields(ctx, .{ .err = err }), + addField(ctx, "err", err), next, ); }; @@ -205,16 +151,20 @@ pub const split_uri = struct { const path = query_split.first(); const query = query_split.rest(); - const added_ctx = .{ - .path = path, - .query_string = query, - .fragment_string = fragment, - }; + const new_ctx = addField( + addField( + addField(ctx, "path", path), + "query_string", + query, + ), + "fragment_string", + fragment, + ); return next.handle( req, res, - addFields(ctx, added_ctx), + new_ctx, {}, ); } @@ -282,6 +232,7 @@ pub const Route = struct { } pub fn handle(self: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { + std.log.debug("Testing path {s} against {s}", .{ ctx.path, self.desc.path }); return if (self.applies(req, ctx)) next.handle(req, res, ctx, {}) else diff --git a/src/http/server.zig b/src/http/server.zig index 4763f19..ae38c0a 100644 --- a/src/http/server.zig +++ b/src/http/server.zig @@ -10,7 +10,7 @@ pub const Response = struct { alloc: std.mem.Allocator, stream: Stream, should_close: bool = false, - was_opened: bool = true, + was_opened: bool = false, pub const ResponseStream = response.ResponseStream(Stream.Writer); pub fn open(self: *Response, status: http.Status, headers: *const http.Fields) !ResponseStream { @@ -126,6 +126,7 @@ pub const Server = struct { initial_context: anytype, handler: anytype, ) void { + defer conn.stream.close(); while (true) { var req = request.parse(allocator, conn.stream.reader()) catch |err| { const status: http.Status = switch (err) { diff --git a/src/main/controllers.zig b/src/main/controllers.zig index 08344f8..3263cfa 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -157,10 +157,8 @@ const api_router = mdw.apply(.{ pub const router = mdw.apply(.{ mdw.split_uri, - //mdw.catchErrors(mdw.default_error_handler), + mdw.catchErrors(mdw.default_error_handler), mdw.router(.{api_router} ++ web_endpoints), - //mdw.router(web_endpoints), - //api_router, }); pub const AllocationStrategy = enum { diff --git a/src/main/controllers/api/auth.zig b/src/main/controllers/api/auth.zig index eb719d8..1b6e652 100644 --- a/src/main/controllers/api/auth.zig +++ b/src/main/controllers/api/auth.zig @@ -3,7 +3,7 @@ const std = @import("std"); pub const login = struct { pub const method = .POST; - pub const path = "/api/v0/auth/login"; + pub const path = "/auth/login"; pub const Body = struct { username: []const u8, @@ -21,7 +21,7 @@ pub const login = struct { pub const verify_login = struct { pub const method = .GET; - pub const path = "/api/v0/auth/login"; + pub const path = "/auth/login"; pub fn handler(_: anytype, res: anytype, srv: anytype) !void { const info = try srv.verifyAuthorization(); diff --git a/src/main/controllers/api/communities.zig b/src/main/controllers/api/communities.zig index 87f744c..f6f475d 100644 --- a/src/main/controllers/api/communities.zig +++ b/src/main/controllers/api/communities.zig @@ -5,7 +5,7 @@ const QueryArgs = api.CommunityQueryArgs; pub const create = struct { pub const method = .POST; - pub const path = "/api/v0/communities"; + pub const path = "/communities"; pub const Body = struct { origin: []const u8, @@ -20,7 +20,7 @@ pub const create = struct { pub const query = struct { pub const method = .GET; - pub const path = "/api/v0/communities"; + pub const path = "/communities"; pub const Query = QueryArgs; diff --git a/src/main/controllers/api/invites.zig b/src/main/controllers/api/invites.zig index 7be7d5d..0355bcf 100644 --- a/src/main/controllers/api/invites.zig +++ b/src/main/controllers/api/invites.zig @@ -2,7 +2,7 @@ const api = @import("api"); pub const create = struct { pub const method = .POST; - pub const path = "/api/v0/invites"; + pub const path = "/invites"; pub const Body = api.InviteOptions; diff --git a/src/main/controllers/api/notes.zig b/src/main/controllers/api/notes.zig index 2622067..11da65c 100644 --- a/src/main/controllers/api/notes.zig +++ b/src/main/controllers/api/notes.zig @@ -3,7 +3,7 @@ const util = @import("util"); pub const create = struct { pub const method = .POST; - pub const path = "/api/v0/notes"; + pub const path = "/notes"; pub const Body = struct { content: []const u8, @@ -18,7 +18,7 @@ pub const create = struct { pub const get = struct { pub const method = .GET; - pub const path = "/api/v0/notes/:id"; + pub const path = "/notes/:id"; pub const Args = struct { id: util.Uuid, diff --git a/src/main/controllers/api/streaming.zig b/src/main/controllers/api/streaming.zig index 263fabb..4b8745b 100644 --- a/src/main/controllers/api/streaming.zig +++ b/src/main/controllers/api/streaming.zig @@ -3,7 +3,7 @@ const std = @import("std"); pub const streaming = struct { pub const method = .GET; - pub const path = "/api/v0/streaming"; + pub const path = "/streaming"; pub fn handler(req: anytype, response: anytype, _: anytype) !void { var iter = req.headers.iterator(); diff --git a/src/main/controllers/api/timelines.zig b/src/main/controllers/api/timelines.zig index 00da7ff..8c30cc1 100644 --- a/src/main/controllers/api/timelines.zig +++ b/src/main/controllers/api/timelines.zig @@ -4,7 +4,7 @@ const controller_utils = @import("../../controllers.zig").helpers; pub const global = struct { pub const method = .GET; - pub const path = "/api/v0/timelines/global"; + pub const path = "/timelines/global"; pub const Query = api.TimelineArgs; @@ -16,7 +16,7 @@ pub const global = struct { pub const local = struct { pub const method = .GET; - pub const path = "/api/v0/timelines/local"; + pub const path = "/timelines/local"; pub const Query = api.TimelineArgs; @@ -28,7 +28,7 @@ pub const local = struct { pub const home = struct { pub const method = .GET; - pub const path = "/api/v0/timelines/home"; + pub const path = "/timelines/home"; pub const Query = api.TimelineArgs; diff --git a/src/main/controllers/api/users.zig b/src/main/controllers/api/users.zig index 5fdc40c..fdc683e 100644 --- a/src/main/controllers/api/users.zig +++ b/src/main/controllers/api/users.zig @@ -2,7 +2,7 @@ const api = @import("api"); pub const create = struct { pub const method = .POST; - pub const path = "/api/v0/users"; + pub const path = "/users"; pub const Body = struct { username: []const u8, diff --git a/src/main/controllers/api/users/follows.zig b/src/main/controllers/api/users/follows.zig index 6e3e6e7..765e7a0 100644 --- a/src/main/controllers/api/users/follows.zig +++ b/src/main/controllers/api/users/follows.zig @@ -6,7 +6,7 @@ const Uuid = util.Uuid; pub const create = struct { pub const method = .POST; - pub const path = "/api/v0/users/:id/follow"; + pub const path = "/users/:id/follow"; pub const Args = struct { id: Uuid, @@ -21,7 +21,7 @@ pub const create = struct { pub const delete = struct { pub const method = .DELETE; - pub const path = "/api/v0/users/:id/follow"; + pub const path = "/users/:id/follow"; pub const Args = struct { id: Uuid, @@ -36,7 +36,7 @@ pub const delete = struct { pub const query_followers = struct { pub const method = .GET; - pub const path = "/api/v0/users/:id/followers"; + pub const path = "/users/:id/followers"; pub const Args = struct { id: Uuid, @@ -53,7 +53,7 @@ pub const query_followers = struct { pub const query_following = struct { pub const method = .GET; - pub const path = "/api/v0/users/:id/following"; + pub const path = "/users/:id/following"; pub const Args = struct { id: Uuid,