Refactor Route

Move all route types into a single union and remove leftover junk.

Deprecate view functions that receive a `*jetzig.Data` argument (we will
stay backward-compatible until we have a valid reason to drop support
for legacy view functions - the extra code overhead is pretty minimal).
This commit is contained in:
Bob Farrell 2024-11-17 15:08:54 +00:00
parent 49c5c2db26
commit e3ab49fa5a
7 changed files with 222 additions and 93 deletions

View File

@ -0,0 +1,14 @@
const std = @import("std");
const jetzig = @import("jetzig");
pub fn post(request: *jetzig.Request) !jetzig.View {
return request.render(.created);
}
test "post" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
const response = try app.request(.POST, "/params", .{});
try response.expectStatus(.created);
}

View File

@ -0,0 +1,3 @@
<div>
<span>Content goes here</span>
</div>

View File

@ -24,6 +24,7 @@ const Function = struct {
path: []const u8, path: []const u8,
source: []const u8, source: []const u8,
params: std.ArrayList([]const u8), params: std.ArrayList([]const u8),
legacy: bool = false,
static: bool = false, static: bool = false,
/// The full name of a route. This **must** match the naming convention used by static route /// The full name of a route. This **must** match the naming convention used by static route
@ -179,7 +180,6 @@ pub fn generateRoutes(self: *Routes) ![]const u8 {
); );
return try self.buffer.toOwnedSlice(); return try self.buffer.toOwnedSlice();
// std.debug.print("routes.zig\n{s}\n", .{self.buffer.items});
} }
pub fn relativePathFrom( pub fn relativePathFrom(
@ -232,7 +232,10 @@ fn writeRoutes(self: *Routes, writer: anytype) !void {
const realpath = try dir.realpathAlloc(self.allocator, entry.path); const realpath = try dir.realpathAlloc(self.allocator, entry.path);
defer self.allocator.free(realpath); defer self.allocator.free(realpath);
const view_routes = try self.generateRoutesForView(dir, try self.allocator.dupe(u8, realpath)); const view_routes = try self.generateRoutesForView(
dir,
try self.allocator.dupe(u8, realpath),
);
for (view_routes.static) |view_route| { for (view_routes.static) |view_route| {
try self.static_routes.append(view_route); try self.static_routes.append(view_route);
@ -272,7 +275,7 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
\\ .name = "{0s}", \\ .name = "{0s}",
\\ .action = .{1s}, \\ .action = .{1s},
\\ .view_name = "{2s}", \\ .view_name = "{2s}",
\\ .view = jetzig.Route.ViewType{{ .{3s} = .{{ .{1s} = @import("{7s}").{1s} }} }}, \\ .view = jetzig.Route.View{{ .{3s} = @import("{7s}").{1s} }},
\\ .path = "{7s}", \\ .path = "{7s}",
\\ .static = {4s}, \\ .static = {4s},
\\ .uri_path = "{5s}", \\ .uri_path = "{5s}",
@ -296,6 +299,36 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
&[_][]const u8{ view_name, "/", route.name }, &[_][]const u8{ view_name, "/", route.name },
); );
const with_id = std.StaticStringMap(bool).initComptime(.{
.{ "index", false },
.{ "post", false },
.{ "new", false },
.{ "get", true },
.{ "edit", true },
.{ "put", true },
.{ "patch", true },
.{ "delete", true },
}).get(route.name).?;
const tag = if (!route.legacy and !route.static and with_id)
"with_id"
else if (!route.legacy and !route.static and !with_id)
"without_id"
else if (!route.legacy and route.static and with_id)
"static_with_id"
else if (!route.legacy and route.static and !with_id)
"static_without_id"
else if (route.legacy and !route.static and with_id)
"legacy_with_id"
else if (route.legacy and !route.static and !with_id)
"legacy_without_id"
else if (route.legacy and route.static and with_id)
"legacy_static_with_id"
else if (route.legacy and route.static and !with_id)
"legacy_static_without_id"
else
unreachable;
std.mem.replaceScalar(u8, module_path, '\\', '/'); std.mem.replaceScalar(u8, module_path, '\\', '/');
try self.module_paths.append(try self.allocator.dupe(u8, module_path)); try self.module_paths.append(try self.allocator.dupe(u8, module_path));
@ -305,7 +338,7 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
full_name, full_name,
route.name, route.name,
view_name, view_name,
if (route.static) "static" else "dynamic", tag,
if (route.static) "true" else "false", if (route.static) "true" else "false",
uri_path, uri_path,
template, template,
@ -325,7 +358,14 @@ const RouteSet = struct {
fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !RouteSet { fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !RouteSet {
const stat = try dir.statFile(path); const stat = try dir.statFile(path);
const source = try dir.readFileAllocOptions(self.allocator, path, @intCast(stat.size), null, @alignOf(u8), 0); const source = try dir.readFileAllocOptions(
self.allocator,
path,
@intCast(stat.size),
null,
@alignOf(u8),
0,
);
defer self.allocator.free(source); defer self.allocator.free(source);
self.ast = try std.zig.Ast.parse(self.allocator, source, .zig); self.ast = try std.zig.Ast.parse(self.allocator, source, .zig);
@ -336,15 +376,25 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
for (self.ast.nodes.items(.tag), 0..) |tag, index| { for (self.ast.nodes.items(.tag), 0..) |tag, index| {
switch (tag) { switch (tag) {
.fn_proto_multi => { .fn_proto_multi, .fn_proto_one, .fn_proto_simple => |function_tag| {
const function = try self.parseFunction(index, path, source); var function = try self.parseFunction(function_tag, index, path, source);
if (function) |*capture| { if (function) |*capture| {
for (capture.args) |arg| { if (capture.args.len == 0) {
std.debug.print(
"Expected at least 1 argument for view function `{s}` in `{s}`",
.{ capture.name, path },
);
return error.JetzigMissingViewArgument;
}
for (capture.args, 0..) |arg, arg_index| {
if (std.mem.eql(u8, try arg.typeBasename(), "StaticRequest")) { if (std.mem.eql(u8, try arg.typeBasename(), "StaticRequest")) {
@constCast(capture).static = true; capture.static = true;
capture.legacy = arg_index + 1 < capture.args.len;
try static_routes.append(capture.*); try static_routes.append(capture.*);
} } else if (std.mem.eql(u8, try arg.typeBasename(), "Request")) {
if (std.mem.eql(u8, try arg.typeBasename(), "Request")) { capture.static = false;
capture.legacy = arg_index + 1 < capture.args.len;
try dynamic_routes.append(capture.*); try dynamic_routes.append(capture.*);
} }
} }
@ -529,14 +579,21 @@ fn isStaticParamsDecl(self: *Routes, decl: std.zig.Ast.full.VarDecl) bool {
fn parseFunction( fn parseFunction(
self: *Routes, self: *Routes,
function_type: std.zig.Ast.Node.Tag,
index: usize, index: usize,
path: []const u8, path: []const u8,
source: []const u8, source: []const u8,
) !?Function { ) !?Function {
const fn_proto = self.ast.fnProtoMulti(@as(u32, @intCast(index))); var buf: [1]std.zig.Ast.Node.Index = undefined;
const fn_proto = switch (function_type) {
.fn_proto_multi => self.ast.fnProtoMulti(@as(u32, @intCast(index))),
.fn_proto_one => self.ast.fnProtoOne(&buf, @as(u32, @intCast(index))),
.fn_proto_simple => self.ast.fnProtoSimple(&buf, @as(u32, @intCast(index))),
else => unreachable,
};
if (fn_proto.name_token) |token| { if (fn_proto.name_token) |token| {
const function_name = try self.allocator.dupe(u8, self.ast.tokenSlice(token)); const function_name = try self.allocator.dupe(u8, self.ast.tokenSlice(token));
var it = fn_proto.iterate(&self.ast);
var args = std.ArrayList(Arg).init(self.allocator); var args = std.ArrayList(Arg).init(self.allocator);
defer args.deinit(); defer args.deinit();
@ -545,6 +602,7 @@ fn parseFunction(
return null; return null;
} }
var it = fn_proto.iterate(&self.ast);
while (it.next()) |arg| { while (it.next()) |arg| {
if (arg.name_token) |arg_token| { if (arg.name_token) |arg_token| {
const arg_name = self.ast.tokenSlice(arg_token); const arg_name = self.ast.tokenSlice(arg_token);

View File

@ -344,6 +344,11 @@ pub fn params(self: *Request) !*jetzig.data.Value {
} }
} }
// TODO
// pub fn expectParams(self: Request, T: type) ?T {
//
// }
/// Retrieve a file from a `multipart/form-data`-encoded request body, if present. /// Retrieve a file from a `multipart/form-data`-encoded request body, if present.
pub fn file(self: *Request, name: []const u8) !?jetzig.http.File { pub fn file(self: *Request, name: []const u8) !?jetzig.http.File {
_ = try self.parseQuery(); _ = try self.parseQuery();
@ -692,12 +697,17 @@ pub fn match(self: *Request, route: jetzig.views.Route) !bool {
}; };
} }
fn isMatch(self: *Request, match_type: enum { exact, resource_id }, route: jetzig.views.Route) bool { fn isMatch(
self: *Request,
match_type: enum { exact, resource_id },
route: jetzig.views.Route,
) bool {
const path = switch (match_type) { const path = switch (match_type) {
.exact => self.path.base_path, .exact => self.path.base_path,
.resource_id => self.path.directory, .resource_id => self.path.directory,
}; };
// Special case for `/foobar/1/new` -> render `new()`
if (route.action == .get and std.mem.eql(u8, self.path.resource_id, "new")) return false; if (route.action == .get and std.mem.eql(u8, self.path.resource_id, "new")) return false;
return std.mem.eql(u8, path, route.uri_path); return std.mem.eql(u8, path, route.uri_path);

View File

@ -567,7 +567,9 @@ fn matchMiddlewareRoute(request: *const jetzig.http.Request) ?jetzig.middleware.
fn matchRoute(self: *Server, request: *jetzig.http.Request, static: bool) !?jetzig.views.Route { fn matchRoute(self: *Server, request: *jetzig.http.Request, static: bool) !?jetzig.views.Route {
for (self.routes) |route| { for (self.routes) |route| {
// .index routes always take precedence. // .index routes always take precedence.
if (route.static == static and route.action == .index and try request.match(route.*)) return route.*; if (route.static == static and route.action == .index and try request.match(route.*)) {
return route.*;
}
} }
for (self.routes) |route| { for (self.routes) |route| {

View File

@ -1,19 +1,33 @@
const std = @import("std"); const std = @import("std");
const jetzig = @import("../../jetzig.zig"); const jetzig = @import("../../jetzig.zig");
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, post, put, patch, delete, custom };
pub const View = union(enum) {
with_id: view_types.ViewWithId,
without_id: view_types.ViewWithoutId,
with_args: view_types.ViewWithArgs,
static_with_id: view_types.StaticViewWithId,
static_without_id: view_types.StaticViewWithoutId,
static_with_args: view_types.StaticViewWithArgs,
legacy_with_id: view_types.LegacyViewWithId,
legacy_without_id: view_types.LegacyViewWithoutId,
legacy_with_args: view_types.LegacyViewWithArgs,
legacy_static_with_id: view_types.LegacyStaticViewWithId,
legacy_static_without_id: view_types.LegacyStaticViewWithoutId,
legacy_static_with_args: view_types.LegacyStaticViewWithArgs,
};
pub const RenderFn = *const fn (Route, *jetzig.http.Request) anyerror!jetzig.views.View; pub const RenderFn = *const fn (Route, *jetzig.http.Request) anyerror!jetzig.views.View;
pub const RenderStaticFn = *const fn (Route, *jetzig.http.StaticRequest) anyerror!jetzig.views.View; pub const RenderStaticFn = *const fn (Route, *jetzig.http.StaticRequest) anyerror!jetzig.views.View;
pub const ViewWithoutId = *const fn (*jetzig.http.Request, *jetzig.data.Data) anyerror!jetzig.views.View;
pub const ViewWithId = *const fn (id: []const u8, *jetzig.http.Request, *jetzig.data.Data) anyerror!jetzig.views.View;
const StaticViewWithoutId = *const fn (*jetzig.http.StaticRequest, *jetzig.data.Data) anyerror!jetzig.views.View;
pub const ViewWithArgs = *const fn ([]const []const u8, *jetzig.http.Request, *jetzig.data.Data) anyerror!jetzig.views.View;
const StaticViewWithId = *const fn (id: []const u8, *jetzig.http.StaticRequest, *jetzig.data.Data) anyerror!jetzig.views.View;
pub const Formats = struct { pub const Formats = struct {
index: ?[]const ResponseFormat = null, index: ?[]const ResponseFormat = null,
get: ?[]const ResponseFormat = null, get: ?[]const ResponseFormat = null,
@ -26,47 +40,13 @@ pub const Formats = struct {
}; };
const ResponseFormat = enum { html, json }; const ResponseFormat = enum { html, json };
pub const DynamicViewType = union(Action) {
index: ViewWithoutId,
get: ViewWithId,
new: ViewWithoutId,
post: ViewWithoutId,
put: ViewWithId,
patch: ViewWithId,
delete: ViewWithId,
custom: CustomViewType,
};
pub const StaticViewType = union(Action) {
index: StaticViewWithoutId,
get: StaticViewWithId,
new: StaticViewWithoutId,
post: StaticViewWithoutId,
put: StaticViewWithId,
patch: StaticViewWithId,
delete: StaticViewWithId,
custom: void,
};
pub const CustomViewType = union(enum) {
with_id: ViewWithId,
without_id: ViewWithoutId,
with_args: ViewWithArgs,
};
pub const ViewType = union(enum) {
static: StaticViewType,
dynamic: DynamicViewType,
custom: CustomViewType,
};
name: []const u8, name: []const u8,
action: Action, action: Action,
method: jetzig.http.Request.Method = undefined, // Used by custom routes only method: jetzig.http.Request.Method = undefined, // Used by custom routes only
view_name: []const u8, view_name: []const u8,
uri_path: []const u8, uri_path: []const u8,
path: ?[]const u8 = null, path: ?[]const u8 = null,
view: ViewType, view: View,
render: RenderFn = renderFn, render: RenderFn = renderFn,
renderStatic: RenderStaticFn = renderStaticFn, renderStatic: RenderStaticFn = renderStaticFn,
static: bool = false, static: bool = false,
@ -97,6 +77,14 @@ pub fn deinitParams(self: *const Route) void {
self.params.deinit(); self.params.deinit();
} }
pub fn format(self: Route, _: []const u8, _: anytype, writer: anytype) !void {
try writer.print(
\\Route{{ .name = "{s}", .action = .{s}, .view_name = "{s}", .static = {} }}
,
.{ self.name, @tagName(self.action), self.view_name, self.static },
);
}
/// Match a **custom** route to a request - not used by auto-generated route matching. /// Match a **custom** route to a request - not used by auto-generated route matching.
pub fn match(self: Route, request: *const jetzig.http.Request) bool { pub fn match(self: Route, request: *const jetzig.http.Request) bool {
if (self.method != request.method) return false; if (self.method != request.method) return false;
@ -140,46 +128,31 @@ pub fn validateFormat(self: Route, request: *const jetzig.http.Request) bool {
} }
fn renderFn(self: Route, request: *jetzig.http.Request) anyerror!jetzig.views.View { fn renderFn(self: Route, request: *jetzig.http.Request) anyerror!jetzig.views.View {
switch (self.view) { return switch (self.view) {
.dynamic => {}, .without_id => |func| try func(request),
.custom => |view_type| switch (view_type) { .legacy_without_id => |func| try func(request, request.response_data),
.with_id => |view| return try view(request.path.resourceId(self), request, request.response_data), .with_id => |func| try func(request.path.resource_id, request),
.without_id => |view| return try view(request, request.response_data), .legacy_with_id => |func| try func(
.with_args => |view| return try view( request.path.resource_id,
try request.path.resourceArgs(self, request.allocator), request,
request, request.response_data,
request.response_data, ),
), else => unreachable,
}, };
// We only end up here if a static route is defined but its output is not found in the
// file system (e.g. if it was manually deleted after build). This should be avoidable by
// including the content as an artifact in the compiled executable (TODO):
.static => return error.JetzigMissingStaticContent,
}
switch (self.view.dynamic) {
.index => |view| return try view(request, request.response_data),
.get => |view| return try view(request.path.resource_id, request, request.response_data),
.new => |view| return try view(request, request.response_data),
.post => |view| return try view(request, request.response_data),
.patch => |view| return try view(request.path.resource_id, request, request.response_data),
.put => |view| return try view(request.path.resource_id, request, request.response_data),
.delete => |view| return try view(request.path.resource_id, request, request.response_data),
.custom => unreachable,
}
} }
fn renderStaticFn(self: Route, request: *jetzig.http.StaticRequest) anyerror!jetzig.views.View { fn renderStaticFn(self: Route, request: *jetzig.http.StaticRequest) anyerror!jetzig.views.View {
request.response_data.* = jetzig.data.Data.init(request.allocator); request.response_data.* = jetzig.data.Data.init(request.allocator);
switch (self.view.static) { return switch (self.view) {
.index => |view| return try view(request, request.response_data), .static_without_id => |func| try func(request),
.get => |view| return try view(try request.resourceId(), request, request.response_data), .legacy_static_without_id => |func| try func(request, request.response_data),
.new => |view| return try view(request, request.response_data), .static_with_id => |func| try func(try request.resourceId(), request),
.post => |view| return try view(request, request.response_data), .legacy_static_with_id => |func| try func(
.patch => |view| return try view(try request.resourceId(), request, request.response_data), try request.resourceId(),
.put => |view| return try view(try request.resourceId(), request, request.response_data), request,
.delete => |view| return try view(try request.resourceId(), request, request.response_data), request.response_data,
.custom => unreachable, ),
} else => unreachable,
};
} }

View File

@ -0,0 +1,69 @@
const jetzig = @import("../../jetzig.zig");
pub const ViewWithoutId = *const fn (
*jetzig.http.Request,
) anyerror!jetzig.views.View;
pub const ViewWithId = *const fn (
id: []const u8,
*jetzig.http.Request,
) anyerror!jetzig.views.View;
pub const ViewWithArgs = *const fn (
[]const []const u8,
*jetzig.http.Request,
) anyerror!jetzig.views.View;
pub const StaticViewWithoutId = *const fn (
*jetzig.http.StaticRequest,
) anyerror!jetzig.views.View;
pub const StaticViewWithId = *const fn (
id: []const u8,
*jetzig.http.StaticRequest,
) anyerror!jetzig.views.View;
pub const StaticViewWithArgs = *const fn (
[]const []const u8,
*jetzig.http.StaticRequest,
) anyerror!jetzig.views.View;
// Legacy view types receive a `data` argument. This made sense when `data.string(...)` etc. were
// needed to create a string, but now we use type inference/coercion when adding values to
// response data.
// `Array.append(.array)`, `Array.append(.object)`, `Object.put(key, .array)`, and
// `Object.put(key, .object)` also remove the need to use `data.array()` and `data.object()`.
// The only remaining use is `data.root(.object)` and `data.root(.array)` which we can move to
// `request.responseData(.object)` and `request.responseData(.array)`.
pub const LegacyViewWithoutId = *const fn (
*jetzig.http.Request,
*jetzig.data.Data,
) anyerror!jetzig.views.View;
pub const LegacyViewWithId = *const fn (
id: []const u8,
*jetzig.http.Request,
*jetzig.data.Data,
) anyerror!jetzig.views.View;
pub const LegacyStaticViewWithoutId = *const fn (
*jetzig.http.StaticRequest,
*jetzig.data.Data,
) anyerror!jetzig.views.View;
pub const LegacyViewWithArgs = *const fn (
[]const []const u8,
*jetzig.http.Request,
*jetzig.data.Data,
) anyerror!jetzig.views.View;
pub const LegacyStaticViewWithId = *const fn (
id: []const u8,
*jetzig.http.StaticRequest,
*jetzig.data.Data,
) anyerror!jetzig.views.View;
pub const LegacyStaticViewWithArgs = *const fn (
[]const []const u8,
*jetzig.http.StaticRequest,
) anyerror!jetzig.views.View;