mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 14:06:08 +00:00
Embed static routes in compiled exe
Remove need for static routes output files to be copied/generated in deployment.
This commit is contained in:
parent
aafcd4cddc
commit
e30d340a7a
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,5 @@
|
||||
zig-out/
|
||||
zig-cache/
|
||||
*.core
|
||||
static/
|
||||
.jetzig
|
||||
.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("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,
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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" } },
|
||||
);
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<div>You made a request to /static/{.id} with:</div>
|
||||
<div>foo: {.foo}</div>
|
||||
<div>bar: {.bar}</div>
|
||||
<div>{{.message}}</div>
|
||||
<div>foo: {{.foo}}</div>
|
||||
<div>bar: {{.bar}}</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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 null;
|
||||
} else {
|
||||
return switch (request_format) {
|
||||
.HTML, .UNKNOWN => static_output.output.html,
|
||||
.JSON => static_output.output.json,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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()`).
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user