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;