diff --git a/src/compile_static_routes.zig b/src/compile_static_routes.zig index b62c4b8..1b744c6 100644 --- a/src/compile_static_routes.zig +++ b/src/compile_static_routes.zig @@ -129,7 +129,9 @@ fn renderMarkdown( jetzig_options.markdown_fragments else null; - const content = try jetzig.markdown.render(allocator, &route, fragments) orelse return null; + const path = try std.mem.join(allocator, "/", &[_][]const u8{ route.uri_path, @tagName(route.action) }); + defer allocator.free(path); + const content = try jetzig.markdown.render(allocator, path, fragments) orelse return null; if (route.layout) |layout_name| { try view.data.addConst("jetzig_view", view.data.string(route.name)); diff --git a/src/jetzig/http.zig b/src/jetzig/http.zig index 21fac26..5151d26 100644 --- a/src/jetzig/http.zig +++ b/src/jetzig/http.zig @@ -10,5 +10,6 @@ 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 StatusCode = status_codes.StatusCode; pub const middleware = @import("http/middleware.zig"); pub const mime = @import("http/mime.zig"); diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig index 7aa0703..23ed2c4 100644 --- a/src/jetzig/http/Request.zig +++ b/src/jetzig/http/Request.zig @@ -327,6 +327,34 @@ pub fn fmtMethod(self: *const Self, colorized: bool) []const u8 { }; } +/// Format a status code appropriately for the current request format. +/// e.g. `.HTML` => `404 Not Found` +/// `.JSON` => `{ "message": "Not Found", "status": "404" }` +pub fn formatStatus(self: *Self, status_code: jetzig.http.StatusCode) ![]const u8 { + const status = jetzig.http.status_codes.get(status_code); + + return switch (self.requestFormat()) { + .JSON => try std.json.stringifyAlloc(self.allocator, .{ + .message = status.getMessage(), + .status = status.getCode(), + }, .{}), + .HTML, .UNKNOWN => status.getFormatted(.{ .linebreak = true }), + }; +} + +pub fn setResponse( + self: *Self, + rendered_view: jetzig.http.Server.RenderedView, + options: struct { content_type: ?[]const u8 = null }, +) 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", + }; +} + // Determine if a given route matches the current request. pub fn match(self: *Self, route: jetzig.views.Route) !bool { return switch (self.method) { diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 2658b7b..1192ff9 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -108,12 +108,12 @@ fn renderResponse(self: *Self, request: *jetzig.http.Request) !void { if (isUnhandledError(err)) return err; const rendered = try self.renderInternalServerError(request, err); - setResponse(request, rendered, .{}); + request.setResponse(rendered, .{}); return; }; if (static_resource) |resource| { - try renderStatic(resource, request.response); + try renderStatic(resource, request); return; } @@ -126,20 +126,11 @@ fn renderResponse(self: *Self, request: *jetzig.http.Request) !void { } } -fn setResponse( - request: *jetzig.http.Request, - rendered_view: RenderedView, - options: struct { content_type: []const u8 = "text/html" }, -) void { - request.response.content = rendered_view.content; - request.response.status_code = rendered_view.view.status_code; - request.response.content_type = options.content_type; -} - -fn renderStatic(resource: StaticResource, response: *jetzig.http.Response) !void { - response.status_code = .ok; - response.content = resource.content; - response.content_type = resource.mime_type; +fn renderStatic(resource: StaticResource, request: *jetzig.http.Request) !void { + request.setResponse( + .{ .view = .{ .data = request.response_data }, .content = resource.content }, + .{ .content_type = resource.mime_type }, + ); } fn renderHTML( @@ -152,19 +143,17 @@ fn renderHTML( const rendered = self.renderView(matched_route, request, template) catch |err| { if (isUnhandledError(err)) return err; const rendered_error = try self.renderInternalServerError(request, err); - setResponse(request, rendered_error, .{}); - return; + return request.setResponse(rendered_error, .{}); }; - setResponse(request, rendered, .{}); - return; + return request.setResponse(rendered, .{}); } } - if (try self.renderMarkdown(request, route)) return; - - request.response.content = ""; - request.response.status_code = .not_found; - request.response.content_type = "text/html"; + if (try self.renderMarkdown(request, route)) |rendered| { + return request.setResponse(rendered, .{}); + } else { + return request.setResponse(try renderNotFound(request), .{}); + } } fn renderJSON( @@ -173,34 +162,34 @@ fn renderJSON( route: ?*jetzig.views.Route, ) !void { if (route) |matched_route| { - const rendered = try self.renderView(matched_route, request, null); + var rendered = try self.renderView(matched_route, request, null); var data = rendered.view.data; if (data.value) |_| {} else _ = try data.object(); - try request.headers.append("Content-Type", "application/json"); - request.response.content = try data.toJson(); - request.response.status_code = rendered.view.status_code; - request.response.content_type = "application/json"; + rendered.content = try data.toJson(); + request.setResponse(rendered, .{}); } else { - request.response.content = ""; - request.response.status_code = .not_found; - request.response.content_type = "application/json"; + request.setResponse(try renderNotFound(request), .{}); } } -fn renderMarkdown(self: *Self, request: *jetzig.http.Request, maybe_route: ?*jetzig.views.Route) !bool { +fn renderMarkdown( + self: *Self, + request: *jetzig.http.Request, + maybe_route: ?*jetzig.views.Route, +) !?RenderedView { const route = maybe_route orelse { - if (request.method != .GET) return false; - const content = try jetzig.markdown.render(request.allocator, request.path.base_path, null) orelse - return false; - - const rendered: RenderedView = .{ - .view = jetzig.views.View{ .data = request.response_data, .status_code = .ok }, - .content = content, - }; - setResponse(request, rendered, .{}); - return true; + // No route recognized, but we can still render a static markdown file if it matches the URI: + if (request.method != .GET) return null; + if (try jetzig.markdown.render(request.allocator, request.path.base_path, null)) |content| { + return .{ + .view = jetzig.views.View{ .data = request.response_data, .status_code = .ok }, + .content = content, + }; + } else { + return null; + } }; const path = try std.mem.join( @@ -209,13 +198,11 @@ fn renderMarkdown(self: *Self, request: *jetzig.http.Request, maybe_route: ?*jet &[_][]const u8{ route.uri_path, @tagName(route.action) }, ); const markdown_content = try jetzig.markdown.render(request.allocator, path, null) orelse - return false; + return null; - const rendered = self.renderView(route, request, null) catch |err| { + var rendered = self.renderView(route, request, null) catch |err| { if (isUnhandledError(err)) return err; - const rendered_error = try self.renderInternalServerError(request, err); - setResponse(request, rendered_error, .{}); - return true; + return try self.renderInternalServerError(request, err); }; try addTemplateConstants(rendered.view, route); @@ -231,18 +218,16 @@ fn renderMarkdown(self: *Self, request: *jetzig.http.Request, maybe_route: ?*jet if (zmpl.manifest.find(prefixed_name)) |layout| { rendered.view.data.content = .{ .data = markdown_content }; - request.response.content = try layout.render(rendered.view.data); + rendered.content = try layout.render(rendered.view.data); } else { try self.logger.WARN("Unknown layout: {s}", .{layout_name}); - request.response.content = markdown_content; + rendered.content = markdown_content; } } - request.response.status_code = rendered.view.status_code; - request.response.content_type = "text/html"; - return true; + return rendered; } -const RenderedView = struct { view: jetzig.views.View, content: []const u8 }; +pub const RenderedView = struct { view: jetzig.views.View, content: []const u8 }; fn renderView( self: *Self, @@ -256,7 +241,7 @@ fn renderView( _ = route.render(route.*, request) catch |err| { try self.logger.ERROR("Encountered error: {s}", .{@errorName(err)}); if (isUnhandledError(err)) return err; - if (isBadRequest(err)) return try self.renderBadRequest(request); + if (isBadRequest(err)) return try renderBadRequest(request); return try self.renderInternalServerError(request, err); }; @@ -347,28 +332,38 @@ fn isBadHttpError(err: anyerror) bool { fn renderInternalServerError(self: *Self, request: *jetzig.http.Request, err: anyerror) !RenderedView { request.response_data.reset(); - var object = try request.response_data.object(); - try object.put("error", request.response_data.string(@errorName(err))); + try self.logger.ERROR("Encountered Error: {s}", .{@errorName(err)}); const stack = @errorReturnTrace(); if (stack) |capture| try self.logStackTrace(capture, request); + const status = .internal_server_error; + const content = try request.formatStatus(status); return .{ - .view = jetzig.views.View{ .data = request.response_data, .status_code = .internal_server_error }, - .content = "Internal Server Error\n", + .view = jetzig.views.View{ .data = request.response_data, .status_code = status }, + .content = content, }; } -fn renderBadRequest(self: *Self, request: *jetzig.http.Request) !RenderedView { - _ = self; +fn renderNotFound(request: *jetzig.http.Request) !RenderedView { request.response_data.reset(); - var object = try request.response_data.object(); - try object.put("error", request.response_data.string("Bad Request")); - + const status: jetzig.http.StatusCode = .not_found; + const content = try request.formatStatus(status); return .{ - .view = jetzig.views.View{ .data = request.response_data, .status_code = .bad_request }, - .content = "Bad Request\n", + .view = .{ .data = request.response_data, .status_code = status }, + .content = content, + }; +} + +fn renderBadRequest(request: *jetzig.http.Request) !RenderedView { + request.response_data.reset(); + + const status: jetzig.http.StatusCode = .not_found; + const content = try request.formatStatus(status); + return .{ + .view = jetzig.views.View{ .data = request.response_data, .status_code = status }, + .content = content, }; } diff --git a/src/jetzig/http/status_codes.zig b/src/jetzig/http/status_codes.zig index 5e78ad9..611cc08 100644 --- a/src/jetzig/http/status_codes.zig +++ b/src/jetzig/http/status_codes.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const jetzig = @import("../../jetzig.zig"); @@ -66,6 +67,8 @@ pub const StatusCode = enum { network_authentication_required, }; +const FormatOptions = struct { colorized: bool = false, linebreak: bool = false }; + pub fn StatusCodeType(comptime code: []const u8, comptime message: []const u8) type { return struct { code: []const u8 = code, @@ -73,11 +76,15 @@ pub fn StatusCodeType(comptime code: []const u8, comptime message: []const u8) t const Self = @This(); - pub fn format(self: Self, colorized: bool) []const u8 { + pub fn getFormatted(self: Self, comptime options: FormatOptions) []const u8 { _ = self; - const full_message = code ++ " " ++ message; + const linebreak = switch (builtin.os.tag) { + .windows => "\r\n", + inline else => "\n", + }; + const full_message = code ++ " " ++ message ++ if (options.linebreak) linebreak else ""; - if (!colorized) return full_message; + if (!options.colorized) return full_message; if (std.mem.startsWith(u8, code, "2")) { return jetzig.colors.green(full_message); @@ -159,9 +166,9 @@ pub const TaggedStatusCode = union(StatusCode) { const Self = @This(); - pub fn format(self: Self, colorized: bool) []const u8 { + pub fn getFormatted(self: Self, comptime options: FormatOptions) []const u8 { return switch (self) { - inline else => |capture| capture.format(colorized), + inline else => |capture| capture.getFormatted(options), }; } @@ -170,4 +177,16 @@ pub const TaggedStatusCode = union(StatusCode) { inline else => |capture| capture.code, }; } + + pub fn getMessage(self: Self) []const u8 { + return switch (self) { + inline else => |capture| capture.message, + }; + } }; + +pub fn get(code: StatusCode) TaggedStatusCode { + switch (code) { + inline else => |capture| return @unionInit(TaggedStatusCode, @tagName(capture), .{}), + } +} diff --git a/src/jetzig/loggers/DevelopmentLogger.zig b/src/jetzig/loggers/DevelopmentLogger.zig index 6575ab5..40cac63 100644 --- a/src/jetzig/loggers/DevelopmentLogger.zig +++ b/src/jetzig/loggers/DevelopmentLogger.zig @@ -83,10 +83,15 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request) ), }; + const formatted_status = if (self.stdout_colorized) + status.getFormatted(.{ .colorized = true }) + else + status.getFormatted(.{}); + const message = try std.fmt.allocPrint(self.allocator, "[{s}/{s}/{s}] {s}", .{ formatted_duration, request.fmtMethod(self.stdout_colorized), - status.format(self.stdout_colorized), + formatted_status, request.path.path, }); defer self.allocator.free(message); diff --git a/src/jetzig/views/View.zig b/src/jetzig/views/View.zig index 2d74334..98beaac 100644 --- a/src/jetzig/views/View.zig +++ b/src/jetzig/views/View.zig @@ -5,7 +5,7 @@ const Self = @This(); const jetzig = @import("../../jetzig.zig"); data: *jetzig.data.Data, -status_code: jetzig.http.status_codes.StatusCode, +status_code: jetzig.http.status_codes.StatusCode = .ok, pub fn deinit(self: Self) void { _ = self;