From 13e77b4520ee7895257f096ff41181eab337cc68 Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Wed, 27 Mar 2024 19:44:19 +0000 Subject: [PATCH] Implement nested routes Allow creating routes nested in sub directories of arbitrary depth in `src/app/views/`. --- demo/src/app/views/nested/route/example.zig | 22 +++++++++ .../app/views/nested/route/example/index.zmpl | 3 ++ demo/src/app/views/zmpl.manifest.zig | 10 ---- src/GenerateRoutes.zig | 25 ++++++---- src/jetzig/Environment.zig | 1 + src/jetzig/http/Server.zig | 49 +++++++++---------- src/jetzig/markdown.zig | 7 ++- 7 files changed, 71 insertions(+), 46 deletions(-) create mode 100644 demo/src/app/views/nested/route/example.zig create mode 100644 demo/src/app/views/nested/route/example/index.zmpl delete mode 100644 demo/src/app/views/zmpl.manifest.zig diff --git a/demo/src/app/views/nested/route/example.zig b/demo/src/app/views/nested/route/example.zig new file mode 100644 index 0000000..c7a1eac --- /dev/null +++ b/demo/src/app/views/nested/route/example.zig @@ -0,0 +1,22 @@ +const std = @import("std"); +const jetzig = @import("jetzig"); + +pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { + _ = data; + return request.render(.ok); +} + +pub const static_params = .{ + .get = .{ + .{ .id = "foo", .params = .{ .foo = "bar" } }, + .{ .id = "foo" }, + }, +}; + +pub fn get(id: []const u8, request: *jetzig.StaticRequest, data: *jetzig.Data) !jetzig.View { + var object = try data.object(); + try object.put("id", data.string(id)); + const params = try request.params(); + if (params.get("foo")) |value| try object.put("foo", value); + return request.render(.ok); +} diff --git a/demo/src/app/views/nested/route/example/index.zmpl b/demo/src/app/views/nested/route/example/index.zmpl new file mode 100644 index 0000000..76457d0 --- /dev/null +++ b/demo/src/app/views/nested/route/example/index.zmpl @@ -0,0 +1,3 @@ +
+ Content goes here +
diff --git a/demo/src/app/views/zmpl.manifest.zig b/demo/src/app/views/zmpl.manifest.zig deleted file mode 100644 index eb2409b..0000000 --- a/demo/src/app/views/zmpl.manifest.zig +++ /dev/null @@ -1,10 +0,0 @@ -// Zmpl template manifest. -// This file is automatically generated at build time. Manual edits will be discarded. -// This file should _not_ be stored in version control. -pub const templates = struct { - pub const static_index = @import("static/.index.zmpl.compiled.zig"); - pub const static_get = @import("static/.get.zmpl.compiled.zig"); - pub const root_index = @import("root/.index.zmpl.compiled.zig"); - pub const quotes_post = @import("quotes/.post.zmpl.compiled.zig"); - pub const quotes_get = @import("quotes/.get.zmpl.compiled.zig"); -}; diff --git a/src/GenerateRoutes.zig b/src/GenerateRoutes.zig index 23026cf..3198e2b 100644 --- a/src/GenerateRoutes.zig +++ b/src/GenerateRoutes.zig @@ -23,23 +23,27 @@ const Function = struct { /// compilation. /// path: `src/app/views/iguanas.zig`, action: `index` => `iguanas_index` pub fn fullName(self: @This(), allocator: std.mem.Allocator) ![]const u8 { - // XXX: Currently we do not support nested routes, so we will need to adjust this if we - // add nested routes in future. - const extension = std.fs.path.extension(self.path); - const basename = std.fs.path.basename(self.path); - const name = basename[0 .. basename.len - extension.len]; + const relative_path = try std.fs.path.relative(allocator, "src/app/views/", self.path); + defer allocator.free(relative_path); - return std.mem.concat(allocator, u8, &[_][]const u8{ name, "_", self.name }); + const path = relative_path[0 .. relative_path.len - std.fs.path.extension(relative_path).len]; + std.mem.replaceScalar(u8, path, '\\', '/'); + std.mem.replaceScalar(u8, path, '/', '_'); + + return std.mem.concat(allocator, u8, &[_][]const u8{ path, "_", self.name }); } /// The path used to match the route. Resource ID and extension is not included here and is /// appended as needed during matching logic at run time. pub fn uriPath(self: @This(), allocator: std.mem.Allocator) ![]const u8 { - const basename = std.fs.path.basename(self.path); - const name = basename[0 .. basename.len - std.fs.path.extension(basename).len]; - if (std.mem.eql(u8, name, "root")) return try allocator.dupe(u8, "/"); + const relative_path = try std.fs.path.relative(allocator, "src/app/views/", self.path); + defer allocator.free(relative_path); - return try std.mem.concat(allocator, u8, &[_][]const u8{ "/", name }); + const path = relative_path[0 .. relative_path.len - std.fs.path.extension(relative_path).len]; + std.mem.replaceScalar(u8, path, '\\', '/'); + if (std.mem.eql(u8, path, "root")) return try allocator.dupe(u8, "/"); + + return try std.mem.concat(allocator, u8, &[_][]const u8{ "/", path }); } pub fn lessThanFn(context: void, lhs: @This(), rhs: @This()) bool { @@ -247,6 +251,7 @@ fn generateRoutesForView(self: *Self, views_dir: std.fs.Dir, path: []const u8) ! .simple_var_decl => { const decl = self.ast.simpleVarDecl(asNodeIndex(index)); if (self.isStaticParamsDecl(decl)) { + self.data.reset(); const params = try self.data.object(); try self.parseStaticParamsDecl(decl, params); static_params = self.data.value; diff --git a/src/jetzig/Environment.zig b/src/jetzig/Environment.zig index be8f842..a822573 100644 --- a/src/jetzig/Environment.zig +++ b/src/jetzig/Environment.zig @@ -62,6 +62,7 @@ pub fn init(allocator: std.mem.Allocator) Environment { /// Generate server initialization options using command line args with defaults. pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions { const options = try args.parseForCurrentProcess(Options, self.allocator, .print); + defer options.deinit(); if (options.options.help) { const writer = std.io.getStdErr().writer(); diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index dbc270d..e8c86c3 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -473,32 +473,31 @@ fn staticPath(request: *jetzig.http.Request, route: jetzig.views.Route) !?[]cons }; for (route.params.items, 0..) |static_params, index| { - if (try static_params.getValue("params")) |expected_params| { - switch (route.action) { - .index, .post => {}, - inline else => { - if (try static_params.getValue("id")) |id| { - switch (id.*) { - .string => |capture| { - if (!std.mem.eql(u8, capture.value, request.path.resource_id)) continue; - }, - // Should be unreachable - this means generated `routes.zig` is incoherent: - inline else => return error.JetzigRouteError, - } - } - }, - } - if (!expected_params.eql(params)) continue; - - const index_fmt = try std.fmt.allocPrint(request.allocator, "{}", .{index}); - defer request.allocator.free(index_fmt); - - return try std.mem.concat( - request.allocator, - u8, - &[_][]const u8{ route.name, "_", index_fmt, extension }, - ); + const expected_params = try static_params.getValue("params"); + switch (route.action) { + .index, .post => {}, + inline else => { + const id = try static_params.getValue("id"); + if (id == null) return error.JetzigRouteError; // `routes.zig` is incoherent. + switch (id.?.*) { + .string => |capture| { + if (!std.mem.eql(u8, capture.value, request.path.resource_id)) continue; + }, + // `routes.zig` is incoherent. + inline else => return error.JetzigRouteError, + } + }, } + if (expected_params != null and !expected_params.?.eql(params)) continue; + + const index_fmt = try std.fmt.allocPrint(request.allocator, "{}", .{index}); + defer request.allocator.free(index_fmt); + + return try std.mem.concat( + request.allocator, + u8, + &[_][]const u8{ route.name, "_", index_fmt, extension }, + ); } switch (route.action) { diff --git a/src/jetzig/markdown.zig b/src/jetzig/markdown.zig index ce9f29c..96105ce 100644 --- a/src/jetzig/markdown.zig +++ b/src/jetzig/markdown.zig @@ -27,7 +27,12 @@ pub fn render( const full_path = try std.mem.concat(allocator, u8, &[_][]const u8{ base_path, ".md" }); defer allocator.free(full_path); - const stat = try std.fs.cwd().statFile(full_path); + const stat = std.fs.cwd().statFile(full_path) catch |err| { + return switch (err) { + error.FileNotFound => null, + else => err, + }; + }; const markdown_content = std.fs.cwd().readFileAlloc(allocator, full_path, stat.size) catch |err| { switch (err) { error.FileNotFound => return null,