diff --git a/.gitignore b/.gitignore
index 638a023..9284511 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
zig-out/
zig-cache/
*.core
-static/
.jetzig
.zig-cache/
diff --git a/build.zig b/build.zig
index 282c6ea..d174ef4 100644
--- a/build.zig
+++ b/build.zig
@@ -201,11 +201,14 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
exe_static_routes.root_module.addImport("routes", routes_module);
exe_static_routes.root_module.addImport("jetzig", jetzig_module);
exe_static_routes.root_module.addImport("zmpl", zmpl_module);
- exe_static_routes.root_module.addImport("jetzig_app", &exe.root_module);
+ // exe_static_routes.root_module.addImport("jetzig_app", &exe.root_module);
const run_static_routes_cmd = b.addRunArtifact(exe_static_routes);
+ const static_outputs_path = run_static_routes_cmd.addOutputFileArg("static.zig");
+ const static_module = b.createModule(.{ .root_source_file = static_outputs_path });
+ exe.root_module.addImport("static", static_module);
+
run_static_routes_cmd.expectExitCode(0);
- exe.step.dependOn(&run_static_routes_cmd.step);
const exe_unit_tests = b.addTest(.{
.root_source_file = tests_file,
@@ -214,10 +217,13 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
.test_runner = jetzig_dep.path("src/test_runner.zig"),
});
exe_unit_tests.root_module.addImport("jetzig", jetzig_module);
+ exe_unit_tests.root_module.addImport("static", static_module);
exe_unit_tests.root_module.addImport("__jetzig_project", &exe.root_module);
var it = exe.root_module.import_table.iterator();
while (it.next()) |import| {
+ if (std.mem.eql(u8, import.key_ptr.*, "static")) continue;
+
routes_module.addImport(import.key_ptr.*, import.value_ptr.*);
exe_static_routes.root_module.addImport(import.key_ptr.*, import.value_ptr.*);
exe_unit_tests.root_module.addImport(import.key_ptr.*, import.value_ptr.*);
@@ -226,14 +232,14 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
const test_step = b.step("jetzig:test", "Run tests");
- test_step.dependOn(&run_exe_unit_tests.step);
test_step.dependOn(&run_static_routes_cmd.step);
+ test_step.dependOn(&run_exe_unit_tests.step);
exe_unit_tests.root_module.addImport("routes", routes_module);
const routes_step = b.step("jetzig:routes", "List all routes in your app");
const exe_routes = b.addExecutable(.{
.name = "routes",
- .root_source_file = jetzig_dep.path("src/routes.zig"),
+ .root_source_file = jetzig_dep.path("src/routes_exe.zig"),
.target = target,
.optimize = optimize,
});
diff --git a/build.zig.zon b/build.zig.zon
index 3d74cb5..4cb7c84 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -7,8 +7,8 @@
.hash = "12203b56c2e17a2fd62ea3d3d9be466f43921a3aef88b381cf58f41251815205fdb5",
},
.zmpl = .{
- .url = "https://github.com/jetzig-framework/zmpl/archive/7dac63a9470cf12a2cbaf8e6e3fc3aeb9ea27658.tar.gz",
- .hash = "1220215354adcea94d36d25c300e291981d3f48c543f7da6e48bb2793a5aed85e768",
+ .url = "https://github.com/jetzig-framework/zmpl/archive/676969d44fe1c3adabd73af983f33aefd8aed1b9.tar.gz",
+ .hash = "1220a6cce7678578e0176796c19d3810cdd3a9c1b0792a3731a8cd25d2aee96bd78a",
},
.jetkv = .{
.url = "https://github.com/jetzig-framework/jetkv/archive/78bcdcc6b0cbd3ca808685c64554a15701f13250.tar.gz",
diff --git a/demo/src/app/views/root.zig b/demo/src/app/views/root.zig
index be8af82..f868587 100644
--- a/demo/src/app/views/root.zig
+++ b/demo/src/app/views/root.zig
@@ -7,9 +7,9 @@ pub const layout = "application";
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
var root = try data.object();
- try root.put("message", data.string("Welcome to Jetzig!"));
- try root.put("custom_number", data.integer(customFunction(100, 200, 300)));
- try root.put("imported_number", data.integer(importedFunction(100, 200, 300)));
+ try root.put("message", "Welcome to Jetzig!");
+ try root.put("custom_number", customFunction(100, 200, 300));
+ try root.put("imported_number", importedFunction(100, 200, 300));
try request.response.headers.append("x-example-header", "example header value");
diff --git a/demo/src/app/views/static.zig b/demo/src/app/views/static.zig
index fa29d85..c2b2ec7 100644
--- a/demo/src/app/views/static.zig
+++ b/demo/src/app/views/static.zig
@@ -37,8 +37,8 @@ pub const static_params = .{
.{ .params = .{ .foo = "hello", .bar = "goodbye" } },
},
.get = .{
- .{ .id = "1", .params = .{ .foo = "hi", .bar = "bye" } },
- .{ .id = "2", .params = .{ .foo = "hello", .bar = "goodbye" } },
+ .{ .id = "123", .params = .{ .foo = "hi", .bar = "bye" } },
+ .{ .id = "456", .params = .{ .foo = "hello", .bar = "goodbye" } },
},
};
@@ -58,8 +58,10 @@ pub fn get(id: []const u8, request: *jetzig.StaticRequest, data: *jetzig.Data) !
const params = try request.params();
- if (std.mem.eql(u8, id, "1")) {
- try root.put("id", data.string("id is '1'"));
+ if (std.mem.eql(u8, id, "123")) {
+ try root.put("message", "id is '123'");
+ } else {
+ try root.put("message", "id is not '123'");
}
if (params.get("foo")) |foo| try root.put("foo", foo);
@@ -89,12 +91,12 @@ test "get json" {
const response = try app.request(
.GET,
- "/static/1.json",
+ "/static/123.json",
.{ .json = .{ .foo = "hi", .bar = "bye" } },
);
try response.expectStatus(.ok);
- try response.expectJson(".id", "id is '1'");
+ try response.expectJson(".message", "id is '123'");
}
test "index html" {
@@ -108,7 +110,8 @@ test "index html" {
);
try response.expectStatus(.ok);
- try response.expectBodyContains("hello");
+ try response.expectBodyContains("foo: hello");
+ try response.expectBodyContains("bar: goodbye");
}
test "get html" {
@@ -117,7 +120,7 @@ test "get html" {
const response = try app.request(
.GET,
- "/static/1.html",
+ "/static/123.html",
.{ .params = .{ .foo = "hi", .bar = "bye" } },
);
diff --git a/demo/src/app/views/static/get.zmpl b/demo/src/app/views/static/get.zmpl
index e977e38..7fa60eb 100644
--- a/demo/src/app/views/static/get.zmpl
+++ b/demo/src/app/views/static/get.zmpl
@@ -1,7 +1,7 @@
- You made a request to /static/{.id} with:
- foo: {.foo}
- bar: {.bar}
+ {{.message}}
+ foo: {{.foo}}
+ bar: {{.bar}}
diff --git a/demo/src/main.zig b/demo/src/main.zig
index 0d5fe94..d835b8c 100644
--- a/demo/src/main.zig
+++ b/demo/src/main.zig
@@ -5,6 +5,7 @@ const jetzig = @import("jetzig");
const zmd = @import("zmd");
pub const routes = @import("routes");
+pub const static = @import("static");
// Override default settings in `jetzig.config` here:
pub const jetzig_options = struct {
diff --git a/src/Routes.zig b/src/Routes.zig
index 963f25c..eb76b4a 100644
--- a/src/Routes.zig
+++ b/src/Routes.zig
@@ -267,6 +267,7 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
const output_template =
\\ .{{
+ \\ .id = "{9s}",
\\ .name = "{0s}",
\\ .action = .{1s},
\\ .view_name = "{2s}",
@@ -296,6 +297,8 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
std.mem.replaceScalar(u8, module_path, '\\', '/');
try self.module_paths.append(try self.allocator.dupe(u8, module_path));
+ var buf: [32]u8 = undefined;
+ const id = jetzig.util.generateVariableName(&buf);
const output = try std.fmt.allocPrint(self.allocator, output_template, .{
full_name,
route.name,
@@ -306,6 +309,7 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
template,
module_path,
try std.mem.join(self.allocator, ", \n", route.params.items),
+ id,
});
defer self.allocator.free(output);
diff --git a/src/compile_static_routes.zig b/src/compile_static_routes.zig
index 70197c0..da9bcec 100644
--- a/src/compile_static_routes.zig
+++ b/src/compile_static_routes.zig
@@ -2,7 +2,7 @@ const std = @import("std");
const jetzig = @import("jetzig");
const routes = @import("routes").routes;
const zmpl = @import("zmpl");
-const jetzig_options = @import("jetzig_app").jetzig_options;
+// const jetzig_options = @import("jetzig_app").jetzig_options;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
@@ -13,14 +13,27 @@ pub fn main() !void {
const allocator = arena.allocator();
defer arena.deinit();
- try compileStaticRoutes(allocator);
+ var it = try std.process.argsWithAllocator(allocator);
+ var index: usize = 0;
+ while (it.next()) |arg| : (index += 1) {
+ if (index == 0) continue;
+ const file = try std.fs.createFileAbsolute(arg, .{});
+ const writer = file.writer();
+ try compileStaticRoutes(allocator, writer);
+ file.close();
+ break;
+ }
}
-fn compileStaticRoutes(allocator: std.mem.Allocator) !void {
- std.fs.cwd().deleteTree("static") catch {};
-
+fn compileStaticRoutes(allocator: std.mem.Allocator, writer: anytype) !void {
var count: usize = 0;
+ try writer.writeAll(
+ \\const StaticOutput = struct { json: ?[]const u8 = null, html: ?[]const u8 = null, params: ?[]const u8 };
+ \\const Compiled = struct { route_id: []const u8, output: StaticOutput };
+ \\pub const compiled = [_]Compiled{
+ \\
+ );
for (routes) |route| {
if (!route.static) continue;
@@ -28,7 +41,7 @@ fn compileStaticRoutes(allocator: std.mem.Allocator) !void {
for (route.json_params, 0..) |json, index| {
var request = try jetzig.http.StaticRequest.init(allocator, json);
defer request.deinit();
- try writeContent(allocator, route, &request, index, &count);
+ try writeContent(allocator, writer, route, &request, index, &count, json);
}
}
@@ -38,20 +51,27 @@ fn compileStaticRoutes(allocator: std.mem.Allocator) !void {
.index, .post => {
var request = try jetzig.http.StaticRequest.init(allocator, "{}");
defer request.deinit();
- try writeContent(allocator, route, &request, null, &count);
+ try writeContent(allocator, writer, route, &request, null, &count, null);
},
inline else => {},
}
}
+
+ try writer.writeAll(
+ \\};
+ \\
+ );
std.debug.print("[jetzig] Compiled {} static output(s)\n", .{count});
}
fn writeContent(
allocator: std.mem.Allocator,
+ writer: anytype,
route: jetzig.views.Route,
request: *jetzig.http.StaticRequest,
index: ?usize,
count: *usize,
+ params_json: ?[]const u8,
) !void {
const index_suffix = if (index) |capture|
try std.fmt.allocPrint(allocator, "_{}", .{capture})
@@ -62,35 +82,28 @@ fn writeContent(
const view = try route.renderStatic(route, request);
defer view.deinit();
- var dir = try std.fs.cwd().makeOpenPath("static", .{});
- defer dir.close();
-
- const json_path = try std.mem.concat(
- allocator,
- u8,
- &[_][]const u8{ route.name, index_suffix, ".json" },
- );
- defer allocator.free(json_path);
-
- const json_file = try dir.createFile(json_path, .{ .truncate = true });
- try json_file.writeAll(try view.data.toJson());
- defer json_file.close();
-
count.* += 1;
const html_content = try renderZmplTemplate(allocator, route, view) orelse
try renderMarkdown(allocator, route, view) orelse
null;
- const html_path = try std.mem.concat(
- allocator,
- u8,
- &[_][]const u8{ route.name, index_suffix, ".html" },
+
+ try writer.print(
+ \\.{{ .route_id = "{s}", .output = StaticOutput{{ .json = "{s}", .html = "{s}", .params = {s}{s}{s} }} }},
+ \\
+ \\
+ ,
+ .{
+ route.id,
+ try zigEscape(allocator, try view.data.toJson()),
+ try zigEscape(allocator, html_content orelse ""),
+ if (params_json) |_| "\"" else "",
+ if (params_json) |params| try zigEscape(allocator, params) else "null",
+ if (params_json) |_| "\"" else "",
+ },
);
+
if (html_content) |content| {
- defer allocator.free(html_path);
- const html_file = try dir.createFile(html_path, .{ .truncate = true });
- try html_file.writeAll(content);
- defer html_file.close();
allocator.free(content);
count.* += 1;
}
@@ -101,10 +114,7 @@ fn renderMarkdown(
route: jetzig.views.Route,
view: jetzig.views.View,
) !?[]const u8 {
- const fragments = if (@hasDecl(jetzig_options, "markdown_fragments"))
- jetzig_options.markdown_fragments
- else
- null;
+ const fragments = 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;
@@ -153,3 +163,10 @@ fn renderZmplTemplate(
}
} else return null;
}
+
+fn zigEscape(allocator: std.mem.Allocator, content: []const u8) ![]const u8 {
+ var buf = std.ArrayList(u8).init(allocator);
+ const writer = buf.writer();
+ try std.zig.stringEscape(content, "", .{}, writer);
+ return try buf.toOwnedSlice();
+}
diff --git a/src/jetzig.zig b/src/jetzig.zig
index 70ab899..75cc24d 100644
--- a/src/jetzig.zig
+++ b/src/jetzig.zig
@@ -64,7 +64,7 @@ pub const MailerDefinition = mail.MailerDefinition;
/// `ERROR`, etc.). Note that all log functions are CAPITALIZED.
pub const Logger = loggers.Logger;
-const root = @import("root");
+pub const root = @import("root");
/// Global configuration. Override these values by defining in `src/main.zig` with:
/// ```zig
diff --git a/src/jetzig/App.zig b/src/jetzig/App.zig
index a71bb3a..89ac3dc 100644
--- a/src/jetzig/App.zig
+++ b/src/jetzig/App.zig
@@ -159,6 +159,7 @@ pub fn route(
std.mem.replaceScalar(u8, &view_name, '.', '/');
self.custom_routes.append(.{
+ .id = "custom",
.name = member,
.action = .custom,
.method = method,
@@ -196,6 +197,7 @@ pub fn createRoutes(
for (comptime_routes) |const_route| {
var var_route = try allocator.create(jetzig.views.Route);
var_route.* = .{
+ .id = const_route.id,
.name = const_route.name,
.action = const_route.action,
.view_name = const_route.view_name,
diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig
index c341dc1..3d437fb 100644
--- a/src/jetzig/http/Server.zig
+++ b/src/jetzig/http/Server.zig
@@ -29,6 +29,7 @@ initialized: bool = false,
store: *jetzig.kv.Store,
job_queue: *jetzig.kv.Store,
cache: *jetzig.kv.Store,
+decoded_static_route_params: []*jetzig.data.Value = &.{},
const Server = @This();
@@ -76,6 +77,8 @@ const Dispatcher = struct {
};
pub fn listen(self: *Server) !void {
+ try self.decodeStaticParams();
+
var httpz_server = try httpz.ServerCtx(Dispatcher, Dispatcher).init(
self.allocator,
.{
@@ -251,7 +254,7 @@ fn renderJSON(
if (data.value) |_| {} else _ = try data.object();
rendered.content = if (self.options.environment == .development)
- try data.toPrettyJson()
+ try data.toJsonOptions(.{ .pretty = true, .color = false })
else
try data.toJson();
@@ -618,72 +621,72 @@ fn matchPublicContent(self: *Server, request: *jetzig.http.Request) !?StaticReso
}
fn matchStaticContent(self: *Server, request: *jetzig.http.Request) !?[]const u8 {
- var static_dir = std.fs.cwd().openDir("static", .{}) catch |err| {
- switch (err) {
- error.FileNotFound => return null,
- else => return err,
- }
- };
- defer static_dir.close();
-
+ const request_format = request.requestFormat();
const matched_route = try self.matchRoute(request, true);
+ const params = try request.params();
if (matched_route) |route| {
- const static_path = try staticPath(request, route);
+ if (@hasDecl(jetzig.root, "static")) {
+ inline for (jetzig.root.static.compiled, 0..) |static_output, index| {
+ if (!@hasField(@TypeOf(static_output), "route_id")) continue;
- if (static_path) |capture| {
- return static_dir.readFileAlloc(
- request.allocator,
- capture,
- jetzig.config.get(usize, "max_bytes_static_content"),
- ) catch |err| {
- switch (err) {
- error.FileNotFound => return null,
- else => return err,
+ if (std.mem.eql(u8, static_output.route_id, route.id)) {
+ if (index < self.decoded_static_route_params.len) {
+ if (matchStaticOutput(
+ self.decoded_static_route_params[index].getT(.string, "id"),
+ self.decoded_static_route_params[index].get("params"),
+ route,
+ request,
+ params,
+ )) return switch (request_format) {
+ .HTML, .UNKNOWN => static_output.output.html,
+ .JSON => static_output.output.json,
+ };
+ } else {
+ return switch (request_format) {
+ .HTML, .UNKNOWN => static_output.output.html,
+ .JSON => static_output.output.json,
+ };
+ }
}
- };
- } else return null;
+ }
+ } else {
+ return null;
+ }
}
return null;
}
-fn staticPath(request: *jetzig.http.Request, route: jetzig.views.Route) !?[]const u8 {
- const params = try request.params();
- defer params.deinit();
-
- const extension = switch (request.requestFormat()) {
- .HTML, .UNKNOWN => ".html",
- .JSON => ".json",
- };
-
- for (route.params.items, 0..) |static_params, index| {
- const expected_params = static_params.get("params");
- switch (route.action) {
- .index, .post => {},
- inline else => {
- const id = static_params.getT(.string, "id") orelse return error.JetzigRouteError;
- if (!std.mem.eql(u8, id, request.path.resource_id)) continue;
- },
+pub fn decodeStaticParams(self: *Server) !void {
+ // Store decoded static params (i.e. declared in views) for faster comparison at request time.
+ var decoded = std.ArrayList(*jetzig.data.Value).init(self.allocator);
+ for (jetzig.root.static.compiled) |compiled| {
+ if (compiled.output.params) |params| {
+ const data = try self.allocator.create(jetzig.data.Data);
+ data.* = jetzig.data.Data.init(self.allocator);
+ try data.fromJson(params);
+ try decoded.append(data.value.?);
}
- if (expected_params != null and !expected_params.?.eql(params)) continue;
-
- const index_fmt = try std.fmt.allocPrint(request.allocator, "{}", .{index});
- defer request.allocator.free(index_fmt);
-
- return try std.mem.concat(
- request.allocator,
- u8,
- &[_][]const u8{ route.name, "_", index_fmt, extension },
- );
}
- switch (route.action) {
- .index, .post => return try std.mem.concat(
- request.allocator,
- u8,
- &[_][]const u8{ route.name, extension },
- ),
- else => return null,
- }
+ self.decoded_static_route_params = try decoded.toOwnedSlice();
+}
+
+fn matchStaticOutput(
+ maybe_id: ?[]const u8,
+ maybe_params: ?*jetzig.data.Value,
+ route: jetzig.views.Route,
+ request: *const jetzig.http.Request,
+ params: *jetzig.data.Value,
+) bool {
+ return if (maybe_params) |expected_params| blk: {
+ break :blk switch (route.action) {
+ .index, .post => expected_params.count() == 0 or expected_params.eql(params),
+ inline else => if (maybe_id) |id|
+ std.mem.eql(u8, id, request.path.resource_id) and expected_params.eql(params)
+ else
+ false,
+ };
+ } else if (maybe_id) |id| std.mem.eql(u8, id, request.path.resource_id) else maybe_params == null;
}
diff --git a/src/jetzig/testing.zig b/src/jetzig/testing.zig
index ff6482d..d1c2459 100644
--- a/src/jetzig/testing.zig
+++ b/src/jetzig/testing.zig
@@ -12,11 +12,52 @@ const testing = @This();
/// Pre-built mime map, assigned by Jetzig test runner.
pub var mime_map: *jetzig.http.mime.MimeMap = undefined;
pub var state: enum { initial, ready } = .initial;
+pub var logger: Logger = undefined;
pub const secret = "secret-bytes-for-use-in-test-environment-only";
pub const app = App.init;
+pub const Logger = struct {
+ allocator: std.mem.Allocator,
+ logs: std.AutoHashMap(usize, *LogCollection),
+ index: usize = 0,
+
+ pub const LogEvent = struct {
+ level: std.log.Level,
+ output: []const u8,
+ };
+
+ const LogCollection = std.ArrayList(LogEvent);
+
+ pub fn init(allocator: std.mem.Allocator) Logger {
+ return .{
+ .allocator = allocator,
+ .logs = std.AutoHashMap(usize, *LogCollection).init(allocator),
+ };
+ }
+
+ pub fn log(
+ self: *Logger,
+ comptime message_level: std.log.Level,
+ comptime scope: @Type(.EnumLiteral),
+ comptime format: []const u8,
+ args: anytype,
+ ) void {
+ _ = scope;
+ const output = std.fmt.allocPrint(self.allocator, format, args) catch @panic("OOM");
+ const log_event: LogEvent = .{ .level = message_level, .output = output };
+ if (self.logs.get(self.index)) |*item| {
+ item.*.append(log_event) catch @panic("OOM");
+ } else {
+ const array = self.allocator.create(LogCollection) catch @panic("OOM");
+ array.* = LogCollection.init(self.allocator);
+ array.append(log_event) catch @panic("OOM");
+ self.logs.put(self.index, array) catch @panic("OOM");
+ }
+ }
+};
+
pub const TestResponse = struct {
allocator: std.mem.Allocator,
status: u16,
@@ -56,15 +97,22 @@ pub fn expectStatus(comptime expected: jetzig.http.status_codes.StatusCode, resp
const expected_code = try jetzig.http.status_codes.get(expected).getCodeInt();
if (response.status != expected_code) {
- log("Expected status: `{}`, actual status: `{}`", .{ expected_code, response.status });
+ logFailure(
+ "Expected status: " ++ jetzig.colors.green("{}") ++ ", actual status: " ++ jetzig.colors.red("{}"),
+ .{ expected_code, response.status },
+ );
return error.JetzigExpectStatusError;
}
}
pub fn expectBodyContains(expected: []const u8, response: TestResponse) !void {
if (!std.mem.containsAtLeast(u8, response.body, 1, expected)) {
- log(
- "Expected content:\n========\n{s}\n========\n\nActual content:\n========\n{s}\n========",
+ logFailure(
+ "\nExpected content:\n" ++
+ jetzig.colors.red("{s}") ++
+ "\n\nActual content:\n" ++
+ jetzig.colors.green("{s}") ++
+ "\n",
.{ expected, response.body },
);
return error.JetzigExpectBodyContainsError;
@@ -94,14 +142,14 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
data.fromJson(response.body) catch |err| {
switch (err) {
error.SyntaxError => {
- log("Expected JSON, encountered parser error.", .{});
+ logFailure("Expected JSON, encountered parser error.", .{});
return error.JetzigExpectJsonError;
},
else => return err,
}
};
- const json_banner = "\n======|json|======\n{s}\n======|/json|=====\n";
+ const json_banner = "\n{s}";
if (try data.getValue(std.mem.trimLeft(u8, expected_path, &.{'.'}))) |value| {
switch (value.*) {
@@ -110,8 +158,8 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
if (std.mem.eql(u8, string.value, expected_value)) return;
},
.Null => {
- log(
- "Expected null/non-existent value for `{s}`, found: `{s}`",
+ logFailure(
+ "Expected null/non-existent value for " ++ jetzig.colors.cyan("{s}") ++ ", found: " ++ jetzig.colors.cyan("{s}"),
.{ expected_path, string.value },
);
return error.JetzigExpectJsonError;
@@ -123,8 +171,8 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
if (integer.value == expected_value) return;
},
.Null => {
- log(
- "Expected null/non-existent value for `{s}`, found: `{}`",
+ logFailure(
+ "Expected null/non-existent value for " ++ jetzig.colors.cyan("{s}") ++ ", found: " ++ jetzig.colors.green("{}"),
.{ expected_path, integer.value },
);
return error.JetzigExpectJsonError;
@@ -136,8 +184,8 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
if (float.value == expected_value) return;
},
.Null => {
- log(
- "Expected null/non-existent value for `{s}`, found: `{}`",
+ logFailure(
+ "Expected null/non-existent value for " ++ jetzig.colors.cyan("{s}") ++ ", found: " ++ jetzig.colors.green("{}"),
.{ expected_path, float.value },
);
return error.JetzigExpectJsonError;
@@ -149,8 +197,8 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
if (boolean.value == expected_value) return;
},
.Null => {
- log(
- "Expected null/non-existent value for `{s}`, found: `{}`",
+ logFailure(
+ "Expected null/non-existent value for " ++ jetzig.colors.cyan("{s}") ++ ", found: " ++ jetzig.colors.green("{}"),
.{ expected_path, boolean.value },
);
return error.JetzigExpectJsonError;
@@ -173,9 +221,9 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
.string => |string| {
switch (@typeInfo(@TypeOf(expected_value))) {
.Pointer, .Array => {
- log(
- "Expected `{s}` in `{s}`, found `{s}` in JSON:" ++ json_banner,
- .{ expected_value, expected_path, string.value, response.body },
+ logFailure(
+ "Expected \"" ++ jetzig.colors.red("{s}") ++ "\" in " ++ jetzig.colors.cyan("{s}") ++ ", found \"" ++ jetzig.colors.green("{s}") ++ "\nJSON:" ++ json_banner,
+ .{ expected_value, expected_path, string.value, try jsonPretty(response) },
);
},
else => unreachable,
@@ -185,9 +233,10 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
=> |integer| {
switch (@typeInfo(@TypeOf(expected_value))) {
.Int, .ComptimeInt => {
- log(
- "Expected `{}` in `{s}`, found `{}` in JSON:" ++ json_banner,
- .{ expected_value, expected_path, integer.value, response.body },
+ logFailure(
+ "Expected " ++ jetzig.colors.red("{}") ++ " in " ++ jetzig.colors.cyan("{s}") ++ ", found " ++ jetzig.colors.green("{}") ++ "\nJSON:" ++ json_banner,
+
+ .{ expected_value, expected_path, integer.value, try jsonPretty(response) },
);
},
else => unreachable,
@@ -196,9 +245,9 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
.float => |float| {
switch (@typeInfo(@TypeOf(expected_value))) {
.Float, .ComptimeFloat => {
- log(
- "Expected `{}` in `{s}`, found `{}` in JSON:" ++ json_banner,
- .{ expected_value, expected_path, float.value, response.body },
+ logFailure(
+ "Expected " ++ jetzig.colors.red("{}") ++ " in " ++ jetzig.colors.cyan("{s}") ++ ", found " ++ jetzig.colors.green("{}") ++ "\nJSON:" ++ json_banner,
+ .{ expected_value, expected_path, float.value, try jsonPretty(response) },
);
},
else => unreachable,
@@ -207,26 +256,26 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
.boolean => |boolean| {
switch (@typeInfo(@TypeOf(expected_value))) {
.Bool => {
- log(
- "Expected `{}` in `{s}`, found `{}` in JSON:" ++ json_banner,
- .{ expected_value, expected_path, boolean.value, response.body },
+ logFailure(
+ "Expected " ++ jetzig.colors.red("{}") ++ " in " ++ jetzig.colors.cyan("{s}") ++ ", found " ++ jetzig.colors.green("{}") ++ "\nJSON:" ++ json_banner,
+ .{ expected_value, expected_path, boolean.value, try jsonPretty(response) },
);
},
else => unreachable,
}
},
.Null => {
- log(
- "Expected value in `{s}`, found `null` in JSON:" ++ json_banner,
- .{ expected_path, response.body },
+ logFailure(
+ "Expected value in " ++ jetzig.colors.cyan("{s}") ++ ", found " ++ jetzig.colors.green("null") ++ "\nJSON:" ++ json_banner,
+ .{ expected_path, try jsonPretty(response) },
);
},
else => unreachable,
}
} else {
- log(
- "Path not found: `{s}` in JSON: " ++ json_banner,
- .{ expected_path, response.body },
+ logFailure(
+ "Path not found: `{s}`\nJSON: " ++ json_banner,
+ .{ expected_path, try jsonPretty(response) },
);
}
return error.JetzigExpectJsonError;
@@ -244,6 +293,18 @@ pub fn expectJob(job_name: []const u8, job_params: anytype, response: TestRespon
return error.JetzigExpectJobError;
}
-fn log(comptime message: []const u8, args: anytype) void {
- std.debug.print("[jetzig.testing] " ++ message ++ "\n", args);
+// fn log(comptime message: []const u8, args: anytype) void {
+// std.log.info("[jetzig.testing] " ++ message ++ "\n", args);
+// }
+
+fn logFailure(comptime message: []const u8, args: anytype) void {
+ std.log.err(message, args);
+}
+
+fn jsonPretty(response: TestResponse) ![]const u8 {
+ var data = jetzig.data.Data.init(response.allocator);
+ defer data.deinit();
+
+ try data.fromJson(response.body);
+ return try data.toJsonOptions(.{ .pretty = true, .color = true });
}
diff --git a/src/jetzig/testing/App.zig b/src/jetzig/testing/App.zig
index 6acb53c..e6d8446 100644
--- a/src/jetzig/testing/App.zig
+++ b/src/jetzig/testing/App.zig
@@ -90,6 +90,8 @@ pub fn request(
.job_queue = self.job_queue,
};
+ try server.decodeStaticParams();
+
var buf: [1024]u8 = undefined;
var httpz_request = try stubbedRequest(allocator, &buf, method, path, options);
var httpz_response = try stubbedResponse(allocator);
diff --git a/src/jetzig/views/Route.zig b/src/jetzig/views/Route.zig
index 94d1d52..b8d4efa 100644
--- a/src/jetzig/views/Route.zig
+++ b/src/jetzig/views/Route.zig
@@ -60,6 +60,7 @@ layout: ?[]const u8 = null,
template: []const u8,
json_params: []const []const u8,
params: std.ArrayList(*jetzig.data.Data) = undefined,
+id: []const u8,
/// 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()`).
diff --git a/src/routes.zig b/src/routes_exe.zig
similarity index 100%
rename from src/routes.zig
rename to src/routes_exe.zig
diff --git a/src/test_runner.zig b/src/test_runner.zig
index 086762c..ddd3cf4 100644
--- a/src/test_runner.zig
+++ b/src/test_runner.zig
@@ -1,6 +1,20 @@
const std = @import("std");
const builtin = @import("builtin");
const jetzig = @import("jetzig");
+pub const static = @import("static");
+
+pub const std_options = .{
+ .logFn = log,
+};
+
+pub fn log(
+ comptime message_level: std.log.Level,
+ comptime scope: @Type(.EnumLiteral),
+ comptime format: []const u8,
+ args: anytype,
+) void {
+ jetzig.testing.logger.log(message_level, scope, format, args);
+}
const Test = struct {
name: []const u8,
@@ -8,7 +22,7 @@ const Test = struct {
module: ?[]const u8 = null,
leaked: bool = false,
result: Result = .success,
- stack_trace_buf: [4096]u8 = undefined,
+ stack_trace_buf: [8192]u8 = undefined,
duration: usize = 0,
pub const TestFn = *const fn () anyerror!void;
@@ -36,7 +50,7 @@ const Test = struct {
.{ .function = test_fn.func, .name = test_fn.name };
}
- pub fn run(self: *Test) !void {
+ pub fn run(self: *Test, allocator: std.mem.Allocator) !void {
std.testing.allocator_instance = .{};
const start = std.time.nanoTimestamp();
@@ -45,7 +59,10 @@ const Test = struct {
error.SkipZigTest => self.result = .skipped,
else => self.result = .{ .failure = .{
.err = err,
- .trace = try self.formatStackTrace(@errorReturnTrace()),
+ .trace = if (try self.formatStackTrace(@errorReturnTrace())) |trace|
+ try allocator.dupe(u8, trace)
+ else
+ null,
} },
}
};
@@ -68,13 +85,17 @@ const Test = struct {
const writer = stream.writer();
switch (self.result) {
- .success => try self.printPassed(writer),
- .failure => |failure| try self.printFailure(failure, writer),
+ .success => {
+ try self.printPassed(writer);
+ if (self.leaked) try self.printLeaked(writer);
+ try self.printDuration(writer);
+ },
+ .failure => |failure| {
+ try self.printFailure(failure, writer);
+ if (self.leaked) try self.printLeaked(writer);
+ },
.skipped => try self.printSkipped(writer),
}
- try self.printDuration(writer);
-
- if (self.leaked) try self.printLeaked(writer);
try writer.writeByte('\n');
}
@@ -91,9 +112,33 @@ const Test = struct {
jetzig.colors.red("[FAIL] ") ++ name_template ++ jetzig.colors.yellow("({s})"),
.{ self.module orelse "tests", self.name, @errorName(failure.err) },
);
+ }
+ fn printFailureDetail(self: Test, index: usize, failure: Failure, writer: anytype) !void {
+ try writer.print("\n", .{});
+
+ const count = " FAILURE: ".len + (self.module orelse "tests").len + ":".len + self.name.len + 1;
+
+ try writer.writeAll(jetzig.colors.red("┌"));
+ for (0..count) |_| try writer.writeAll(jetzig.colors.red("─"));
+ try writer.writeAll(jetzig.colors.red("┐"));
+
+ try writer.print(
+ jetzig.colors.red("\n│ FAILURE: ") ++ name_template ++ jetzig.colors.red("│") ++ "\n",
+ .{ self.module orelse "tests", self.name },
+ );
+ try writer.writeAll(jetzig.colors.red("├"));
+ for (0..count) |_| try writer.writeAll(jetzig.colors.red("─"));
+ try writer.writeAll(jetzig.colors.red("┘"));
+ try writer.writeByte('\n');
+
+ const maybe_log_events = jetzig.testing.logger.logs.get(index);
+ if (maybe_log_events) |log_events| {
+ for (log_events.items) |log_event| try indent(log_event.output, jetzig.colors.red("│ "), writer);
+ }
if (failure.trace) |trace| {
- try writer.print("{s}", .{trace});
+ try writer.writeAll(jetzig.colors.red("┆\n"));
+ try indent(trace, jetzig.colors.red("┆ "), writer);
}
}
@@ -132,11 +177,13 @@ pub fn main() !void {
try std.io.getStdErr().writer().writeAll("\n[jetzig] Launching Test Runner...\n\n");
+ jetzig.testing.logger = jetzig.testing.Logger.init(allocator);
jetzig.testing.state = .ready;
- for (builtin.test_functions) |test_function| {
+ for (builtin.test_functions, 0..) |test_function, index| {
+ jetzig.testing.logger.index = index;
var t = Test.init(test_function);
- try t.run();
+ try t.run(allocator);
try t.print(std.io.getStdErr());
try tests.append(t);
}
@@ -170,6 +217,13 @@ fn printSummary(tests: []const Test, start: i128) !void {
false,
);
+ for (tests, 0..) |t, index| {
+ switch (t.result) {
+ .success, .skipped => {},
+ .failure => |capture| try t.printFailureDetail(index, capture, writer),
+ }
+ }
+
try writer.print(
"\n {s}{s}{}" ++
"\n {s}{s}{}" ++
@@ -200,3 +254,24 @@ fn printSummary(tests: []const Test, start: i128) !void {
std.process.exit(1);
}
}
+
+fn indent(message: []const u8, comptime indent_sequence: []const u8, writer: anytype) !void {
+ var it = std.mem.tokenizeScalar(u8, message, '\n');
+ var color: ?[]const u8 = null;
+
+ const escape = jetzig.colors.codes.escape;
+
+ while (it.next()) |line| {
+ try writer.print(indent_sequence ++ "{s}{s}\n", .{ color orelse "", line });
+
+ // Preserve last color used in previous line (including reset) in case indent changes color.
+ if (std.mem.lastIndexOf(u8, line, escape)) |index| {
+ inline for (std.meta.fields(@TypeOf(jetzig.colors.codes))) |field| {
+ const code = @field(jetzig.colors.codes, field.name);
+ if (std.mem.startsWith(u8, line[index..], escape ++ code)) {
+ color = escape ++ code;
+ }
+ }
+ }
+ }
+}