mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 14:06:08 +00:00
Merge pull request #121 from jetzig-framework/debug-console
Debug console
This commit is contained in:
commit
835885a947
2
.github/workflows/CI.yml
vendored
2
.github/workflows/CI.yml
vendored
@ -15,8 +15,6 @@ jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
#Deactivated windows for I don't know why it fails
|
||||
#os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
21
LICENSE-Zig
Normal file
21
LICENSE-Zig
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (Expat)
|
||||
|
||||
Copyright (c) Zig contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
14
build.zig
14
build.zig
@ -110,6 +110,7 @@ pub fn build(b: *std.Build) !void {
|
||||
const test_build_options = b.addOptions();
|
||||
test_build_options.addOption(Environment, "environment", .testing);
|
||||
test_build_options.addOption(bool, "build_static", true);
|
||||
test_build_options.addOption(bool, "debug_console", false);
|
||||
const run_main_tests = b.addRunArtifact(main_tests);
|
||||
main_tests.root_module.addOptions("build_options", test_build_options);
|
||||
|
||||
@ -138,12 +139,24 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
"environment",
|
||||
"Jetzig server environment.",
|
||||
) orelse .development;
|
||||
|
||||
const build_static = b.option(
|
||||
bool,
|
||||
"build_static",
|
||||
"Pre-render static routes. [default: false in development, true in testing/production]",
|
||||
) orelse (environment != .development);
|
||||
|
||||
const debug_console = b.option(
|
||||
bool,
|
||||
"debug_console",
|
||||
"Render a debug console on error. Default: false.",
|
||||
) orelse false;
|
||||
|
||||
if (debug_console == true and environment != .development) {
|
||||
std.debug.print("Environment must be `development` when `debug_console` is enabled.", .{});
|
||||
return error.JetzigBuildError;
|
||||
}
|
||||
|
||||
const jetzig_dep = b.dependency(
|
||||
"jetzig",
|
||||
.{
|
||||
@ -171,6 +184,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
const build_options = b.addOptions();
|
||||
build_options.addOption(Environment, "environment", environment);
|
||||
build_options.addOption(bool, "build_static", build_static);
|
||||
build_options.addOption(bool, "debug_console", debug_console);
|
||||
jetzig_module.addOptions("build_options", build_options);
|
||||
|
||||
exe.root_module.addImport("jetzig", jetzig_module);
|
||||
|
@ -9,22 +9,20 @@ pub const watch_changes_pause_duration = 1 * 1000 * 1000 * 1000;
|
||||
/// Command line options for the `server` command.
|
||||
pub const Options = struct {
|
||||
reload: bool = true,
|
||||
debug: bool = true,
|
||||
|
||||
pub const meta = .{
|
||||
.full_text =
|
||||
\\Launches a development server.
|
||||
\\
|
||||
\\The development server reloads when files in `src/` are updated.
|
||||
\\
|
||||
\\To disable this behaviour, pass `--reload=false`
|
||||
\\
|
||||
\\Example:
|
||||
\\
|
||||
\\ jetzig server
|
||||
\\ jetzig server --reload=false
|
||||
\\ jetzig server --reload=false --debug=false
|
||||
,
|
||||
.option_docs = .{
|
||||
.reload = "Enable or disable automatic reload on update (default: true)",
|
||||
.debug = "Enable or disable the development debug console (default: true)",
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -62,18 +60,27 @@ pub fn run(
|
||||
},
|
||||
);
|
||||
|
||||
var argv = std.ArrayList([]const u8).init(allocator);
|
||||
defer argv.deinit();
|
||||
|
||||
try argv.appendSlice(&.{
|
||||
"zig",
|
||||
"build",
|
||||
util.environmentBuildOption(main_options.options.environment),
|
||||
"-Djetzig_runner=true",
|
||||
});
|
||||
|
||||
if (options.debug) try argv.append("-Ddebug_console=true");
|
||||
try argv.appendSlice(&.{
|
||||
"install",
|
||||
"--color",
|
||||
"on",
|
||||
});
|
||||
|
||||
while (true) {
|
||||
util.runCommandInDir(
|
||||
allocator,
|
||||
&.{
|
||||
"zig",
|
||||
"build",
|
||||
util.environmentBuildOption(main_options.options.environment),
|
||||
"-Djetzig_runner=true",
|
||||
"install",
|
||||
"--color",
|
||||
"on",
|
||||
},
|
||||
argv.items,
|
||||
.{ .path = realpath },
|
||||
) catch {
|
||||
std.debug.print("Build failed, waiting for file change...\n", .{});
|
||||
@ -88,10 +95,9 @@ pub fn run(
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
const argv = &[_][]const u8{exe_path.?};
|
||||
defer allocator.free(exe_path.?);
|
||||
|
||||
var process = std.process.Child.init(argv, allocator);
|
||||
var process = std.process.Child.init(&.{exe_path.?}, allocator);
|
||||
process.stdin_behavior = .Inherit;
|
||||
process.stdout_behavior = .Inherit;
|
||||
process.stderr_behavior = .Inherit;
|
||||
|
@ -1,7 +1,12 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
pub const layout = "application";
|
||||
// Anti-CSRF middleware can be included in the view's `actions` declaration to apply CSRF
|
||||
// protection just to this specific view, or it can be added to your application's global
|
||||
// middleware stack defined in `jetzig_options` in `src/main.zig`.
|
||||
//
|
||||
// Use `{{context.authenticityToken()}}` or `{{context.authenticityFormField()}}` in a Zmpl
|
||||
// template to generate a token, store it in the user's session, and inject it into the page.
|
||||
|
||||
pub const actions = .{
|
||||
.before = .{jetzig.middleware.AntiCsrfMiddleware},
|
||||
|
@ -6,3 +6,5 @@
|
||||
|
||||
<input type="submit" value="Submit Spam" />
|
||||
</form>
|
||||
|
||||
<div>Try clearing `_jetzig_session` cookie before clicking "Submit Spam"</div>
|
||||
|
15
demo/src/app/views/render_template.zig
Normal file
15
demo/src/app/views/render_template.zig
Normal file
@ -0,0 +1,15 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
pub fn index(request: *jetzig.Request) !jetzig.View {
|
||||
return request.renderTemplate("basic/index", .ok);
|
||||
}
|
||||
|
||||
test "index" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.GET, "/render_template", .{});
|
||||
try response.expectStatus(.ok);
|
||||
try response.expectBodyContains("Hello");
|
||||
}
|
3
demo/src/app/views/render_template/index.zmpl
Normal file
3
demo/src/app/views/render_template/index.zmpl
Normal file
@ -0,0 +1,3 @@
|
||||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
@ -90,6 +90,7 @@ pub const jetzig_options = struct {
|
||||
.same_site = true,
|
||||
.secure = true,
|
||||
.http_only = true,
|
||||
.path = "/",
|
||||
},
|
||||
};
|
||||
|
||||
|
121
src/assets/debug.css
Normal file
121
src/assets/debug.css
Normal file
@ -0,0 +1,121 @@
|
||||
/* reset https://www.joshwcomeau.com/css/custom-css-reset/ */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
img, picture, video, canvas, svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
input, button, textarea, select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
p, h1, h2, h3, h4, h5, h6 {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
p {
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
#root, #__next {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
/* styles */
|
||||
|
||||
body {
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: monospace;
|
||||
color: #e55;
|
||||
padding: 1rem;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: monospace;
|
||||
color: #a0a0a0;
|
||||
padding: 1rem;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.stack-trace {
|
||||
/* background-color: #e555; */
|
||||
padding: 1rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.stack-trace .stack-source-line .file-name {
|
||||
color: #90bfd7;
|
||||
display: block;
|
||||
padding: 0.4rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stack-trace .stack-source-line {
|
||||
background-color: #333;
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid #ffa3;
|
||||
}
|
||||
|
||||
.stack-trace .stack-source-line:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stack-trace .stack-source-line .line-content {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.stack-trace .stack-source-line .line-content.surrounding {
|
||||
color: #ffa;
|
||||
}
|
||||
|
||||
.stack-trace .stack-source-line .line-content.target {
|
||||
color: #faa;
|
||||
background-color: #e552;
|
||||
}
|
||||
|
||||
.stack-trace .stack-source-line .line-content .line-number {
|
||||
display: inline;
|
||||
|
||||
}
|
||||
|
||||
pre {
|
||||
display: inline;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.response-data {
|
||||
color: #fff;
|
||||
background-color: #333;
|
||||
padding: 1rem;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
/* PrismJS 1.29.0
|
||||
https://prismjs.com/download.html#themes=prism-tomorrow&languages=json+zig */
|
||||
code[class*=language-],pre[class*=language-]{color:#ccc;font-family:monospace;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]:not(pre)>code[class*=language-]{white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
|
||||
|
||||
.stack-trace .stack-source-line .line-number {
|
||||
color: #ffa !important;
|
||||
}
|
5
src/assets/debug.js
Normal file
5
src/assets/debug.js
Normal file
File diff suppressed because one or more lines are too long
@ -23,6 +23,7 @@ pub const testing = @import("jetzig/testing.zig");
|
||||
pub const config = @import("jetzig/config.zig");
|
||||
pub const auth = @import("jetzig/auth.zig");
|
||||
pub const callbacks = @import("jetzig/callbacks.zig");
|
||||
pub const debug = @import("jetzig/debug.zig");
|
||||
pub const TemplateContext = @import("jetzig/TemplateContext.zig");
|
||||
|
||||
pub const DateTime = jetcommon.types.DateTime;
|
||||
|
218
src/jetzig/debug.zig
Normal file
218
src/jetzig/debug.zig
Normal file
@ -0,0 +1,218 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
pub const ErrorInfo = struct {
|
||||
stack_trace: ?*std.builtin.StackTrace = null,
|
||||
err: ?anyerror = null,
|
||||
};
|
||||
|
||||
pub fn sourceLocations(
|
||||
allocator: std.mem.Allocator,
|
||||
debug_info: *std.debug.SelfInfo,
|
||||
stack_trace: *std.builtin.StackTrace,
|
||||
) ![]const std.debug.SourceLocation {
|
||||
var source_locations = std.ArrayList(std.debug.SourceLocation).init(allocator);
|
||||
|
||||
if (builtin.strip_debug_info) return error.MissingDebugInfo;
|
||||
|
||||
var frame_index: usize = 0;
|
||||
var frames_left: usize = @min(stack_trace.index, stack_trace.instruction_addresses.len);
|
||||
|
||||
while (frames_left != 0) : ({
|
||||
frames_left -= 1;
|
||||
frame_index = (frame_index + 1) % stack_trace.instruction_addresses.len;
|
||||
}) {
|
||||
const return_address = stack_trace.instruction_addresses[frame_index];
|
||||
const address = return_address - 1;
|
||||
const module = try debug_info.getModuleForAddress(address);
|
||||
const symbol_info = try module.getSymbolAtAddress(debug_info.allocator, address);
|
||||
|
||||
if (symbol_info.source_location) |source_location| {
|
||||
try source_locations.append(source_location);
|
||||
}
|
||||
}
|
||||
|
||||
return try source_locations.toOwnedSlice();
|
||||
}
|
||||
|
||||
pub const HtmlStackTrace = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
stack_trace: *std.builtin.StackTrace,
|
||||
|
||||
pub fn format(self: HtmlStackTrace, _: anytype, _: anytype, writer: anytype) !void {
|
||||
const debug_info = try std.debug.getSelfDebugInfo();
|
||||
const source_locations = try sourceLocations(
|
||||
self.allocator,
|
||||
debug_info,
|
||||
self.stack_trace,
|
||||
);
|
||||
for (source_locations) |source_location| {
|
||||
defer debug_info.allocator.free(source_location.file_name);
|
||||
try writer.print(
|
||||
\\<div class='stack-source-line'>
|
||||
\\ <span class='file-name'>{s}:{d}</span>
|
||||
\\
|
||||
,
|
||||
.{
|
||||
source_location.file_name,
|
||||
source_location.line,
|
||||
},
|
||||
);
|
||||
const surrounding_previous = try surroundingLinesFromFile(
|
||||
self.allocator,
|
||||
.previous,
|
||||
3,
|
||||
source_location.file_name,
|
||||
source_location.line,
|
||||
);
|
||||
const surrounding_next = try surroundingLinesFromFile(
|
||||
self.allocator,
|
||||
.next,
|
||||
3,
|
||||
source_location.file_name,
|
||||
source_location.line,
|
||||
);
|
||||
const target_source_line = try readLineFromFile(
|
||||
self.allocator,
|
||||
source_location.file_name,
|
||||
source_location.line,
|
||||
);
|
||||
|
||||
for (surrounding_previous) |source_line| {
|
||||
try writer.print(surrounding_line_template, .{ source_line.line, source_line.content });
|
||||
}
|
||||
try writer.print(target_line_template, .{ target_source_line.line, target_source_line.content });
|
||||
for (surrounding_next) |source_line| {
|
||||
try writer.print(surrounding_line_template, .{ source_line.line, source_line.content });
|
||||
}
|
||||
|
||||
try writer.print(
|
||||
\\</div>
|
||||
\\
|
||||
, .{});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const SourceLine = struct { content: []const u8, line: usize };
|
||||
|
||||
pub fn readLineFromFile(allocator: std.mem.Allocator, path: []const u8, line: usize) !SourceLine {
|
||||
const file = try std.fs.openFileAbsolute(path, .{});
|
||||
var buf: [std.mem.page_size]u8 = undefined;
|
||||
|
||||
var count: usize = 1;
|
||||
var cursor: usize = 0;
|
||||
|
||||
seek: {
|
||||
while (true) {
|
||||
const bytes_read = try file.readAll(buf[0..]);
|
||||
for (buf[0..bytes_read]) |char| {
|
||||
if (char == '\n') count += 1;
|
||||
cursor += 1;
|
||||
if (count == line) {
|
||||
cursor += 1;
|
||||
break :seek;
|
||||
}
|
||||
}
|
||||
if (bytes_read < buf.len) return error.EndOfFile;
|
||||
}
|
||||
}
|
||||
|
||||
var size: usize = 0;
|
||||
try file.seekTo(cursor);
|
||||
read: {
|
||||
const bytes_read = try file.readAll(buf[0..]);
|
||||
if (std.mem.indexOf(u8, buf[0..bytes_read], "\n")) |index| {
|
||||
size += index;
|
||||
break :read;
|
||||
} else if (bytes_read < buf.len) {
|
||||
size += bytes_read;
|
||||
break :read;
|
||||
} else {
|
||||
while (true) {
|
||||
const more_bytes_read = try file.readAll(buf[0..]);
|
||||
if (std.mem.indexOf(u8, buf[0..more_bytes_read], "\n")) |index| {
|
||||
size += index;
|
||||
break :read;
|
||||
} else if (more_bytes_read < buf.len) {
|
||||
size += more_bytes_read;
|
||||
break :read;
|
||||
} else {
|
||||
size += more_bytes_read;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const line_content = try allocator.alloc(u8, size);
|
||||
try file.seekTo(cursor);
|
||||
const bytes_read = try file.readAll(line_content[0..]);
|
||||
|
||||
std.debug.assert(bytes_read == size);
|
||||
|
||||
return .{ .content = line_content[0..], .line = line };
|
||||
}
|
||||
|
||||
fn surroundingLinesFromFile(
|
||||
allocator: std.mem.Allocator,
|
||||
context: enum { previous, next },
|
||||
desired_count: usize,
|
||||
path: []const u8,
|
||||
target_line: usize,
|
||||
) ![]SourceLine {
|
||||
// This isn't very efficient but we only use it in debug mode so not a huge deal.
|
||||
const start = switch (context) {
|
||||
.previous => if (target_line > desired_count)
|
||||
target_line - desired_count
|
||||
else
|
||||
target_line,
|
||||
.next => target_line + 2,
|
||||
};
|
||||
|
||||
var lines = std.ArrayList(SourceLine).init(allocator);
|
||||
|
||||
switch (context) {
|
||||
.previous => {
|
||||
for (start..target_line) |line| {
|
||||
try lines.append(try readLineFromFile(allocator, path, line));
|
||||
}
|
||||
},
|
||||
.next => {
|
||||
for (0..desired_count, start..) |_, line| {
|
||||
try lines.append(try readLineFromFile(allocator, path, line));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return try lines.toOwnedSlice();
|
||||
}
|
||||
|
||||
pub const console_template =
|
||||
\\<!DOCTYPE html>
|
||||
\\<html>
|
||||
\\ <head>
|
||||
\\ <style>
|
||||
\\{3s}
|
||||
\\ </style>
|
||||
\\ </head>
|
||||
\\ <body>
|
||||
\\ <h1>Encountered Error: {0s}</h1>
|
||||
\\ <div class="stack-trace">
|
||||
\\ {1}
|
||||
\\ </div>
|
||||
\\ <h2>Response Data</h2>
|
||||
\\ <div class="response-data">
|
||||
\\ <pre><code class="language-json">{2s}</code></pre>
|
||||
\\ </div>
|
||||
\\ <script src="/_jetzig_debug.js"></script>
|
||||
\\ </body>
|
||||
\\</html>
|
||||
;
|
||||
|
||||
const surrounding_line_template =
|
||||
\\<div class='line-content surrounding'><pre class="line-number">{d: >4}</pre><pre><code class="language-zig">{s}</code></pre></div>
|
||||
\\
|
||||
;
|
||||
const target_line_template =
|
||||
\\<div class='line-content target'><pre class="line-number">{d: >4}</pre><pre><code class="language-zig">{s}</code></pre></div>
|
||||
\\
|
||||
;
|
@ -11,6 +11,16 @@ pub const Method = enum { DELETE, GET, PATCH, POST, HEAD, PUT, CONNECT, OPTIONS,
|
||||
pub const Modifier = enum { edit, new };
|
||||
pub const Format = enum { HTML, JSON, UNKNOWN };
|
||||
pub const Protocol = enum { http, https };
|
||||
pub const RequestState = enum {
|
||||
initial, // No processing has taken place
|
||||
processed, // Request headers have been processed
|
||||
after_request, // Initial middleware processing
|
||||
rendered, // Rendered by middleware or view
|
||||
redirected, // Redirected by middleware or view
|
||||
failed, // Failed by middleware or view
|
||||
before_response, // Post middleware processing
|
||||
finalized, // Response generated
|
||||
};
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
path: jetzig.http.Path,
|
||||
@ -30,19 +40,12 @@ parsed_multipart: ?*jetzig.data.Data = null,
|
||||
_cookies: ?*jetzig.http.Cookies = null,
|
||||
_session: ?*jetzig.http.Session = null,
|
||||
body: []const u8 = undefined,
|
||||
state: enum { initial, processed } = .initial,
|
||||
response_started: bool = false,
|
||||
state: RequestState = .initial,
|
||||
dynamic_assigned_template: ?[]const u8 = null,
|
||||
layout: ?[]const u8 = null,
|
||||
layout_disabled: bool = false,
|
||||
// TODO: Squash rendered/redirected/failed into
|
||||
// `state: enum { initial, rendered, redirected, failed }`
|
||||
rendered: bool = false,
|
||||
redirected: bool = false,
|
||||
failed: bool = false,
|
||||
redirect_state: ?RedirectState = null,
|
||||
middleware_rendered: ?struct { name: []const u8, action: []const u8 } = null,
|
||||
middleware_rendered_during_response: bool = false,
|
||||
middleware_data: jetzig.http.middleware.MiddlewareData = undefined,
|
||||
rendered_multiple: bool = false,
|
||||
rendered_view: ?jetzig.views.View = null,
|
||||
@ -177,6 +180,7 @@ pub fn respond(self: *Request) !void {
|
||||
const status = jetzig.http.status_codes.get(self.response.status_code);
|
||||
self.httpz_response.status = try status.getCodeInt();
|
||||
self.httpz_response.body = self.response.content;
|
||||
self.state = .finalized;
|
||||
}
|
||||
|
||||
/// Set the root value for response data.
|
||||
@ -191,10 +195,9 @@ pub fn data(self: Request, comptime root: @TypeOf(.enum_literal)) !*jetzig.Data.
|
||||
/// Render a response. This function can only be called once per request (repeat calls will
|
||||
/// trigger an error).
|
||||
pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
|
||||
if (self.rendered or self.failed) self.rendered_multiple = true;
|
||||
if (self.isRendered()) self.rendered_multiple = true;
|
||||
|
||||
self.rendered = true;
|
||||
if (self.response_started) self.middleware_rendered_during_response = true;
|
||||
self.state = .rendered;
|
||||
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
|
||||
return self.rendered_view.?;
|
||||
}
|
||||
@ -202,15 +205,20 @@ pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode)
|
||||
/// Render an error. This function can only be called once per request (repeat calls will
|
||||
/// trigger an error).
|
||||
pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
|
||||
if (self.rendered or self.redirected) self.rendered_multiple = true;
|
||||
if (self.isRendered()) self.rendered_multiple = true;
|
||||
|
||||
self.rendered = true;
|
||||
self.failed = true;
|
||||
if (self.response_started) self.middleware_rendered_during_response = true;
|
||||
self.state = .failed;
|
||||
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
|
||||
return self.rendered_view.?;
|
||||
}
|
||||
|
||||
pub inline fn isRendered(self: *const Request) bool {
|
||||
return switch (self.state) {
|
||||
.initial, .processed, .after_request, .before_response => false,
|
||||
.rendered, .redirected, .failed, .finalized => true,
|
||||
};
|
||||
}
|
||||
|
||||
/// Issue a redirect to a new location.
|
||||
/// ```zig
|
||||
/// return request.redirect("https://www.example.com/", .moved_permanently);
|
||||
@ -224,11 +232,9 @@ pub fn redirect(
|
||||
location: []const u8,
|
||||
redirect_status: enum { moved_permanently, found },
|
||||
) jetzig.views.View {
|
||||
if (self.rendered or self.failed) self.rendered_multiple = true;
|
||||
if (self.isRendered()) self.rendered_multiple = true;
|
||||
|
||||
self.rendered = true;
|
||||
self.redirected = true;
|
||||
if (self.response_started) self.middleware_rendered_during_response = true;
|
||||
self.state = .redirected;
|
||||
|
||||
const status_code = switch (redirect_status) {
|
||||
.moved_permanently => jetzig.http.status_codes.StatusCode.moved_permanently,
|
||||
@ -687,6 +693,19 @@ pub fn setTemplate(self: *Request, name: []const u8) void {
|
||||
self.dynamic_assigned_template = name;
|
||||
}
|
||||
|
||||
/// Set a custom template and render the response.
|
||||
/// ```zig
|
||||
/// return request.renderTemplate("blogs/comments/get", .ok);
|
||||
/// ```
|
||||
pub fn renderTemplate(
|
||||
self: *Request,
|
||||
name: []const u8,
|
||||
status_code: jetzig.http.StatusCode,
|
||||
) jetzig.views.View {
|
||||
self.dynamic_assigned_template = name;
|
||||
return self.render(status_code);
|
||||
}
|
||||
|
||||
pub fn joinPath(self: *const Request, args: anytype) ![]const u8 {
|
||||
const fields = std.meta.fields(@TypeOf(args));
|
||||
var buf: [fields.len][]const u8 = undefined;
|
||||
|
@ -138,7 +138,8 @@ pub fn processNextRequest(
|
||||
|
||||
var middleware_data = try jetzig.http.middleware.afterRequest(&request);
|
||||
|
||||
if (request.middleware_rendered) |_| { // Request processing ends when a middleware renders or redirects.
|
||||
if (request.middleware_rendered) |_| {
|
||||
// Request processing ends when a middleware renders or redirects.
|
||||
if (request.redirect_state) |state| {
|
||||
try request.renderRedirect(state);
|
||||
} else if (request.rendered_view) |rendered| {
|
||||
@ -150,8 +151,8 @@ pub fn processNextRequest(
|
||||
} else {
|
||||
try self.renderResponse(&request);
|
||||
try request.response.headers.append("Content-Type", response.content_type);
|
||||
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
|
||||
|
||||
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
|
||||
try request.respond();
|
||||
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
|
||||
jetzig.http.middleware.deinit(&middleware_data, &request);
|
||||
@ -197,9 +198,12 @@ fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
|
||||
for (route.before_callbacks) |callback| {
|
||||
try callback(request, route);
|
||||
if (request.rendered_view) |view| {
|
||||
if (request.failed) {
|
||||
request.setResponse(try self.renderError(request, view.status_code), .{});
|
||||
} else if (request.rendered) {
|
||||
if (request.state == .failed) {
|
||||
request.setResponse(
|
||||
try self.renderError(request, view.status_code, .{}),
|
||||
.{},
|
||||
);
|
||||
} else if (request.state == .rendered) {
|
||||
// TODO: Allow callbacks to set content
|
||||
}
|
||||
return;
|
||||
@ -259,7 +263,9 @@ fn renderHTML(
|
||||
return request.setResponse(rendered_error, .{});
|
||||
};
|
||||
|
||||
return if (request.redirected or request.failed or request.dynamic_assigned_template != null)
|
||||
return if (request.state == .redirected or
|
||||
request.state == .failed or
|
||||
request.dynamic_assigned_template != null)
|
||||
request.setResponse(rendered, .{})
|
||||
else
|
||||
request.setResponse(try self.renderNotFound(request), .{});
|
||||
@ -327,12 +333,12 @@ fn renderView(
|
||||
return try self.renderInternalServerError(request, @errorReturnTrace(), err);
|
||||
};
|
||||
|
||||
if (request.failed) {
|
||||
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);
|
||||
return try self.renderError(request, view.status_code, .{});
|
||||
}
|
||||
|
||||
const template: ?zmpl.Template = if (request.dynamic_assigned_template) |request_template|
|
||||
@ -343,7 +349,7 @@ fn renderView(
|
||||
if (request.rendered_multiple) return error.JetzigMultipleRenderError;
|
||||
|
||||
if (request.rendered_view) |rendered_view| {
|
||||
if (request.redirected) return .{ .view = rendered_view, .content = "" };
|
||||
if (request.state == .redirected) return .{ .view = rendered_view, .content = "" };
|
||||
|
||||
if (template) |capture| {
|
||||
return .{
|
||||
@ -352,12 +358,21 @@ fn renderView(
|
||||
};
|
||||
} else {
|
||||
return switch (request.requestFormat()) {
|
||||
.HTML, .UNKNOWN => try self.renderNotFound(request),
|
||||
.HTML, .UNKNOWN => blk: {
|
||||
try self.logger.DEBUG(
|
||||
"Missing template for route `{s}.{s}`. Expected: `src/app/views/{s}.zmpl`.",
|
||||
.{ route.view_name, @tagName(route.action), route.template },
|
||||
);
|
||||
if (comptime jetzig.build_options.debug_console) {
|
||||
return error.ZmplTemplateNotFound;
|
||||
}
|
||||
break :blk try self.renderNotFound(request);
|
||||
},
|
||||
.JSON => .{ .view = rendered_view, .content = "" },
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (!request.redirected) {
|
||||
if (request.state == .processed) {
|
||||
try self.logger.WARN("`request.render` was not invoked. Rendering empty content.", .{});
|
||||
}
|
||||
request.response_data.reset();
|
||||
@ -461,32 +476,42 @@ fn renderInternalServerError(
|
||||
stack_trace: ?*std.builtin.StackTrace,
|
||||
err: anyerror,
|
||||
) !RenderedView {
|
||||
request.response_data.reset();
|
||||
|
||||
try self.logger.logError(stack_trace, err);
|
||||
|
||||
const status = .internal_server_error;
|
||||
return try self.renderError(request, status);
|
||||
const status = jetzig.http.StatusCode.internal_server_error;
|
||||
|
||||
return try self.renderError(request, status, .{ .stack_trace = stack_trace, .err = err });
|
||||
}
|
||||
|
||||
fn renderNotFound(self: *Server, request: *jetzig.http.Request) !RenderedView {
|
||||
request.response_data.reset();
|
||||
|
||||
const status: jetzig.http.StatusCode = .not_found;
|
||||
return try self.renderError(request, status);
|
||||
return try self.renderError(request, status, .{});
|
||||
}
|
||||
|
||||
fn renderBadRequest(self: *Server, request: *jetzig.http.Request) !RenderedView {
|
||||
request.response_data.reset();
|
||||
|
||||
const status: jetzig.http.StatusCode = .bad_request;
|
||||
return try self.renderError(request, status);
|
||||
return try self.renderError(request, status, .{});
|
||||
}
|
||||
|
||||
fn renderError(
|
||||
self: Server,
|
||||
request: *jetzig.http.Request,
|
||||
status_code: jetzig.http.StatusCode,
|
||||
error_info: jetzig.debug.ErrorInfo,
|
||||
) !RenderedView {
|
||||
if (comptime jetzig.build_options.debug_console) {
|
||||
return try self.renderDebugConsole(request, status_code, error_info);
|
||||
} else return try self.renderGeneralError(request, status_code);
|
||||
}
|
||||
|
||||
fn renderGeneralError(
|
||||
self: Server,
|
||||
request: *jetzig.http.Request,
|
||||
status_code: jetzig.http.StatusCode,
|
||||
) !RenderedView {
|
||||
if (try self.renderErrorView(request, status_code)) |view| return view;
|
||||
if (try renderStaticErrorPage(request, status_code)) |view| return view;
|
||||
@ -494,6 +519,42 @@ fn renderError(
|
||||
return try renderDefaultError(request, status_code);
|
||||
}
|
||||
|
||||
fn renderDebugConsole(
|
||||
self: Server,
|
||||
request: *jetzig.http.Request,
|
||||
status_code: jetzig.http.StatusCode,
|
||||
error_info: jetzig.debug.ErrorInfo,
|
||||
) !RenderedView {
|
||||
if (comptime jetzig.build_options.debug_console) {
|
||||
var buf = std.ArrayList(u8).init(request.allocator);
|
||||
const writer = buf.writer();
|
||||
|
||||
if (error_info.stack_trace) |stack_trace| {
|
||||
const debug_content = jetzig.debug.HtmlStackTrace{
|
||||
.allocator = request.allocator,
|
||||
.stack_trace = stack_trace,
|
||||
};
|
||||
const error_name = if (error_info.err) |err| @errorName(err) else "[UnknownError]";
|
||||
try writer.print(
|
||||
jetzig.debug.console_template,
|
||||
.{
|
||||
error_name,
|
||||
debug_content,
|
||||
try request.response_data.toJsonOptions(.{ .pretty = true }),
|
||||
@embedFile("../../assets/debug.css"),
|
||||
},
|
||||
);
|
||||
} else return try self.renderGeneralError(request, status_code);
|
||||
|
||||
const content = try buf.toOwnedSlice();
|
||||
|
||||
return .{
|
||||
.view = .{ .data = request.response_data, .status_code = status_code },
|
||||
.content = if (content.len == 0) "" else content,
|
||||
};
|
||||
} else unreachable;
|
||||
}
|
||||
|
||||
fn renderErrorView(
|
||||
self: Server,
|
||||
request: *jetzig.http.Request,
|
||||
@ -641,6 +702,13 @@ const StaticResource = struct {
|
||||
};
|
||||
|
||||
fn matchStaticResource(self: *Server, request: *jetzig.http.Request) !?StaticResource {
|
||||
if (comptime jetzig.build_options.debug_console) {
|
||||
if (std.mem.eql(u8, request.path.path, "/_jetzig_debug.js")) return .{
|
||||
.content = @embedFile("../../assets/debug.js"),
|
||||
.mime_type = "text/javascript",
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Map public and static routes at launch to avoid accessing the file system when
|
||||
// matching any route - currently every request causes file system traversal.
|
||||
const public_resource = try self.matchPublicContent(request);
|
||||
|
@ -60,8 +60,11 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
|
||||
}
|
||||
}
|
||||
|
||||
request.state = .after_request;
|
||||
|
||||
inline for (middlewares, 0..) |middleware, index| {
|
||||
if (comptime !@hasDecl(middleware, "afterRequest")) continue;
|
||||
|
||||
if (comptime @hasDecl(middleware, "init")) {
|
||||
const data = middleware_data.get(index).?;
|
||||
try @call(
|
||||
@ -72,7 +75,8 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
|
||||
} else {
|
||||
try @call(.always_inline, middleware.afterRequest, .{request});
|
||||
}
|
||||
if (request.rendered or request.redirected) {
|
||||
|
||||
if (request.state != .after_request) {
|
||||
request.middleware_rendered = .{ .name = @typeName(middleware), .action = "afterRequest" };
|
||||
break;
|
||||
}
|
||||
@ -86,11 +90,11 @@ pub fn beforeResponse(
|
||||
middleware_data: *MiddlewareData,
|
||||
request: *jetzig.http.Request,
|
||||
) !void {
|
||||
request.response_started = true;
|
||||
request.state = .before_response;
|
||||
|
||||
inline for (middlewares, 0..) |middleware, index| {
|
||||
if (comptime !@hasDecl(middleware, "beforeResponse")) continue;
|
||||
if (!request.middleware_rendered_during_response) {
|
||||
if (request.state == .before_response) {
|
||||
if (comptime @hasDecl(middleware, "init")) {
|
||||
const data = middleware_data.get(index).?;
|
||||
try @call(
|
||||
@ -99,11 +103,19 @@ pub fn beforeResponse(
|
||||
.{ @as(*middleware, @ptrCast(@alignCast(data))), request, request.response },
|
||||
);
|
||||
} else {
|
||||
try @call(.always_inline, middleware.beforeResponse, .{ request, request.response });
|
||||
try @call(
|
||||
.always_inline,
|
||||
middleware.beforeResponse,
|
||||
.{ request, request.response },
|
||||
);
|
||||
}
|
||||
}
|
||||
if (request.middleware_rendered_during_response) {
|
||||
request.middleware_rendered = .{ .name = @typeName(middleware), .action = "beforeResponse" };
|
||||
|
||||
if (request.state != .before_response) {
|
||||
request.middleware_rendered = .{
|
||||
.name = @typeName(middleware),
|
||||
.action = "beforeResponse",
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request)
|
||||
if (request.middleware_rendered) |middleware| middleware.action else "",
|
||||
if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.white ++ ":" else "",
|
||||
if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.bright_cyan else "",
|
||||
if (request.middleware_rendered) |_| if (request.redirected) "redirect" else "render" else "",
|
||||
if (request.middleware_rendered) |_| @tagName(request.state) else "",
|
||||
if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.reset else "",
|
||||
if (request.middleware_rendered) |_| "]" else "",
|
||||
request.path.path,
|
||||
|
@ -86,7 +86,7 @@ pub fn logRequest(self: ProductionLogger, request: *const jetzig.http.Request) !
|
||||
if (request.middleware_rendered) |_| ":" else "",
|
||||
if (request.middleware_rendered) |middleware| middleware.action else "",
|
||||
if (request.middleware_rendered) |_| ":" else "",
|
||||
if (request.middleware_rendered) |_| if (request.redirected) "redirect" else "render" else "",
|
||||
if (request.middleware_rendered) |_| @tagName(request.state) else "",
|
||||
if (request.middleware_rendered) |_| "]" else "",
|
||||
request.path.path,
|
||||
}, .stdout);
|
||||
|
@ -20,7 +20,11 @@ pub fn afterRequest(request: *jetzig.http.Request) !void {
|
||||
/// If a redirect was issued during request processing, reset any response data, set response
|
||||
/// status to `200 OK` and replace the `Location` header with a `HX-Redirect` header.
|
||||
pub fn beforeResponse(request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
|
||||
if (response.status_code != .moved_permanently and response.status_code != .found) return;
|
||||
switch (response.status_code) {
|
||||
.moved_permanently, .found => {},
|
||||
else => return,
|
||||
}
|
||||
|
||||
if (request.headers.get("HX-Request") == null) return;
|
||||
|
||||
if (response.headers.get("Location")) |location| {
|
||||
|
Loading…
x
Reference in New Issue
Block a user