From 1feb18fb74e626fe068ec67532318640a9cb83be Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Sat, 26 Apr 2025 18:14:50 +0100 Subject: [PATCH 1/2] Add note to README about Zig compatibility --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3004ca4..83ee9a0 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ _Jetzig_ is a web framework written in 100% pure [Zig](https://ziglang.org) :liz Official website: [jetzig.dev](https://www.jetzig.dev/) +Please note that _Jetzig_'s `main` branch aims to be compatible with the latest [Zig nightly master build](https://ziglang.org/download/) and older versions of _Zig_ are not supported. + _Jetzig_ aims to provide a rich set of user-friendly tools for building modern web applications quickly. See the checklist below. Join us on Discord ! [https://discord.gg/eufqssz7X6](https://discord.gg/eufqssz7X6). From 6a4f99ca14174bf797806a2521d50d4963736224 Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Sat, 14 Dec 2024 20:04:59 +0000 Subject: [PATCH 2/2] Inertia - WIP Not yet complete but provides some functionality with internal templates that Websockets/Channels needs so merging in to main and can pick up later. --- build.zig | 11 ++++ build.zig.zon | 4 +- demo/src/app/views/inertia/_head.zmpl | 1 + demo/src/main.zig | 7 ++- src/compile_static_routes.zig | 5 +- src/jetzig/TemplateContext.zig | 14 +++++ src/jetzig/http/Request.zig | 18 +++++- src/jetzig/http/Server.zig | 61 ++++++++++++++++----- src/jetzig/http/middleware.zig | 32 +++++++++++ src/jetzig/mail/Job.zig | 4 +- src/jetzig/middleware.zig | 1 + src/jetzig/middleware/InertiaMiddleware.zig | 6 ++ src/jetzig/templates/inertia.zmpl | 13 +++++ src/jetzig/views/View.zig | 1 + 14 files changed, 153 insertions(+), 25 deletions(-) create mode 100644 demo/src/app/views/inertia/_head.zmpl create mode 100644 src/jetzig/middleware/InertiaMiddleware.zig create mode 100644 src/jetzig/templates/inertia.zmpl diff --git a/build.zig b/build.zig index 5337a24..e64f999 100644 --- a/build.zig +++ b/build.zig @@ -12,11 +12,22 @@ const use_llvm_default = builtin.os.tag != .linux; pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + var jetzig_templates_path = std.ArrayList([]const u8).init(b.allocator); + try jetzig_templates_path.append("/"); + var it = std.mem.splitSequence( + u8, + try b.path("src/jetzig/templates").getPath3(b, null).toString(b.allocator), + std.fs.path.sep_str, + ); + while (it.next()) |segment| { + try jetzig_templates_path.append(segment); + } const templates_paths = try zmpl_build.templatesPaths( b.allocator, &.{ .{ .prefix = "views", .path = &.{ "src", "app", "views" } }, .{ .prefix = "mailers", .path = &.{ "src", "app", "mailers" } }, + .{ .prefix = "jetzig", .path = jetzig_templates_path.items }, }, ); diff --git a/build.zig.zon b/build.zig.zon index 0061b20..cbefcb8 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -25,8 +25,8 @@ .hash = "jetkv-0.0.0-zCv0fmCGAgCyYqwHjk0P5KrYVRew1MJAtbtAcIO-WPpT", }, .zmpl = .{ - .url = "https://github.com/jetzig-framework/zmpl/archive/c57fc9b83027e8c1459d9625c3509f59f0fb89f3.tar.gz", - .hash = "zmpl-0.0.1-SYFGBgdqAwDeA6xm4KAhpKoNrWs5CMQK6x447zhWclCs", + .url = "https://github.com/jetzig-framework/zmpl/archive/89ee0ce9b4c96c316cc0575266fb66c864f24a49.tar.gz", + .hash = "zmpl-0.0.1-SYFGBtuNAwCj2YbqnoEJt3bk1iFIZjGK6JwMc72toZBR", }, .httpz = .{ .url = "https://github.com/karlseguin/http.zig/archive/37d7cb9819b804ade5f4b974b82f8dd0622225ed.tar.gz", diff --git a/demo/src/app/views/inertia/_head.zmpl b/demo/src/app/views/inertia/_head.zmpl new file mode 100644 index 0000000..783e4ab --- /dev/null +++ b/demo/src/app/views/inertia/_head.zmpl @@ -0,0 +1 @@ +My Inertia App diff --git a/demo/src/main.zig b/demo/src/main.zig index b306e70..8306b6e 100644 --- a/demo/src/main.zig +++ b/demo/src/main.zig @@ -14,9 +14,10 @@ pub const jetzig_options = struct { pub const middleware: []const type = &.{ // jetzig.middleware.AuthMiddleware, // jetzig.middleware.AntiCsrfMiddleware, - // jetzig.middleware.HtmxMiddleware, - // jetzig.middleware.CompressionMiddleware, - // @import("app/middleware/DemoMiddleware.zig"), + jetzig.middleware.HtmxMiddleware, + // jetzig.middleware.InertiaMiddleware, + // jetzig.middleware.CompressionMiddleware, + // @import("app/middleware/DemoMiddleware.zig"), }; // Maximum bytes to allow in request body. diff --git a/src/compile_static_routes.zig b/src/compile_static_routes.zig index 1f45ec5..b027218 100644 --- a/src/compile_static_routes.zig +++ b/src/compile_static_routes.zig @@ -147,7 +147,7 @@ fn renderMarkdown( if (zmpl.findPrefixed("views", prefixed_name)) |layout| { view.data.content = .{ .data = content }; - return try layout.render(view.data, jetzig.TemplateContext, .{}, .{}); + return try layout.render(view.data, jetzig.TemplateContext, .{}, &.{}, .{}); } else { std.debug.print("Unknown layout: {s}\n", .{layout_name}); return content; @@ -174,6 +174,7 @@ fn renderZmplTemplate( view.data, jetzig.TemplateContext, .{}, + &.{}, .{ .layout = layout }, ); } else { @@ -181,7 +182,7 @@ fn renderZmplTemplate( return try allocator.dupe(u8, ""); } } else { - return try template.render(view.data, jetzig.TemplateContext, .{}, .{}); + return try template.render(view.data, jetzig.TemplateContext, .{}, &.{}, .{}); } } else return null; } diff --git a/src/jetzig/TemplateContext.zig b/src/jetzig/TemplateContext.zig index 321e527..b413285 100644 --- a/src/jetzig/TemplateContext.zig +++ b/src/jetzig/TemplateContext.zig @@ -1,13 +1,18 @@ const std = @import("std"); pub const http = @import("http.zig"); +pub const views = @import("views.zig"); pub const config = @import("config.zig"); /// Context available in every Zmpl template as `context`. pub const TemplateContext = @This(); request: ?*http.Request = null, +route: ?views.Route = null, +/// Return an authenticity token stored in the current request's session. If no token exists, +/// generate and store before returning. +/// Use to create a form element which can be verified by `AntiCsrfMiddleware`. pub fn authenticityToken(self: TemplateContext) !?[]const u8 { return if (self.request) |request| try request.authenticityToken() @@ -15,6 +20,8 @@ pub fn authenticityToken(self: TemplateContext) !?[]const u8 { null; } +/// Generate a hidden form element containing an authenticity token provided by +/// `authenticityToken`. Use as `{{context.authenticityFormElement()}}` in a Zmpl template. pub fn authenticityFormElement(self: TemplateContext) !?[]const u8 { return if (self.request) |request| blk: { const token = try request.authenticityToken(); @@ -23,3 +30,10 @@ pub fn authenticityFormElement(self: TemplateContext) !?[]const u8 { , .{ config.get([]const u8, "authenticity_token_name"), token }); } else null; } + +pub fn path(self: TemplateContext) ?[]const u8 { + return if (self.request) |request| + request.path.path + else + null; +} diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig index 4e31d3e..fd06a76 100644 --- a/src/jetzig/http/Request.zig +++ b/src/jetzig/http/Request.zig @@ -15,6 +15,7 @@ pub const RequestState = enum { initial, // No processing has taken place processed, // Request headers have been processed after_request, // Initial middleware processing + after_view, // View returned, response data ready for full response render rendered, // Rendered by middleware or view redirected, // Redirected by middleware or view failed, // Failed by middleware or view @@ -217,6 +218,20 @@ pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode) return self.rendered_view.?; } +/// Render a response with pre-rendered content. This function can only be called once per +/// request (repeat calls will trigger an error). +pub fn renderContent( + self: *Request, + status_code: jetzig.http.status_codes.StatusCode, + content: []const u8, +) jetzig.views.View { + if (self.isRendered()) self.rendered_multiple = true; + + self.state = .rendered; + self.rendered_view = .{ .data = self.response_data, .status_code = status_code, .content = content }; + return self.rendered_view.?; +} + /// Render an error. This function can only be called once per request (repeat calls will /// trigger an error). pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View { @@ -230,7 +245,7 @@ pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) je pub inline fn isRendered(self: *const Request) bool { return switch (self.state) { .initial, .processed, .after_request, .before_response => false, - .rendered, .redirected, .failed, .finalized => true, + .after_view, .rendered, .redirected, .failed, .finalized => true, }; } @@ -306,6 +321,7 @@ pub fn renderRedirect(self: *Request, state: RedirectState) !void { self.response_data, jetzig.TemplateContext, .{ .request = self }, + &.{}, .{}, ); } else try std.fmt.allocPrint(self.allocator, "Redirecting to {s}", .{state.location}), diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 9fd9ce1..f23c6e4 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -152,13 +152,18 @@ pub fn processNextRequest( try request.process(); + // Allow middleware to render templates even though we have not mapped a view/action yet. + // TODO: We should probably separate the routing and map routes before we invoke middleware. + try request.response_data.addConst("jetzig_action", request.response_data.string("")); + try request.response_data.addConst("jetzig_view", request.response_data.string("")); + var middleware_data = try jetzig.http.middleware.afterRequest(&request); if (try maybeMiddlewareRender(&request, &response)) { try self.logger.logRequest(&request); return; } - try self.renderResponse(&request); + try self.renderResponse(&request, &middleware_data); try request.response.headers.append("Content-Type", response.content_type); try jetzig.http.middleware.beforeResponse(&middleware_data, &request); @@ -175,8 +180,7 @@ fn maybeMiddlewareRender(request: *jetzig.http.Request, response: *const jetzig. if (request.redirect_state) |state| { try request.renderRedirect(state); } else if (request.rendered_view) |rendered| { - // TODO: Allow middleware to set content - request.setResponse(.{ .view = rendered, .content = "" }, .{}); + request.setResponse(.{ .view = rendered, .content = rendered.content orelse "" }, .{}); } try request.response.headers.append("Content-Type", response.content_type); try request.respond(); @@ -184,7 +188,11 @@ fn maybeMiddlewareRender(request: *jetzig.http.Request, response: *const jetzig. } else return false; } -fn renderResponse(self: *Server, request: *jetzig.http.Request) !void { +fn renderResponse( + self: *Server, + request: *jetzig.http.Request, + middleware_data: *jetzig.http.middleware.MiddlewareData, +) !void { const static_resource = self.matchStaticResource(request) catch |err| { if (isUnhandledError(err)) return err; @@ -236,6 +244,34 @@ fn renderResponse(self: *Server, request: *jetzig.http.Request) !void { return; } } + + // View functions return a `View` to encourage users to return from a view function with + // `return request.render(.ok)`, but the actual rendered view is stored in + // `request.rendered_view`. + _ = route.render(route, request) catch |err| { + if (isUnhandledError(err)) return err; + const rendered_error = if (isBadRequest(err)) + try self.renderBadRequest(request) + else + try self.renderInternalServerError(request, @errorReturnTrace(), err); + request.setResponse(rendered_error, .{}); + return; + }; + + if (request.rendered_view != null) { + try jetzig.http.middleware.afterView(middleware_data, request, route); + } + } + + if (request.middleware_rendered) |_| { + // Request processing ends when a middleware renders or redirects. + if (request.redirect_state) |state| { + try request.renderRedirect(state); + } else if (request.rendered_view) |rendered| { + request.setResponse(.{ .view = rendered, .content = rendered.content orelse "" }, .{}); + } + try request.response.headers.append("Content-Type", request.response.content_type); + return try request.respond(); } switch (request.requestFormat()) { @@ -347,15 +383,6 @@ fn renderView( request: *jetzig.http.Request, maybe_template: ?zmpl.Template, ) !RenderedView { - // View functions return a `View` to encourage users to return from a view function with - // `return request.render(.ok)`, but the actual rendered view is stored in - // `request.rendered_view`. - _ = route.render(route, request) catch |err| { - if (isUnhandledError(err)) return err; - if (isBadRequest(err)) return try self.renderBadRequest(request); - return try self.renderInternalServerError(request, @errorReturnTrace(), err); - }; - if (request.state == .failed) { const view: jetzig.views.View = request.rendered_view orelse .{ .data = request.response_data, @@ -415,7 +442,7 @@ fn renderTemplateWithLayout( ) ![]const u8 { try addTemplateConstants(view, route); - const template_context = jetzig.TemplateContext{ .request = request }; + const template_context = jetzig.TemplateContext{ .request = request, .route = route }; if (request.getLayout(route)) |layout_name| { // TODO: Allow user to configure layouts directory other than src/app/views/layouts/ @@ -431,6 +458,7 @@ fn renderTemplateWithLayout( view.data, jetzig.TemplateContext, template_context, + &.{}, .{ .layout = layout }, ); } else { @@ -439,6 +467,7 @@ fn renderTemplateWithLayout( view.data, jetzig.TemplateContext, template_context, + &.{}, .{}, ); } @@ -446,6 +475,7 @@ fn renderTemplateWithLayout( view.data, jetzig.TemplateContext, template_context, + &.{}, .{}, ); } @@ -608,7 +638,8 @@ fn renderErrorView( .content = try template.render( request.response_data, jetzig.TemplateContext, - .{ .request = request }, + .{ .request = request, .route = route.* }, + &.{}, .{}, ), }; diff --git a/src/jetzig/http/middleware.zig b/src/jetzig/http/middleware.zig index e0a4a15..cd42b87 100644 --- a/src/jetzig/http/middleware.zig +++ b/src/jetzig/http/middleware.zig @@ -94,6 +94,38 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData { return middleware_data; } +pub fn afterView(middleware_data: *MiddlewareData, request: *jetzig.http.Request, route: jetzig.views.Route) !void { + if (request.state != .failed) request.state = .after_view; + + inline for (middlewares, 0..) |middleware, index| { + if (comptime !@hasDecl(middleware, "afterView")) continue; + if (request.state == .after_view) { + if (comptime @hasDecl(middleware, "init")) { + const data = middleware_data.get(index).?; + try @call( + .always_inline, + middleware.afterView, + .{ @as(*middleware, @ptrCast(@alignCast(data))), request, route }, + ); + } else { + try @call( + .always_inline, + middleware.afterView, + .{ request, route }, + ); + } + } + + if (request.state != .after_view) { + request.middleware_rendered = .{ + .name = @typeName(middleware), + .action = "afterView", + }; + break; + } + } +} + pub fn beforeResponse( middleware_data: *MiddlewareData, request: *jetzig.http.Request, diff --git a/src/jetzig/mail/Job.zig b/src/jetzig/mail/Job.zig index dccf828..657ed07 100644 --- a/src/jetzig/mail/Job.zig +++ b/src/jetzig/mail/Job.zig @@ -148,7 +148,7 @@ fn defaultHtml( try data.addConst("jetzig_view", data.string("")); try data.addConst("jetzig_action", data.string("")); return if (jetzig.zmpl.findPrefixed("mailers", mailer.html_template)) |template| - try template.render(&data, jetzig.TemplateContext, .{}, .{}) + try template.render(&data, jetzig.TemplateContext, .{}, &.{}, .{}) else null; } @@ -166,7 +166,7 @@ fn defaultText( try data.addConst("jetzig_view", data.string("")); try data.addConst("jetzig_action", data.string("")); return if (jetzig.zmpl.findPrefixed("mailers", mailer.text_template)) |template| - try template.render(&data, jetzig.TemplateContext, .{}, .{}) + try template.render(&data, jetzig.TemplateContext, .{}, &.{}, .{}) else null; } diff --git a/src/jetzig/middleware.zig b/src/jetzig/middleware.zig index 4be3640..0da095c 100644 --- a/src/jetzig/middleware.zig +++ b/src/jetzig/middleware.zig @@ -5,6 +5,7 @@ pub const HtmxMiddleware = @import("middleware/HtmxMiddleware.zig"); pub const CompressionMiddleware = @import("middleware/CompressionMiddleware.zig"); pub const AuthMiddleware = @import("middleware/AuthMiddleware.zig"); pub const AntiCsrfMiddleware = @import("middleware/AntiCsrfMiddleware.zig"); +pub const InertiaMiddleware = @import("middleware/InertiaMiddleware.zig"); const RouteOptions = struct { content: ?[]const u8 = null, diff --git a/src/jetzig/middleware/InertiaMiddleware.zig b/src/jetzig/middleware/InertiaMiddleware.zig new file mode 100644 index 0000000..4272eb2 --- /dev/null +++ b/src/jetzig/middleware/InertiaMiddleware.zig @@ -0,0 +1,6 @@ +const std = @import("std"); +const jetzig = @import("../../jetzig.zig"); + +// WIP + +const InertiaMiddleware = @This(); diff --git a/src/jetzig/templates/inertia.zmpl b/src/jetzig/templates/inertia.zmpl new file mode 100644 index 0000000..51732d1 --- /dev/null +++ b/src/jetzig/templates/inertia.zmpl @@ -0,0 +1,13 @@ + + + + @partial views:inertia/head + + +
+
+ + diff --git a/src/jetzig/views/View.zig b/src/jetzig/views/View.zig index 98beaac..f1fbefd 100644 --- a/src/jetzig/views/View.zig +++ b/src/jetzig/views/View.zig @@ -6,6 +6,7 @@ const jetzig = @import("../../jetzig.zig"); data: *jetzig.data.Data, status_code: jetzig.http.status_codes.StatusCode = .ok, +content: ?[]const u8 = null, pub fn deinit(self: Self) void { _ = self;