mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 14:06:08 +00:00
Merge pull request #76 from jetzig-framework/template-inheritance-and-dynamic-templates
Update Zmpl for template inheritance
This commit is contained in:
commit
4b93069daf
@ -7,8 +7,8 @@
|
||||
.hash = "12203b56c2e17a2fd62ea3d3d9be466f43921a3aef88b381cf58f41251815205fdb5",
|
||||
},
|
||||
.zmpl = .{
|
||||
.url = "https://github.com/jetzig-framework/zmpl/archive/918b74353c5680b54b435dcacd67268b23ffa129.tar.gz",
|
||||
.hash = "1220bf01968a822771a33bb51e37ff8ee13d21437f95ec55150ffa7c6a9fb1dfcbc5",
|
||||
.url = "https://github.com/jetzig-framework/zmpl/archive/aa4d8ad5b63976d96e3b2c187f5b0b2c693905a1.tar.gz",
|
||||
.hash = "1220b6dfedaf6ad2b464ebcec2aafdc01ba593a07a53885033d56c50a5a04334b517",
|
||||
},
|
||||
.jetkv = .{
|
||||
.url = "https://github.com/jetzig-framework/jetkv/archive/78bcdcc6b0cbd3ca808685c64554a15701f13250.tar.gz",
|
||||
|
@ -24,7 +24,6 @@ pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
const params = try request.params();
|
||||
try root.put("param", params.get("foo").?);
|
||||
|
||||
std.debug.print("{}\n", .{params});
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,9 @@ pub const View = views.View;
|
||||
/// A route definition. Generated at build type by `Routes.zig`.
|
||||
pub const Route = views.Route;
|
||||
|
||||
const root = @import("root");
|
||||
/// A middleware route definition. Allows middleware to define custom routes in order to serve
|
||||
/// content.
|
||||
pub const MiddlewareRoute = middleware.MiddlewareRoute;
|
||||
|
||||
/// An asynchronous job that runs outside of the request/response flow. Create via `Request.job`
|
||||
/// and set params with `Job.put`, then call `Job.schedule()` to add to the
|
||||
@ -61,6 +63,8 @@ pub const MailerDefinition = mail.MailerDefinition;
|
||||
/// `ERROR`, etc.). Note that all log functions are CAPITALIZED.
|
||||
pub const Logger = loggers.Logger;
|
||||
|
||||
const root = @import("root");
|
||||
|
||||
/// Global configuration. Override these values by defining in `src/main.zig` with:
|
||||
/// ```zig
|
||||
/// pub const jetzig_options = struct {
|
||||
|
@ -181,15 +181,25 @@ pub fn route(
|
||||
.method = method,
|
||||
.view_name = self.allocator.dupe(u8, &view_name) catch @panic("OOM"),
|
||||
.uri_path = path,
|
||||
.view = comptime switch (isIncludeId(path)) {
|
||||
true => .{ .custom = .{ .with_id = viewFn } },
|
||||
false => .{ .custom = .{ .without_id = viewFn } },
|
||||
.layout = if (@hasDecl(module, "layout")) module.layout else null,
|
||||
.view = comptime switch (viewType(path)) {
|
||||
.with_id => .{ .custom = .{ .with_id = viewFn } },
|
||||
.with_args => .{ .custom = .{ .with_args = viewFn } },
|
||||
.without_id => .{ .custom = .{ .without_id = viewFn } },
|
||||
},
|
||||
.template = self.allocator.dupe(u8, &template) catch @panic("OOM"),
|
||||
.json_params = &.{},
|
||||
}) catch @panic("OOM");
|
||||
}
|
||||
|
||||
inline fn isIncludeId(comptime path: []const u8) bool {
|
||||
return std.mem.containsAtLeast(u8, path, 1, "/:");
|
||||
inline fn viewType(path: []const u8) enum { with_id, without_id, with_args } {
|
||||
var it = std.mem.tokenizeSequence(u8, path, "/");
|
||||
while (it.next()) |segment| {
|
||||
if (std.mem.startsWith(u8, segment, ":")) {
|
||||
if (std.mem.endsWith(u8, segment, "*")) return .with_args;
|
||||
return .with_id;
|
||||
}
|
||||
}
|
||||
|
||||
return .without_id;
|
||||
}
|
||||
|
@ -54,6 +54,30 @@ pub fn resourceId(self: Self, route: jetzig.views.Route) []const u8 {
|
||||
return self.resource_id;
|
||||
}
|
||||
|
||||
pub fn resourceArgs(self: Self, route: jetzig.views.Route, allocator: std.mem.Allocator) ![]const []const u8 {
|
||||
var args = std.ArrayList([]const u8).init(allocator);
|
||||
var route_uri_path_it = std.mem.splitScalar(u8, route.uri_path, '/');
|
||||
var path_it = std.mem.splitScalar(u8, self.base_path, '/');
|
||||
|
||||
var matched = false;
|
||||
|
||||
while (path_it.next()) |path_segment| {
|
||||
const route_uri_path_segment = route_uri_path_it.next();
|
||||
if (!matched and
|
||||
route_uri_path_segment != null and
|
||||
std.mem.startsWith(u8, route_uri_path_segment.?, ":") and
|
||||
std.mem.endsWith(u8, route_uri_path_segment.?, "*"))
|
||||
{
|
||||
matched = true;
|
||||
}
|
||||
if (matched) {
|
||||
try args.append(path_segment);
|
||||
}
|
||||
}
|
||||
|
||||
return try args.toOwnedSlice();
|
||||
}
|
||||
|
||||
// Extract `"/foo/bar/baz"` from:
|
||||
// * `"/foo/bar/baz"`
|
||||
// * `"/foo/bar/baz.html"`
|
||||
|
@ -27,6 +27,7 @@ _cookies: ?*jetzig.http.Cookies = null,
|
||||
_session: ?*jetzig.http.Session = null,
|
||||
body: []const u8 = undefined,
|
||||
processed: bool = false,
|
||||
dynamic_assigned_template: ?[]const u8 = null,
|
||||
layout: ?[]const u8 = null,
|
||||
layout_disabled: bool = false,
|
||||
rendered: bool = false,
|
||||
@ -513,6 +514,21 @@ pub fn formatStatus(self: *const Request, status_code: jetzig.http.StatusCode) !
|
||||
};
|
||||
}
|
||||
|
||||
/// Override default template name for a matched route.
|
||||
pub fn setTemplate(self: *Request, name: []const u8) void {
|
||||
self.dynamic_assigned_template = name;
|
||||
}
|
||||
|
||||
pub fn joinPaths(self: *const Request, paths: []const []const []const u8) ![]const u8 {
|
||||
var buf = std.ArrayList([]const u8).init(self.allocator);
|
||||
defer buf.deinit();
|
||||
|
||||
for (paths) |subpaths| {
|
||||
for (subpaths) |path| try buf.append(path);
|
||||
}
|
||||
return try std.mem.join(self.allocator, "/", buf.items);
|
||||
}
|
||||
|
||||
pub fn setResponse(
|
||||
self: *Request,
|
||||
rendered_view: jetzig.http.Server.RenderedView,
|
||||
|
@ -166,6 +166,17 @@ fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchMiddlewareRoute(request)) |route| {
|
||||
if (route.content) |content| {
|
||||
const rendered: RenderedView = .{
|
||||
.view = .{ .data = request.response_data, .status_code = route.status },
|
||||
.content = content,
|
||||
};
|
||||
request.setResponse(rendered, .{ .content_type = route.content_type });
|
||||
return;
|
||||
} else unreachable; // In future a MiddlewareRoute might provide a render function etc.
|
||||
}
|
||||
|
||||
const route = self.matchCustomRoute(request) orelse try self.matchRoute(request, false);
|
||||
|
||||
switch (request.requestFormat()) {
|
||||
@ -196,19 +207,21 @@ fn renderHTML(
|
||||
};
|
||||
return request.setResponse(rendered, .{});
|
||||
} else {
|
||||
// Try rendering without a template to see if we get a redirect.
|
||||
// Try rendering without a template to see if we get a redirect or a template
|
||||
// assigned in a view.
|
||||
const rendered = self.renderView(matched_route, request, null) catch |err| {
|
||||
if (isUnhandledError(err)) return err;
|
||||
const rendered_error = try self.renderInternalServerError(request, err);
|
||||
return request.setResponse(rendered_error, .{});
|
||||
};
|
||||
|
||||
return if (request.redirected)
|
||||
return if (request.redirected or request.dynamic_assigned_template != null)
|
||||
request.setResponse(rendered, .{})
|
||||
else
|
||||
request.setResponse(try self.renderNotFound(request), .{});
|
||||
}
|
||||
} else {
|
||||
// If no matching route found, try to render a Markdown file in views directory.
|
||||
if (try self.renderMarkdown(request)) |rendered| {
|
||||
return request.setResponse(rendered, .{});
|
||||
} else {
|
||||
@ -259,7 +272,7 @@ fn renderView(
|
||||
self: *Server,
|
||||
route: jetzig.views.Route,
|
||||
request: *jetzig.http.Request,
|
||||
template: ?zmpl.Template,
|
||||
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
|
||||
@ -271,6 +284,11 @@ fn renderView(
|
||||
return try self.renderInternalServerError(request, err);
|
||||
};
|
||||
|
||||
const template: ?zmpl.Template = if (request.dynamic_assigned_template) |request_template|
|
||||
zmpl.findPrefixed("views", request_template) orelse maybe_template
|
||||
else
|
||||
maybe_template;
|
||||
|
||||
if (request.rendered_multiple) return error.JetzigMultipleRenderError;
|
||||
|
||||
if (request.rendered_view) |rendered_view| {
|
||||
@ -507,6 +525,20 @@ fn matchCustomRoute(self: Server, request: *const jetzig.http.Request) ?jetzig.v
|
||||
return null;
|
||||
}
|
||||
|
||||
fn matchMiddlewareRoute(request: *const jetzig.http.Request) ?jetzig.middleware.MiddlewareRoute {
|
||||
const middlewares = jetzig.config.get([]const type, "middleware");
|
||||
|
||||
inline for (middlewares) |middleware| {
|
||||
if (@hasDecl(middleware, "routes")) {
|
||||
inline for (middleware.routes) |route| {
|
||||
if (route.match(request)) return route;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn matchRoute(self: *Server, request: *jetzig.http.Request, static: bool) !?jetzig.views.Route {
|
||||
for (self.routes) |route| {
|
||||
// .index routes always take precedence.
|
||||
|
@ -1 +1,35 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("../jetzig.zig");
|
||||
|
||||
pub const HtmxMiddleware = @import("middleware/HtmxMiddleware.zig");
|
||||
|
||||
const RouteOptions = struct {
|
||||
content: ?[]const u8 = null,
|
||||
content_type: []const u8 = "text/html",
|
||||
status: jetzig.http.StatusCode = .ok,
|
||||
};
|
||||
|
||||
pub const MiddlewareRoute = struct {
|
||||
method: jetzig.http.Request.Method,
|
||||
path: []const u8,
|
||||
content: ?[]const u8,
|
||||
content_type: []const u8,
|
||||
status: jetzig.http.StatusCode,
|
||||
|
||||
pub fn match(self: MiddlewareRoute, request: *const jetzig.http.Request) bool {
|
||||
if (self.method != request.method) return false;
|
||||
if (!std.mem.eql(u8, self.path, request.path.file_path)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn route(method: jetzig.http.Request.Method, path: []const u8, options: RouteOptions) MiddlewareRoute {
|
||||
return .{
|
||||
.method = method,
|
||||
.path = path,
|
||||
.content = options.content,
|
||||
.content_type = options.content_type,
|
||||
.status = options.status,
|
||||
};
|
||||
}
|
||||
|
@ -1,20 +1,13 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Initialize htmx middleware.
|
||||
pub fn init(request: *jetzig.http.Request) !*Self {
|
||||
const middleware = try request.allocator.create(Self);
|
||||
return middleware;
|
||||
}
|
||||
const HtmxMiddleware = @This();
|
||||
|
||||
/// Detects the `HX-Request` header and, if present, disables the default layout for the current
|
||||
/// request. This allows a view to specify a layout that will render the full page when the
|
||||
/// request doesn't come via htmx and, when the request does come from htmx, only return the
|
||||
/// content rendered directly by the view function.
|
||||
pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
|
||||
_ = self;
|
||||
pub fn afterRequest(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})",
|
||||
@ -26,8 +19,7 @@ pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
|
||||
|
||||
/// If a redirect was issued during request processing, reset any response data, set response
|
||||
/// status to `200 OK` and replace the `Location` header with a `HX-Redirect` header.
|
||||
pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
|
||||
_ = self;
|
||||
pub fn beforeResponse(request: *const jetzig.http.Request, response: *jetzig.http.Response) !void {
|
||||
if (response.status_code != .moved_permanently and response.status_code != .found) return;
|
||||
if (request.headers.get("HX-Request") == null) return;
|
||||
|
||||
@ -37,8 +29,3 @@ pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jet
|
||||
try response.headers.append("HX-Redirect", location);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clean up the allocated htmx middleware.
|
||||
pub fn deinit(self: *Self, request: *jetzig.http.Request) void {
|
||||
request.allocator.destroy(self);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ pub const RenderStaticFn = *const fn (Route, *jetzig.http.StaticRequest) anyerro
|
||||
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 DynamicViewType = union(Action) {
|
||||
@ -36,6 +37,7 @@ pub const StaticViewType = union(Action) {
|
||||
pub const CustomViewType = union(enum) {
|
||||
with_id: ViewWithId,
|
||||
without_id: ViewWithoutId,
|
||||
with_args: ViewWithArgs,
|
||||
};
|
||||
|
||||
pub const ViewType = union(enum) {
|
||||
@ -87,7 +89,10 @@ pub fn match(self: Route, request: *const jetzig.http.Request) bool {
|
||||
|
||||
while (uri_path_it.next()) |expected_segment| {
|
||||
const actual_segment = request_path_it.next() orelse return false;
|
||||
if (std.mem.startsWith(u8, expected_segment, ":")) continue;
|
||||
if (std.mem.startsWith(u8, expected_segment, ":")) {
|
||||
if (std.mem.endsWith(u8, expected_segment, "*")) return true;
|
||||
continue;
|
||||
}
|
||||
if (!std.mem.eql(u8, expected_segment, actual_segment)) return false;
|
||||
}
|
||||
|
||||
@ -100,6 +105,11 @@ fn renderFn(self: Route, request: *jetzig.http.Request) anyerror!jetzig.views.Vi
|
||||
.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
|
||||
|
Loading…
x
Reference in New Issue
Block a user