mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 22:16:08 +00:00
Merge pull request #86 from jetzig-framework/embed-static-routes
Embed static routes in compiled exe
This commit is contained in:
commit
055c018368
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,5 @@
|
|||||||
zig-out/
|
zig-out/
|
||||||
zig-cache/
|
zig-cache/
|
||||||
*.core
|
*.core
|
||||||
static/
|
|
||||||
.jetzig
|
.jetzig
|
||||||
.zig-cache/
|
.zig-cache/
|
||||||
|
14
build.zig
14
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("routes", routes_module);
|
||||||
exe_static_routes.root_module.addImport("jetzig", jetzig_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("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 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);
|
run_static_routes_cmd.expectExitCode(0);
|
||||||
exe.step.dependOn(&run_static_routes_cmd.step);
|
|
||||||
|
|
||||||
const exe_unit_tests = b.addTest(.{
|
const exe_unit_tests = b.addTest(.{
|
||||||
.root_source_file = tests_file,
|
.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"),
|
.test_runner = jetzig_dep.path("src/test_runner.zig"),
|
||||||
});
|
});
|
||||||
exe_unit_tests.root_module.addImport("jetzig", jetzig_module);
|
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);
|
exe_unit_tests.root_module.addImport("__jetzig_project", &exe.root_module);
|
||||||
|
|
||||||
var it = exe.root_module.import_table.iterator();
|
var it = exe.root_module.import_table.iterator();
|
||||||
while (it.next()) |import| {
|
while (it.next()) |import| {
|
||||||
|
if (std.mem.eql(u8, import.key_ptr.*, "static")) continue;
|
||||||
|
|
||||||
routes_module.addImport(import.key_ptr.*, import.value_ptr.*);
|
routes_module.addImport(import.key_ptr.*, import.value_ptr.*);
|
||||||
exe_static_routes.root_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.*);
|
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 run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
|
||||||
|
|
||||||
const test_step = b.step("jetzig:test", "Run 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_static_routes_cmd.step);
|
||||||
|
test_step.dependOn(&run_exe_unit_tests.step);
|
||||||
exe_unit_tests.root_module.addImport("routes", routes_module);
|
exe_unit_tests.root_module.addImport("routes", routes_module);
|
||||||
|
|
||||||
const routes_step = b.step("jetzig:routes", "List all routes in your app");
|
const routes_step = b.step("jetzig:routes", "List all routes in your app");
|
||||||
const exe_routes = b.addExecutable(.{
|
const exe_routes = b.addExecutable(.{
|
||||||
.name = "routes",
|
.name = "routes",
|
||||||
.root_source_file = jetzig_dep.path("src/routes.zig"),
|
.root_source_file = jetzig_dep.path("src/routes_exe.zig"),
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
.hash = "12203b56c2e17a2fd62ea3d3d9be466f43921a3aef88b381cf58f41251815205fdb5",
|
.hash = "12203b56c2e17a2fd62ea3d3d9be466f43921a3aef88b381cf58f41251815205fdb5",
|
||||||
},
|
},
|
||||||
.zmpl = .{
|
.zmpl = .{
|
||||||
.url = "https://github.com/jetzig-framework/zmpl/archive/7dac63a9470cf12a2cbaf8e6e3fc3aeb9ea27658.tar.gz",
|
.url = "https://github.com/jetzig-framework/zmpl/archive/676969d44fe1c3adabd73af983f33aefd8aed1b9.tar.gz",
|
||||||
.hash = "1220215354adcea94d36d25c300e291981d3f48c543f7da6e48bb2793a5aed85e768",
|
.hash = "1220a6cce7678578e0176796c19d3810cdd3a9c1b0792a3731a8cd25d2aee96bd78a",
|
||||||
},
|
},
|
||||||
.jetkv = .{
|
.jetkv = .{
|
||||||
.url = "https://github.com/jetzig-framework/jetkv/archive/78bcdcc6b0cbd3ca808685c64554a15701f13250.tar.gz",
|
.url = "https://github.com/jetzig-framework/jetkv/archive/78bcdcc6b0cbd3ca808685c64554a15701f13250.tar.gz",
|
||||||
|
@ -7,9 +7,9 @@ pub const layout = "application";
|
|||||||
|
|
||||||
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||||
var root = try data.object();
|
var root = try data.object();
|
||||||
try root.put("message", data.string("Welcome to Jetzig!"));
|
try root.put("message", "Welcome to Jetzig!");
|
||||||
try root.put("custom_number", data.integer(customFunction(100, 200, 300)));
|
try root.put("custom_number", customFunction(100, 200, 300));
|
||||||
try root.put("imported_number", data.integer(importedFunction(100, 200, 300)));
|
try root.put("imported_number", importedFunction(100, 200, 300));
|
||||||
|
|
||||||
try request.response.headers.append("x-example-header", "example header value");
|
try request.response.headers.append("x-example-header", "example header value");
|
||||||
|
|
||||||
|
@ -37,8 +37,8 @@ pub const static_params = .{
|
|||||||
.{ .params = .{ .foo = "hello", .bar = "goodbye" } },
|
.{ .params = .{ .foo = "hello", .bar = "goodbye" } },
|
||||||
},
|
},
|
||||||
.get = .{
|
.get = .{
|
||||||
.{ .id = "1", .params = .{ .foo = "hi", .bar = "bye" } },
|
.{ .id = "123", .params = .{ .foo = "hi", .bar = "bye" } },
|
||||||
.{ .id = "2", .params = .{ .foo = "hello", .bar = "goodbye" } },
|
.{ .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();
|
const params = try request.params();
|
||||||
|
|
||||||
if (std.mem.eql(u8, id, "1")) {
|
if (std.mem.eql(u8, id, "123")) {
|
||||||
try root.put("id", data.string("id is '1'"));
|
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);
|
if (params.get("foo")) |foo| try root.put("foo", foo);
|
||||||
@ -89,12 +91,12 @@ test "get json" {
|
|||||||
|
|
||||||
const response = try app.request(
|
const response = try app.request(
|
||||||
.GET,
|
.GET,
|
||||||
"/static/1.json",
|
"/static/123.json",
|
||||||
.{ .json = .{ .foo = "hi", .bar = "bye" } },
|
.{ .json = .{ .foo = "hi", .bar = "bye" } },
|
||||||
);
|
);
|
||||||
|
|
||||||
try response.expectStatus(.ok);
|
try response.expectStatus(.ok);
|
||||||
try response.expectJson(".id", "id is '1'");
|
try response.expectJson(".message", "id is '123'");
|
||||||
}
|
}
|
||||||
|
|
||||||
test "index html" {
|
test "index html" {
|
||||||
@ -108,7 +110,8 @@ test "index html" {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try response.expectStatus(.ok);
|
try response.expectStatus(.ok);
|
||||||
try response.expectBodyContains("hello");
|
try response.expectBodyContains("foo: hello");
|
||||||
|
try response.expectBodyContains("bar: goodbye");
|
||||||
}
|
}
|
||||||
|
|
||||||
test "get html" {
|
test "get html" {
|
||||||
@ -117,7 +120,7 @@ test "get html" {
|
|||||||
|
|
||||||
const response = try app.request(
|
const response = try app.request(
|
||||||
.GET,
|
.GET,
|
||||||
"/static/1.html",
|
"/static/123.html",
|
||||||
.{ .params = .{ .foo = "hi", .bar = "bye" } },
|
.{ .params = .{ .foo = "hi", .bar = "bye" } },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
<div>You made a request to /static/{.id} with:</div>
|
<div>{{.message}}</div>
|
||||||
<div>foo: {.foo}</div>
|
<div>foo: {{.foo}}</div>
|
||||||
<div>bar: {.bar}</div>
|
<div>bar: {{.bar}}</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -5,6 +5,7 @@ const jetzig = @import("jetzig");
|
|||||||
const zmd = @import("zmd");
|
const zmd = @import("zmd");
|
||||||
|
|
||||||
pub const routes = @import("routes");
|
pub const routes = @import("routes");
|
||||||
|
pub const static = @import("static");
|
||||||
|
|
||||||
// Override default settings in `jetzig.config` here:
|
// Override default settings in `jetzig.config` here:
|
||||||
pub const jetzig_options = struct {
|
pub const jetzig_options = struct {
|
||||||
|
@ -267,6 +267,7 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
|
|||||||
|
|
||||||
const output_template =
|
const output_template =
|
||||||
\\ .{{
|
\\ .{{
|
||||||
|
\\ .id = "{9s}",
|
||||||
\\ .name = "{0s}",
|
\\ .name = "{0s}",
|
||||||
\\ .action = .{1s},
|
\\ .action = .{1s},
|
||||||
\\ .view_name = "{2s}",
|
\\ .view_name = "{2s}",
|
||||||
@ -296,6 +297,8 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
|
|||||||
std.mem.replaceScalar(u8, module_path, '\\', '/');
|
std.mem.replaceScalar(u8, module_path, '\\', '/');
|
||||||
try self.module_paths.append(try self.allocator.dupe(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, .{
|
const output = try std.fmt.allocPrint(self.allocator, output_template, .{
|
||||||
full_name,
|
full_name,
|
||||||
route.name,
|
route.name,
|
||||||
@ -306,6 +309,7 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
|
|||||||
template,
|
template,
|
||||||
module_path,
|
module_path,
|
||||||
try std.mem.join(self.allocator, ", \n", route.params.items),
|
try std.mem.join(self.allocator, ", \n", route.params.items),
|
||||||
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
defer self.allocator.free(output);
|
defer self.allocator.free(output);
|
||||||
|
@ -2,7 +2,7 @@ const std = @import("std");
|
|||||||
const jetzig = @import("jetzig");
|
const jetzig = @import("jetzig");
|
||||||
const routes = @import("routes").routes;
|
const routes = @import("routes").routes;
|
||||||
const zmpl = @import("zmpl");
|
const zmpl = @import("zmpl");
|
||||||
const jetzig_options = @import("jetzig_app").jetzig_options;
|
// const jetzig_options = @import("jetzig_app").jetzig_options;
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
@ -13,14 +13,27 @@ pub fn main() !void {
|
|||||||
const allocator = arena.allocator();
|
const allocator = arena.allocator();
|
||||||
defer arena.deinit();
|
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 {
|
fn compileStaticRoutes(allocator: std.mem.Allocator, writer: anytype) !void {
|
||||||
std.fs.cwd().deleteTree("static") catch {};
|
|
||||||
|
|
||||||
var count: usize = 0;
|
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| {
|
for (routes) |route| {
|
||||||
if (!route.static) continue;
|
if (!route.static) continue;
|
||||||
|
|
||||||
@ -28,7 +41,7 @@ fn compileStaticRoutes(allocator: std.mem.Allocator) !void {
|
|||||||
for (route.json_params, 0..) |json, index| {
|
for (route.json_params, 0..) |json, index| {
|
||||||
var request = try jetzig.http.StaticRequest.init(allocator, json);
|
var request = try jetzig.http.StaticRequest.init(allocator, json);
|
||||||
defer request.deinit();
|
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 => {
|
.index, .post => {
|
||||||
var request = try jetzig.http.StaticRequest.init(allocator, "{}");
|
var request = try jetzig.http.StaticRequest.init(allocator, "{}");
|
||||||
defer request.deinit();
|
defer request.deinit();
|
||||||
try writeContent(allocator, route, &request, null, &count);
|
try writeContent(allocator, writer, route, &request, null, &count, null);
|
||||||
},
|
},
|
||||||
inline else => {},
|
inline else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try writer.writeAll(
|
||||||
|
\\};
|
||||||
|
\\
|
||||||
|
);
|
||||||
std.debug.print("[jetzig] Compiled {} static output(s)\n", .{count});
|
std.debug.print("[jetzig] Compiled {} static output(s)\n", .{count});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn writeContent(
|
fn writeContent(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
|
writer: anytype,
|
||||||
route: jetzig.views.Route,
|
route: jetzig.views.Route,
|
||||||
request: *jetzig.http.StaticRequest,
|
request: *jetzig.http.StaticRequest,
|
||||||
index: ?usize,
|
index: ?usize,
|
||||||
count: *usize,
|
count: *usize,
|
||||||
|
params_json: ?[]const u8,
|
||||||
) !void {
|
) !void {
|
||||||
const index_suffix = if (index) |capture|
|
const index_suffix = if (index) |capture|
|
||||||
try std.fmt.allocPrint(allocator, "_{}", .{capture})
|
try std.fmt.allocPrint(allocator, "_{}", .{capture})
|
||||||
@ -62,35 +82,28 @@ fn writeContent(
|
|||||||
const view = try route.renderStatic(route, request);
|
const view = try route.renderStatic(route, request);
|
||||||
defer view.deinit();
|
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;
|
count.* += 1;
|
||||||
|
|
||||||
const html_content = try renderZmplTemplate(allocator, route, view) orelse
|
const html_content = try renderZmplTemplate(allocator, route, view) orelse
|
||||||
try renderMarkdown(allocator, route, view) orelse
|
try renderMarkdown(allocator, route, view) orelse
|
||||||
null;
|
null;
|
||||||
const html_path = try std.mem.concat(
|
|
||||||
allocator,
|
try writer.print(
|
||||||
u8,
|
\\.{{ .route_id = "{s}", .output = StaticOutput{{ .json = "{s}", .html = "{s}", .params = {s}{s}{s} }} }},
|
||||||
&[_][]const u8{ route.name, index_suffix, ".html" },
|
\\
|
||||||
|
\\
|
||||||
|
,
|
||||||
|
.{
|
||||||
|
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| {
|
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);
|
allocator.free(content);
|
||||||
count.* += 1;
|
count.* += 1;
|
||||||
}
|
}
|
||||||
@ -101,10 +114,7 @@ fn renderMarkdown(
|
|||||||
route: jetzig.views.Route,
|
route: jetzig.views.Route,
|
||||||
view: jetzig.views.View,
|
view: jetzig.views.View,
|
||||||
) !?[]const u8 {
|
) !?[]const u8 {
|
||||||
const fragments = if (@hasDecl(jetzig_options, "markdown_fragments"))
|
const fragments = null;
|
||||||
jetzig_options.markdown_fragments
|
|
||||||
else
|
|
||||||
null;
|
|
||||||
const path = try std.mem.join(allocator, "/", &[_][]const u8{ route.uri_path, @tagName(route.action) });
|
const path = try std.mem.join(allocator, "/", &[_][]const u8{ route.uri_path, @tagName(route.action) });
|
||||||
defer allocator.free(path);
|
defer allocator.free(path);
|
||||||
const content = try jetzig.markdown.render(allocator, path, fragments) orelse return null;
|
const content = try jetzig.markdown.render(allocator, path, fragments) orelse return null;
|
||||||
@ -153,3 +163,10 @@ fn renderZmplTemplate(
|
|||||||
}
|
}
|
||||||
} else return null;
|
} 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();
|
||||||
|
}
|
||||||
|
@ -64,7 +64,7 @@ pub const MailerDefinition = mail.MailerDefinition;
|
|||||||
/// `ERROR`, etc.). Note that all log functions are CAPITALIZED.
|
/// `ERROR`, etc.). Note that all log functions are CAPITALIZED.
|
||||||
pub const Logger = loggers.Logger;
|
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:
|
/// Global configuration. Override these values by defining in `src/main.zig` with:
|
||||||
/// ```zig
|
/// ```zig
|
||||||
|
@ -159,6 +159,7 @@ pub fn route(
|
|||||||
std.mem.replaceScalar(u8, &view_name, '.', '/');
|
std.mem.replaceScalar(u8, &view_name, '.', '/');
|
||||||
|
|
||||||
self.custom_routes.append(.{
|
self.custom_routes.append(.{
|
||||||
|
.id = "custom",
|
||||||
.name = member,
|
.name = member,
|
||||||
.action = .custom,
|
.action = .custom,
|
||||||
.method = method,
|
.method = method,
|
||||||
@ -196,6 +197,7 @@ pub fn createRoutes(
|
|||||||
for (comptime_routes) |const_route| {
|
for (comptime_routes) |const_route| {
|
||||||
var var_route = try allocator.create(jetzig.views.Route);
|
var var_route = try allocator.create(jetzig.views.Route);
|
||||||
var_route.* = .{
|
var_route.* = .{
|
||||||
|
.id = const_route.id,
|
||||||
.name = const_route.name,
|
.name = const_route.name,
|
||||||
.action = const_route.action,
|
.action = const_route.action,
|
||||||
.view_name = const_route.view_name,
|
.view_name = const_route.view_name,
|
||||||
|
@ -29,6 +29,7 @@ initialized: bool = false,
|
|||||||
store: *jetzig.kv.Store,
|
store: *jetzig.kv.Store,
|
||||||
job_queue: *jetzig.kv.Store,
|
job_queue: *jetzig.kv.Store,
|
||||||
cache: *jetzig.kv.Store,
|
cache: *jetzig.kv.Store,
|
||||||
|
decoded_static_route_params: []*jetzig.data.Value = &.{},
|
||||||
|
|
||||||
const Server = @This();
|
const Server = @This();
|
||||||
|
|
||||||
@ -76,6 +77,8 @@ const Dispatcher = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn listen(self: *Server) !void {
|
pub fn listen(self: *Server) !void {
|
||||||
|
try self.decodeStaticParams();
|
||||||
|
|
||||||
var httpz_server = try httpz.ServerCtx(Dispatcher, Dispatcher).init(
|
var httpz_server = try httpz.ServerCtx(Dispatcher, Dispatcher).init(
|
||||||
self.allocator,
|
self.allocator,
|
||||||
.{
|
.{
|
||||||
@ -251,7 +254,7 @@ fn renderJSON(
|
|||||||
if (data.value) |_| {} else _ = try data.object();
|
if (data.value) |_| {} else _ = try data.object();
|
||||||
|
|
||||||
rendered.content = if (self.options.environment == .development)
|
rendered.content = if (self.options.environment == .development)
|
||||||
try data.toPrettyJson()
|
try data.toJsonOptions(.{ .pretty = true, .color = false })
|
||||||
else
|
else
|
||||||
try data.toJson();
|
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 {
|
fn matchStaticContent(self: *Server, request: *jetzig.http.Request) !?[]const u8 {
|
||||||
var static_dir = std.fs.cwd().openDir("static", .{}) catch |err| {
|
const request_format = request.requestFormat();
|
||||||
switch (err) {
|
|
||||||
error.FileNotFound => return null,
|
|
||||||
else => return err,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
defer static_dir.close();
|
|
||||||
|
|
||||||
const matched_route = try self.matchRoute(request, true);
|
const matched_route = try self.matchRoute(request, true);
|
||||||
|
const params = try request.params();
|
||||||
|
|
||||||
if (matched_route) |route| {
|
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| {
|
if (std.mem.eql(u8, static_output.route_id, route.id)) {
|
||||||
return static_dir.readFileAlloc(
|
if (index < self.decoded_static_route_params.len) {
|
||||||
request.allocator,
|
if (matchStaticOutput(
|
||||||
capture,
|
self.decoded_static_route_params[index].getT(.string, "id"),
|
||||||
jetzig.config.get(usize, "max_bytes_static_content"),
|
self.decoded_static_route_params[index].get("params"),
|
||||||
) catch |err| {
|
route,
|
||||||
switch (err) {
|
request,
|
||||||
error.FileNotFound => return null,
|
params,
|
||||||
else => return err,
|
)) return switch (request_format) {
|
||||||
}
|
.HTML, .UNKNOWN => static_output.output.html,
|
||||||
|
.JSON => static_output.output.json,
|
||||||
};
|
};
|
||||||
} else return null;
|
} else {
|
||||||
|
return switch (request_format) {
|
||||||
|
.HTML, .UNKNOWN => static_output.output.html,
|
||||||
|
.JSON => static_output.output.json,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn staticPath(request: *jetzig.http.Request, route: jetzig.views.Route) !?[]const u8 {
|
pub fn decodeStaticParams(self: *Server) !void {
|
||||||
const params = try request.params();
|
// Store decoded static params (i.e. declared in views) for faster comparison at request time.
|
||||||
defer params.deinit();
|
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.?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const extension = switch (request.requestFormat()) {
|
self.decoded_static_route_params = try decoded.toOwnedSlice();
|
||||||
.HTML, .UNKNOWN => ".html",
|
}
|
||||||
.JSON => ".json",
|
|
||||||
|
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;
|
||||||
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;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -12,11 +12,52 @@ const testing = @This();
|
|||||||
/// Pre-built mime map, assigned by Jetzig test runner.
|
/// Pre-built mime map, assigned by Jetzig test runner.
|
||||||
pub var mime_map: *jetzig.http.mime.MimeMap = undefined;
|
pub var mime_map: *jetzig.http.mime.MimeMap = undefined;
|
||||||
pub var state: enum { initial, ready } = .initial;
|
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 secret = "secret-bytes-for-use-in-test-environment-only";
|
||||||
|
|
||||||
pub const app = App.init;
|
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 {
|
pub const TestResponse = struct {
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
status: u16,
|
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();
|
const expected_code = try jetzig.http.status_codes.get(expected).getCodeInt();
|
||||||
|
|
||||||
if (response.status != expected_code) {
|
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;
|
return error.JetzigExpectStatusError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn expectBodyContains(expected: []const u8, response: TestResponse) !void {
|
pub fn expectBodyContains(expected: []const u8, response: TestResponse) !void {
|
||||||
if (!std.mem.containsAtLeast(u8, response.body, 1, expected)) {
|
if (!std.mem.containsAtLeast(u8, response.body, 1, expected)) {
|
||||||
log(
|
logFailure(
|
||||||
"Expected content:\n========\n{s}\n========\n\nActual content:\n========\n{s}\n========",
|
"\nExpected content:\n" ++
|
||||||
|
jetzig.colors.red("{s}") ++
|
||||||
|
"\n\nActual content:\n" ++
|
||||||
|
jetzig.colors.green("{s}") ++
|
||||||
|
"\n",
|
||||||
.{ expected, response.body },
|
.{ expected, response.body },
|
||||||
);
|
);
|
||||||
return error.JetzigExpectBodyContainsError;
|
return error.JetzigExpectBodyContainsError;
|
||||||
@ -94,14 +142,14 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
|
|||||||
data.fromJson(response.body) catch |err| {
|
data.fromJson(response.body) catch |err| {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
error.SyntaxError => {
|
error.SyntaxError => {
|
||||||
log("Expected JSON, encountered parser error.", .{});
|
logFailure("Expected JSON, encountered parser error.", .{});
|
||||||
return error.JetzigExpectJsonError;
|
return error.JetzigExpectJsonError;
|
||||||
},
|
},
|
||||||
else => return err,
|
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| {
|
if (try data.getValue(std.mem.trimLeft(u8, expected_path, &.{'.'}))) |value| {
|
||||||
switch (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;
|
if (std.mem.eql(u8, string.value, expected_value)) return;
|
||||||
},
|
},
|
||||||
.Null => {
|
.Null => {
|
||||||
log(
|
logFailure(
|
||||||
"Expected null/non-existent value for `{s}`, found: `{s}`",
|
"Expected null/non-existent value for " ++ jetzig.colors.cyan("{s}") ++ ", found: " ++ jetzig.colors.cyan("{s}"),
|
||||||
.{ expected_path, string.value },
|
.{ expected_path, string.value },
|
||||||
);
|
);
|
||||||
return error.JetzigExpectJsonError;
|
return error.JetzigExpectJsonError;
|
||||||
@ -123,8 +171,8 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
|
|||||||
if (integer.value == expected_value) return;
|
if (integer.value == expected_value) return;
|
||||||
},
|
},
|
||||||
.Null => {
|
.Null => {
|
||||||
log(
|
logFailure(
|
||||||
"Expected null/non-existent value for `{s}`, found: `{}`",
|
"Expected null/non-existent value for " ++ jetzig.colors.cyan("{s}") ++ ", found: " ++ jetzig.colors.green("{}"),
|
||||||
.{ expected_path, integer.value },
|
.{ expected_path, integer.value },
|
||||||
);
|
);
|
||||||
return error.JetzigExpectJsonError;
|
return error.JetzigExpectJsonError;
|
||||||
@ -136,8 +184,8 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
|
|||||||
if (float.value == expected_value) return;
|
if (float.value == expected_value) return;
|
||||||
},
|
},
|
||||||
.Null => {
|
.Null => {
|
||||||
log(
|
logFailure(
|
||||||
"Expected null/non-existent value for `{s}`, found: `{}`",
|
"Expected null/non-existent value for " ++ jetzig.colors.cyan("{s}") ++ ", found: " ++ jetzig.colors.green("{}"),
|
||||||
.{ expected_path, float.value },
|
.{ expected_path, float.value },
|
||||||
);
|
);
|
||||||
return error.JetzigExpectJsonError;
|
return error.JetzigExpectJsonError;
|
||||||
@ -149,8 +197,8 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
|
|||||||
if (boolean.value == expected_value) return;
|
if (boolean.value == expected_value) return;
|
||||||
},
|
},
|
||||||
.Null => {
|
.Null => {
|
||||||
log(
|
logFailure(
|
||||||
"Expected null/non-existent value for `{s}`, found: `{}`",
|
"Expected null/non-existent value for " ++ jetzig.colors.cyan("{s}") ++ ", found: " ++ jetzig.colors.green("{}"),
|
||||||
.{ expected_path, boolean.value },
|
.{ expected_path, boolean.value },
|
||||||
);
|
);
|
||||||
return error.JetzigExpectJsonError;
|
return error.JetzigExpectJsonError;
|
||||||
@ -173,9 +221,9 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
|
|||||||
.string => |string| {
|
.string => |string| {
|
||||||
switch (@typeInfo(@TypeOf(expected_value))) {
|
switch (@typeInfo(@TypeOf(expected_value))) {
|
||||||
.Pointer, .Array => {
|
.Pointer, .Array => {
|
||||||
log(
|
logFailure(
|
||||||
"Expected `{s}` in `{s}`, found `{s}` in JSON:" ++ json_banner,
|
"Expected \"" ++ jetzig.colors.red("{s}") ++ "\" in " ++ jetzig.colors.cyan("{s}") ++ ", found \"" ++ jetzig.colors.green("{s}") ++ "\nJSON:" ++ json_banner,
|
||||||
.{ expected_value, expected_path, string.value, response.body },
|
.{ expected_value, expected_path, string.value, try jsonPretty(response) },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
@ -185,9 +233,10 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
|
|||||||
=> |integer| {
|
=> |integer| {
|
||||||
switch (@typeInfo(@TypeOf(expected_value))) {
|
switch (@typeInfo(@TypeOf(expected_value))) {
|
||||||
.Int, .ComptimeInt => {
|
.Int, .ComptimeInt => {
|
||||||
log(
|
logFailure(
|
||||||
"Expected `{}` in `{s}`, found `{}` in JSON:" ++ json_banner,
|
"Expected " ++ jetzig.colors.red("{}") ++ " in " ++ jetzig.colors.cyan("{s}") ++ ", found " ++ jetzig.colors.green("{}") ++ "\nJSON:" ++ json_banner,
|
||||||
.{ expected_value, expected_path, integer.value, response.body },
|
|
||||||
|
.{ expected_value, expected_path, integer.value, try jsonPretty(response) },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
@ -196,9 +245,9 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
|
|||||||
.float => |float| {
|
.float => |float| {
|
||||||
switch (@typeInfo(@TypeOf(expected_value))) {
|
switch (@typeInfo(@TypeOf(expected_value))) {
|
||||||
.Float, .ComptimeFloat => {
|
.Float, .ComptimeFloat => {
|
||||||
log(
|
logFailure(
|
||||||
"Expected `{}` in `{s}`, found `{}` in JSON:" ++ json_banner,
|
"Expected " ++ jetzig.colors.red("{}") ++ " in " ++ jetzig.colors.cyan("{s}") ++ ", found " ++ jetzig.colors.green("{}") ++ "\nJSON:" ++ json_banner,
|
||||||
.{ expected_value, expected_path, float.value, response.body },
|
.{ expected_value, expected_path, float.value, try jsonPretty(response) },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
@ -207,26 +256,26 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
|
|||||||
.boolean => |boolean| {
|
.boolean => |boolean| {
|
||||||
switch (@typeInfo(@TypeOf(expected_value))) {
|
switch (@typeInfo(@TypeOf(expected_value))) {
|
||||||
.Bool => {
|
.Bool => {
|
||||||
log(
|
logFailure(
|
||||||
"Expected `{}` in `{s}`, found `{}` in JSON:" ++ json_banner,
|
"Expected " ++ jetzig.colors.red("{}") ++ " in " ++ jetzig.colors.cyan("{s}") ++ ", found " ++ jetzig.colors.green("{}") ++ "\nJSON:" ++ json_banner,
|
||||||
.{ expected_value, expected_path, boolean.value, response.body },
|
.{ expected_value, expected_path, boolean.value, try jsonPretty(response) },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.Null => {
|
.Null => {
|
||||||
log(
|
logFailure(
|
||||||
"Expected value in `{s}`, found `null` in JSON:" ++ json_banner,
|
"Expected value in " ++ jetzig.colors.cyan("{s}") ++ ", found " ++ jetzig.colors.green("null") ++ "\nJSON:" ++ json_banner,
|
||||||
.{ expected_path, response.body },
|
.{ expected_path, try jsonPretty(response) },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log(
|
logFailure(
|
||||||
"Path not found: `{s}` in JSON: " ++ json_banner,
|
"Path not found: `{s}`\nJSON: " ++ json_banner,
|
||||||
.{ expected_path, response.body },
|
.{ expected_path, try jsonPretty(response) },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return error.JetzigExpectJsonError;
|
return error.JetzigExpectJsonError;
|
||||||
@ -244,6 +293,18 @@ pub fn expectJob(job_name: []const u8, job_params: anytype, response: TestRespon
|
|||||||
return error.JetzigExpectJobError;
|
return error.JetzigExpectJobError;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log(comptime message: []const u8, args: anytype) void {
|
// fn log(comptime message: []const u8, args: anytype) void {
|
||||||
std.debug.print("[jetzig.testing] " ++ message ++ "\n", args);
|
// 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 });
|
||||||
}
|
}
|
||||||
|
@ -90,6 +90,8 @@ pub fn request(
|
|||||||
.job_queue = self.job_queue,
|
.job_queue = self.job_queue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try server.decodeStaticParams();
|
||||||
|
|
||||||
var buf: [1024]u8 = undefined;
|
var buf: [1024]u8 = undefined;
|
||||||
var httpz_request = try stubbedRequest(allocator, &buf, method, path, options);
|
var httpz_request = try stubbedRequest(allocator, &buf, method, path, options);
|
||||||
var httpz_response = try stubbedResponse(allocator);
|
var httpz_response = try stubbedResponse(allocator);
|
||||||
|
@ -60,6 +60,7 @@ layout: ?[]const u8 = null,
|
|||||||
template: []const u8,
|
template: []const u8,
|
||||||
json_params: []const []const u8,
|
json_params: []const []const u8,
|
||||||
params: std.ArrayList(*jetzig.data.Data) = undefined,
|
params: std.ArrayList(*jetzig.data.Data) = undefined,
|
||||||
|
id: []const u8,
|
||||||
|
|
||||||
/// Initializes a route's static params on server launch. Converts static params (JSON strings)
|
/// 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()`).
|
/// to `jetzig.data.Data` values. Memory is owned by caller (`App.start()`).
|
||||||
|
@ -1,6 +1,20 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const jetzig = @import("jetzig");
|
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 {
|
const Test = struct {
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
@ -8,7 +22,7 @@ const Test = struct {
|
|||||||
module: ?[]const u8 = null,
|
module: ?[]const u8 = null,
|
||||||
leaked: bool = false,
|
leaked: bool = false,
|
||||||
result: Result = .success,
|
result: Result = .success,
|
||||||
stack_trace_buf: [4096]u8 = undefined,
|
stack_trace_buf: [8192]u8 = undefined,
|
||||||
duration: usize = 0,
|
duration: usize = 0,
|
||||||
|
|
||||||
pub const TestFn = *const fn () anyerror!void;
|
pub const TestFn = *const fn () anyerror!void;
|
||||||
@ -36,7 +50,7 @@ const Test = struct {
|
|||||||
.{ .function = test_fn.func, .name = test_fn.name };
|
.{ .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 = .{};
|
std.testing.allocator_instance = .{};
|
||||||
const start = std.time.nanoTimestamp();
|
const start = std.time.nanoTimestamp();
|
||||||
|
|
||||||
@ -45,7 +59,10 @@ const Test = struct {
|
|||||||
error.SkipZigTest => self.result = .skipped,
|
error.SkipZigTest => self.result = .skipped,
|
||||||
else => self.result = .{ .failure = .{
|
else => self.result = .{ .failure = .{
|
||||||
.err = err,
|
.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();
|
const writer = stream.writer();
|
||||||
|
|
||||||
switch (self.result) {
|
switch (self.result) {
|
||||||
.success => try self.printPassed(writer),
|
.success => {
|
||||||
.failure => |failure| try self.printFailure(failure, writer),
|
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),
|
.skipped => try self.printSkipped(writer),
|
||||||
}
|
}
|
||||||
try self.printDuration(writer);
|
|
||||||
|
|
||||||
if (self.leaked) try self.printLeaked(writer);
|
|
||||||
|
|
||||||
try writer.writeByte('\n');
|
try writer.writeByte('\n');
|
||||||
}
|
}
|
||||||
@ -91,9 +112,33 @@ const Test = struct {
|
|||||||
jetzig.colors.red("[FAIL] ") ++ name_template ++ jetzig.colors.yellow("({s})"),
|
jetzig.colors.red("[FAIL] ") ++ name_template ++ jetzig.colors.yellow("({s})"),
|
||||||
.{ self.module orelse "tests", self.name, @errorName(failure.err) },
|
.{ 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| {
|
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");
|
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;
|
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);
|
var t = Test.init(test_function);
|
||||||
try t.run();
|
try t.run(allocator);
|
||||||
try t.print(std.io.getStdErr());
|
try t.print(std.io.getStdErr());
|
||||||
try tests.append(t);
|
try tests.append(t);
|
||||||
}
|
}
|
||||||
@ -170,6 +217,13 @@ fn printSummary(tests: []const Test, start: i128) !void {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
for (tests, 0..) |t, index| {
|
||||||
|
switch (t.result) {
|
||||||
|
.success, .skipped => {},
|
||||||
|
.failure => |capture| try t.printFailureDetail(index, capture, writer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try writer.print(
|
try writer.print(
|
||||||
"\n {s}{s}{}" ++
|
"\n {s}{s}{}" ++
|
||||||
"\n {s}{s}{}" ++
|
"\n {s}{s}{}" ++
|
||||||
@ -200,3 +254,24 @@ fn printSummary(tests: []const Test, start: i128) !void {
|
|||||||
std.process.exit(1);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user