diff --git a/demo/src/app/views/format.zig b/demo/src/app/views/format.zig
new file mode 100644
index 0000000..6e9a83c
--- /dev/null
+++ b/demo/src/app/views/format.zig
@@ -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);
+}
diff --git a/demo/src/app/views/format/get.zmpl b/demo/src/app/views/format/get.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/demo/src/app/views/format/get.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/demo/src/app/views/format/index.zmpl b/demo/src/app/views/format/index.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/demo/src/app/views/format/index.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/Routes.zig b/src/Routes.zig
index eb76b4a..eb3bc44 100644
--- a/src/Routes.zig
+++ b/src/Routes.zig
@@ -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,
\\ }},
\\
;
diff --git a/src/jetzig/App.zig b/src/jetzig/App.zig
index 89ac3dc..8e607f2 100644
--- a/src/jetzig/App.zig
+++ b/src/jetzig/App.zig
@@ -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);
diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig
index e6b0337..4a73c9e 100644
--- a/src/jetzig/http/Server.zig
+++ b/src/jetzig/http/Server.zig
@@ -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),
diff --git a/src/jetzig/views/Route.zig b/src/jetzig/views/Route.zig
index b8d4efa..a16e9fb 100644
--- a/src/jetzig/views/Route.zig
+++ b/src/jetzig/views/Route.zig
@@ -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 => {},
diff --git a/src/routes_exe.zig b/src/routes_exe.zig
index 50945ce..58f8406 100644
--- a/src/routes_exe.zig
+++ b/src/routes_exe.zig
@@ -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,
};
diff --git a/src/test_runner.zig b/src/test_runner.zig
index 6f9711b..32b596d 100644
--- a/src/test_runner.zig
+++ b/src/test_runner.zig
@@ -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|