From 625257bcfc2997087ee7851daccf432adf81459c Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Wed, 27 Mar 2024 21:30:38 +0000 Subject: [PATCH 1/3] Allow auto-routing to markdown files If a route isn't matched but the URI can match directly to a markdown file, render it directly. This allows putting arbitrary markdown files in e.g. `/foo/bar/abc.md`, `/foo/bar/def.md` and rendering them as `/foo/bar/abc.html`, `/foo/bar/def.html`. Since markdown are static content only (i.e. no template data etc.) it makes sense for them to be renderable without needing to create a view for each one. --- src/jetzig/http/Server.zig | 85 ++++++++++++++++++++++++-------------- src/jetzig/markdown.zig | 5 +-- 2 files changed, 57 insertions(+), 33 deletions(-) diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index e8c86c3..2658b7b 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -157,39 +157,11 @@ fn renderHTML( }; setResponse(request, rendered, .{}); return; - } else if (try jetzig.markdown.render(request.allocator, matched_route, null)) |markdown_content| { - const rendered = self.renderView(matched_route, request, null) catch |err| { - if (isUnhandledError(err)) return err; - const rendered_error = try self.renderInternalServerError(request, err); - setResponse(request, rendered_error, .{}); - return; - }; - - try addTemplateConstants(rendered.view, matched_route); - - if (request.getLayout(matched_route)) |layout_name| { - // TODO: Allow user to configure layouts directory other than src/app/views/layouts/ - const prefixed_name = try std.mem.concat( - self.allocator, - u8, - &[_][]const u8{ "layouts_", layout_name }, - ); - defer self.allocator.free(prefixed_name); - - if (zmpl.manifest.find(prefixed_name)) |layout| { - rendered.view.data.content = .{ .data = markdown_content }; - request.response.content = try layout.render(rendered.view.data); - } else { - try self.logger.WARN("Unknown layout: {s}", .{layout_name}); - request.response.content = markdown_content; - } - } - request.response.status_code = rendered.view.status_code; - request.response.content_type = "text/html"; - return; } } + if (try self.renderMarkdown(request, route)) return; + request.response.content = ""; request.response.status_code = .not_found; request.response.content_type = "text/html"; @@ -217,6 +189,59 @@ fn renderJSON( } } +fn renderMarkdown(self: *Self, request: *jetzig.http.Request, maybe_route: ?*jetzig.views.Route) !bool { + 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; + }; + + const path = try std.mem.join( + request.allocator, + "/", + &[_][]const u8{ route.uri_path, @tagName(route.action) }, + ); + const markdown_content = try jetzig.markdown.render(request.allocator, path, null) orelse + return false; + + const 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; + }; + + try addTemplateConstants(rendered.view, route); + + if (request.getLayout(route)) |layout_name| { + // TODO: Allow user to configure layouts directory other than src/app/views/layouts/ + const prefixed_name = try std.mem.concat( + self.allocator, + u8, + &[_][]const u8{ "layouts_", layout_name }, + ); + defer self.allocator.free(prefixed_name); + + if (zmpl.manifest.find(prefixed_name)) |layout| { + rendered.view.data.content = .{ .data = markdown_content }; + request.response.content = try layout.render(rendered.view.data); + } else { + try self.logger.WARN("Unknown layout: {s}", .{layout_name}); + request.response.content = markdown_content; + } + } + request.response.status_code = rendered.view.status_code; + request.response.content_type = "text/html"; + return true; +} + const RenderedView = struct { view: jetzig.views.View, content: []const u8 }; fn renderView( diff --git a/src/jetzig/markdown.zig b/src/jetzig/markdown.zig index 96105ce..1e92410 100644 --- a/src/jetzig/markdown.zig +++ b/src/jetzig/markdown.zig @@ -5,7 +5,7 @@ const jetzig = @import("../jetzig.zig"); const Zmd = @import("zmd").Zmd; pub fn render( allocator: std.mem.Allocator, - route: *const jetzig.views.Route, + path: []const u8, custom_fragments: ?type, ) !?[]const u8 { const fragments = custom_fragments orelse jetzig.config.get(type, "markdown_fragments"); @@ -15,11 +15,10 @@ pub fn render( try path_buf.appendSlice(&[_][]const u8{ "src", "app", "views" }); - var it = std.mem.splitScalar(u8, route.uri_path, '/'); + var it = std.mem.splitScalar(u8, path, '/'); while (it.next()) |segment| { try path_buf.append(segment); } - try path_buf.append(@tagName(route.action)); const base_path = try std.fs.path.join(allocator, path_buf.items); defer allocator.free(base_path); From 95a8330629aa358db988e6d77935326781456efa Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Thu, 28 Mar 2024 19:23:11 +0000 Subject: [PATCH 2/3] Refactor rendering Render appropriately-formatted errors for HTML/JSON. --- src/compile_static_routes.zig | 4 +- src/jetzig/http.zig | 1 + src/jetzig/http/Request.zig | 28 +++++ src/jetzig/http/Server.zig | 127 +++++++++++------------ src/jetzig/http/status_codes.zig | 29 +++++- src/jetzig/loggers/DevelopmentLogger.zig | 7 +- src/jetzig/views/View.zig | 2 +- 7 files changed, 124 insertions(+), 74 deletions(-) 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 = .{ .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 = .bad_request }, - .content = "Bad Request\n", + .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; From ec045cccd653cfb63c524beb91a6d119bf77ad40 Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Thu, 28 Mar 2024 22:37:59 +0000 Subject: [PATCH 3/3] Add environments, use pretty-printed JSON in development Fix secret generation - overallocate length to ensure we have enough bytes. Error if no secret provided in production mode. --- build.zig.zon | 4 ++-- cli/commands/generate/secret.zig | 4 ++-- src/GenerateRoutes.zig | 2 +- src/jetzig/Environment.zig | 29 +++++++++++++++++++---------- src/jetzig/http/Server.zig | 13 +++++++++++-- 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 8a1cfbe..dbb8401 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -7,8 +7,8 @@ .hash = "1220bfc5c29bc930b5a524c210712ef65c6cde6770450899bef01164a3089e6707fa", }, .zmpl = .{ - .url = "https://github.com/jetzig-framework/zmpl/archive/9e2df115c9f17e92fb60a4d09bf55ea48d0388b0.tar.gz", - .hash = "1220820b7f5f3e01b7dc976d32cf9ff65d44dee2642533f4b8104e19a824e802d7e1", + .url = "https://github.com/jetzig-framework/zmpl/archive/ffdbd3767da28d5ab07c1a786ea778152d2f79f6.tar.gz", + .hash = "12202cf05fd4ba2482a9b4b89c632b435310a76ac501b7a3d87dfd41006748dd138d", }, .args = .{ .url = "https://github.com/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz", diff --git a/cli/commands/generate/secret.zig b/cli/commands/generate/secret.zig index 25da07f..6f9c9d5 100644 --- a/cli/commands/generate/secret.zig +++ b/cli/commands/generate/secret.zig @@ -6,9 +6,9 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8) !v _ = args; _ = cwd; const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - var secret: [44]u8 = undefined; + var secret: [128]u8 = undefined; - for (0..44) |index| { + for (0..128) |index| { secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len)]; } diff --git a/src/GenerateRoutes.zig b/src/GenerateRoutes.zig index 3198e2b..26dc339 100644 --- a/src/GenerateRoutes.zig +++ b/src/GenerateRoutes.zig @@ -271,7 +271,7 @@ fn generateRoutesForView(self: *Self, views_dir: std.fs.Dir, path: []const u8) ! var json_buf = std.ArrayList(u8).init(self.allocator); defer json_buf.deinit(); const json_writer = json_buf.writer(); - try item.toJson(json_writer); + try item.toJson(json_writer, false, 0); var encoded_buf = std.ArrayList(u8).init(self.allocator); defer encoded_buf.deinit(); const writer = encoded_buf.writer(); diff --git a/src/jetzig/Environment.zig b/src/jetzig/Environment.zig index a822573..85ac264 100644 --- a/src/jetzig/Environment.zig +++ b/src/jetzig/Environment.zig @@ -8,12 +8,13 @@ const Environment = @This(); allocator: std.mem.Allocator, +pub const EnvironmentName = enum { development, production }; + const Options = struct { help: bool = false, bind: []const u8 = "127.0.0.1", port: u16 = 8080, - // TODO: - // environment: []const u8 = "development", + environment: EnvironmentName = .development, log: []const u8 = "-", @"log-error": []const u8 = "-", @"log-level": jetzig.loggers.LogLevel = .INFO, @@ -24,8 +25,7 @@ const Options = struct { .h = "help", .b = "bind", .p = "port", - // TODO: - // .e = "environment", + .e = "environment", .d = "detach", }; @@ -35,8 +35,7 @@ const Options = struct { .option_docs = .{ .bind = "IP address/hostname to bind to (default: 127.0.0.1)", .port = "Port to listen on (default: 8080)", - // TODO: - // .environment = "Load an environment configuration from src/app/environments/.zig", + .environment = "Set the server environment. Must be one of: { development, production } (default: development)", .log = "Path to log file. Use '-' for stdout (default: '-')", .@"log-error" = \\Optional path to separate error log file. Use '-' for stderr. If omitted, errors are logged to the location specified by the `log` option (or stderr if `log` is '-'). @@ -94,9 +93,11 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions { std.process.exit(1); } + const environment = options.options.environment; + // TODO: Generate nonce per session - do research to confirm correct best practice. const secret_len = jetzig.http.Session.Cipher.key_length + jetzig.http.Session.Cipher.nonce_length; - const secret = try self.getSecret(&logger, secret_len); + const secret = (try self.getSecret(&logger, secret_len, environment))[0..secret_len]; if (secret.len != secret_len) { try logger.ERROR("Expected secret length: {}, found: {}.", .{ secret_len, secret.len }); @@ -110,6 +111,7 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions { .bind = try self.allocator.dupe(u8, options.options.bind), .port = options.options.port, .detach = options.options.detach, + .environment = environment, }; } @@ -132,11 +134,18 @@ fn getLogFile(stream: enum { stdout, stderr }, options: Options) !std.fs.File { return file; } -fn getSecret(self: Environment, logger: *jetzig.loggers.Logger, comptime len: u10) ![]const u8 { - return std.process.getEnvVarOwned(self.allocator, "JETZIG_SECRET") catch |err| { +fn getSecret(self: Environment, logger: *jetzig.loggers.Logger, comptime len: u10, environment: EnvironmentName) ![]const u8 { + const env_var = "JETZIG_SECRET"; + + return std.process.getEnvVarOwned(self.allocator, env_var) catch |err| { switch (err) { error.EnvironmentVariableNotFound => { - // TODO: Make this a failure when running in non-development mode. + if (environment != .development) { + try logger.ERROR("Environment variable `{s}` must be defined in production mode.", .{env_var}); + try logger.ERROR("Run `jetzig generate secret` to generate an appropriate value.", .{}); + std.process.exit(1); + } + const secret = try jetzig.util.generateSecret(self.allocator, len); try logger.WARN( "Running in development mode, using auto-generated cookie encryption key: {s}", diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 1192ff9..797e4f8 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -9,6 +9,7 @@ pub const ServerOptions = struct { port: u16, secret: []const u8, detach: bool, + environment: jetzig.Environment.EnvironmentName, }; allocator: std.mem.Allocator, @@ -48,7 +49,11 @@ pub fn listen(self: *Self) !void { self.initialized = true; - try self.logger.INFO("Listening on http://{s}:{}", .{ self.options.bind, self.options.port }); + try self.logger.INFO("Listening on http://{s}:{} [{s}]", .{ + self.options.bind, + self.options.port, + @tagName(self.options.environment), + }); try self.processRequests(); } @@ -167,7 +172,11 @@ fn renderJSON( if (data.value) |_| {} else _ = try data.object(); - rendered.content = try data.toJson(); + rendered.content = if (self.options.environment == .development) + try data.toPrettyJson() + else + try data.toJson(); + request.setResponse(rendered, .{}); } else { request.setResponse(try renderNotFound(request), .{});