From 1406447f24c4bc733c1969db208215fdee8ff1c5 Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Sun, 4 Aug 2024 16:04:10 +0100 Subject: [PATCH] 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. --- demo/src/app/views/format.zig | 53 ++++++++++++++++++++++++++++ demo/src/app/views/format/get.zmpl | 3 ++ demo/src/app/views/format/index.zmpl | 3 ++ src/Routes.zig | 1 + src/jetzig/App.zig | 1 + src/jetzig/http/Server.zig | 6 ++++ src/jetzig/views/Route.zig | 34 ++++++++++++++++++ src/routes_exe.zig | 2 +- src/test_runner.zig | 2 +- 9 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 demo/src/app/views/format.zig create mode 100644 demo/src/app/views/format/get.zmpl create mode 100644 demo/src/app/views/format/index.zmpl 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|