diff --git a/src/api/lib.zig b/src/api/lib.zig index f153140..e28bda5 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -781,9 +781,13 @@ fn ApiConn(comptime DbConn: type) type { return try self.backendDriveEntryToFrontend(entry, true); } - pub fn driveMkdir(self: *Self, parent_path: []const u8, name: []const u8) !DriveEntry { + pub fn driveMkdir(self: *Self, path: []const u8) !DriveEntry { const user_id = self.user_id orelse return error.NoToken; - const entry = try services.drive.create(self.db, user_id, parent_path, name, null, self.allocator); + var split = std.mem.splitBackwards(u8, path, "/"); + std.log.debug("{s}", .{path}); + const base = split.first(); + const dir = split.rest(); + const entry = try services.drive.create(self.db, user_id, dir, base, null, self.allocator); errdefer util.deepFree(self.allocator, entry); return try self.backendDriveEntryToFrontend(entry, true); } diff --git a/src/http/multipart.zig b/src/http/multipart.zig index 854b7c3..176ca7b 100644 --- a/src/http/multipart.zig +++ b/src/http/multipart.zig @@ -153,7 +153,7 @@ pub fn openForm(multipart_stream: anytype) MultipartForm(@TypeOf(multipart_strea fn Deserializer(comptime Result: type) type { return util.DeserializerContext(Result, MultipartFormField, struct { - pub const options = .{ .isScalar = isScalar }; + pub const options = .{ .isScalar = isScalar, .embed_unions = true }; pub fn isScalar(comptime T: type) bool { if (T == FormFile or T == ?FormFile) return true; diff --git a/src/http/urlencode.zig b/src/http/urlencode.zig index bd97f50..ad532e2 100644 --- a/src/http/urlencode.zig +++ b/src/http/urlencode.zig @@ -183,7 +183,7 @@ pub fn EncodeStruct(comptime Params: type) type { return struct { params: Params, pub fn format(v: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - try formatQuery("", v.params, writer); + try formatQuery("", "", v.params, writer); } }; } @@ -221,16 +221,16 @@ fn formatScalar(comptime name: []const u8, val: anytype, writer: anytype) !void try writer.writeByte('&'); } -fn formatQuery(comptime prefix: []const u8, params: anytype, writer: anytype) !void { +fn formatQuery(comptime prefix: []const u8, comptime name: []const u8, params: anytype, writer: anytype) !void { const T = @TypeOf(params); - if (comptime isScalar(T)) return formatScalar(prefix, params, writer); + const eff_prefix = if (prefix.len == 0) "" else prefix ++ "."; + if (comptime isScalar(T)) return formatScalar(eff_prefix ++ name, params, writer); switch (@typeInfo(T)) { .Struct => { - const eff_prefix = if (prefix.len == 0) "" else prefix ++ "."; inline for (std.meta.fields(T)) |field| { const val = @field(params, field.name); - try formatQuery(eff_prefix ++ field.name, val, writer); + try formatQuery(eff_prefix ++ name, field.name, val, writer); } }, .Union => { @@ -239,12 +239,12 @@ fn formatQuery(comptime prefix: []const u8, params: anytype, writer: anytype) !v const tag_name = field.name; if (@as(std.meta.Tag(T), params) == tag) { const val = @field(params, tag_name); - try formatQuery(prefix, val, writer); + try formatQuery(prefix, tag_name, val, writer); } } }, .Optional => { - if (params) |p| try formatQuery(prefix, p, writer); + if (params) |p| try formatQuery(prefix, name, p, writer); }, else => @compileError("Unsupported query type"), } diff --git a/src/main/controllers/api/communities.zig b/src/main/controllers/api/communities.zig index 161fbb0..224e33c 100644 --- a/src/main/controllers/api/communities.zig +++ b/src/main/controllers/api/communities.zig @@ -1,5 +1,4 @@ const api = @import("api"); -const util = @import("util"); const controller_utils = @import("../../controllers.zig").helpers; const QueryArgs = api.CommunityQueryArgs; @@ -24,85 +23,11 @@ pub const query = struct { pub const method = .GET; pub const path = "/communities"; - pub const Query = struct { - const OrderBy = api.CommunityQueryArgs.OrderBy; - const Direction = api.CommunityQueryArgs.Direction; - const PageDirection = api.CommunityQueryArgs.PageDirection; - - // Max items to fetch - max_items: usize = 20, - - // Selection filters - owner_id: ?util.Uuid = null, - like: ?[]const u8 = null, - created_before: ?util.DateTime = null, - created_after: ?util.DateTime = null, - - // Ordering parameter - order_by: OrderBy = .created_at, - direction: Direction = .ascending, - - // Page start parameter - prev: ?union(OrderBy) { - name: struct { - id: util.Uuid, - name: []const u8, - }, - host: struct { - id: util.Uuid, - host: []const u8, - }, - created_at: struct { - id: util.Uuid, - created_at: util.DateTime, - }, - } = null, - - page_direction: PageDirection = .forward, - }; + pub const Query = QueryArgs; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { - const q = req.query; - const results = try srv.queryCommunities(.{ - .max_items = q.max_items, - .owner_id = q.owner_id, - .like = q.like, - .created_before = q.created_before, - .created_after = q.created_after, - .order_by = q.order_by, - .direction = q.direction, - .prev = if (q.prev) |prev| switch (prev) { - .name => |p| .{ .id = p.id, .order_val = .{ .name = p.name } }, - .host => |p| .{ .id = p.id, .order_val = .{ .host = p.host } }, - .created_at => |p| .{ .id = p.id, .order_val = .{ .created_at = p.created_at } }, - } else null, - .page_direction = q.page_direction, - }); + const results = try srv.queryCommunities(req.query); - const convert = struct { - fn func(args: api.CommunityQueryArgs) Query { - return .{ - .max_items = args.max_items, - .owner_id = args.owner_id, - .like = args.like, - .created_before = args.created_before, - .created_after = args.created_after, - .order_by = args.order_by, - .direction = args.direction, - .prev = if (args.prev) |prev| switch (prev.order_val) { - .name => |v| .{ .name = .{ .id = prev.id, .name = v } }, - .host => |v| .{ .host = .{ .id = prev.id, .host = v } }, - .created_at => |v| .{ .created_at = .{ .id = prev.id, .created_at = v } }, - } else null, - .page_direction = args.page_direction, - }; - } - }.func; - - try controller_utils.paginate(.{ - .items = results.items, - .next_page = convert(results.next_page), - .prev_page = convert(results.prev_page), - }, res, req.allocator); + try controller_utils.paginate(results, res, req.allocator); } }; diff --git a/src/main/controllers/api/drive.zig b/src/main/controllers/api/drive.zig index 17e8a27..4650106 100644 --- a/src/main/controllers/api/drive.zig +++ b/src/main/controllers/api/drive.zig @@ -1,4 +1,3 @@ -const std = @import("std"); const api = @import("api"); const http = @import("http"); const util = @import("util"); @@ -69,12 +68,7 @@ pub const mkdir = struct { pub const Args = DriveArgs; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { - var split = std.mem.splitBackwards(u8, std.mem.trim(u8, req.args.path, "/"), "/"); - const name = split.first(); - const parent = split.rest(); - std.log.debug("{s}, {s}", .{ parent, name }); - - const result = try srv.driveMkdir(parent, name); + const result = try srv.driveMkdir(req.args.path); errdefer util.deepFree(srv.allocator, result); try res.json(.created, result); diff --git a/src/main/controllers/web.zig b/src/main/controllers/web.zig index 835e64e..cde66a8 100644 --- a/src/main/controllers/web.zig +++ b/src/main/controllers/web.zig @@ -18,7 +18,6 @@ pub const routes = .{ controllers.apiEndpoint(cluster.communities.create.page), controllers.apiEndpoint(cluster.communities.create.submit), controllers.apiEndpoint(drive.details), - controllers.apiEndpoint(drive.form), }; const static = struct { @@ -232,31 +231,6 @@ const user_details = struct { }; const drive = struct { - const dir_tmpl = @embedFile("./web/drive/directory.tmpl.html"); - fn servePage(req: anytype, res: anytype, srv: anytype) !void { - const info = try srv.driveGet(req.args.path); - defer util.deepFree(srv.allocator, info); - - var breadcrumbs = std.ArrayList([]const u8).init(srv.allocator); - defer breadcrumbs.deinit(); - - var iter = util.PathIter.from(req.args.path); - while (iter.next()) |p| { - std.log.debug("breadcrumb: {s}", .{p}); - try breadcrumbs.append(if (p.len != 0) p else continue); - } - - switch (info) { - .dir => |dir| try res.template(.ok, srv, dir_tmpl, .{ - .dir = dir, - .breadcrumbs = breadcrumbs.items, - .mount_path = req.mount_path, - .base_drive_path = "drive", - }), - else => unreachable, - } - } - const details = struct { pub const path = "/drive/:path*"; pub const method = .GET; @@ -265,41 +239,29 @@ const drive = struct { path: []const u8, }; - pub fn handler(req: anytype, res: anytype, srv: anytype) !void { - try servePage(req, res, srv); - } - }; - - const form = struct { - pub const path = "/drive/:path*"; - pub const method = .POST; - - pub const Args = struct { - path: []const u8, - }; - - const Action = enum { - mkcol, - }; - - pub const Body = struct { - action: Action, - data: union(Action) { - mkcol: struct { - name: []const u8, - }, - }, - }; + pub const dir_tmpl = @embedFile("./web/drive/directory.tmpl.html"); pub fn handler(req: anytype, res: anytype, srv: anytype) !void { - if (req.body.action != req.body.data) return error.BadRequest; - switch (req.body.data) { - .mkcol => |data| { - _ = try srv.driveMkdir(req.args.path, data.name); - // TODO + const info = try srv.driveGet(req.args.path); + defer util.deepFree(srv.allocator, info); - try servePage(req, res, srv); - }, + var breadcrumbs = std.ArrayList([]const u8).init(srv.allocator); + defer breadcrumbs.deinit(); + + var iter = util.PathIter.from(req.args.path); + while (iter.next()) |p| { + std.log.debug("breadcrumb: {s}", .{p}); + try breadcrumbs.append(if (p.len != 0) p else continue); + } + + switch (info) { + .dir => |dir| try res.template(.ok, srv, dir_tmpl, .{ + .dir = dir, + .breadcrumbs = breadcrumbs.items, + .mount_path = req.mount_path, + .base_drive_path = "drive", + }), + else => unreachable, } } }; diff --git a/src/main/controllers/web/drive/directory.tmpl.html b/src/main/controllers/web/drive/directory.tmpl.html index 5135b6a..f6d59bf 100644 --- a/src/main/controllers/web/drive/directory.tmpl.html +++ b/src/main/controllers/web/drive/directory.tmpl.html @@ -17,28 +17,6 @@ {/for =} - - {#for .dir.children.? |$child| =} diff --git a/src/util/serialize.zig b/src/util/serialize.zig index a7ca8e1..bd2e2a4 100644 --- a/src/util/serialize.zig +++ b/src/util/serialize.zig @@ -40,14 +40,23 @@ pub fn deserializeString(allocator: std.mem.Allocator, comptime T: type, value: fn getStaticFieldList(comptime T: type, comptime prefix: FieldRef, comptime options: SerializationOptions) []const FieldRef { comptime { + if (std.meta.trait.is(.Union)(T) and prefix.len == 0 and options.embed_unions) { + @compileError("Cannot embed a union into nothing"); + } + if (options.isScalar(T)) return &.{prefix}; if (std.meta.trait.is(.Optional)(T)) return getStaticFieldList(std.meta.Child(T), prefix, options); if (std.meta.trait.isSlice(T) and !std.meta.trait.isZigString(T)) return &.{}; + const eff_prefix: FieldRef = if (std.meta.trait.is(.Union)(T) and options.embed_unions) + prefix[0 .. prefix.len - 1] + else + prefix; + var fields: []const FieldRef = &.{}; for (std.meta.fields(T)) |f| { - const new_prefix = if (std.meta.trait.is(.Union)(T)) prefix else prefix ++ &[_][]const u8{f.name}; + const new_prefix = eff_prefix ++ &[_][]const u8{f.name}; const F = f.field_type; fields = fields ++ getStaticFieldList(F, new_prefix, options); } @@ -58,16 +67,25 @@ fn getStaticFieldList(comptime T: type, comptime prefix: FieldRef, comptime opti fn getDynamicFieldList(comptime T: type, comptime prefix: FieldRef, comptime options: SerializationOptions) []const DynamicField { comptime { + if (std.meta.trait.is(.Union)(T) and prefix.len == 0 and options.embed_unions) { + @compileError("Cannot embed a union into nothing"); + } + if (options.isScalar(T)) return &.{}; if (std.meta.trait.is(.Optional)(T)) return getDynamicFieldList(std.meta.Child(T), prefix, options); if (std.meta.trait.isSlice(T) and !std.meta.trait.isZigString(T)) return &.{ .{ .ref = prefix, .child_type = std.meta.Child(T) }, }; + const eff_prefix: FieldRef = if (std.meta.trait.is(.Union)(T) and options.embed_unions) + prefix[0 .. prefix.len - 1] + else + prefix; + var fields: []const DynamicField = &.{}; for (std.meta.fields(T)) |f| { - const new_prefix = if (std.meta.trait.is(.Union)(T)) prefix else prefix ++ &[_][]const u8{f.name}; + const new_prefix = eff_prefix ++ &[_][]const u8{f.name}; const F = f.field_type; fields = fields ++ getDynamicFieldList(F, new_prefix, options); } @@ -82,42 +100,39 @@ const DynamicField = struct { }; pub const SerializationOptions = struct { + embed_unions: bool, isScalar: fn (type) bool, }; pub const default_options = SerializationOptions{ + .embed_unions = true, .isScalar = defaultIsScalar, }; fn StaticIntermediary(comptime Result: type, comptime From: type, comptime options: SerializationOptions) type { const field_refs = getStaticFieldList(Result, &.{}, options); - // avert compiler crash by having at least one field - var fields = [_]std.builtin.Type.StructField{.{ - .name = "__dummy", - .default_value = &{}, - .field_type = void, - .is_comptime = false, - .alignment = 0, - }} ** (field_refs.len + 1); - - var count: usize = 1; - outer: for (field_refs) |ref| { - const name = util.comptimeJoin(".", ref); - for (fields[0..count]) |f| if (std.mem.eql(u8, f.name, name)) continue :outer; - fields[count] = .{ - .name = name, + var fields: [field_refs.len + 1]std.builtin.Type.StructField = undefined; + for (field_refs) |ref, i| { + fields[i] = .{ + .name = util.comptimeJoin(".", ref), .field_type = ?From, .default_value = &@as(?From, null), .is_comptime = false, .alignment = @alignOf(?From), }; - count += 1; } + fields[fields.len - 1] = .{ + .name = "__dummy", + .default_value = &1, + .field_type = usize, + .is_comptime = false, + .alignment = @alignOf(usize), + }; return @Type(.{ .Struct = .{ .layout = .Auto, - .fields = fields[0..count], + .fields = &fields, .decls = &.{}, .is_tuple = false, } }); @@ -126,33 +141,28 @@ fn StaticIntermediary(comptime Result: type, comptime From: type, comptime optio fn DynamicIntermediary(comptime Result: type, comptime From: type, comptime options: SerializationOptions) type { const field_refs = getDynamicFieldList(Result, &.{}, options); - var fields = [_]std.builtin.Type.StructField{.{ - .name = "__dummy", - .default_value = &{}, - .field_type = void, - .is_comptime = false, - .alignment = 0, - }} ** (field_refs.len + 1); - - var count: usize = 1; - outer: for (field_refs) |ref| { - const name = util.comptimeJoin(".", ref.ref); - for (fields[0..count]) |f| if (std.mem.eql(u8, f.name, name)) continue :outer; - - const T = std.ArrayListUnmanaged(Intermediary(ref.child_type, From, options)); - fields[count] = .{ - .name = name, + var fields: [field_refs.len + 1]std.builtin.Type.StructField = undefined; + for (field_refs) |f, i| { + const T = std.ArrayListUnmanaged(Intermediary(f.child_type, From, options)); + fields[i] = .{ + .name = util.comptimeJoin(".", f.ref), .default_value = &T{}, .field_type = T, .is_comptime = false, .alignment = @alignOf(T), }; - count += 1; } + fields[fields.len - 1] = .{ + .name = "__dummy", + .default_value = &1, + .field_type = usize, + .is_comptime = false, + .alignment = @alignOf(usize), + }; return @Type(.{ .Struct = .{ .layout = .Auto, - .fields = fields[0..count], + .fields = &fields, .decls = &.{}, .is_tuple = false, } }); @@ -278,42 +288,37 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime util.deepFree(allocator, val); } - const DeserializeError = error{ ParseFailure, MissingField, DuplicateUnionMember, SparseSlice, OutOfMemory }; - fn deserialize( self: *@This(), allocator: std.mem.Allocator, comptime T: type, intermediary: anytype, comptime field_ref: FieldRef, - ) DeserializeError!?T { + ) !?T { if (comptime Context.options.isScalar(T)) { const val = @field(intermediary.static, util.comptimeJoin(".", field_ref)); - return self.context.deserializeScalar(allocator, T, val orelse return null) catch return error.ParseFailure; + return try self.context.deserializeScalar(allocator, T, val orelse return null); } switch (@typeInfo(T)) { - // At most one of any union field can be active at a time + // At most one of any union field can be active at a time, and it is embedded + // in its parent container .Union => |info| { var result: ?T = null; errdefer if (result) |v| self.deserializeFree(allocator, v); - var partial_match_found: bool = false; + // TODO: errdefer cleanup + const union_ref: FieldRef = if (Context.options.embed_unions) field_ref[0 .. field_ref.len - 1] else field_ref; inline for (info.fields) |field| { const F = field.field_type; - const maybe_value = self.deserialize(allocator, F, intermediary, field_ref) catch |err| switch (err) { - error.MissingField => blk: { - partial_match_found = true; - break :blk @as(?F, null); - }, - else => |e| return e, - }; + const new_field_ref = union_ref ++ &[_][]const u8{field.name}; + const maybe_value = try self.deserialize(allocator, F, intermediary, new_field_ref); if (maybe_value) |value| { + // TODO: errdefer cleanup errdefer self.deserializeFree(allocator, value); if (result != null) return error.DuplicateUnionMember; result = @unionInit(T, field.name, value); } } - if (partial_match_found and result == null) return error.MissingField; return result; }, diff --git a/static/site.css b/static/site.css index b029fa3..1a172eb 100644 --- a/static/site.css +++ b/static/site.css @@ -1,13 +1,8 @@ @import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap'); * { - --theme-accent: #713c8c; - --theme-accent-highlight: #9b52bf; - --theme-accent-contrast: #fff; - --theme-fg-color: #000; - --theme-bg-color: #fff; - - --fa-inverse: var(--theme-accent); + --theme-color: #713c8c; + --theme-color-highlight: #9b52bf; } body { @@ -97,7 +92,7 @@ form .textinput input:focus-visible{ outline: none; } form .textinput:focus-within { - outline: solid 2px var(--theme-accent); + outline: solid 2px var(--theme-color); } form .textinput span.prefix { user-select: none; @@ -129,14 +124,14 @@ button, a.button { border-radius: 10px; border: none; color: #fff; - background-color: var(--theme-accent); + background-color: var(--theme-color); font-weight: bold; transition: background-color 0.2s; cursor: pointer; } button:hover, a.button:hover { - background-color: var(--theme-accent-highlight); + background-color: var(--theme-color-highlight); } .user-profile img.banner { @@ -233,32 +228,3 @@ button:hover, a.button:hover { justify-content: flex-end; vertical-align: bottom; } - -.drive .buttons a[href="#mkdir"] span.fa-stack { - font-size: 8pt; - vertical-align: bottom; -} - -.drive .buttons a[href="#mkdir"] .fa-plus { - position: relative; - bottom: -1px; -} - -.popup :is(.popup-close, .popup-dialog) { - display: none; -} - -.popup:target .popup-open { - display: none; -} - -.popup:target :is(.popup-close, .popup-dialog) { - display: unset; -} - -.popup:target .popup-dialog { - position: absolute; - color: var(--theme-fg-color); - background-color: var(--theme-bg-color); - border: 1px solid var(--theme-accent); -}