Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
},
);

Expand Down
4 changes: 2 additions & 2 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions demo/src/app/views/inertia/_head.zmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<title>My Inertia App</title>
7 changes: 4 additions & 3 deletions demo/src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions src/compile_static_routes.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -174,14 +174,15 @@ fn renderZmplTemplate(
view.data,
jetzig.TemplateContext,
.{},
&.{},
.{ .layout = layout },
);
} else {
std.debug.print("Unknown layout: {s}\n", .{layout_name});
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;
}
Expand Down
14 changes: 14 additions & 0 deletions src/jetzig/TemplateContext.zig
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
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()
else
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();
Expand All @@ -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;
}
18 changes: 17 additions & 1 deletion src/jetzig/http/Request.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
};
}

Expand Down Expand Up @@ -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}),
Expand Down
61 changes: 46 additions & 15 deletions src/jetzig/http/Server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -175,16 +180,19 @@ 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();
return true;
} 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;

Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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/
Expand All @@ -431,6 +458,7 @@ fn renderTemplateWithLayout(
view.data,
jetzig.TemplateContext,
template_context,
&.{},
.{ .layout = layout },
);
} else {
Expand All @@ -439,13 +467,15 @@ fn renderTemplateWithLayout(
view.data,
jetzig.TemplateContext,
template_context,
&.{},
.{},
);
}
} else return try template.render(
view.data,
jetzig.TemplateContext,
template_context,
&.{},
.{},
);
}
Expand Down Expand Up @@ -608,7 +638,8 @@ fn renderErrorView(
.content = try template.render(
request.response_data,
jetzig.TemplateContext,
.{ .request = request },
.{ .request = request, .route = route.* },
&.{},
.{},
),
};
Expand Down
32 changes: 32 additions & 0 deletions src/jetzig/http/middleware.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/jetzig/mail/Job.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions src/jetzig/middleware.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/jetzig/middleware/InertiaMiddleware.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");

// WIP

const InertiaMiddleware = @This();
13 changes: 13 additions & 0 deletions src/jetzig/templates/inertia.zmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
@partial views:inertia/head
</head>
<body>
<div
id="app"
data-page='{"component":"{{jetzig_view}}","props":{{zmpl.toJson()}},"url":"{{context.path()}}","version":"c32b8e4965f418ad16eaebba1d4e960f"}'
>
</div>
</body>
</html>
1 change: 1 addition & 0 deletions src/jetzig/views/View.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading