Compare commits
7 commits
a6b928b42b
...
f47c1ee751
Author | SHA1 | Date | |
---|---|---|---|
f47c1ee751 | |||
b015bb8356 | |||
91edd17801 | |||
cd311cfa3c | |||
8b1ff1149f | |||
38f91dd890 | |||
7d362f0708 |
9 changed files with 265 additions and 99 deletions
|
@ -781,13 +781,9 @@ fn ApiConn(comptime DbConn: type) type {
|
||||||
return try self.backendDriveEntryToFrontend(entry, true);
|
return try self.backendDriveEntryToFrontend(entry, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn driveMkdir(self: *Self, path: []const u8) !DriveEntry {
|
pub fn driveMkdir(self: *Self, parent_path: []const u8, name: []const u8) !DriveEntry {
|
||||||
const user_id = self.user_id orelse return error.NoToken;
|
const user_id = self.user_id orelse return error.NoToken;
|
||||||
var split = std.mem.splitBackwards(u8, path, "/");
|
const entry = try services.drive.create(self.db, user_id, parent_path, name, null, self.allocator);
|
||||||
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);
|
errdefer util.deepFree(self.allocator, entry);
|
||||||
return try self.backendDriveEntryToFrontend(entry, true);
|
return try self.backendDriveEntryToFrontend(entry, true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,7 +153,7 @@ pub fn openForm(multipart_stream: anytype) MultipartForm(@TypeOf(multipart_strea
|
||||||
|
|
||||||
fn Deserializer(comptime Result: type) type {
|
fn Deserializer(comptime Result: type) type {
|
||||||
return util.DeserializerContext(Result, MultipartFormField, struct {
|
return util.DeserializerContext(Result, MultipartFormField, struct {
|
||||||
pub const options = .{ .isScalar = isScalar, .embed_unions = true };
|
pub const options = .{ .isScalar = isScalar };
|
||||||
|
|
||||||
pub fn isScalar(comptime T: type) bool {
|
pub fn isScalar(comptime T: type) bool {
|
||||||
if (T == FormFile or T == ?FormFile) return true;
|
if (T == FormFile or T == ?FormFile) return true;
|
||||||
|
|
|
@ -183,7 +183,7 @@ pub fn EncodeStruct(comptime Params: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
params: Params,
|
params: Params,
|
||||||
pub fn format(v: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
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('&');
|
try writer.writeByte('&');
|
||||||
}
|
}
|
||||||
|
|
||||||
fn formatQuery(comptime prefix: []const u8, comptime name: []const u8, params: anytype, writer: anytype) !void {
|
fn formatQuery(comptime prefix: []const u8, params: anytype, writer: anytype) !void {
|
||||||
const T = @TypeOf(params);
|
const T = @TypeOf(params);
|
||||||
const eff_prefix = if (prefix.len == 0) "" else prefix ++ ".";
|
if (comptime isScalar(T)) return formatScalar(prefix, params, writer);
|
||||||
if (comptime isScalar(T)) return formatScalar(eff_prefix ++ name, params, writer);
|
|
||||||
|
|
||||||
switch (@typeInfo(T)) {
|
switch (@typeInfo(T)) {
|
||||||
.Struct => {
|
.Struct => {
|
||||||
|
const eff_prefix = if (prefix.len == 0) "" else prefix ++ ".";
|
||||||
inline for (std.meta.fields(T)) |field| {
|
inline for (std.meta.fields(T)) |field| {
|
||||||
const val = @field(params, field.name);
|
const val = @field(params, field.name);
|
||||||
try formatQuery(eff_prefix ++ name, field.name, val, writer);
|
try formatQuery(eff_prefix ++ field.name, val, writer);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.Union => {
|
.Union => {
|
||||||
|
@ -239,12 +239,12 @@ fn formatQuery(comptime prefix: []const u8, comptime name: []const u8, params: a
|
||||||
const tag_name = field.name;
|
const tag_name = field.name;
|
||||||
if (@as(std.meta.Tag(T), params) == tag) {
|
if (@as(std.meta.Tag(T), params) == tag) {
|
||||||
const val = @field(params, tag_name);
|
const val = @field(params, tag_name);
|
||||||
try formatQuery(prefix, tag_name, val, writer);
|
try formatQuery(prefix, val, writer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.Optional => {
|
.Optional => {
|
||||||
if (params) |p| try formatQuery(prefix, name, p, writer);
|
if (params) |p| try formatQuery(prefix, p, writer);
|
||||||
},
|
},
|
||||||
else => @compileError("Unsupported query type"),
|
else => @compileError("Unsupported query type"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const api = @import("api");
|
const api = @import("api");
|
||||||
|
const util = @import("util");
|
||||||
const controller_utils = @import("../../controllers.zig").helpers;
|
const controller_utils = @import("../../controllers.zig").helpers;
|
||||||
|
|
||||||
const QueryArgs = api.CommunityQueryArgs;
|
const QueryArgs = api.CommunityQueryArgs;
|
||||||
|
@ -23,11 +24,85 @@ pub const query = struct {
|
||||||
pub const method = .GET;
|
pub const method = .GET;
|
||||||
pub const path = "/communities";
|
pub const path = "/communities";
|
||||||
|
|
||||||
pub const Query = QueryArgs;
|
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 fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||||
const results = try srv.queryCommunities(req.query);
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
try controller_utils.paginate(results, res, req.allocator);
|
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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
const std = @import("std");
|
||||||
const api = @import("api");
|
const api = @import("api");
|
||||||
const http = @import("http");
|
const http = @import("http");
|
||||||
const util = @import("util");
|
const util = @import("util");
|
||||||
|
@ -68,7 +69,12 @@ pub const mkdir = struct {
|
||||||
pub const Args = DriveArgs;
|
pub const Args = DriveArgs;
|
||||||
|
|
||||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||||
const result = try srv.driveMkdir(req.args.path);
|
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);
|
||||||
errdefer util.deepFree(srv.allocator, result);
|
errdefer util.deepFree(srv.allocator, result);
|
||||||
|
|
||||||
try res.json(.created, result);
|
try res.json(.created, result);
|
||||||
|
|
|
@ -18,6 +18,7 @@ pub const routes = .{
|
||||||
controllers.apiEndpoint(cluster.communities.create.page),
|
controllers.apiEndpoint(cluster.communities.create.page),
|
||||||
controllers.apiEndpoint(cluster.communities.create.submit),
|
controllers.apiEndpoint(cluster.communities.create.submit),
|
||||||
controllers.apiEndpoint(drive.details),
|
controllers.apiEndpoint(drive.details),
|
||||||
|
controllers.apiEndpoint(drive.form),
|
||||||
};
|
};
|
||||||
|
|
||||||
const static = struct {
|
const static = struct {
|
||||||
|
@ -231,6 +232,31 @@ const user_details = struct {
|
||||||
};
|
};
|
||||||
|
|
||||||
const drive = 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 {
|
const details = struct {
|
||||||
pub const path = "/drive/:path*";
|
pub const path = "/drive/:path*";
|
||||||
pub const method = .GET;
|
pub const method = .GET;
|
||||||
|
@ -239,29 +265,41 @@ const drive = struct {
|
||||||
path: []const u8,
|
path: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const dir_tmpl = @embedFile("./web/drive/directory.tmpl.html");
|
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 fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||||
const info = try srv.driveGet(req.args.path);
|
if (req.body.action != req.body.data) return error.BadRequest;
|
||||||
defer util.deepFree(srv.allocator, info);
|
switch (req.body.data) {
|
||||||
|
.mkcol => |data| {
|
||||||
|
_ = try srv.driveMkdir(req.args.path, data.name);
|
||||||
|
// TODO
|
||||||
|
|
||||||
var breadcrumbs = std.ArrayList([]const u8).init(srv.allocator);
|
try servePage(req, res, srv);
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,6 +17,28 @@
|
||||||
</li>
|
</li>
|
||||||
{/for =}
|
{/for =}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
|
<div class="popup-buttons">
|
||||||
|
<div class="popup" id="mkdir">
|
||||||
|
<a class="button popup-open" href="#mkdir">
|
||||||
|
<span class="fa-stack small">
|
||||||
|
<i class="fa-solid fa-stack-2x fa-folder"></i>
|
||||||
|
<i class="fa-solid fa-stack-1x fa-inverse fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a class="button popup-close" href="#">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</a>
|
||||||
|
<form class="popup-dialog" method="post" enctype="multipart/form-data">
|
||||||
|
<label>
|
||||||
|
<div>Create Directory</div>
|
||||||
|
<input type="text" name="mkcol.name" /> <!-- TODO: Rename this form param -->
|
||||||
|
</label>
|
||||||
|
<input type="hidden" name="action" value="mkcol" />
|
||||||
|
<button type="submit">Create</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<table class="directory-listing">
|
<table class="directory-listing">
|
||||||
{#for .dir.children.? |$child| =}
|
{#for .dir.children.? |$child| =}
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -40,23 +40,14 @@ pub fn deserializeString(allocator: std.mem.Allocator, comptime T: type, value:
|
||||||
|
|
||||||
fn getStaticFieldList(comptime T: type, comptime prefix: FieldRef, comptime options: SerializationOptions) []const FieldRef {
|
fn getStaticFieldList(comptime T: type, comptime prefix: FieldRef, comptime options: SerializationOptions) []const FieldRef {
|
||||||
comptime {
|
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 (options.isScalar(T)) return &.{prefix};
|
||||||
if (std.meta.trait.is(.Optional)(T)) return getStaticFieldList(std.meta.Child(T), prefix, options);
|
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 &.{};
|
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 = &.{};
|
var fields: []const FieldRef = &.{};
|
||||||
|
|
||||||
for (std.meta.fields(T)) |f| {
|
for (std.meta.fields(T)) |f| {
|
||||||
const new_prefix = eff_prefix ++ &[_][]const u8{f.name};
|
const new_prefix = if (std.meta.trait.is(.Union)(T)) prefix else prefix ++ &[_][]const u8{f.name};
|
||||||
const F = f.field_type;
|
const F = f.field_type;
|
||||||
fields = fields ++ getStaticFieldList(F, new_prefix, options);
|
fields = fields ++ getStaticFieldList(F, new_prefix, options);
|
||||||
}
|
}
|
||||||
|
@ -67,25 +58,16 @@ fn getStaticFieldList(comptime T: type, comptime prefix: FieldRef, comptime opti
|
||||||
|
|
||||||
fn getDynamicFieldList(comptime T: type, comptime prefix: FieldRef, comptime options: SerializationOptions) []const DynamicField {
|
fn getDynamicFieldList(comptime T: type, comptime prefix: FieldRef, comptime options: SerializationOptions) []const DynamicField {
|
||||||
comptime {
|
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 (options.isScalar(T)) return &.{};
|
||||||
if (std.meta.trait.is(.Optional)(T)) return getDynamicFieldList(std.meta.Child(T), prefix, options);
|
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 &.{
|
if (std.meta.trait.isSlice(T) and !std.meta.trait.isZigString(T)) return &.{
|
||||||
.{ .ref = prefix, .child_type = std.meta.Child(T) },
|
.{ .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 = &.{};
|
var fields: []const DynamicField = &.{};
|
||||||
|
|
||||||
for (std.meta.fields(T)) |f| {
|
for (std.meta.fields(T)) |f| {
|
||||||
const new_prefix = eff_prefix ++ &[_][]const u8{f.name};
|
const new_prefix = if (std.meta.trait.is(.Union)(T)) prefix else prefix ++ &[_][]const u8{f.name};
|
||||||
const F = f.field_type;
|
const F = f.field_type;
|
||||||
fields = fields ++ getDynamicFieldList(F, new_prefix, options);
|
fields = fields ++ getDynamicFieldList(F, new_prefix, options);
|
||||||
}
|
}
|
||||||
|
@ -100,39 +82,42 @@ const DynamicField = struct {
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const SerializationOptions = struct {
|
pub const SerializationOptions = struct {
|
||||||
embed_unions: bool,
|
|
||||||
isScalar: fn (type) bool,
|
isScalar: fn (type) bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const default_options = SerializationOptions{
|
pub const default_options = SerializationOptions{
|
||||||
.embed_unions = true,
|
|
||||||
.isScalar = defaultIsScalar,
|
.isScalar = defaultIsScalar,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn StaticIntermediary(comptime Result: type, comptime From: type, comptime options: SerializationOptions) type {
|
fn StaticIntermediary(comptime Result: type, comptime From: type, comptime options: SerializationOptions) type {
|
||||||
const field_refs = getStaticFieldList(Result, &.{}, options);
|
const field_refs = getStaticFieldList(Result, &.{}, options);
|
||||||
|
|
||||||
var fields: [field_refs.len + 1]std.builtin.Type.StructField = undefined;
|
// avert compiler crash by having at least one field
|
||||||
for (field_refs) |ref, i| {
|
var fields = [_]std.builtin.Type.StructField{.{
|
||||||
fields[i] = .{
|
.name = "__dummy",
|
||||||
.name = util.comptimeJoin(".", ref),
|
.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,
|
||||||
.field_type = ?From,
|
.field_type = ?From,
|
||||||
.default_value = &@as(?From, null),
|
.default_value = &@as(?From, null),
|
||||||
.is_comptime = false,
|
.is_comptime = false,
|
||||||
.alignment = @alignOf(?From),
|
.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 = .{
|
return @Type(.{ .Struct = .{
|
||||||
.layout = .Auto,
|
.layout = .Auto,
|
||||||
.fields = &fields,
|
.fields = fields[0..count],
|
||||||
.decls = &.{},
|
.decls = &.{},
|
||||||
.is_tuple = false,
|
.is_tuple = false,
|
||||||
} });
|
} });
|
||||||
|
@ -141,28 +126,33 @@ fn StaticIntermediary(comptime Result: type, comptime From: type, comptime optio
|
||||||
fn DynamicIntermediary(comptime Result: type, comptime From: type, comptime options: SerializationOptions) type {
|
fn DynamicIntermediary(comptime Result: type, comptime From: type, comptime options: SerializationOptions) type {
|
||||||
const field_refs = getDynamicFieldList(Result, &.{}, options);
|
const field_refs = getDynamicFieldList(Result, &.{}, options);
|
||||||
|
|
||||||
var fields: [field_refs.len + 1]std.builtin.Type.StructField = undefined;
|
var fields = [_]std.builtin.Type.StructField{.{
|
||||||
for (field_refs) |f, i| {
|
.name = "__dummy",
|
||||||
const T = std.ArrayListUnmanaged(Intermediary(f.child_type, From, options));
|
.default_value = &{},
|
||||||
fields[i] = .{
|
.field_type = void,
|
||||||
.name = util.comptimeJoin(".", f.ref),
|
.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,
|
||||||
.default_value = &T{},
|
.default_value = &T{},
|
||||||
.field_type = T,
|
.field_type = T,
|
||||||
.is_comptime = false,
|
.is_comptime = false,
|
||||||
.alignment = @alignOf(T),
|
.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 = .{
|
return @Type(.{ .Struct = .{
|
||||||
.layout = .Auto,
|
.layout = .Auto,
|
||||||
.fields = &fields,
|
.fields = fields[0..count],
|
||||||
.decls = &.{},
|
.decls = &.{},
|
||||||
.is_tuple = false,
|
.is_tuple = false,
|
||||||
} });
|
} });
|
||||||
|
@ -288,37 +278,42 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime
|
||||||
util.deepFree(allocator, val);
|
util.deepFree(allocator, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DeserializeError = error{ ParseFailure, MissingField, DuplicateUnionMember, SparseSlice, OutOfMemory };
|
||||||
|
|
||||||
fn deserialize(
|
fn deserialize(
|
||||||
self: *@This(),
|
self: *@This(),
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
comptime T: type,
|
comptime T: type,
|
||||||
intermediary: anytype,
|
intermediary: anytype,
|
||||||
comptime field_ref: FieldRef,
|
comptime field_ref: FieldRef,
|
||||||
) !?T {
|
) DeserializeError!?T {
|
||||||
if (comptime Context.options.isScalar(T)) {
|
if (comptime Context.options.isScalar(T)) {
|
||||||
const val = @field(intermediary.static, util.comptimeJoin(".", field_ref));
|
const val = @field(intermediary.static, util.comptimeJoin(".", field_ref));
|
||||||
return try self.context.deserializeScalar(allocator, T, val orelse return null);
|
return self.context.deserializeScalar(allocator, T, val orelse return null) catch return error.ParseFailure;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (@typeInfo(T)) {
|
switch (@typeInfo(T)) {
|
||||||
// At most one of any union field can be active at a time, and it is embedded
|
// At most one of any union field can be active at a time
|
||||||
// in its parent container
|
|
||||||
.Union => |info| {
|
.Union => |info| {
|
||||||
var result: ?T = null;
|
var result: ?T = null;
|
||||||
errdefer if (result) |v| self.deserializeFree(allocator, v);
|
errdefer if (result) |v| self.deserializeFree(allocator, v);
|
||||||
// TODO: errdefer cleanup
|
var partial_match_found: bool = false;
|
||||||
const union_ref: FieldRef = if (Context.options.embed_unions) field_ref[0 .. field_ref.len - 1] else field_ref;
|
|
||||||
inline for (info.fields) |field| {
|
inline for (info.fields) |field| {
|
||||||
const F = field.field_type;
|
const F = field.field_type;
|
||||||
const new_field_ref = union_ref ++ &[_][]const u8{field.name};
|
const maybe_value = self.deserialize(allocator, F, intermediary, field_ref) catch |err| switch (err) {
|
||||||
const maybe_value = try self.deserialize(allocator, F, intermediary, new_field_ref);
|
error.MissingField => blk: {
|
||||||
|
partial_match_found = true;
|
||||||
|
break :blk @as(?F, null);
|
||||||
|
},
|
||||||
|
else => |e| return e,
|
||||||
|
};
|
||||||
if (maybe_value) |value| {
|
if (maybe_value) |value| {
|
||||||
// TODO: errdefer cleanup
|
|
||||||
errdefer self.deserializeFree(allocator, value);
|
errdefer self.deserializeFree(allocator, value);
|
||||||
if (result != null) return error.DuplicateUnionMember;
|
if (result != null) return error.DuplicateUnionMember;
|
||||||
result = @unionInit(T, field.name, value);
|
result = @unionInit(T, field.name, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (partial_match_found and result == null) return error.MissingField;
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
* {
|
* {
|
||||||
--theme-color: #713c8c;
|
--theme-accent: #713c8c;
|
||||||
--theme-color-highlight: #9b52bf;
|
--theme-accent-highlight: #9b52bf;
|
||||||
|
--theme-accent-contrast: #fff;
|
||||||
|
--theme-fg-color: #000;
|
||||||
|
--theme-bg-color: #fff;
|
||||||
|
|
||||||
|
--fa-inverse: var(--theme-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -92,7 +97,7 @@ form .textinput input:focus-visible{
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
form .textinput:focus-within {
|
form .textinput:focus-within {
|
||||||
outline: solid 2px var(--theme-color);
|
outline: solid 2px var(--theme-accent);
|
||||||
}
|
}
|
||||||
form .textinput span.prefix {
|
form .textinput span.prefix {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
@ -124,14 +129,14 @@ button, a.button {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--theme-color);
|
background-color: var(--theme-accent);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover, a.button:hover {
|
button:hover, a.button:hover {
|
||||||
background-color: var(--theme-color-highlight);
|
background-color: var(--theme-accent-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-profile img.banner {
|
.user-profile img.banner {
|
||||||
|
@ -228,3 +233,32 @@ button:hover, a.button:hover {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
vertical-align: bottom;
|
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);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue