diff --git a/demo/src/app/views/params.zig b/demo/src/app/views/params.zig
new file mode 100644
index 0000000..16a47b0
--- /dev/null
+++ b/demo/src/app/views/params.zig
@@ -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);
+}
diff --git a/demo/src/app/views/params/post.zmpl b/demo/src/app/views/params/post.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/demo/src/app/views/params/post.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/Routes.zig b/src/Routes.zig
index b46d833..eadedb2 100644
--- a/src/Routes.zig
+++ b/src/Routes.zig
@@ -24,6 +24,7 @@ const Function = struct {
path: []const u8,
source: []const u8,
params: std.ArrayList([]const u8),
+ legacy: bool = false,
static: bool = false,
/// 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();
- // std.debug.print("routes.zig\n{s}\n", .{self.buffer.items});
}
pub fn relativePathFrom(
@@ -232,7 +232,10 @@ fn writeRoutes(self: *Routes, writer: anytype) !void {
const realpath = try dir.realpathAlloc(self.allocator, entry.path);
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| {
try self.static_routes.append(view_route);
@@ -272,7 +275,7 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
\\ .name = "{0s}",
\\ .action = .{1s},
\\ .view_name = "{2s}",
- \\ .view = jetzig.Route.ViewType{{ .{3s} = .{{ .{1s} = @import("{7s}").{1s} }} }},
+ \\ .view = jetzig.Route.View{{ .{3s} = @import("{7s}").{1s} }},
\\ .path = "{7s}",
\\ .static = {4s},
\\ .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 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, '\\', '/');
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,
route.name,
view_name,
- if (route.static) "static" else "dynamic",
+ tag,
if (route.static) "true" else "false",
uri_path,
template,
@@ -325,7 +358,14 @@ const RouteSet = struct {
fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !RouteSet {
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);
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| {
switch (tag) {
- .fn_proto_multi => {
- const function = try self.parseFunction(index, path, source);
+ .fn_proto_multi, .fn_proto_one, .fn_proto_simple => |function_tag| {
+ var function = try self.parseFunction(function_tag, index, path, source);
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")) {
- @constCast(capture).static = true;
+ capture.static = true;
+ capture.legacy = arg_index + 1 < capture.args.len;
try static_routes.append(capture.*);
- }
- if (std.mem.eql(u8, try arg.typeBasename(), "Request")) {
+ } else 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.*);
}
}
@@ -529,14 +579,21 @@ fn isStaticParamsDecl(self: *Routes, decl: std.zig.Ast.full.VarDecl) bool {
fn parseFunction(
self: *Routes,
+ function_type: std.zig.Ast.Node.Tag,
index: usize,
path: []const u8,
source: []const u8,
) !?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| {
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);
defer args.deinit();
@@ -545,6 +602,7 @@ fn parseFunction(
return null;
}
+ var it = fn_proto.iterate(&self.ast);
while (it.next()) |arg| {
if (arg.name_token) |arg_token| {
const arg_name = self.ast.tokenSlice(arg_token);
diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig
index 0b046b3..a7fe119 100644
--- a/src/jetzig/http/Request.zig
+++ b/src/jetzig/http/Request.zig
@@ -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.
pub fn file(self: *Request, name: []const u8) !?jetzig.http.File {
_ = 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) {
.exact => self.path.base_path,
.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;
return std.mem.eql(u8, path, route.uri_path);
diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig
index 991805a..ba84f78 100644
--- a/src/jetzig/http/Server.zig
+++ b/src/jetzig/http/Server.zig
@@ -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 {
for (self.routes) |route| {
// .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| {
diff --git a/src/jetzig/views/Route.zig b/src/jetzig/views/Route.zig
index 2ed4104..9a616b7 100644
--- a/src/jetzig/views/Route.zig
+++ b/src/jetzig/views/Route.zig
@@ -1,19 +1,33 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
+const view_types = @import("view_types.zig");
const Route = @This();
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 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 {
index: ?[]const ResponseFormat = null,
get: ?[]const ResponseFormat = null,
@@ -26,47 +40,13 @@ pub const Formats = struct {
};
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,
action: Action,
method: jetzig.http.Request.Method = undefined, // Used by custom routes only
view_name: []const u8,
uri_path: []const u8,
path: ?[]const u8 = null,
-view: ViewType,
+view: View,
render: RenderFn = renderFn,
renderStatic: RenderStaticFn = renderStaticFn,
static: bool = false,
@@ -97,6 +77,14 @@ pub fn deinitParams(self: *const Route) void {
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.
pub fn match(self: Route, request: *const jetzig.http.Request) bool {
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 {
- switch (self.view) {
- .dynamic => {},
- .custom => |view_type| switch (view_type) {
- .with_id => |view| return try view(request.path.resourceId(self), request, request.response_data),
- .without_id => |view| return try view(request, request.response_data),
- .with_args => |view| return try view(
- try request.path.resourceArgs(self, request.allocator),
- request,
- request.response_data,
- ),
- },
- // 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,
- }
+ return switch (self.view) {
+ .without_id => |func| try func(request),
+ .legacy_without_id => |func| try func(request, request.response_data),
+ .with_id => |func| try func(request.path.resource_id, request),
+ .legacy_with_id => |func| try func(
+ request.path.resource_id,
+ request,
+ request.response_data,
+ ),
+ else => unreachable,
+ };
}
fn renderStaticFn(self: Route, request: *jetzig.http.StaticRequest) anyerror!jetzig.views.View {
request.response_data.* = jetzig.data.Data.init(request.allocator);
- switch (self.view.static) {
- .index => |view| return try view(request, request.response_data),
- .get => |view| return try view(try request.resourceId(), 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(try request.resourceId(), request, request.response_data),
- .put => |view| return try view(try request.resourceId(), request, request.response_data),
- .delete => |view| return try view(try request.resourceId(), request, request.response_data),
- .custom => unreachable,
- }
+ return switch (self.view) {
+ .static_without_id => |func| try func(request),
+ .legacy_static_without_id => |func| try func(request, request.response_data),
+ .static_with_id => |func| try func(try request.resourceId(), request),
+ .legacy_static_with_id => |func| try func(
+ try request.resourceId(),
+ request,
+ request.response_data,
+ ),
+ else => unreachable,
+ };
}
diff --git a/src/jetzig/views/view_types.zig b/src/jetzig/views/view_types.zig
new file mode 100644
index 0000000..5eea082
--- /dev/null
+++ b/src/jetzig/views/view_types.zig
@@ -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;