Implement response format constraints

Define `pub const formats` in a view to specify which formats are
available. Use this to e.g. disable JSON responses.
This commit is contained in:
Bob Farrell 2024-08-04 16:04:10 +01:00
parent 68656fae35
commit 1406447f24
9 changed files with 103 additions and 2 deletions

View File

@ -0,0 +1,53 @@
const std = @import("std");
const jetzig = @import("jetzig");
// Define `pub const formats` to apply constraints to specific view functions. By default, all
// view functions respond to `json` and `html` requests. Use this feature to override those
// defaults.
pub const formats: jetzig.Route.Formats = .{
.index = &.{ .json, .html },
.get = &.{.html},
};
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
_ = data;
return request.render(.ok);
}
pub fn get(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
_ = data;
_ = id;
return request.render(.ok);
}
test "index (json)" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
const response = try app.request(.GET, "/format.json", .{});
try response.expectStatus(.ok);
}
test "index (html)" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
const response = try app.request(.GET, "/format.html", .{});
try response.expectStatus(.ok);
}
test "get (html)" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
const response = try app.request(.GET, "/format/example-id.html", .{});
try response.expectStatus(.ok);
}
test "get (json)" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
const response = try app.request(.GET, "/format/example-id.json", .{});
try response.expectStatus(.not_found);
}

View File

@ -0,0 +1,3 @@
<div>
<span>Content goes here</span>
</div>

View File

@ -0,0 +1,3 @@
<div>
<span>Content goes here</span>
</div>

View File

@ -278,6 +278,7 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
\\ .template = "{6s}",
\\ .layout = if (@hasDecl(@import("{7s}"), "layout")) @import("{7s}").layout else null,
\\ .json_params = &[_][]const u8 {{ {8s} }},
\\ .formats = if (@hasDecl(@import("{7s}"), "formats")) @import("{7s}").formats else null,
\\ }},
\\
;

View File

@ -207,6 +207,7 @@ pub fn createRoutes(
.layout = const_route.layout,
.template = const_route.template,
.json_params = const_route.json_params,
.formats = const_route.formats,
};
try var_route.initParams(allocator);

View File

@ -192,6 +192,12 @@ fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
const route = self.matchCustomRoute(request) orelse try self.matchRoute(request, false);
if (route) |capture| {
if (!capture.validateFormat(request)) {
return request.setResponse(try self.renderNotFound(request), .{});
}
}
switch (request.requestFormat()) {
.HTML => try self.renderHTML(request, route),
.JSON => try self.renderJSON(request, route),

View File

@ -14,6 +14,17 @@ const StaticViewWithoutId = *const fn (*jetzig.http.StaticRequest, *jetzig.data.
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 Formats = struct {
index: ?[]const ResponseFormat = null,
get: ?[]const ResponseFormat = null,
post: ?[]const ResponseFormat = null,
put: ?[]const ResponseFormat = null,
patch: ?[]const ResponseFormat = null,
delete: ?[]const ResponseFormat = null,
custom: ?[]const ResponseFormat = null,
};
const ResponseFormat = enum { html, json };
pub const DynamicViewType = union(Action) {
index: ViewWithoutId,
get: ViewWithId,
@ -61,6 +72,7 @@ template: []const u8,
json_params: []const []const u8,
params: std.ArrayList(*jetzig.data.Data) = undefined,
id: []const u8,
formats: ?Formats,
/// Initializes a route's static params on server launch. Converts static params (JSON strings)
/// to `jetzig.data.Data` values. Memory is owned by caller (`App.start()`).
@ -101,6 +113,28 @@ pub fn match(self: Route, request: *const jetzig.http.Request) bool {
return true;
}
/// Return `true` if a format specification is defined for the current route/view function
/// **and** the format is supported by the current view function, otherwise return `false`.
pub fn validateFormat(self: Route, request: *const jetzig.http.Request) bool {
const formats = self.formats orelse return true;
const supported_formats = switch (self.action) {
.index => formats.index orelse return true,
.get => formats.get orelse return true,
.post => formats.post orelse return true,
.put => formats.put orelse return true,
.patch => formats.patch orelse return true,
.delete => formats.delete orelse return true,
.custom => formats.custom orelse return true,
};
const request_format = request.requestFormat();
for (supported_formats) |supported_format| {
if ((request_format == .HTML or request_format == .UNKNOWN) and supported_format == .html) return true;
if (request_format == .JSON and supported_format == .json) return true;
}
return false;
}
fn renderFn(self: Route, request: *jetzig.http.Request) anyerror!jetzig.views.View {
switch (self.view) {
.dynamic => {},

View File

@ -23,7 +23,7 @@ pub fn main() !void {
.index => jetzig.colors.blue("{s: <7}"),
.post => jetzig.colors.yellow("{s: <7}"),
.put => jetzig.colors.magenta("{s: <7}"),
.patch => jetzig.colors.purple("{s: <7}"),
.patch => jetzig.colors.bright_magenta("{s: <7}"),
.delete => jetzig.colors.red("{s: <7}"),
.custom => unreachable,
};

View File

@ -37,7 +37,7 @@ const Test = struct {
trace: ?[]const u8,
};
const name_template = jetzig.colors.blue("{s}") ++ jetzig.colors.yellow("->") ++ "\"" ++ jetzig.colors.cyan("{s}") ++ "\" ";
const name_template = jetzig.colors.blue("{s}") ++ jetzig.colors.yellow("::") ++ "\"" ++ jetzig.colors.cyan("{s}") ++ "\" ";
pub fn init(test_fn: std.builtin.TestFn) Test {
return if (std.mem.indexOf(u8, test_fn.name, ".test.")) |index|