Compare commits

...

5 commits

Author SHA1 Message Date
d4703a2127 Add delete button to drive page 2022-12-16 02:37:13 -08:00
6dc8447343 Use body_tag_from_query_param 2022-12-16 02:05:20 -08:00
57f2bd821e Add body_tag_from_query_param option
When you provide a string for this option, and the request body type
is a union, the query param provided will be treated as a value of
std.meta.Tag(Body). Then the associated value will be parsed from the
body during the request.
2022-12-16 02:02:13 -08:00
e2281f7c14 rename parseBodyFromRequest to parseBodyFromReader 2022-12-16 02:01:26 -08:00
471ca527bb Rename mkcol -> mkdir 2022-12-16 02:00:26 -08:00
5 changed files with 128 additions and 22 deletions

View file

@ -634,7 +634,7 @@ const BaseContentType = enum {
other, other,
}; };
fn parseBodyFromRequest( pub fn parseBodyFromReader(
comptime T: type, comptime T: type,
comptime options: ParseBodyOptions, comptime options: ParseBodyOptions,
content_type: ?[]const u8, content_type: ?[]const u8,
@ -703,7 +703,7 @@ pub fn ParseBody(comptime Body: type, comptime options: ParseBodyOptions) type {
} }
var stream = req.body orelse return error.NoBody; var stream = req.body orelse return error.NoBody;
const body = try parseBodyFromRequest(Body, options, content_type, stream.reader(), ctx.allocator); const body = try parseBodyFromReader(Body, options, content_type, stream.reader(), ctx.allocator);
defer util.deepFree(ctx.allocator, body); defer util.deepFree(ctx.allocator, body);
return next.handle( return next.handle(
@ -719,11 +719,11 @@ pub fn parseBody(comptime Body: type) ParseBody(Body) {
return .{}; return .{};
} }
test "parseBodyFromRequest" { test "parseBodyFromReader" {
const testCase = struct { const testCase = struct {
fn case(content_type: []const u8, body: []const u8, expected: anytype) !void { fn case(content_type: []const u8, body: []const u8, expected: anytype) !void {
var stream = std.io.StreamSource{ .const_buffer = std.io.fixedBufferStream(body) }; var stream = std.io.StreamSource{ .const_buffer = std.io.fixedBufferStream(body) };
const result = try parseBodyFromRequest(@TypeOf(expected), .{}, content_type, stream.reader(), std.testing.allocator); const result = try parseBodyFromReader(@TypeOf(expected), .{}, content_type, stream.reader(), std.testing.allocator);
defer util.deepFree(std.testing.allocator, result); defer util.deepFree(std.testing.allocator, result);
try util.testing.expectDeepEqual(expected, result); try util.testing.expectDeepEqual(expected, result);

View file

@ -80,6 +80,11 @@ pub fn EndpointRequest(comptime Endpoint: type) type {
false, false,
}; };
const union_tag_from_query_param = if (@hasDecl(Endpoint, "body_tag_from_query_param")) blk: {
if (!std.meta.trait.is(.Union)(Body)) @compileError("body_tag_from_query_param only valid if body is a union");
break :blk @as(?[]const u8, Endpoint.body_tag_from_query_param);
} else null;
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
method: http.Method, method: http.Method,
@ -97,9 +102,9 @@ pub fn EndpointRequest(comptime Endpoint: type) type {
//else //else
mdw.ParsePathArgs(Endpoint.path, Args){}; mdw.ParsePathArgs(Endpoint.path, Args){};
const body_middleware = //if (Body == void) const body_middleware = if (union_tag_from_query_param) |param|
//mdw.injectContext(.{ .body = {} }) ParseBodyWithQueryType(Body, param, body_options){}
//else else
mdw.ParseBody(Body, body_options){}; mdw.ParseBody(Body, body_options){};
const query_middleware = //if (Query == void) const query_middleware = //if (Query == void)
@ -109,6 +114,56 @@ pub fn EndpointRequest(comptime Endpoint: type) type {
}; };
} }
/// Gets a tag from the query param with the given name, then treats the request body
/// as the respective union type
fn ParseBodyWithQueryType(comptime Union: type, comptime query_param_name: []const u8, comptime options: anytype) type {
return struct {
pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void {
const Tag = std.meta.Tag(Union);
const Param = @Type(.{ .Struct = .{
.fields = &.{.{
.name = query_param_name,
.field_type = Tag,
.default_value = null,
.is_comptime = false,
.alignment = if (@sizeOf(Tag) == 0) 0 else @alignOf(Tag),
}},
.decls = &.{},
.layout = .Auto,
.is_tuple = false,
} });
const param = try http.urlencode.parse(ctx.allocator, true, Param, ctx.query_string);
var result: ?Union = null;
const content_type = req.headers.get("Content-Type");
inline for (comptime std.meta.tags(Tag)) |tag| {
if (@field(param, query_param_name) == tag) {
std.debug.assert(result == null);
const P = std.meta.TagPayload(Union, tag);
std.log.debug("Deserializing to type {}", .{P});
var stream = req.body orelse return error.NoBody;
result = @unionInit(Union, @tagName(tag), try mdw.parseBodyFromReader(
P,
options,
content_type,
stream.reader(),
ctx.allocator,
));
}
}
return mdw.injectContextValue("body", result.?).handle(
req,
res,
ctx,
next,
);
}
};
}
fn CallApiEndpoint(comptime Endpoint: type) type { fn CallApiEndpoint(comptime Endpoint: type) type {
return struct { return struct {
pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, _: void) !void { pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, _: void) !void {

View file

@ -279,27 +279,39 @@ const drive = struct {
}; };
const Action = enum { const Action = enum {
mkcol, mkdir,
delete,
}; };
pub const Body = struct { pub const body_tag_from_query_param = "action";
action: Action, pub const Body = union(Action) {
data: union(Action) { mkdir: struct {
mkcol: struct { name: []const u8,
name: []const u8,
},
}, },
delete: struct {},
}; };
pub fn handler(req: anytype, res: anytype, srv: anytype) !void { pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
if (req.body.action != req.body.data) return error.BadRequest; switch (req.body) {
switch (req.body.data) { .mkdir => |body| {
.mkcol => |data| { _ = try srv.driveMkdir(req.args.path, body.name);
_ = try srv.driveMkdir(req.args.path, data.name);
// TODO // TODO
try servePage(req, res, srv); try servePage(req, res, srv);
}, },
.delete => {
const trimmed_path = std.mem.trim(u8, req.args.path, "/");
_ = try srv.driveDelete(trimmed_path);
const dir = trimmed_path[0 .. std.mem.lastIndexOfScalar(u8, trimmed_path, '/') orelse trimmed_path.len];
const url = try std.fmt.allocPrint(srv.allocator, "{s}/drive/{s}", .{
req.mount_path,
dir,
});
defer srv.allocator.free(url);
try res.headers.put("Location", url);
return res.status(.see_other);
},
} }
} }
}; };

View file

@ -29,12 +29,11 @@
<a class="button popup-close" href="#"> <a class="button popup-close" href="#">
<i class="fa-solid fa-xmark"></i> <i class="fa-solid fa-xmark"></i>
</a> </a>
<form class="popup-dialog" method="post" enctype="multipart/form-data"> <form class="popup-dialog" action="?action=mkdir" method="post" enctype="multipart/form-data">
<label> <label>
<div>Create Directory</div> <div>Create Directory</div>
<input type="text" name="mkcol.name" /> <!-- TODO: Rename this form param --> <input type="text" name="name" />
</label> </label>
<input type="hidden" name="action" value="mkcol" />
<button type="submit">Create</button> <button type="submit">Create</button>
</form> </form>
</div> </div>
@ -49,6 +48,25 @@
{$dir.name.?} {$dir.name.?}
</a> </a>
</td> </td>
<td />
<td />
<td />
<td class="actions">
<div class="popup" id="delete-{$dir.name.?}">
<a href="#delete-{$dir.name.?}">
<i class="fa-solid fa-trash"></i>
</a>
<form class="popup-dialog" action="
{= .mount_path}/{.base_drive_path}
{= #for @slice(.breadcrumbs, 0, .breadcrumbs.len) |$c|}/{$c}{/for =}/{$dir.name.? =}
?action=delete" method="post"
>
<div>Are you sure you want to delete this directory?</div>
<button type="submit">Yes, Delete</button>
<a href="#">No, Cancel</a>
</form>
</div>
</td>
{#case file |$file|} {#case file |$file|}
<td class="icons"> <td class="icons">
{#if %user |$u|} {#if %user |$u|}
@ -72,6 +90,23 @@
<td class="content-type">{#if $file.meta.content_type |$t|}{$t}{/if}</td> <td class="content-type">{#if $file.meta.content_type |$t|}{$t}{/if}</td>
<td class="size">{$file.meta.size}</td> <td class="size">{$file.meta.size}</td>
<td class="created-at">{$file.meta.created_at}</td> <td class="created-at">{$file.meta.created_at}</td>
<td class="actions">
<div class="popup" id="delete-{$file.name.?}">
<a href="#delete-{$file.name.?}">
<i class="fa-solid fa-trash"></i>
</a>
<form class="popup-dialog" action="
{= .mount_path}/{.base_drive_path}
{= #for @slice(.breadcrumbs, 0, .breadcrumbs.len) |$c|}/{$c}{/for =}/{$file.name.? =}
?action=delete" method="post"
>
<div>Are you sure you want to delete this file?</div>
<input type="hidden" name="action" value="delete" />
<button type="submit">Yes, Delete</button>
<a href="#">No, Cancel</a>
</form>
</div>
</td>
{/switch =} {/switch =}
</tr> </tr>
{/for=} {/for=}

View file

@ -266,7 +266,11 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime
} }
pub fn finish(self: *@This(), allocator: std.mem.Allocator) !Result { pub fn finish(self: *@This(), allocator: std.mem.Allocator) !Result {
return (try self.deserialize(allocator, Result, self.data, &.{})) orelse error.MissingField; return (try self.deserialize(allocator, Result, self.data, &.{})) orelse
if (std.meta.fields(Result).len == 0)
return .{}
else
return error.MissingField;
} }
fn getSerializedField(self: *@This(), comptime field_ref: FieldRef) ?From { fn getSerializedField(self: *@This(), comptime field_ref: FieldRef) ?From {