diff --git a/demo/src/app/views/render_text.zig b/demo/src/app/views/render_text.zig new file mode 100644 index 0000000..dba87f9 --- /dev/null +++ b/demo/src/app/views/render_text.zig @@ -0,0 +1,17 @@ +const std = @import("std"); +const jetzig = @import("jetzig"); + +pub fn index(request: *jetzig.Request) !jetzig.View { + request.response.content_type = "text/xml"; + return request.renderText("baz", .ok); +} + +test "index" { + var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); + defer app.deinit(); + + const response = try app.request(.GET, "/render_text", .{}); + try response.expectStatus(.ok); + try response.expectBodyContains("baz"); + try response.expectHeader("content-type", "text/xml"); +} diff --git a/demo/src/app/views/render_text/index.zmpl b/demo/src/app/views/render_text/index.zmpl new file mode 100644 index 0000000..76457d0 --- /dev/null +++ b/demo/src/app/views/render_text/index.zmpl @@ -0,0 +1,3 @@ +
+ Content goes here +
diff --git a/src/jetzig/http.zig b/src/jetzig/http.zig index 5137903..f3caf66 100644 --- a/src/jetzig/http.zig +++ b/src/jetzig/http.zig @@ -26,3 +26,5 @@ pub const params = @import("http/params.zig"); pub const SimplifiedRequest = struct { location: ?[]const u8, }; + +pub const default_content_type = "application/octet-stream"; diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig index 4e31d3e..582706a 100644 --- a/src/jetzig/http/Request.zig +++ b/src/jetzig/http/Request.zig @@ -16,6 +16,7 @@ pub const RequestState = enum { processed, // Request headers have been processed after_request, // Initial middleware processing rendered, // Rendered by middleware or view + rendered_text, // Rendered text by middleware or view redirected, // Redirected by middleware or view failed, // Failed by middleware or view before_response, // Post middleware processing @@ -230,7 +231,7 @@ pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) je pub inline fn isRendered(self: *const Request) bool { return switch (self.state) { .initial, .processed, .after_request, .before_response => false, - .rendered, .redirected, .failed, .finalized => true, + .rendered, .rendered_text, .redirected, .failed, .finalized => true, }; } @@ -736,6 +737,18 @@ pub fn joinPath(self: *const Request, args: anytype) ![]const u8 { return try std.mem.join(self.allocator, "/", buf[0..]); } +pub fn renderText( + self: *Request, + text: []const u8, + status_code: jetzig.http.StatusCode, +) jetzig.views.View { + self.state = .rendered_text; + self.rendered_view = .{ .data = self.response_data, .status_code = status_code }; + self.setResponse(.{ .view = self.rendered_view.?, .content = text }, .{}); + + return self.rendered_view.?; +} + 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(); @@ -753,10 +766,12 @@ pub fn setResponse( ) void { self.response.content = rendered_view.content; self.response.status_code = rendered_view.view.status_code; - self.response.content_type = options.content_type orelse switch (self.requestFormat()) { - .HTML, .UNKNOWN => "text/html", - .JSON => "application/json", - }; + if (self.response.content_type == null) { + self.response.content_type = options.content_type orelse switch (self.requestFormat()) { + .HTML, .UNKNOWN => "text/html", + .JSON => "application/json", + }; + } } fn setCookieHeaders(self: *Request) !void { diff --git a/src/jetzig/http/Response.zig b/src/jetzig/http/Response.zig index f2c26b5..db1e843 100644 --- a/src/jetzig/http/Response.zig +++ b/src/jetzig/http/Response.zig @@ -11,7 +11,7 @@ allocator: std.mem.Allocator, headers: jetzig.http.Headers, content: []const u8, status_code: http.status_codes.StatusCode, -content_type: []const u8, +content_type: ?[]const u8 = null, httpz_response: *httpz.Response, pub fn init( @@ -22,8 +22,11 @@ pub fn init( .allocator = allocator, .httpz_response = httpz_response, .status_code = .no_content, - .content_type = "application/octet-stream", .content = "", .headers = jetzig.http.Headers.init(allocator, &httpz_response.headers), }; } + +pub inline fn contentType(self: *const jetzig.http.Response) []const u8 { + return self.content_type orelse jetzig.http.default_content_type; +} diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 9fd9ce1..7c1e073 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -159,7 +159,7 @@ pub fn processNextRequest( } try self.renderResponse(&request); - try request.response.headers.append("Content-Type", response.content_type); + try request.response.headers.append("Content-Type", response.contentType()); try jetzig.http.middleware.beforeResponse(&middleware_data, &request); try request.respond(); @@ -178,7 +178,7 @@ fn maybeMiddlewareRender(request: *jetzig.http.Request, response: *const jetzig. // TODO: Allow middleware to set content request.setResponse(.{ .view = rendered, .content = "" }, .{}); } - try request.response.headers.append("Content-Type", response.content_type); + try request.response.headers.append("Content-Type", response.contentType()); try request.respond(); return true; } else return false; @@ -356,12 +356,16 @@ fn renderView( return try self.renderInternalServerError(request, @errorReturnTrace(), err); }; - if (request.state == .failed) { - const view: jetzig.views.View = request.rendered_view orelse .{ - .data = request.response_data, - .status_code = .internal_server_error, - }; - return try self.renderError(request, view.status_code, .{}); + switch (request.state) { + .failed => { + const status_code = request.rendered_view.?.status_code; + return try self.renderError(request, status_code, .{}); + }, + .rendered_text => { + const view = request.rendered_view.?; // a panic here is a bug. + return .{ .view = view, .content = request.response.content }; + }, + else => {}, } const template: ?zmpl.Template = if (request.dynamic_assigned_template) |request_template| @@ -721,7 +725,7 @@ fn matchRoute(self: *Server, request: *jetzig.http.Request, static: bool) !?jetz const StaticResource = struct { content: []const u8, - mime_type: []const u8 = "application/octet-stream", + mime_type: []const u8 = jetzig.http.default_content_type, }; fn matchStaticResource(self: *Server, request: *jetzig.http.Request) !?StaticResource { @@ -780,7 +784,7 @@ fn matchPublicContent(self: *Server, request: *jetzig.http.Request) !?StaticReso jetzig.config.get(usize, "max_bytes_public_content"), ); const extension = std.fs.path.extension(file_path); - const mime_type = if (self.mime_map.get(extension)) |mime| mime else "application/octet-stream"; + const mime_type = if (self.mime_map.get(extension)) |mime| mime else jetzig.http.default_content_type; return .{ .content = content, .mime_type = mime_type, diff --git a/src/jetzig/testing.zig b/src/jetzig/testing.zig index 6b9cbf4..10db7d6 100644 --- a/src/jetzig/testing.zig +++ b/src/jetzig/testing.zig @@ -120,14 +120,31 @@ pub fn expectBodyContains(expected: []const u8, response: TestResponse) !void { } pub fn expectHeader(expected_name: []const u8, expected_value: ?[]const u8, response: TestResponse) !void { + var mismatches = std.ArrayList([]const u8).init(response.allocator); + defer mismatches.deinit(); + for (response.headers) |header| { if (!std.ascii.eqlIgnoreCase(header.name, expected_name)) continue; if (expected_value) |value| { - if (std.mem.eql(u8, header.value, value)) return; + if (std.mem.eql(u8, header.value, value)) { + return; + } else { + try mismatches.append(header.value); + } } else { return; } } + + logFailure( + "Expected header " ++ + jetzig.colors.cyan("{s}") ++ + ": " ++ + jetzig.colors.green("{?s}") ++ + ", found: " ++ + jetzig.colors.red("{s}"), + .{ expected_name, expected_value, mismatches.items }, + ); return error.JetzigExpectHeaderError; }