This commit is contained in:
Bob Farrell 2024-12-14 20:04:59 +00:00
parent 5ebb60d759
commit 444db1a5e3
10 changed files with 156 additions and 16 deletions

View File

@ -9,11 +9,22 @@ const Environment = enum { development, testing, production };
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 },
},
);

View File

@ -7,8 +7,9 @@
.hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163",
},
.zmpl = .{
.url = "https://github.com/jetzig-framework/zmpl/archive/f9a3c602d060d6b337312b820c852376cd111766.tar.gz",
.hash = "1220f61c70456b8bb7f407ff539d1d8569acea64f68db12f9245f1d4113495c33907",
// .url = "https://github.com/jetzig-framework/zmpl/archive/7b5e0309ee49c06b99c242fecd218d3f3d15cd40.tar.gz",
// .hash = "12204d61eb58ee860f748e5817ef9300ad56c9d5efef84864ae590c87baf2e0380a1",
.path = "../zmpl",
},
.jetkv = .{
.url = "https://github.com/jetzig-framework/jetkv/archive/acaa30db281f1c331d20c48cfe6539186549ad45.tar.gz",

View File

@ -15,6 +15,7 @@ pub const jetzig_options = struct {
// jetzig.middleware.AuthMiddleware,
// jetzig.middleware.AntiCsrfMiddleware,
// jetzig.middleware.HtmxMiddleware,
jetzig.middleware.InertiaMiddleware,
// jetzig.middleware.CompressionMiddleware,
// @import("app/middleware/DemoMiddleware.zig"),
};

View File

@ -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
@ -216,6 +217,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 {
@ -229,7 +244,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,
};
}

View File

@ -150,6 +150,11 @@ 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 (request.middleware_rendered) |_| {
@ -157,13 +162,12 @@ pub fn processNextRequest(
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();
} else {
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,7 +179,11 @@ pub fn processNextRequest(
try self.logger.logRequest(&request);
}
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;
@ -227,6 +235,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);
}
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()) {
@ -338,15 +374,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,

View File

@ -86,6 +86,38 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
return middleware_data;
}
pub fn afterView(middleware_data: *MiddlewareData, request: *jetzig.http.Request) !void {
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 },
);
} else {
try @call(
.always_inline,
middleware.afterView,
.{request},
);
}
}
if (request.state != .after_view) {
request.middleware_rendered = .{
.name = @typeName(middleware),
.action = "afterView",
};
break;
}
}
}
pub fn beforeResponse(
middleware_data: *MiddlewareData,
request: *jetzig.http.Request,

View File

@ -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,

View File

@ -0,0 +1,38 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
const InertiaMiddleware = @This();
pub fn afterView(request: *jetzig.http.Request) !void {
if (request.headers.get("HX-Target")) |target| {
try request.server.logger.DEBUG(
"[middleware-htmx] htmx request detected, disabling layout. (#{s})",
.{target},
);
request.setLayout(null);
} else {
const template_context = jetzig.TemplateContext{ .request = request };
const template = jetzig.zmpl.findPrefixed("jetzig", "inertia").?;
_ = request.renderContent(.ok, try template.render(
request.response_data,
jetzig.TemplateContext,
template_context,
.{},
));
}
}
pub fn beforeResponse(request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
switch (response.status_code) {
.moved_permanently, .found => {},
else => return,
}
if (request.headers.get("HX-Request") == null) return;
if (response.headers.get("Location")) |location| {
response.status_code = .ok;
request.response_data.reset();
try response.headers.append("HX-Redirect", location);
}
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>My app</title>
<link href="/css/app.css" rel="stylesheet">
<script src="/js/app.js" defer></script>
</head>
<body>
<div id="app" data-page='{"component":"Event","props": {{zmpl.toJson()}},"url":"/events/80","version":"c32b8e4965f418ad16eaebba1d4e960f"}'></div>
</body>
</html>

View File

@ -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;