Routing updates

Implement `/foo/1/edit` route/view.

Allow setting HTTP verb by passing e.g. `/_PATCH` as the last segment of
a URL - this allows browsers to submit a `POST` request with a
pseudo-HTTP verb encoded in the URL which Jetzig can translate, i.e.
allowing forms to submit a `PATCH`.
This commit is contained in:
Bob Farrell 2024-11-24 20:43:15 +00:00
parent 835885a947
commit f44a6b33c5
9 changed files with 186 additions and 65 deletions

View File

@ -7,16 +7,16 @@
.hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163", .hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163",
}, },
.zmpl = .{ .zmpl = .{
.url = "https://github.com/jetzig-framework/zmpl/archive/af75c8b842c3957eb97b4fc4bc49c7b2243968fa.tar.gz", .url = "https://github.com/jetzig-framework/zmpl/archive/ef1930b08e1f174ddb02a3a0a01b35aa8a4af235.tar.gz",
.hash = "1220ecac93d295dafd2f034a86f0979f6108d40e5ea1a39e3a2b9977c35147cac684", .hash = "1220a7bacb828f12cd013b0906da61a17fac6819ab8cee81e00d9ae1aa0faa992720",
}, },
.jetkv = .{ .jetkv = .{
.url = "https://github.com/jetzig-framework/jetkv/archive/2b1130a48979ea2871c8cf6ca89c38b1e7062839.tar.gz", .url = "https://github.com/jetzig-framework/jetkv/archive/2b1130a48979ea2871c8cf6ca89c38b1e7062839.tar.gz",
.hash = "12201d75d73aad5e1c996de4d5ae87a00e58479c8d469bc2eeb5fdeeac8857bc09af", .hash = "12201d75d73aad5e1c996de4d5ae87a00e58479c8d469bc2eeb5fdeeac8857bc09af",
}, },
.jetquery = .{ .jetquery = .{
.url = "https://github.com/jetzig-framework/jetquery/archive/a31db467c4af1c97bc7c806e1cc1a81a39162954.tar.gz", .url = "https://github.com/jetzig-framework/jetquery/archive/5394d7cf5d7360bd5052cd13902e26f08610423b.tar.gz",
.hash = "12203af0466ccc3a9ab57fcdf57c92c57989fa7e827d81bc98d0a5787d65402c73c3", .hash = "1220119a1ee89d8d4b7e984a82bc70fe5d57aa412b821c561ce80a93fd8806bc4b8a",
}, },
.jetcommon = .{ .jetcommon = .{
.url = "https://github.com/jetzig-framework/jetcommon/archive/86f24cfdf2aaa0e8ada4539a6edef882708ced2b.tar.gz", .url = "https://github.com/jetzig-framework/jetcommon/archive/86f24cfdf2aaa0e8ada4539a6edef882708ced2b.tar.gz",
@ -26,10 +26,7 @@
.url = "https://github.com/ikskuh/zig-args/archive/0abdd6947a70e6d8cc83b66228cea614aa856206.tar.gz", .url = "https://github.com/ikskuh/zig-args/archive/0abdd6947a70e6d8cc83b66228cea614aa856206.tar.gz",
.hash = "1220411a8c46d95bbf3b6e2059854bcb3c5159d428814099df5294232b9980517e9c", .hash = "1220411a8c46d95bbf3b6e2059854bcb3c5159d428814099df5294232b9980517e9c",
}, },
.pg = .{ .pg = .{ .url = "https://github.com/karlseguin/pg.zig/archive/f376f4b30c63f1fdf90bc3afe246d3bc4175cd46.tar.gz", .hash = "12200a55304988e942015b6244570b2dc0e87e5764719c9e7d5c812cd7ad34f6b138" },
.url = "https://github.com/karlseguin/pg.zig/archive/f376f4b30c63f1fdf90bc3afe246d3bc4175cd46.tar.gz",
.hash = "12200a55304988e942015b6244570b2dc0e87e5764719c9e7d5c812cd7ad34f6b138"
},
.smtp_client = .{ .smtp_client = .{
.url = "https://github.com/karlseguin/smtp_client.zig/archive/3cbe8f269e4c3a6bce407e7ae48b2c76307c559f.tar.gz", .url = "https://github.com/karlseguin/smtp_client.zig/archive/3cbe8f269e4c3a6bce407e7ae48b2c76307c559f.tar.gz",
.hash = "1220de146446d0cae4396e346cb8283dd5e086491f8577ddbd5e03ad0928111d8bc6", .hash = "1220de146446d0cae4396e346cb8283dd5e086491f8577ddbd5e03ad0928111d8bc6",

View File

@ -43,7 +43,7 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he
const action_args = if (args.len > 1) const action_args = if (args.len > 1)
args[1..] args[1..]
else else
&[_][]const u8{ "index", "get", "new", "post", "put", "patch", "delete" }; &[_][]const u8{ "index", "get", "new", "edit", "post", "put", "patch", "delete" };
var actions = std.ArrayList(Action).init(allocator); var actions = std.ArrayList(Action).init(allocator);
defer actions.deinit(); defer actions.deinit();
@ -92,7 +92,7 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he
std.debug.print("Generated view: {s}\n", .{realpath}); std.debug.print("Generated view: {s}\n", .{realpath});
} }
const Method = enum { index, get, new, post, put, patch, delete }; const Method = enum { index, get, new, edit, post, put, patch, delete };
const Action = struct { const Action = struct {
method: Method, method: Method,
static: bool, static: bool,
@ -126,15 +126,15 @@ fn writeAction(allocator: std.mem.Allocator, writer: anytype, action: Action) !v
@tagName(action.method), @tagName(action.method),
switch (action.method) { switch (action.method) {
.index, .post, .new => "", .index, .post, .new => "",
.get, .put, .patch, .delete => "id: []const u8, ", .get, .edit, .put, .patch, .delete => "id: []const u8, ",
}, },
if (action.static) "StaticRequest" else "Request", if (action.static) "StaticRequest" else "Request",
switch (action.method) { switch (action.method) {
.index, .post, .new => "", .index, .post, .new => "",
.get, .put, .patch, .delete => "_ = id;\n ", .get, .edit, .put, .patch, .delete => "_ = id;\n ",
}, },
switch (action.method) { switch (action.method) {
.index, .get, .new => ".ok", .index, .get, .edit, .new => ".ok",
.post => ".created", .post => ".created",
.put, .patch, .delete => ".ok", .put, .patch, .delete => ".ok",
}, },
@ -164,17 +164,18 @@ fn writeTest(allocator: std.mem.Allocator, writer: anytype, name: []const u8, ac
.{ .{
@tagName(action.method), @tagName(action.method),
switch (action.method) { switch (action.method) {
.index, .get, .new => "GET", .index, .get, .edit, .new => "GET",
.put, .patch, .delete, .post => action_upper, .put, .patch, .delete, .post => action_upper,
}, },
name, name,
switch (action.method) { switch (action.method) {
.index, .post => "", .index, .post => "",
.edit => "/example-id/edit",
.new => "/new", .new => "/new",
.get, .put, .patch, .delete => "/example-id", .get, .put, .patch, .delete => "/example-id",
}, },
switch (action.method) { switch (action.method) {
.index, .get, .new => ".ok", .index, .get, .new, .edit => ".ok",
.post => ".created", .post => ".created",
.put, .patch, .delete => ".ok", .put, .patch, .delete => ".ok",
}, },
@ -208,7 +209,7 @@ fn writeStaticParams(allocator: std.mem.Allocator, actions: []Action, writer: an
defer allocator.free(output); defer allocator.free(output);
try writer.writeAll(output); try writer.writeAll(output);
}, },
.get, .put, .patch, .delete => { .get, .put, .patch, .delete, .edit => {
const output = try std.fmt.allocPrint( const output = try std.fmt.allocPrint(
allocator, allocator,
\\ .{s} = .{{ \\ .{s} = .{{

View File

@ -16,6 +16,12 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
return request.render(.ok); return request.render(.ok);
} }
pub fn edit(id: []const u8, request: *jetzig.Request) !jetzig.View {
var root = try request.data(.object);
try root.put("id", id);
return request.render(.ok);
}
fn customFunction(a: i32, b: i32, c: i32) i32 { fn customFunction(a: i32, b: i32, c: i32) i32 {
return a + b + c; return a + b + c;
} }

View File

@ -15,6 +15,11 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
return request.render(.ok); return request.render(.ok);
} }
pub fn edit(id: []const u8, request: *jetzig.Request) !jetzig.View {
try request.server.logger.INFO("id: {s}", .{id});
return request.render(.ok);
}
pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
_ = data; _ = data;
const params = try request.params(); const params = try request.params();

View File

@ -54,10 +54,20 @@ const Function = struct {
defer self.routes.allocator.free(relative_path); defer self.routes.allocator.free(relative_path);
const path = relative_path[0 .. relative_path.len - std.fs.path.extension(relative_path).len]; const path = relative_path[0 .. relative_path.len - std.fs.path.extension(relative_path).len];
if (std.mem.eql(u8, path, "root")) return try self.routes.allocator.dupe(u8, "/"); const is_root = std.mem.eql(u8, path, "root");
const is_new = std.mem.eql(u8, self.name, "new");
const is_edit = std.mem.eql(u8, self.name, "edit");
if (is_root) {
if (is_edit) return try self.routes.allocator.dupe(u8, "/edit");
if (is_new) return try self.routes.allocator.dupe(u8, "/new");
return try self.routes.allocator.dupe(u8, "/");
}
const maybe_new = if (std.mem.eql(u8, self.name, "new")) "/new" else ""; const maybe_new = if (is_new) ("/new") else "";
return try std.mem.concat(self.routes.allocator, u8, &[_][]const u8{ "/", path, maybe_new }); // jetzig.http.Path.actionPath translates `/foo/bar/1/edit` to `/foo/bar/edit`
const maybe_edit = if (is_edit) ("/edit") else "";
return try std.mem.concat(self.routes.allocator, u8, &[_][]const u8{ "/", path, maybe_new, maybe_edit });
} }
pub fn lessThanFn(context: void, lhs: Function, rhs: Function) bool { pub fn lessThanFn(context: void, lhs: Function, rhs: Function) bool {
@ -305,6 +315,7 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
.{ "index", false }, .{ "index", false },
.{ "post", false }, .{ "post", false },
.{ "new", false }, .{ "new", false },
.{ "edit", true },
.{ "get", true }, .{ "get", true },
.{ "edit", true }, .{ "edit", true },
.{ "put", true }, .{ "put", true },

View File

@ -16,11 +16,12 @@ file_path: []const u8,
resource_id: []const u8, resource_id: []const u8,
extension: ?[]const u8, extension: ?[]const u8,
query: ?[]const u8, query: ?[]const u8,
method: ?jetzig.Request.Method,
const Self = @This(); const Path = @This();
/// Initialize a new HTTP Path. /// Initialize a new HTTP Path.
pub fn init(path: []const u8) Self { pub fn init(path: []const u8) Path {
const base_path = getBasePath(path); const base_path = getBasePath(path);
return .{ return .{
@ -31,18 +32,19 @@ pub fn init(path: []const u8) Self {
.resource_id = getResourceId(base_path), .resource_id = getResourceId(base_path),
.extension = getExtension(path), .extension = getExtension(path),
.query = getQuery(path), .query = getQuery(path),
.method = getMethod(path),
}; };
} }
/// No-op - no allocations currently performed. /// No-op - no allocations currently performed.
pub fn deinit(self: *Self) void { pub fn deinit(self: *Path) void {
_ = self; _ = self;
} }
/// For a given route with a possible `:id` placeholder, return the matching URL segment for that /// For a given route with a possible `:id` placeholder, return the matching URL segment for that
/// placeholder. e.g. route with path `/foo/:id/bar` and request path `/foo/1234/bar` returns /// placeholder. e.g. route with path `/foo/:id/bar` and request path `/foo/1234/bar` returns
/// `"1234"`. /// `"1234"`.
pub fn resourceId(self: Self, route: jetzig.views.Route) []const u8 { pub fn resourceId(self: Path, route: jetzig.views.Route) []const u8 {
var route_uri_path_it = std.mem.splitScalar(u8, route.uri_path, '/'); var route_uri_path_it = std.mem.splitScalar(u8, route.uri_path, '/');
var base_path_it = std.mem.splitScalar(u8, self.base_path, '/'); var base_path_it = std.mem.splitScalar(u8, self.base_path, '/');
@ -54,7 +56,7 @@ pub fn resourceId(self: Self, route: jetzig.views.Route) []const u8 {
return self.resource_id; return self.resource_id;
} }
pub fn resourceArgs(self: Self, route: jetzig.views.Route, allocator: std.mem.Allocator) ![]const []const u8 { pub fn resourceArgs(self: Path, route: jetzig.views.Route, allocator: std.mem.Allocator) ![]const []const u8 {
var args = std.ArrayList([]const u8).init(allocator); var args = std.ArrayList([]const u8).init(allocator);
var route_uri_path_it = std.mem.splitScalar(u8, route.uri_path, '/'); var route_uri_path_it = std.mem.splitScalar(u8, route.uri_path, '/');
var path_it = std.mem.splitScalar(u8, self.base_path, '/'); var path_it = std.mem.splitScalar(u8, self.base_path, '/');
@ -82,21 +84,41 @@ pub fn resourceArgs(self: Self, route: jetzig.views.Route, allocator: std.mem.Al
// * `"/foo/bar/baz"` // * `"/foo/bar/baz"`
// * `"/foo/bar/baz.html"` // * `"/foo/bar/baz.html"`
// * `"/foo/bar/baz.html?qux=quux&corge=grault"` // * `"/foo/bar/baz.html?qux=quux&corge=grault"`
// * `"/foo/bar/baz/_PATCH"`
fn getBasePath(path: []const u8) []const u8 { fn getBasePath(path: []const u8) []const u8 {
if (std.mem.indexOfScalar(u8, path, '?')) |query_index| { const base = if (std.mem.indexOfScalar(u8, path, '?')) |query_index| blk: {
if (std.mem.lastIndexOfScalar(u8, path[0..query_index], '.')) |extension_index| { if (std.mem.lastIndexOfScalar(u8, path[0..query_index], '.')) |extension_index| {
return path[0..extension_index]; break :blk path[0..extension_index];
} else { } else {
return path[0..query_index]; break :blk path[0..query_index];
} }
} else if (std.mem.lastIndexOfScalar(u8, path, '.')) |extension_index| { } else if (std.mem.lastIndexOfScalar(u8, path, '.')) |extension_index| blk: {
return if (isRootPath(path[0..extension_index])) break :blk if (isRootPath(path[0..extension_index]))
path[0..extension_index] path[0..extension_index]
else else
std.mem.trimRight(u8, path[0..extension_index], "/"); std.mem.trimRight(u8, path[0..extension_index], "/");
} else { } else blk: {
return if (isRootPath(path)) path else std.mem.trimRight(u8, path, "/"); break :blk if (isRootPath(path)) path else std.mem.trimRight(u8, path, "/");
};
if (std.mem.lastIndexOfScalar(u8, base, '/')) |last_index| {
if (std.mem.startsWith(u8, base[last_index..], "/_")) {
return base[0..last_index];
} else {
return base;
}
} else return base;
}
fn getMethod(path: []const u8) ?jetzig.Request.Method {
var it = std.mem.splitBackwardsScalar(u8, path, '/');
const last_segment = it.next() orelse return null;
inline for (comptime std.enums.values(jetzig.Request.Method)) |method| {
if (std.mem.startsWith(u8, last_segment, "_" ++ @tagName(method))) {
return method;
}
} }
return null;
} }
// Extract `"/foo/bar"` from: // Extract `"/foo/bar"` from:
@ -131,6 +153,9 @@ fn getFilePath(path: []const u8) []const u8 {
// * `"/baz"` // * `"/baz"`
fn getResourceId(base_path: []const u8) []const u8 { fn getResourceId(base_path: []const u8) []const u8 {
var it = std.mem.splitBackwardsScalar(u8, base_path, '/'); var it = std.mem.splitBackwardsScalar(u8, base_path, '/');
if (std.mem.endsWith(u8, base_path, "/edit")) _ = it.next();
while (it.next()) |segment| return segment; while (it.next()) |segment| return segment;
return base_path; return base_path;
} }
@ -164,169 +189,228 @@ fn getQuery(path: []const u8) ?[]const u8 {
} }
} }
// Extract `/foo/bar/edit` from `/foo/bar/1/edit`
// Extract `/foo/bar` from `/foo/bar/1`
pub fn actionPath(self: Path, buf: *[2048]u8) []const u8 {
if (self.path.len > 2048) return self.path; // Should never happen but we don't want to panic or overflow.
if (std.mem.endsWith(u8, self.path, "/edit")) {
var it = std.mem.tokenizeScalar(u8, self.path, '/');
var cursor: usize = 0;
const count = std.mem.count(u8, self.path, "/");
var index: usize = 0;
buf[0] = '/';
cursor += 1;
while (it.next()) |segment| : (index += 1) {
if (index + 2 == count) continue; // Skip ID - we special-case this in `resourceId`
@memcpy(buf[cursor .. cursor + segment.len], segment);
cursor += segment.len;
if (index + 1 < count) {
@memcpy(buf[cursor .. cursor + 1], "/");
cursor += 1;
}
}
return buf[0..cursor];
} else return self.path;
}
inline fn isRootPath(path: []const u8) bool { inline fn isRootPath(path: []const u8) bool {
return std.mem.eql(u8, path, "/"); return std.mem.eql(u8, path, "/");
} }
test ".base_path (with extension, with query)" { test ".base_path (with extension, with query)" {
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault"); const path = Path.init("/foo/bar/baz.html?qux=quux&corge=grault");
try std.testing.expectEqualStrings("/foo/bar/baz", path.base_path); try std.testing.expectEqualStrings("/foo/bar/baz", path.base_path);
} }
test ".base_path (with extension, without query)" { test ".base_path (with extension, without query)" {
const path = Self.init("/foo/bar/baz.html"); const path = Path.init("/foo/bar/baz.html");
try std.testing.expectEqualStrings("/foo/bar/baz", path.base_path); try std.testing.expectEqualStrings("/foo/bar/baz", path.base_path);
} }
test ".base_path (without extension, without query)" { test ".base_path (without extension, without query)" {
const path = Self.init("/foo/bar/baz"); const path = Path.init("/foo/bar/baz");
try std.testing.expectEqualStrings("/foo/bar/baz", path.base_path); try std.testing.expectEqualStrings("/foo/bar/baz", path.base_path);
} }
test ".base_path (with trailing slash)" { test ".base_path (with trailing slash)" {
const path = Self.init("/foo/bar/"); const path = Path.init("/foo/bar/");
try std.testing.expectEqualStrings("/foo/bar", path.base_path); try std.testing.expectEqualStrings("/foo/bar", path.base_path);
} }
test ".base_path (root path)" { test ".base_path (root path)" {
const path = Self.init("/"); const path = Path.init("/");
try std.testing.expectEqualStrings("/", path.base_path); try std.testing.expectEqualStrings("/", path.base_path);
} }
test ".base_path (root path with extension)" { test ".base_path (root path with extension)" {
const path = Self.init("/.json"); const path = Path.init("/.json");
try std.testing.expectEqualStrings("/", path.base_path); try std.testing.expectEqualStrings("/", path.base_path);
try std.testing.expectEqualStrings(".json", path.extension.?); try std.testing.expectEqualStrings(".json", path.extension.?);
} }
test ".directory (with extension, with query)" { test ".directory (with extension, with query)" {
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault"); const path = Path.init("/foo/bar/baz.html?qux=quux&corge=grault");
try std.testing.expectEqualStrings("/foo/bar", path.directory); try std.testing.expectEqualStrings("/foo/bar", path.directory);
} }
test ".directory (with extension, without query)" { test ".directory (with extension, without query)" {
const path = Self.init("/foo/bar/baz.html"); const path = Path.init("/foo/bar/baz.html");
try std.testing.expectEqualStrings("/foo/bar", path.directory); try std.testing.expectEqualStrings("/foo/bar", path.directory);
} }
test ".directory (without extension, without query)" { test ".directory (without extension, without query)" {
const path = Self.init("/foo/bar/baz"); const path = Path.init("/foo/bar/baz");
try std.testing.expectEqualStrings("/foo/bar", path.directory); try std.testing.expectEqualStrings("/foo/bar", path.directory);
} }
test ".directory (without extension, without query, root path)" { test ".directory (without extension, without query, root path)" {
const path = Self.init("/"); const path = Path.init("/");
try std.testing.expectEqualStrings("/", path.directory); try std.testing.expectEqualStrings("/", path.directory);
} }
test ".resource_id (with extension, with query)" { test ".resource_id (with extension, with query)" {
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault"); const path = Path.init("/foo/bar/baz.html?qux=quux&corge=grault");
try std.testing.expectEqualStrings("baz", path.resource_id); try std.testing.expectEqualStrings("baz", path.resource_id);
} }
test ".resource_id (with extension, without query)" { test ".resource_id (with extension, without query)" {
const path = Self.init("/foo/bar/baz.html"); const path = Path.init("/foo/bar/baz.html");
try std.testing.expectEqualStrings("baz", path.resource_id); try std.testing.expectEqualStrings("baz", path.resource_id);
} }
test ".resource_id (without extension, without query)" { test ".resource_id (without extension, without query)" {
const path = Self.init("/foo/bar/baz"); const path = Path.init("/foo/bar/baz");
try std.testing.expectEqualStrings("baz", path.resource_id); try std.testing.expectEqualStrings("baz", path.resource_id);
} }
test ".resource_id (without extension, without query, without base path)" { test ".resource_id (without extension, without query, without base path)" {
const path = Self.init("/baz"); const path = Path.init("/baz");
try std.testing.expectEqualStrings("baz", path.resource_id); try std.testing.expectEqualStrings("baz", path.resource_id);
} }
test ".resource_id (with trailing slash)" { test ".resource_id (with trailing slash)" {
const path = Self.init("/foo/bar/"); const path = Path.init("/foo/bar/");
try std.testing.expectEqualStrings("bar", path.resource_id); try std.testing.expectEqualStrings("bar", path.resource_id);
} }
test ".extension (with query)" { test ".extension (with query)" {
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault"); const path = Path.init("/foo/bar/baz.html?qux=quux&corge=grault");
try std.testing.expectEqualStrings(".html", path.extension.?); try std.testing.expectEqualStrings(".html", path.extension.?);
} }
test ".extension (without query)" { test ".extension (without query)" {
const path = Self.init("/foo/bar/baz.html"); const path = Path.init("/foo/bar/baz.html");
try std.testing.expectEqualStrings(".html", path.extension.?); try std.testing.expectEqualStrings(".html", path.extension.?);
} }
test ".extension (without extension)" { test ".extension (without extension)" {
const path = Self.init("/foo/bar/baz"); const path = Path.init("/foo/bar/baz");
try std.testing.expect(path.extension == null); try std.testing.expect(path.extension == null);
} }
test ".query (with extension, with query)" { test ".query (with extension, with query)" {
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault"); const path = Path.init("/foo/bar/baz.html?qux=quux&corge=grault");
try std.testing.expectEqualStrings(path.query.?, "qux=quux&corge=grault"); try std.testing.expectEqualStrings(path.query.?, "qux=quux&corge=grault");
} }
test ".query (without extension, with query)" { test ".query (without extension, with query)" {
const path = Self.init("/foo/bar/baz?qux=quux&corge=grault"); const path = Path.init("/foo/bar/baz?qux=quux&corge=grault");
try std.testing.expectEqualStrings(path.query.?, "qux=quux&corge=grault"); try std.testing.expectEqualStrings(path.query.?, "qux=quux&corge=grault");
} }
test ".query (with extension, without query)" { test ".query (with extension, without query)" {
const path = Self.init("/foo/bar/baz.json"); const path = Path.init("/foo/bar/baz.json");
try std.testing.expect(path.query == null); try std.testing.expect(path.query == null);
} }
test ".query (without extension, without query)" { test ".query (without extension, without query)" {
const path = Self.init("/foo/bar/baz"); const path = Path.init("/foo/bar/baz");
try std.testing.expect(path.query == null); try std.testing.expect(path.query == null);
} }
test ".query (with empty query)" { test ".query (with empty query)" {
const path = Self.init("/foo/bar/baz?"); const path = Path.init("/foo/bar/baz?");
try std.testing.expect(path.query == null); try std.testing.expect(path.query == null);
} }
test ".file_path (with extension, with query)" { test ".file_path (with extension, with query)" {
const path = Self.init("/foo/bar/baz.json?qux=quux&corge=grault"); const path = Path.init("/foo/bar/baz.json?qux=quux&corge=grault");
try std.testing.expectEqualStrings("/foo/bar/baz.json", path.file_path); try std.testing.expectEqualStrings("/foo/bar/baz.json", path.file_path);
} }
test ".file_path (with extension, without query)" { test ".file_path (with extension, without query)" {
const path = Self.init("/foo/bar/baz.json"); const path = Path.init("/foo/bar/baz.json");
try std.testing.expectEqualStrings("/foo/bar/baz.json", path.file_path); try std.testing.expectEqualStrings("/foo/bar/baz.json", path.file_path);
} }
test ".file_path (without extension, without query)" { test ".file_path (without extension, without query)" {
const path = Self.init("/foo/bar/baz"); const path = Path.init("/foo/bar/baz");
try std.testing.expectEqualStrings("/foo/bar/baz", path.file_path); try std.testing.expectEqualStrings("/foo/bar/baz", path.file_path);
} }
test ".file_path (without extension, with query)" { test ".file_path (without extension, with query)" {
const path = Self.init("/foo/bar/baz?qux=quux&corge=grault"); const path = Path.init("/foo/bar/baz?qux=quux&corge=grault");
try std.testing.expectEqualStrings("/foo/bar/baz", path.file_path); try std.testing.expectEqualStrings("/foo/bar/baz", path.file_path);
} }
test ".resource_id (/foo/bar/123/edit)" {
const path = Path.init("/foo/bar/123/edit");
try std.testing.expectEqualStrings("123", path.resource_id);
}
test ".actionPath (/foo/bar/123/edit)" {
var buf: [2048]u8 = undefined;
const path = Path.init("/foo/bar/123/edit").actionPath(&buf);
try std.testing.expectEqualStrings("/foo/bar/edit", path);
}
test ".actionPath (/foo/bar)" {
var buf: [2048]u8 = undefined;
const path = Path.init("/foo/bar").actionPath(&buf);
try std.testing.expectEqualStrings("/foo/bar", path);
}
test ".base_path (/foo/bar/1/_PATCH" {
const path = Path.init("/foo/bar/1/_PATCH");
try std.testing.expectEqualStrings("/foo/bar/1", path.base_path);
try std.testing.expectEqualStrings("1", path.resource_id);
}
test ".method (/foo/bar/1/_PATCH" {
const path = Path.init("/foo/bar/1/_PATCH");
try std.testing.expect(path.method.? == .PATCH);
}

View File

@ -119,7 +119,11 @@ pub fn init(
response: *jetzig.http.Response, response: *jetzig.http.Response,
repo: *jetzig.database.Repo, repo: *jetzig.database.Repo,
) !Request { ) !Request {
const method = switch (httpz_request.method) { const path = jetzig.http.Path.init(httpz_request.url.raw);
// We can fake the HTTP method by appending `/_PATCH` (e.g.) to the end of the URL.
// This allows using PATCH, PUT, DELETE from HTML forms.
const method = path.method orelse switch (httpz_request.method) {
.DELETE => Method.DELETE, .DELETE => Method.DELETE,
.GET => Method.GET, .GET => Method.GET,
.PATCH => Method.PATCH, .PATCH => Method.PATCH,
@ -134,7 +138,7 @@ pub fn init(
return .{ return .{
.allocator = allocator, .allocator = allocator,
.path = jetzig.http.Path.init(httpz_request.url.raw), .path = path,
.method = method, .method = method,
.headers = jetzig.http.Headers.init(allocator, httpz_request.headers), .headers = jetzig.http.Headers.init(allocator, httpz_request.headers),
.server = server, .server = server,
@ -764,6 +768,7 @@ pub fn match(self: *Request, route: jetzig.views.Route) !bool {
.index => self.isMatch(.exact, route), .index => self.isMatch(.exact, route),
.get => self.isMatch(.resource_id, route), .get => self.isMatch(.resource_id, route),
.new => self.isMatch(.exact, route), .new => self.isMatch(.exact, route),
.edit => self.isMatch(.exact, route),
else => false, else => false,
}, },
.POST => switch (route.action) { .POST => switch (route.action) {
@ -796,8 +801,18 @@ fn isMatch(
.resource_id => self.path.directory, .resource_id => self.path.directory,
}; };
// Special case for `/foobar/1/new` -> render `new()` if (route.action == .get) {
if (route.action == .get and std.mem.eql(u8, self.path.resource_id, "new")) return false; // Special case for `/foobar/1/new` -> render `new()` - prevent matching `get`
if (std.mem.eql(u8, self.path.resource_id, "new")) return false;
// Special case for `/foobar/1/edit` -> render `edit()` - prevent matching `get`
if (std.mem.eql(u8, self.path.resource_id, "edit")) return false;
}
if (route.action == .edit and std.mem.endsWith(u8, self.path.path, "/edit")) {
var buf: [2048]u8 = undefined;
const action_path = self.path.actionPath(&buf);
if (std.mem.eql(u8, action_path, route.uri_path)) return true;
}
return std.mem.eql(u8, path, route.uri_path); return std.mem.eql(u8, path, route.uri_path);
} }

View File

@ -786,7 +786,7 @@ fn matchStaticContent(self: *Server, request: *jetzig.http.Request) !?[]const u8
self.decoded_static_route_params[index].get("params"), self.decoded_static_route_params[index].get("params"),
route, route,
request, request,
params, params.*,
)) return switch (request_format) { )) return switch (request_format) {
.HTML, .UNKNOWN => static_output.output.html, .HTML, .UNKNOWN => static_output.output.html,
.JSON => static_output.output.json, .JSON => static_output.output.json,
@ -822,7 +822,7 @@ fn matchStaticOutput(
maybe_expected_params: ?*jetzig.data.Value, maybe_expected_params: ?*jetzig.data.Value,
route: jetzig.views.Route, route: jetzig.views.Route,
request: *const jetzig.http.Request, request: *const jetzig.http.Request,
params: *jetzig.data.Value, params: jetzig.data.Value,
) bool { ) bool {
return if (maybe_expected_params) |expected_params| blk: { return if (maybe_expected_params) |expected_params| blk: {
const params_match = expected_params.count() == 0 or expected_params.eql(params); const params_match = expected_params.count() == 0 or expected_params.eql(params);

View File

@ -5,7 +5,7 @@ const view_types = @import("view_types.zig");
const Route = @This(); const Route = @This();
pub const Action = enum { index, get, new, post, put, patch, delete, custom }; pub const Action = enum { index, get, new, edit, post, put, patch, delete, custom };
pub const View = union(enum) { pub const View = union(enum) {
with_id: view_types.ViewWithId, with_id: view_types.ViewWithId,
@ -32,6 +32,7 @@ pub const Formats = struct {
index: ?[]const ResponseFormat = null, index: ?[]const ResponseFormat = null,
get: ?[]const ResponseFormat = null, get: ?[]const ResponseFormat = null,
new: ?[]const ResponseFormat = null, new: ?[]const ResponseFormat = null,
edit: ?[]const ResponseFormat = null,
post: ?[]const ResponseFormat = null, post: ?[]const ResponseFormat = null,
put: ?[]const ResponseFormat = null, put: ?[]const ResponseFormat = null,
patch: ?[]const ResponseFormat = null, patch: ?[]const ResponseFormat = null,
@ -114,6 +115,7 @@ pub fn validateFormat(self: Route, request: *const jetzig.http.Request) bool {
.index => formats.index orelse return true, .index => formats.index orelse return true,
.get => formats.get orelse return true, .get => formats.get orelse return true,
.new => formats.new orelse return true, .new => formats.new orelse return true,
.edit => formats.edit orelse return true,
.post => formats.post orelse return true, .post => formats.post orelse return true,
.put => formats.put orelse return true, .put => formats.put orelse return true,
.patch => formats.patch orelse return true, .patch => formats.patch orelse return true,