diff --git a/src/main/api.zig b/src/main/api.zig index f063820..0126d04 100644 --- a/src/main/api.zig +++ b/src/main/api.zig @@ -57,6 +57,8 @@ pub const NoteResponse = struct { created_at: DateTime, }; +pub const CommunityQueryArgs = services.communities.QueryArgs; + // Frees an api struct and its fields allocated from alloc pub fn free(alloc: std.mem.Allocator, val: anytype) void { switch (@typeInfo(@TypeOf(val))) { @@ -230,7 +232,7 @@ fn ApiConn(comptime DbConn: type) type { }; } - return error.Unauthorized; + return error.TokenRequired; } pub fn createCommunity(self: *Self, origin: []const u8) !services.communities.Community { @@ -334,10 +336,12 @@ fn ApiConn(comptime DbConn: type) type { } pub fn createNote(self: *Self, content: []const u8) !NoteResponse { + // You cannot post on admin accounts if (self.community.kind == .admin) return error.WrongCommunity; - const user_id = self.user_id orelse return error.TokenRequired; - const note_id = try services.notes.create(self.db, user_id, content); + // Only authenticated users can post + const user_id = self.user_id orelse return error.TokenRequired; + const note_id = try services.notes.create(self.db, user_id, content, self.arena.allocator()); return self.getNote(note_id) catch |err| switch (err) { error.NotFound => error.Unexpected, diff --git a/src/main/api/communities.zig b/src/main/api/communities.zig index bec3a58..04b5d61 100644 --- a/src/main/api/communities.zig +++ b/src/main/api/communities.zig @@ -151,6 +151,8 @@ pub const QueryArgs = struct { name, host, created_at, + + pub const jsonStringify = util.jsonSerializeEnumAsString; }; // Max items to fetch @@ -167,6 +169,8 @@ pub const QueryArgs = struct { direction: enum { ascending, descending, + + pub const jsonStringify = util.jsonSerializeEnumAsString; } = .ascending, // Page start parameter @@ -190,6 +194,8 @@ pub const QueryArgs = struct { page_direction: enum { forward, backward, + + pub const jsonStringify = util.jsonSerializeEnumAsString; } = .forward, }; diff --git a/src/main/api/notes.zig b/src/main/api/notes.zig index 4cb081b..37c9836 100644 --- a/src/main/api/notes.zig +++ b/src/main/api/notes.zig @@ -39,10 +39,15 @@ pub const GetError = error{ DatabaseFailure, NotFound, }; +const selectStarFromNote = std.fmt.comptimePrint( + \\SELECT {s} + \\FROM note + \\ +, .{util.comptimeJoin(",", std.meta.fieldNames(Note))}); pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) GetError!Note { return db.queryRow( Note, - sql.selectStar(Note, "note") ++ + selectStarFromNote ++ \\WHERE id = $1 \\LIMIT 1 , diff --git a/src/main/controllers.zig b/src/main/controllers.zig index bd5dd46..2ee5c8f 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -3,7 +3,8 @@ const root = @import("root"); const builtin = @import("builtin"); const http = @import("http"); const api = @import("./api.zig"); -const Uuid = @import("util").Uuid; +const util = @import("util"); +const Uuid = util.Uuid; pub const auth = @import("./controllers/auth.zig"); pub const communities = @import("./controllers/communities.zig"); @@ -57,6 +58,85 @@ pub const utils = struct { return parsed; } + pub fn parseQueryParams(comptime T: type, ctx: *http.server.Context) !T { + // TODO: clean up parsing + const path = ctx.request.path; + const start = (std.mem.indexOfScalar(u8, path, '?') orelse return error.NoQuery) + 1; + const rest = path[start..]; + const query = std.mem.sliceTo(rest, '#'); + + const fake_url = util.Url{ + .scheme = "", + .hostport = "", + .path = "", + .query = query, + .fragment = "", + }; + + var result: T = .{}; + inline for (std.meta.fields(T)) |f| { + if (fake_url.getQuery(f.name)) |param| { + const F = if (comptime @typeInfo(f.field_type) == .Optional) std.meta.Child(f.field_type) else f.field_type; + std.log.debug("{}: {s}", .{ F, param }); + + @field(result, f.name) = switch (F) { + []const u8 => param, + + else => switch (@typeInfo(F)) { + .Struct => if (@hasDecl(F, "parse")) try F.parse(param) else @compileError("Invalid type " ++ @typeName(F)), + .Enum => std.meta.stringToEnum(F, param) orelse return error.ParseError, + .Int => try std.fmt.parseInt(F, param, 10), + + else => {}, + }, + }; + } + } + if (false) { + inline for (std.meta.fields(T)) |f| { + if (fake_url.getQuery(f.name)) |param| { + const F = if (comptime @typeInfo(f.field_type) == .Optional) std.meta.Child(f.field_type) else f.field_type; + + switch (F) { + []const u8 => @field(result, f.name) = param, + + else => switch (@typeInfo(F)) { + .Struct, + .Opaque, + //.Union, + => if (@hasDecl(F, "parse")) { + @compileLog(F); + if (true) @compileError(F); + @field(result, f.name) = try F.parse(param); + }, + + //.Int => @field(result, f.name) = try std.fmt.parseInt(F, param), + }, + } + } + } + } + + return result; + } + + fn parseTypeFromQueryParams(comptime T: type, comptime name_prefix: []const u8, url: util.Url) !T { + var result: T = .{}; + inline for (std.meta.fields(T)) |field| { + const FieldType = switch (@typeInfo(field.field_type)) { + .Optional => |info| info.child, + else => field.field_type, + }; + _ = FieldType; + _ = result; + + const qualified_name = name_prefix ++ field.name; + if (url.getQuery(qualified_name)) |param| { + _ = param; + } + } + } + pub fn freeRequestBody(value: anytype, alloc: std.mem.Allocator) void { std.json.parseFree(@TypeOf(value), value, .{ .allocator = alloc }); } diff --git a/src/main/main.zig b/src/main/main.zig index 527186b..7dc4fdc 100644 --- a/src/main/main.zig +++ b/src/main/main.zig @@ -26,8 +26,10 @@ const router = Router{ prepare(c.users.create), - //prepare(c.notes.create), - //prepare(c.notes.get), + prepare(c.notes.create), + prepare(c.notes.get), + + //prepare(c.communities.query), //Route.new(.GET, "/notes/:id/reacts", &c.notes.reacts.list), //Route.new(.POST, "/notes/:id/reacts", &c.notes.reacts.create), diff --git a/src/main/query.zig b/src/main/query.zig new file mode 100644 index 0000000..875b2b2 --- /dev/null +++ b/src/main/query.zig @@ -0,0 +1,53 @@ +const std = @import("std"); +const ParamIter = struct { + remaining: []const u8, + target: []const u8, + + fn next(self: *ParamIter) ?[]const u8 { + // + _ = self; + unreachable; + } +}; + +pub fn getParam(str: []const u8, param: []const u8) !?[]const u8 { + var iter = ParamIter{ .remaining = str, .target = param }; + const result = iter.next() orelse return null; + if (iter.next() != null) return error.TooMany; + return result; +} + +fn isScalarType(comptime T: type) bool { + return switch (T) { + []const u8 => true, + + else => switch (@typeInfo(T)) { + .Int, .Float, .Bool => true, + + .Optional => |info| isScalarType(info.child), + .Enum => |info| if (info.is_exhaustive) + true + else + @compileError("Unsupported type " ++ @typeName(T)), + + .Struct => false, + else => @compileError("Unsupported type " ++ @typeName(T)), + }, + }; +} + +pub fn parseQueryArgs(comptime T: type, str: []const u8) !T { + var result = std.mem.zeroInit(T, .{}); + _ = str; + + for (std.meta.fields(T)) |field| { + const ParseType = switch (@typeInfo(field.field_type)) { + .Optional => |info| info.child, + else => field.field_type, + }; + + _ = ParseType; + } + + return result; +}