mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 22:16:08 +00:00
Merge pull request #28 from jetzig-framework/htmx-middleware
Htmx middleware
This commit is contained in:
commit
adba4756c5
@ -16,8 +16,8 @@ const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
/// Define any custom data fields you want to store here. Assigning to these fields in the `init`
|
||||
/// function allows you to access them in the `beforeRequest` and `afterRequest` functions, where
|
||||
/// they can also be modified.
|
||||
/// function allows you to access them in various middleware callbacks defined below, where they
|
||||
/// can also be modified.
|
||||
my_custom_value: []const u8,
|
||||
|
||||
const Self = @This();
|
||||
@ -29,25 +29,34 @@ pub fn init(request: *jetzig.http.Request) !*Self {
|
||||
return middleware;
|
||||
}
|
||||
|
||||
/// Invoked immediately after the request head has been processed, before relevant view function
|
||||
/// is processed. This gives you access to request headers but not the request body.
|
||||
pub fn beforeRequest(self: *Self, request: *jetzig.http.Request) !void {
|
||||
request.server.logger.debug("[DemoMiddleware] my_custom_value: {s}", .{self.my_custom_value});
|
||||
/// Invoked immediately after the request is received but before it has started processing.
|
||||
/// Any calls to `request.render` or `request.redirect` will prevent further processing of the
|
||||
/// request, including any other middleware in the chain.
|
||||
pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
|
||||
request.server.logger.debug("[DemoMiddleware:afterRequest] my_custom_value: {s}", .{self.my_custom_value});
|
||||
self.my_custom_value = @tagName(request.method);
|
||||
}
|
||||
|
||||
/// Invoked immediately after the request has finished responding. Provides full access to the
|
||||
/// response as well as the request.
|
||||
pub fn afterRequest(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
|
||||
/// Invoked immediately before the response renders to the client.
|
||||
/// The response can be modified here if needed.
|
||||
pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
|
||||
request.server.logger.debug(
|
||||
"[DemoMiddleware] my_custom_value: {s}, response status: {s}",
|
||||
"[DemoMiddleware:beforeResponse] my_custom_value: {s}, response status: {s}",
|
||||
.{ self.my_custom_value, @tagName(response.status_code) },
|
||||
);
|
||||
}
|
||||
|
||||
/// Invoked after `afterRequest` is called, use this function to do any clean-up.
|
||||
/// Invoked immediately after the response has been finalized and sent to the client.
|
||||
/// Response data can be accessed for logging, but any modifications will have no impact.
|
||||
pub fn afterResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
|
||||
_ = self;
|
||||
_ = response;
|
||||
request.server.logger.debug("[DemoMiddleware:afterResponse] response completed", .{});
|
||||
}
|
||||
|
||||
/// Invoked after `afterResponse` is called. Use this function to do any clean-up.
|
||||
/// Note that `request.allocator` is an arena allocator, so any allocations are automatically
|
||||
/// done before the next request starts processing.
|
||||
/// freed before the next request starts processing.
|
||||
pub fn deinit(self: *Self, request: *jetzig.http.Request) void {
|
||||
request.allocator.destroy(self);
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -1,6 +0,0 @@
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<main>{zmpl.content}</main>
|
||||
</body>
|
||||
</html>
|
@ -1,6 +1,8 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
pub const layout = "application";
|
||||
|
||||
pub fn get(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
var body = try data.object();
|
||||
|
||||
|
12
demo/src/app/views/redirect.zig
Normal file
12
demo/src/app/views/redirect.zig
Normal file
@ -0,0 +1,12 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
_ = data;
|
||||
const params = try request.params();
|
||||
if (params.get("redirect")) |location| {
|
||||
return request.redirect(try location.toString(), .moved_permanently);
|
||||
}
|
||||
|
||||
return request.render(.ok);
|
||||
}
|
3
demo/src/app/views/redirect/index.zmpl
Normal file
3
demo/src/app/views/redirect/index.zmpl
Normal file
@ -0,0 +1,3 @@
|
||||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
@ -2,6 +2,8 @@ const jetzig = @import("jetzig");
|
||||
|
||||
const importedFunction = @import("../lib/example.zig").exampleFunction;
|
||||
|
||||
pub const layout = "application";
|
||||
|
||||
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
var root = try data.object();
|
||||
try root.put("message", data.string("Welcome to Jetzig!"));
|
||||
|
@ -1,26 +1,15 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<div class="text-center pt-10 m-auto">
|
||||
<div><img class="p-3 mx-auto" src="/jetzig.png" /></div>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
// Renders `src/app/views/root/_quotes.zmpl`:
|
||||
<div>{^root/quotes}</div>
|
||||
|
||||
<body>
|
||||
<div class="text-center pt-10 m-auto">
|
||||
<div><img class="p-3 mx-auto" src="/jetzig.png" /></div>
|
||||
<div>
|
||||
<a href="https://github.com/jetzig-framework/zmpl">
|
||||
<img class="p-3 m-3 mx-auto" src="/zmpl.png" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
// Renders `src/app/views/root/_quotes.zmpl`:
|
||||
<div>{^root/quotes}</div>
|
||||
|
||||
<div>
|
||||
<a href="https://github.com/jetzig-framework/zmpl">
|
||||
<img class="p-3 m-3 mx-auto" src="/zmpl.png" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>Take a look at the <span class="font-mono">/demo/src/app/</span> directory to see how this application works.</div>
|
||||
<div>Visit <a class="font-bold text-[#39b54a]" href="https://jetzig.dev/">jetzig.dev</a> to get started.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<div>Take a look at the <span class="font-mono">/demo/src/app/</span> directory to see how this application works.</div>
|
||||
<div>Visit <a class="font-bold text-[#39b54a]" href="https://jetzig.dev/">jetzig.dev</a> to get started.</div>
|
||||
</div>
|
||||
|
@ -4,7 +4,12 @@ pub const jetzig = @import("jetzig");
|
||||
pub const routes = @import("routes").routes;
|
||||
|
||||
pub const jetzig_options = struct {
|
||||
pub const middleware: []const type = &.{@import("app/middleware/DemoMiddleware.zig")};
|
||||
pub const middleware: []const type = &.{
|
||||
// htmx middleware skips layouts when `HX-Target` header is present and issues
|
||||
// `HX-Redirect` instead of a regular HTTP redirect when `request.redirect` is called.
|
||||
jetzig.middleware.HtmxMiddleware,
|
||||
@import("app/middleware/DemoMiddleware.zig"),
|
||||
};
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
|
@ -8,6 +8,8 @@ pub const data = @import("jetzig/data.zig");
|
||||
pub const caches = @import("jetzig/caches.zig");
|
||||
pub const views = @import("jetzig/views.zig");
|
||||
pub const colors = @import("jetzig/colors.zig");
|
||||
pub const middleware = @import("jetzig/middleware.zig");
|
||||
pub const util = @import("jetzig/util.zig");
|
||||
|
||||
/// The primary interface for a Jetzig application. Create an `App` in your application's
|
||||
/// `src/main.zig` and call `start` to launch the application.
|
||||
|
@ -8,6 +8,7 @@ pub const Session = @import("http/Session.zig");
|
||||
pub const Cookies = @import("http/Cookies.zig");
|
||||
pub const Headers = @import("http/Headers.zig");
|
||||
pub const Query = @import("http/Query.zig");
|
||||
pub const Path = @import("http/Path.zig");
|
||||
pub const status_codes = @import("http/status_codes.zig");
|
||||
pub const middleware = @import("http/middleware.zig");
|
||||
pub const mime = @import("http/mime.zig");
|
||||
|
@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
headers: HeadersArray,
|
||||
@ -18,14 +19,10 @@ pub fn deinit(self: *Self) void {
|
||||
self.headers.deinit(self.allocator);
|
||||
}
|
||||
|
||||
// Gets the first value for a given header identified by `name`. Case-insensitive string comparison.
|
||||
// Gets the first value for a given header identified by `name`. Names are case insensitive.
|
||||
pub fn getFirstValue(self: *Self, name: []const u8) ?[]const u8 {
|
||||
headers: for (self.headers.items) |header| {
|
||||
if (name.len != header.name.len) continue;
|
||||
for (name, header.name) |expected, actual| {
|
||||
if (std.ascii.toLower(expected) != std.ascii.toLower(actual)) continue :headers;
|
||||
}
|
||||
return header.value;
|
||||
for (self.headers.items) |header| {
|
||||
if (jetzig.util.equalStringsCaseInsensitive(name, header.name)) return header.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -35,6 +32,20 @@ pub fn append(self: *Self, name: []const u8, value: []const u8) !void {
|
||||
self.headers.appendAssumeCapacity(.{ .name = name, .value = value });
|
||||
}
|
||||
|
||||
/// Removes **all** header entries matching `name`. Names are case-insensitive.
|
||||
pub fn remove(self: *Self, name: []const u8) void {
|
||||
if (self.headers.items.len == 0) return;
|
||||
|
||||
var index: usize = self.headers.items.len;
|
||||
|
||||
while (index > 0) {
|
||||
index -= 1;
|
||||
if (jetzig.util.equalStringsCaseInsensitive(name, self.headers.items[index].name)) {
|
||||
_ = self.headers.orderedRemove(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator which implements `next()` returning each name/value of the stored headers.
|
||||
pub fn iterator(self: *Self) Iterator {
|
||||
return Iterator{ .headers = self.headers };
|
||||
@ -116,6 +127,18 @@ test "iterator" {
|
||||
}
|
||||
}
|
||||
|
||||
test "remove" {
|
||||
const allocator = std.testing.allocator;
|
||||
var headers = Self.init(allocator);
|
||||
defer headers.deinit();
|
||||
try headers.append("foo", "baz");
|
||||
try headers.append("foo", "qux");
|
||||
try headers.append("bar", "quux");
|
||||
headers.remove("Foo"); // Headers are case-insensitive.
|
||||
try std.testing.expect(headers.getFirstValue("foo") == null);
|
||||
try std.testing.expectEqualStrings(headers.getFirstValue("bar").?, "quux");
|
||||
}
|
||||
|
||||
test "stdHeaders" {
|
||||
const allocator = std.testing.allocator;
|
||||
var headers = Self.init(allocator);
|
||||
|
260
src/jetzig/http/Path.zig
Normal file
260
src/jetzig/http/Path.zig
Normal file
@ -0,0 +1,260 @@
|
||||
/// Abstraction of the path component of a URI.
|
||||
/// Provides access to:
|
||||
/// * Unaltered original path
|
||||
/// * Base path (without extension and query string)
|
||||
/// * Directory (parent path from base path)
|
||||
/// * Resource ID (final component of base path)
|
||||
/// * Extension (".json", ".html", etc.)
|
||||
/// * Query (everything after first "?" character)
|
||||
const std = @import("std");
|
||||
|
||||
path: []const u8,
|
||||
base_path: []const u8,
|
||||
directory: []const u8,
|
||||
file_path: []const u8,
|
||||
resource_id: []const u8,
|
||||
extension: ?[]const u8,
|
||||
query: ?[]const u8,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Initialize a new HTTP Path.
|
||||
pub fn init(path: []const u8) Self {
|
||||
const base_path = getBasePath(path);
|
||||
|
||||
return .{
|
||||
.path = path,
|
||||
.base_path = base_path,
|
||||
.directory = getDirectory(base_path),
|
||||
.file_path = getFilePath(path),
|
||||
.resource_id = getResourceId(base_path),
|
||||
.extension = getExtension(path),
|
||||
.query = getQuery(path),
|
||||
};
|
||||
}
|
||||
|
||||
/// No-op - no allocations currently performed.
|
||||
pub fn deinit(self: *Self) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
// Extract `"/foo/bar/baz"` from:
|
||||
// * `"/foo/bar/baz"`
|
||||
// * `"/foo/bar/baz.html"`
|
||||
// * `"/foo/bar/baz.html?qux=quux&corge=grault"`
|
||||
fn getBasePath(path: []const u8) []const u8 {
|
||||
if (std.mem.indexOfScalar(u8, path, '?')) |query_index| {
|
||||
if (std.mem.lastIndexOfScalar(u8, path[0..query_index], '.')) |extension_index| {
|
||||
return path[0..extension_index];
|
||||
} else {
|
||||
return path[0..query_index];
|
||||
}
|
||||
} else if (std.mem.lastIndexOfScalar(u8, path, '.')) |extension_index| {
|
||||
return path[0..extension_index];
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract `"/foo/bar"` from:
|
||||
// * `"/foo/bar/baz"`
|
||||
// Special case:
|
||||
// * `"/"` returns `"/"`
|
||||
pub fn getDirectory(base_path: []const u8) []const u8 {
|
||||
if (std.mem.eql(u8, base_path, "/")) return "/";
|
||||
|
||||
if (std.mem.lastIndexOfScalar(u8, base_path, '/')) |index| {
|
||||
return base_path[0..index];
|
||||
} else {
|
||||
return "/";
|
||||
}
|
||||
}
|
||||
|
||||
// Extract `"/foo/bar/baz.html"` from:
|
||||
// * `"/foo/bar/baz.html"`
|
||||
// * `"/foo/bar/baz.html?qux=quux&corge=grault"`
|
||||
// Special case:
|
||||
// * `"/foo/bar/baz"` returns `"/foo/bar/baz"`
|
||||
fn getFilePath(path: []const u8) []const u8 {
|
||||
if (std.mem.indexOfScalar(u8, path, '?')) |query_index| {
|
||||
return path[0..query_index];
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract `"baz"` from:
|
||||
// * `"/foo/bar/baz"`
|
||||
// * `"/baz"`
|
||||
fn getResourceId(base_path: []const u8) []const u8 {
|
||||
var it = std.mem.splitBackwardsScalar(u8, base_path, '/');
|
||||
while (it.next()) |segment| return segment;
|
||||
return base_path;
|
||||
}
|
||||
|
||||
// Extract `".html"` from:
|
||||
// * `"/foo/bar/baz.html"`
|
||||
// * `"/foo/bar/baz.html?qux=quux&corge=grault"`
|
||||
fn getExtension(path: []const u8) ?[]const u8 {
|
||||
if (std.mem.indexOfScalar(u8, path, '?')) |query_index| {
|
||||
if (std.mem.lastIndexOfScalar(u8, path[0..query_index], '.')) |extension_index| {
|
||||
return path[extension_index..query_index];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (std.mem.lastIndexOfScalar(u8, path, '.')) |extension_index| {
|
||||
return path[extension_index..];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract `"qux=quux&corge=grault"` from:
|
||||
// * `"/foo/bar/baz.html?qux=quux&corge=grault"`
|
||||
// * `"/foo/bar/baz?qux=quux&corge=grault"`
|
||||
fn getQuery(path: []const u8) ?[]const u8 {
|
||||
if (std.mem.indexOfScalar(u8, path, '?')) |query_index| {
|
||||
if (path.len - 1 <= query_index) return null;
|
||||
return path[query_index + 1 ..];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
test ".base_path (with extension, with query)" {
|
||||
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault");
|
||||
|
||||
try std.testing.expectEqualStrings("/foo/bar/baz", path.base_path);
|
||||
}
|
||||
|
||||
test ".base_path (with extension, without query)" {
|
||||
const path = Self.init("/foo/bar/baz.html");
|
||||
|
||||
try std.testing.expectEqualStrings("/foo/bar/baz", path.base_path);
|
||||
}
|
||||
|
||||
test ".base_path (without extension, without query)" {
|
||||
const path = Self.init("/foo/bar/baz");
|
||||
|
||||
try std.testing.expectEqualStrings("/foo/bar/baz", path.base_path);
|
||||
}
|
||||
|
||||
test ".directory (with extension, with query)" {
|
||||
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault");
|
||||
|
||||
try std.testing.expectEqualStrings("/foo/bar", path.directory);
|
||||
}
|
||||
|
||||
test ".directory (with extension, without query)" {
|
||||
const path = Self.init("/foo/bar/baz.html");
|
||||
|
||||
try std.testing.expectEqualStrings("/foo/bar", path.directory);
|
||||
}
|
||||
|
||||
test ".directory (without extension, without query)" {
|
||||
const path = Self.init("/foo/bar/baz");
|
||||
|
||||
try std.testing.expectEqualStrings("/foo/bar", path.directory);
|
||||
}
|
||||
|
||||
test ".directory (without extension, without query, root path)" {
|
||||
const path = Self.init("/");
|
||||
|
||||
try std.testing.expectEqualStrings("/", path.directory);
|
||||
}
|
||||
|
||||
test ".resource_id (with extension, with query)" {
|
||||
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault");
|
||||
|
||||
try std.testing.expectEqualStrings("baz", path.resource_id);
|
||||
}
|
||||
|
||||
test ".resource_id (with extension, without query)" {
|
||||
const path = Self.init("/foo/bar/baz.html");
|
||||
|
||||
try std.testing.expectEqualStrings("baz", path.resource_id);
|
||||
}
|
||||
|
||||
test ".resource_id (without extension, without query)" {
|
||||
const path = Self.init("/foo/bar/baz");
|
||||
|
||||
try std.testing.expectEqualStrings("baz", path.resource_id);
|
||||
}
|
||||
|
||||
test ".resource_id (without extension, without query, without base path)" {
|
||||
const path = Self.init("/baz");
|
||||
|
||||
try std.testing.expectEqualStrings("baz", path.resource_id);
|
||||
}
|
||||
|
||||
test ".extension (with query)" {
|
||||
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault");
|
||||
|
||||
try std.testing.expectEqualStrings(".html", path.extension.?);
|
||||
}
|
||||
|
||||
test ".extension (without query)" {
|
||||
const path = Self.init("/foo/bar/baz.html");
|
||||
|
||||
try std.testing.expectEqualStrings(".html", path.extension.?);
|
||||
}
|
||||
|
||||
test ".extension (without extension)" {
|
||||
const path = Self.init("/foo/bar/baz");
|
||||
|
||||
try std.testing.expect(path.extension == null);
|
||||
}
|
||||
|
||||
test ".query (with extension, with query)" {
|
||||
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault");
|
||||
|
||||
try std.testing.expectEqualStrings(path.query.?, "qux=quux&corge=grault");
|
||||
}
|
||||
|
||||
test ".query (without extension, with query)" {
|
||||
const path = Self.init("/foo/bar/baz?qux=quux&corge=grault");
|
||||
|
||||
try std.testing.expectEqualStrings(path.query.?, "qux=quux&corge=grault");
|
||||
}
|
||||
|
||||
test ".query (with extension, without query)" {
|
||||
const path = Self.init("/foo/bar/baz.json");
|
||||
|
||||
try std.testing.expect(path.query == null);
|
||||
}
|
||||
|
||||
test ".query (without extension, without query)" {
|
||||
const path = Self.init("/foo/bar/baz");
|
||||
|
||||
try std.testing.expect(path.query == null);
|
||||
}
|
||||
|
||||
test ".query (with empty query)" {
|
||||
const path = Self.init("/foo/bar/baz?");
|
||||
|
||||
try std.testing.expect(path.query == null);
|
||||
}
|
||||
|
||||
test ".file_path (with extension, with query)" {
|
||||
const path = Self.init("/foo/bar/baz.json?qux=quux&corge=grault");
|
||||
|
||||
try std.testing.expectEqualStrings("/foo/bar/baz.json", path.file_path);
|
||||
}
|
||||
|
||||
test ".file_path (with extension, without query)" {
|
||||
const path = Self.init("/foo/bar/baz.json");
|
||||
|
||||
try std.testing.expectEqualStrings("/foo/bar/baz.json", path.file_path);
|
||||
}
|
||||
|
||||
test ".file_path (without extension, without query)" {
|
||||
const path = Self.init("/foo/bar/baz");
|
||||
|
||||
try std.testing.expectEqualStrings("/foo/bar/baz", path.file_path);
|
||||
}
|
||||
|
||||
test ".file_path (without extension, with query)" {
|
||||
const path = Self.init("/foo/bar/baz?qux=quux&corge=grault");
|
||||
|
||||
try std.testing.expectEqualStrings("/foo/bar/baz", path.file_path);
|
||||
}
|
@ -10,10 +10,9 @@ pub const Modifier = enum { edit, new };
|
||||
pub const Format = enum { HTML, JSON, UNKNOWN };
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
path: []const u8,
|
||||
path: jetzig.http.Path,
|
||||
method: Method,
|
||||
headers: jetzig.http.Headers,
|
||||
segments: std.ArrayList([]const u8),
|
||||
server: *jetzig.http.Server,
|
||||
std_http_request: std.http.Server.Request,
|
||||
response: *jetzig.http.Response,
|
||||
@ -25,6 +24,12 @@ cookies: *jetzig.http.Cookies = undefined,
|
||||
session: *jetzig.http.Session = undefined,
|
||||
body: []const u8 = undefined,
|
||||
processed: bool = false,
|
||||
layout: ?[]const u8 = null,
|
||||
layout_disabled: bool = false,
|
||||
rendered: bool = false,
|
||||
redirected: bool = false,
|
||||
rendered_multiple: bool = false,
|
||||
rendered_view: ?jetzig.views.View = null,
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
@ -45,23 +50,6 @@ pub fn init(
|
||||
_ => return error.JetzigUnsupportedHttpMethod,
|
||||
};
|
||||
|
||||
// TODO: Replace all this with a `Path` type which exposes all components of the path in a
|
||||
// sensible way:
|
||||
// * Array of segments: "/foo/bar/baz" => .{ "foo", "bar", "baz" }
|
||||
// * Resource ID: "/foo/bar/baz/1" => "1"
|
||||
// * Extension: "/foo/bar/baz/1.json" => ".json"
|
||||
// * Query params: "/foo/bar/baz?foo=bar&baz=qux" => .{ .foo = "bar", .baz => "qux" }
|
||||
// * Anything else ?
|
||||
var it = std.mem.splitScalar(u8, std_http_request.head.target, '/');
|
||||
var segments = std.ArrayList([]const u8).init(allocator);
|
||||
while (it.next()) |segment| {
|
||||
if (std.mem.indexOfScalar(u8, segment, '?')) |query_index| {
|
||||
try segments.append(segment[0..query_index]);
|
||||
} else {
|
||||
try segments.append(segment);
|
||||
}
|
||||
}
|
||||
|
||||
const response_data = try allocator.create(jetzig.data.Data);
|
||||
response_data.* = jetzig.data.Data.init(allocator);
|
||||
|
||||
@ -72,11 +60,10 @@ pub fn init(
|
||||
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.path = std_http_request.head.target,
|
||||
.path = jetzig.http.Path.init(std_http_request.head.target),
|
||||
.method = method,
|
||||
.headers = jetzig.http.Headers.init(allocator),
|
||||
.server = server,
|
||||
.segments = segments,
|
||||
.response = response,
|
||||
.response_data = response_data,
|
||||
.query_data = query_data,
|
||||
@ -87,7 +74,6 @@ pub fn init(
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
// self.session.deinit();
|
||||
self.segments.deinit();
|
||||
self.allocator.destroy(self.cookies);
|
||||
self.allocator.destroy(self.session);
|
||||
if (self.processed) self.allocator.free(self.body);
|
||||
@ -142,14 +128,63 @@ pub fn respond(self: *Self) !void {
|
||||
|
||||
try self.std_http_request.respond(
|
||||
self.response.content,
|
||||
.{ .keep_alive = false, .extra_headers = std_response_headers.items },
|
||||
.{
|
||||
.keep_alive = false,
|
||||
.status = switch (self.response.status_code) {
|
||||
inline else => |tag| @field(std.http.Status, @tagName(tag)),
|
||||
},
|
||||
.extra_headers = std_response_headers.items,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Render a response. This function can only be called once per request (repeat calls will
|
||||
/// trigger an error).
|
||||
pub fn render(self: *Self, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
|
||||
return .{ .data = self.response_data, .status_code = status_code };
|
||||
if (self.rendered) self.rendered_multiple = true;
|
||||
|
||||
self.rendered = true;
|
||||
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
|
||||
return self.rendered_view.?;
|
||||
}
|
||||
|
||||
/// Issue a redirect to a new location.
|
||||
/// ```zig
|
||||
/// return request.redirect("https://www.example.com/", .moved_permanently);
|
||||
/// ```
|
||||
/// ```zig
|
||||
/// return request.redirect("https://www.example.com/", .found);
|
||||
/// ```
|
||||
/// The second argument must be `moved_permanently` or `found`.
|
||||
pub fn redirect(
|
||||
self: *Self,
|
||||
location: []const u8,
|
||||
redirect_status: enum { moved_permanently, found },
|
||||
) jetzig.views.View {
|
||||
if (self.rendered) self.rendered_multiple = true;
|
||||
|
||||
self.rendered = true;
|
||||
self.redirected = true;
|
||||
|
||||
const status_code = switch (redirect_status) {
|
||||
.moved_permanently => jetzig.http.status_codes.StatusCode.moved_permanently,
|
||||
.found => jetzig.http.status_codes.StatusCode.found,
|
||||
};
|
||||
|
||||
self.response_data.reset();
|
||||
|
||||
self.response.headers.remove("Location");
|
||||
self.response.headers.append("Location", location) catch @panic("OOM");
|
||||
|
||||
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
|
||||
return self.rendered_view.?;
|
||||
}
|
||||
|
||||
/// Infer the current format (JSON or HTML) from the request in this order:
|
||||
/// * Extension (path ends in `.json` or `.html`)
|
||||
/// * `Accept` header (`application/json` or `text/html`)
|
||||
/// * `Content-Type` header (`application/json` or `text/html`)
|
||||
/// * Fall back to default: HTML
|
||||
pub fn requestFormat(self: *Self) jetzig.http.Request.Format {
|
||||
return self.extensionFormat() orelse
|
||||
self.acceptHeaderFormat() orelse
|
||||
@ -157,11 +192,34 @@ pub fn requestFormat(self: *Self) jetzig.http.Request.Format {
|
||||
.UNKNOWN;
|
||||
}
|
||||
|
||||
/// Set the layout for the current request/response. Use this to override a `pub const layout`
|
||||
/// declaration in a view, either in middleware or in a view function itself.
|
||||
pub fn setLayout(self: *Self, layout: ?[]const u8) void {
|
||||
if (layout) |layout_name| {
|
||||
self.layout = layout_name;
|
||||
self.layout_disabled = false;
|
||||
} else {
|
||||
self.layout_disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive a layout name from the current request if defined, otherwise from the route (if
|
||||
/// defined).
|
||||
pub fn getLayout(self: *Self, route: *jetzig.views.Route) ?[]const u8 {
|
||||
if (self.layout_disabled) return null;
|
||||
if (self.layout) |capture| return capture;
|
||||
if (route.layout) |capture| return capture;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Shortcut for `request.headers.getFirstValue`. Returns the first matching value for a given
|
||||
/// header name or `null` if not found. Header names are case-insensitive.
|
||||
pub fn getHeader(self: *Self, key: []const u8) ?[]const u8 {
|
||||
return self.headers.getFirstValue(key);
|
||||
}
|
||||
|
||||
/// Provides a `Value` representing request parameters. Parameters are normalized, meaning that
|
||||
/// Return a `Value` representing request parameters. Parameters are normalized, meaning that
|
||||
/// both the JSON request body and query parameters are accessed via the same interface.
|
||||
/// Note that query parameters are supported for JSON requests if no request body is present,
|
||||
/// otherwise the parsed JSON request body will take precedence and query parameters will be
|
||||
@ -197,13 +255,10 @@ fn queryParams(self: *Self) !*jetzig.data.Value {
|
||||
}
|
||||
|
||||
fn parseQueryString(self: *Self) !bool {
|
||||
const delimiter_index = std.mem.indexOfScalar(u8, self.path, '?');
|
||||
if (delimiter_index) |index| {
|
||||
if (self.path.len - 1 < index + 1) return false;
|
||||
|
||||
if (self.path.query) |query| {
|
||||
self.query.* = jetzig.http.Query.init(
|
||||
self.allocator,
|
||||
self.path[index + 1 ..],
|
||||
query,
|
||||
self.query_data,
|
||||
);
|
||||
try self.query.parse();
|
||||
@ -214,7 +269,7 @@ fn parseQueryString(self: *Self) !bool {
|
||||
}
|
||||
|
||||
fn extensionFormat(self: *Self) ?jetzig.http.Request.Format {
|
||||
const extension = std.fs.path.extension(self.path);
|
||||
const extension = self.path.extension orelse return null;
|
||||
|
||||
if (std.mem.eql(u8, extension, ".html")) {
|
||||
return .HTML;
|
||||
@ -267,41 +322,6 @@ pub fn fmtMethod(self: *Self) []const u8 {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn resourceModifier(self: *Self) ?Modifier {
|
||||
const basename = std.fs.path.basename(self.segments.items[self.segments.items.len - 1]);
|
||||
const extension = std.fs.path.extension(basename);
|
||||
const resource = basename[0 .. basename.len - extension.len];
|
||||
if (std.mem.eql(u8, resource, "edit")) return .edit;
|
||||
if (std.mem.eql(u8, resource, "new")) return .new;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn resourceName(self: *Self) []const u8 {
|
||||
if (self.segments.items.len == 0) return "default"; // Should never happen ?
|
||||
|
||||
const basename = std.fs.path.basename(self.segments.items[self.segments.items.len - 1]);
|
||||
if (std.mem.indexOfScalar(u8, basename, '?')) |index| {
|
||||
return basename[0..index];
|
||||
}
|
||||
const extension = std.fs.path.extension(basename);
|
||||
return basename[0 .. basename.len - extension.len];
|
||||
}
|
||||
|
||||
pub fn resourcePath(self: *Self) ![]const u8 {
|
||||
const path = try std.fs.path.join(
|
||||
self.allocator,
|
||||
self.segments.items[0 .. self.segments.items.len - 1],
|
||||
);
|
||||
defer self.allocator.free(path);
|
||||
return try std.mem.concat(self.allocator, u8, &[_][]const u8{ "/", path });
|
||||
}
|
||||
|
||||
/// For a path `/foo/bar/baz/123.json`, returns `"123"`.
|
||||
pub fn resourceId(self: *Self) []const u8 {
|
||||
return self.resourceName();
|
||||
}
|
||||
|
||||
// Determine if a given route matches the current request.
|
||||
pub fn match(self: *Self, route: jetzig.views.Route) !bool {
|
||||
return switch (self.method) {
|
||||
@ -332,55 +352,9 @@ pub fn match(self: *Self, route: jetzig.views.Route) !bool {
|
||||
|
||||
fn isMatch(self: *Self, match_type: enum { exact, resource_id }, route: jetzig.views.Route) bool {
|
||||
const path = switch (match_type) {
|
||||
.exact => self.pathWithoutExtension(),
|
||||
.resource_id => self.pathWithoutExtensionAndResourceId(),
|
||||
.exact => self.path.base_path,
|
||||
.resource_id => self.path.directory,
|
||||
};
|
||||
|
||||
return std.mem.eql(u8, path, route.uri_path);
|
||||
}
|
||||
|
||||
// TODO: Be a bit more deterministic in identifying extension, e.g. deal with `.` characters
|
||||
// elsewhere in the path (e.g. in query string).
|
||||
fn pathWithoutExtension(self: *Self) []const u8 {
|
||||
const extension_index = std.mem.lastIndexOfScalar(u8, self.path, '.');
|
||||
if (extension_index) |capture| return self.path[0..capture];
|
||||
|
||||
const query_index = std.mem.indexOfScalar(u8, self.path, '?');
|
||||
if (query_index) |capture| return self.path[0..capture];
|
||||
|
||||
return self.path;
|
||||
}
|
||||
|
||||
fn pathWithoutExtensionAndResourceId(self: *Self) []const u8 {
|
||||
const path = self.pathWithoutExtension();
|
||||
const index = std.mem.lastIndexOfScalar(u8, self.path, '/');
|
||||
if (index) |capture| {
|
||||
if (capture == 0) return "/";
|
||||
return path[0..capture];
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
fn fullName(self: *Self) ![]const u8 {
|
||||
return try self.name(true);
|
||||
}
|
||||
|
||||
fn fullNameWithStrippedResourceId(self: *Self) ![]const u8 {
|
||||
return try self.name(false);
|
||||
}
|
||||
|
||||
fn name(self: *Self, with_resource_id: bool) ![]const u8 {
|
||||
const dirname = try std.mem.join(
|
||||
self.allocator,
|
||||
"_",
|
||||
self.segments.items[0 .. self.segments.items.len - 1],
|
||||
);
|
||||
defer self.allocator.free(dirname);
|
||||
|
||||
return std.mem.concat(self.allocator, u8, &[_][]const u8{
|
||||
dirname,
|
||||
if (with_resource_id) "." else "",
|
||||
if (with_resource_id) self.resourceName() else "",
|
||||
});
|
||||
}
|
||||
|
@ -99,13 +99,16 @@ fn processNextRequest(self: *Self, allocator: std.mem.Allocator, std_http_server
|
||||
|
||||
try request.process();
|
||||
|
||||
var middleware_data = try jetzig.http.middleware.beforeMiddleware(&request);
|
||||
var middleware_data = try jetzig.http.middleware.afterRequest(&request);
|
||||
|
||||
try self.renderResponse(&request);
|
||||
try request.response.headers.append("content-type", response.content_type);
|
||||
|
||||
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
|
||||
|
||||
try request.respond();
|
||||
|
||||
try jetzig.http.middleware.afterMiddleware(&middleware_data, &request);
|
||||
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
|
||||
jetzig.http.middleware.deinit(&middleware_data, &request);
|
||||
|
||||
const log_message = try self.requestLogMessage(&request);
|
||||
@ -203,30 +206,47 @@ fn renderView(
|
||||
request: *jetzig.http.Request,
|
||||
template: ?zmpl.manifest.Template,
|
||||
) !RenderedView {
|
||||
const view = route.render(route.*, request) catch |err| {
|
||||
// View functions return a `View` to help 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| {
|
||||
self.logger.debug("Encountered error: {s}", .{@errorName(err)});
|
||||
if (isUnhandledError(err)) return err;
|
||||
if (isBadRequest(err)) return try self.renderBadRequest(request);
|
||||
return try self.renderInternalServerError(request, err);
|
||||
};
|
||||
if (template) |capture| {
|
||||
return .{
|
||||
.view = view,
|
||||
.content = try self.renderTemplateWithLayout(capture, view, route),
|
||||
};
|
||||
|
||||
if (request.rendered_multiple) return error.JetzigMultipleRenderError;
|
||||
if (request.rendered_view) |rendered_view| {
|
||||
if (request.redirected) return .{ .view = rendered_view, .content = "" };
|
||||
|
||||
if (template) |capture| {
|
||||
return .{
|
||||
.view = rendered_view,
|
||||
.content = try self.renderTemplateWithLayout(request, capture, rendered_view, route),
|
||||
};
|
||||
} else {
|
||||
// We are rendering JSON, content is the result of `toJson` on view data.
|
||||
return .{ .view = rendered_view, .content = "" };
|
||||
}
|
||||
} else {
|
||||
// We are rendering JSON, content is ignored.
|
||||
return .{ .view = view, .content = "" };
|
||||
self.logger.debug("`request.render` was not invoked. Rendering empty content.", .{});
|
||||
request.response_data.reset();
|
||||
return .{
|
||||
.view = .{ .data = request.response_data, .status_code = .no_content },
|
||||
.content = "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn renderTemplateWithLayout(
|
||||
self: *Self,
|
||||
request: *jetzig.http.Request,
|
||||
template: zmpl.manifest.Template,
|
||||
view: jetzig.views.View,
|
||||
route: *jetzig.views.Route,
|
||||
) ![]const u8 {
|
||||
if (route.layout) |layout_name| {
|
||||
if (request.getLayout(route)) |layout_name| {
|
||||
// TODO: Allow user to configure layouts directory other than src/app/views/layouts/
|
||||
const prefixed_name = try std.mem.concat(self.allocator, u8, &[_][]const u8{ "layouts_", layout_name });
|
||||
defer self.allocator.free(prefixed_name);
|
||||
@ -332,7 +352,7 @@ fn requestLogMessage(self: *Self, request: *jetzig.http.Request) ![]const u8 {
|
||||
formatted_duration,
|
||||
request.fmtMethod(),
|
||||
status.format(),
|
||||
request.path,
|
||||
request.path.path,
|
||||
});
|
||||
}
|
||||
|
||||
@ -374,7 +394,7 @@ fn matchStaticResource(self: *Self, request: *jetzig.http.Request) !?StaticResou
|
||||
}
|
||||
|
||||
fn matchPublicContent(self: *Self, request: *jetzig.http.Request) !?StaticResource {
|
||||
if (request.path.len < 2) return null;
|
||||
if (request.path.file_path.len <= 1) return null;
|
||||
if (request.method != .GET) return null;
|
||||
|
||||
var iterable_dir = std.fs.cwd().openDir(
|
||||
@ -394,7 +414,7 @@ fn matchPublicContent(self: *Self, request: *jetzig.http.Request) !?StaticResour
|
||||
while (try walker.next()) |file| {
|
||||
if (file.kind != .file) continue;
|
||||
|
||||
if (std.mem.eql(u8, file.path, request.path[1..])) {
|
||||
if (std.mem.eql(u8, file.path, request.path.file_path[1..])) {
|
||||
const content = try iterable_dir.readFileAlloc(
|
||||
request.allocator,
|
||||
file.path,
|
||||
@ -460,7 +480,7 @@ fn staticPath(request: *jetzig.http.Request, route: jetzig.views.Route) !?[]cons
|
||||
if (try static_params.getValue("id")) |id| {
|
||||
switch (id.*) {
|
||||
.string => |capture| {
|
||||
if (!std.mem.eql(u8, capture.value, request.resourceId())) continue;
|
||||
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,
|
||||
|
@ -10,7 +10,7 @@ else
|
||||
|
||||
const MiddlewareData = std.BoundedArray(*anyopaque, middlewares.len);
|
||||
|
||||
pub fn beforeMiddleware(request: *jetzig.http.Request) !MiddlewareData {
|
||||
pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
|
||||
var middleware_data = MiddlewareData.init(0) catch unreachable;
|
||||
|
||||
inline for (middlewares, 0..) |middleware, index| {
|
||||
@ -20,27 +20,6 @@ pub fn beforeMiddleware(request: *jetzig.http.Request) !MiddlewareData {
|
||||
middleware_data.insert(index, data) catch unreachable;
|
||||
}
|
||||
|
||||
inline for (middlewares, 0..) |middleware, index| {
|
||||
if (comptime !@hasDecl(middleware, "beforeRequest")) continue;
|
||||
if (comptime @hasDecl(middleware, "init")) {
|
||||
const data = middleware_data.get(index);
|
||||
try @call(
|
||||
.always_inline,
|
||||
middleware.beforeRequest,
|
||||
.{ @as(*middleware, @ptrCast(@alignCast(data))), request },
|
||||
);
|
||||
} else {
|
||||
try @call(.always_inline, middleware.beforeRequest, .{request});
|
||||
}
|
||||
}
|
||||
|
||||
return middleware_data;
|
||||
}
|
||||
|
||||
pub fn afterMiddleware(
|
||||
middleware_data: *MiddlewareData,
|
||||
request: *jetzig.http.Request,
|
||||
) !void {
|
||||
inline for (middlewares, 0..) |middleware, index| {
|
||||
if (comptime !@hasDecl(middleware, "afterRequest")) continue;
|
||||
if (comptime @hasDecl(middleware, "init")) {
|
||||
@ -48,10 +27,50 @@ pub fn afterMiddleware(
|
||||
try @call(
|
||||
.always_inline,
|
||||
middleware.afterRequest,
|
||||
.{ @as(*middleware, @ptrCast(@alignCast(data))), request },
|
||||
);
|
||||
} else {
|
||||
try @call(.always_inline, middleware.afterRequest, .{request});
|
||||
}
|
||||
}
|
||||
|
||||
return middleware_data;
|
||||
}
|
||||
|
||||
pub fn beforeResponse(
|
||||
middleware_data: *MiddlewareData,
|
||||
request: *jetzig.http.Request,
|
||||
) !void {
|
||||
inline for (middlewares, 0..) |middleware, index| {
|
||||
if (comptime !@hasDecl(middleware, "beforeResponse")) continue;
|
||||
if (comptime @hasDecl(middleware, "init")) {
|
||||
const data = middleware_data.get(index);
|
||||
try @call(
|
||||
.always_inline,
|
||||
middleware.beforeResponse,
|
||||
.{ @as(*middleware, @ptrCast(@alignCast(data))), request, request.response },
|
||||
);
|
||||
} else {
|
||||
try @call(.always_inline, middleware.afterRequest, .{ request, request.response });
|
||||
try @call(.always_inline, middleware.beforeResponse, .{ request, request.response });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn afterResponse(
|
||||
middleware_data: *MiddlewareData,
|
||||
request: *jetzig.http.Request,
|
||||
) !void {
|
||||
inline for (middlewares, 0..) |middleware, index| {
|
||||
if (comptime !@hasDecl(middleware, "afterResponse")) continue;
|
||||
if (comptime @hasDecl(middleware, "init")) {
|
||||
const data = middleware_data.get(index);
|
||||
try @call(
|
||||
.always_inline,
|
||||
middleware.afterResponse,
|
||||
.{ @as(*middleware, @ptrCast(@alignCast(data))), request, request.response },
|
||||
);
|
||||
} else {
|
||||
try @call(.always_inline, middleware.afterResponse, .{ request, request.response });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
1
src/jetzig/middleware.zig
Normal file
1
src/jetzig/middleware.zig
Normal file
@ -0,0 +1 @@
|
||||
pub const HtmxMiddleware = @import("middleware/HtmxMiddleware.zig");
|
45
src/jetzig/middleware/HtmxMiddleware.zig
Normal file
45
src/jetzig/middleware/HtmxMiddleware.zig
Normal file
@ -0,0 +1,45 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/// 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;
|
||||
if (request.getHeader("HX-Target")) |target| {
|
||||
request.server.logger.debug(
|
||||
"[middleware-htmx] htmx request detected, disabling layout. (#{s})",
|
||||
.{target},
|
||||
);
|
||||
request.setLayout(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
if (response.status_code != .moved_permanently and response.status_code != .found) return;
|
||||
if (request.headers.getFirstValue("HX-Request") == null) return;
|
||||
|
||||
if (response.headers.getFirstValue("Location")) |location| {
|
||||
response.headers.remove("Location");
|
||||
response.status_code = .ok;
|
||||
request.response_data.reset();
|
||||
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);
|
||||
}
|
9
src/jetzig/util.zig
Normal file
9
src/jetzig/util.zig
Normal file
@ -0,0 +1,9 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn equalStringsCaseInsensitive(expected: []const u8, actual: []const u8) bool {
|
||||
if (expected.len != actual.len) return false;
|
||||
for (expected, actual) |expected_char, actual_char| {
|
||||
if (std.ascii.toLower(expected_char) != std.ascii.toLower(actual_char)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
@ -80,11 +80,11 @@ fn renderFn(self: Self, request: *jetzig.http.Request) anyerror!jetzig.views.Vie
|
||||
|
||||
switch (self.view.?.dynamic) {
|
||||
.index => |view| return try view(request, request.response_data),
|
||||
.get => |view| return try view(request.resourceId(), request, request.response_data),
|
||||
.get => |view| return try view(request.path.resource_id, request, request.response_data),
|
||||
.post => |view| return try view(request, request.response_data),
|
||||
.patch => |view| return try view(request.resourceId(), request, request.response_data),
|
||||
.put => |view| return try view(request.resourceId(), request, request.response_data),
|
||||
.delete => |view| return try view(request.resourceId(), request, request.response_data),
|
||||
.patch => |view| return try view(request.path.resource_id, request, request.response_data),
|
||||
.put => |view| return try view(request.path.resource_id, request, request.response_data),
|
||||
.delete => |view| return try view(request.path.resource_id, request, request.response_data),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,5 +2,6 @@ test {
|
||||
_ = @import("jetzig/http/Query.zig");
|
||||
_ = @import("jetzig/http/Headers.zig");
|
||||
_ = @import("jetzig/http/Cookies.zig");
|
||||
_ = @import("jetzig/http/Path.zig");
|
||||
@import("std").testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user