diff --git a/demo/src/app/views/session.zig b/demo/src/app/views/session.zig
index 15b0e43..27373cc 100644
--- a/demo/src/app/views/session.zig
+++ b/demo/src/app/views/session.zig
@@ -16,7 +16,7 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
}
pub fn edit(id: []const u8, request: *jetzig.Request) !jetzig.View {
- try request.server.logger.INFO("id: {s}", .{id});
+ try request.logger.INFO("id: {s}", .{id});
return request.render(.ok);
}
diff --git a/demo/src/app/views/websockets.zig b/demo/src/app/views/websockets.zig
index 3aa1e4f..dce713d 100644
--- a/demo/src/app/views/websockets.zig
+++ b/demo/src/app/views/websockets.zig
@@ -1,37 +1,7 @@
const std = @import("std");
const jetzig = @import("jetzig");
-pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
- _ = data;
- return request.render(.ok);
-}
-
-pub fn get(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
- _ = data;
- _ = id;
- return request.render(.ok);
-}
-
-pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
- _ = data;
- return request.render(.created);
-}
-
-pub fn put(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
- _ = data;
- _ = id;
- return request.render(.ok);
-}
-
-pub fn patch(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
- _ = data;
- _ = id;
- return request.render(.ok);
-}
-
-pub fn delete(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
- _ = data;
- _ = id;
+pub fn index(request: *jetzig.Request) !jetzig.View {
return request.render(.ok);
}
@@ -90,6 +60,13 @@ pub const Channel = struct {
try message.channel.sync();
}
+ pub const Actions = struct {
+ pub fn reset(channel: jetzig.channels.Channel) !void {
+ _ = channel;
+ std.debug.print("here\n", .{});
+ }
+ };
+
fn resetGame(channel: jetzig.channels.Channel) !void {
try channel.put("winner", null);
var cells = try channel.put("cells", .array);
@@ -172,51 +149,3 @@ const Game = struct {
}
}
};
-
-test "index" {
- var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
- defer app.deinit();
-
- const response = try app.request(.GET, "/websockets", .{});
- try response.expectStatus(.ok);
-}
-
-test "get" {
- var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
- defer app.deinit();
-
- const response = try app.request(.GET, "/websockets/example-id", .{});
- try response.expectStatus(.ok);
-}
-
-test "post" {
- var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
- defer app.deinit();
-
- const response = try app.request(.POST, "/websockets", .{});
- try response.expectStatus(.created);
-}
-
-test "put" {
- var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
- defer app.deinit();
-
- const response = try app.request(.PUT, "/websockets/example-id", .{});
- try response.expectStatus(.ok);
-}
-
-test "patch" {
- var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
- defer app.deinit();
-
- const response = try app.request(.PATCH, "/websockets/example-id", .{});
- try response.expectStatus(.ok);
-}
-
-test "delete" {
- var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
- defer app.deinit();
-
- const response = try app.request(.DELETE, "/websockets/example-id", .{});
- try response.expectStatus(.ok);
-}
diff --git a/demo/src/app/views/websockets/index.zmpl b/demo/src/app/views/websockets/index.zmpl
index 55f869f..df24e3d 100644
--- a/demo/src/app/views/websockets/index.zmpl
+++ b/demo/src/app/views/websockets/index.zmpl
@@ -57,15 +57,15 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -86,7 +86,7 @@
Object.entries(state.cells).forEach(([cell, state]) => {
const element = document.querySelector(`#tic-tac-toe-cell-${cell}`);
- element.innerHTML = { player: "✈", cpu: "🦎" }[state] || "";
+ element.innerHTML = { player: "✈️", cpu: "🦎" }[state] || "";
});
});
diff --git a/src/Routes.zig b/src/Routes.zig
index 61cc13e..2ceaa34 100644
--- a/src/Routes.zig
+++ b/src/Routes.zig
@@ -12,7 +12,7 @@ buffer: std.ArrayList(u8),
dynamic_routes: std.ArrayList(Function),
static_routes: std.ArrayList(Function),
channel_routes: std.ArrayList([]const u8),
-module_paths: std.ArrayList([]const u8),
+module_paths: std.StringHashMap(void),
data: *jetzig.data.Data,
const receive_message = "receiveMessage";
@@ -124,7 +124,7 @@ pub fn init(
.static_routes = std.ArrayList(Function).init(allocator),
.dynamic_routes = std.ArrayList(Function).init(allocator),
.channel_routes = std.ArrayList([]const u8).init(allocator),
- .module_paths = std.ArrayList([]const u8).init(allocator),
+ .module_paths = std.StringHashMap(void).init(allocator),
.data = data,
};
}
@@ -186,16 +186,29 @@ pub fn generateRoutes(self: *Routes) ![]const u8 {
\\
);
+ try writer.writeAll(
+ \\
+ \\pub const View = struct { name: []const u8, module: type };
+ \\pub const views = [_]View{
+ \\
+ );
+ try self.writeViewsArray(writer);
+ try writer.writeAll(
+ \\};
+ \\
+ );
+
try writer.writeAll(
\\test {
\\
);
- for (self.module_paths.items) |module_path| {
+ var it = self.module_paths.keyIterator();
+ while (it.next()) |module_path| {
try writer.print(
\\ _ = @import("{s}");
\\
- , .{module_path});
+ , .{module_path.*});
}
try writer.writeAll(
@@ -362,7 +375,7 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
unreachable;
std.mem.replaceScalar(u8, module_path, '\\', '/');
- try self.module_paths.append(try self.allocator.dupe(u8, module_path));
+ try self.module_paths.put(try self.allocator.dupe(u8, module_path), {});
var buf: [32]u8 = undefined;
const id = jetzig.util.generateVariableName(&buf);
@@ -879,3 +892,15 @@ fn writeJobs(self: Routes, writer: anytype) !void {
std.debug.print("[jetzig] Imported {} job(s)\n", .{count});
}
+
+fn writeViewsArray(self: Routes, writer: anytype) !void {
+ var it = self.module_paths.keyIterator();
+ while (it.next()) |path| {
+ try writer.print(
+ \\.{{ .name = "{s}", .module = @import("{s}") }},
+ \\
+ ,
+ .{ chompExtension(try self.relativePathFrom(.views, path.*, .posix)), path.* },
+ );
+ }
+}
diff --git a/src/jetzig/App.zig b/src/jetzig/App.zig
index 7ba4244..91c5e66 100644
--- a/src/jetzig/App.zig
+++ b/src/jetzig/App.zig
@@ -11,7 +11,7 @@ env: jetzig.Environment,
allocator: std.mem.Allocator,
custom_routes: std.ArrayList(jetzig.views.Route),
initHook: ?*const fn (*App) anyerror!void,
-server: *jetzig.http.Server = undefined,
+server: *anyopaque = undefined,
pub fn deinit(self: *const App) void {
@constCast(self).custom_routes.deinit();
@@ -28,6 +28,10 @@ const AppOptions = struct {
pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
defer self.env.deinit();
+ const action_router = comptime jetzig.channels.ActionRouter.initComptime(routes_module);
+ _ = action_router;
+ // inline for (action_router.actions) |action| std.debug.print("{s}\n", .{@typeName(action.params[0].type.?)});
+
if (self.initHook) |hook| try hook(@constCast(self));
var mime_map = jetzig.http.mime.MimeMap.init(self.allocator);
@@ -93,7 +97,7 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
std.process.exit(0);
}
- var server = jetzig.http.Server.init(
+ var server = jetzig.http.Server.RoutedServer(routes_module).init(
self.allocator,
self.env,
routes,
diff --git a/src/jetzig/channels.zig b/src/jetzig/channels.zig
index ceeac5f..86737ba 100644
--- a/src/jetzig/channels.zig
+++ b/src/jetzig/channels.zig
@@ -1,3 +1,4 @@
pub const Channel = @import("channels/Channel.zig");
pub const Message = @import("channels/Message.zig");
pub const Route = @import("channels/Route.zig");
+pub const ActionRouter = @import("channels/ActionRouter.zig");
diff --git a/src/jetzig/channels/ActionRouter.zig b/src/jetzig/channels/ActionRouter.zig
new file mode 100644
index 0000000..8397c42
--- /dev/null
+++ b/src/jetzig/channels/ActionRouter.zig
@@ -0,0 +1,45 @@
+const std = @import("std");
+
+pub fn initComptime(T: type) ActionRouter {
+ comptime {
+ var len: usize = 0;
+ for (T.views) |view| {
+ if (!@hasDecl(view.module, "Channel")) continue;
+ if (!@hasDecl(view.module.Channel, "Actions")) continue;
+
+ const actions = view.module.Channel.Actions;
+ for (std.meta.declarations(actions)) |_| {
+ len += 1;
+ }
+ }
+ var actions: [len]Action = undefined;
+ var index: usize = 0;
+ for (T.views) |view| {
+ if (!@hasDecl(view.module, "Channel")) continue;
+ if (!@hasDecl(view.module.Channel, "Actions")) continue;
+
+ const channel_actions = view.module.Channel.Actions;
+ for (std.meta.declarations(channel_actions)) |decl| {
+ actions[index] = .{
+ .view = view.name,
+ .name = decl.name,
+ .params = &.{}, //@typeInfo(@TypeOf(@field(view.module.Channel.Actions, decl.name))).@"fn".params,
+ };
+ index += 1;
+ }
+ }
+
+ const result = actions;
+ return .{ .actions = &result };
+ }
+}
+
+pub const Action = struct {
+ view: []const u8,
+ name: []const u8,
+ params: []const u8,
+};
+
+pub const ActionRouter = struct {
+ actions: []const Action,
+};
diff --git a/src/jetzig/channels/Message.zig b/src/jetzig/channels/Message.zig
index 5b55a9f..4e3b8ca 100644
--- a/src/jetzig/channels/Message.zig
+++ b/src/jetzig/channels/Message.zig
@@ -30,7 +30,12 @@ pub fn value(message: Message) !*jetzig.data.Value {
test "message with payload" {
const message = Message.init(
std.testing.allocator,
- Channel{ .websocket = undefined, .state = undefined },
+ Channel{
+ .websocket = undefined,
+ .state = undefined,
+ .allocator = undefined,
+ .data = undefined,
+ },
"foo",
);
try std.testing.expectEqualStrings(message.payload, "foo");
diff --git a/src/jetzig/database.zig b/src/jetzig/database.zig
index b10a8af..3007098 100644
--- a/src/jetzig/database.zig
+++ b/src/jetzig/database.zig
@@ -39,9 +39,10 @@ pub fn repo(allocator: std.mem.Allocator, app: anytype) !Repo {
}
fn eventCallback(event: jetzig.jetquery.events.Event, app: anytype) !void {
- try app.server.logger.logSql(event);
+ var server: *jetzig.http.Server.RoutedServer(@import("root").routes) = @ptrCast(@alignCast(app.server));
+ try server.logger.logSql(event);
if (event.err) |err| {
- try app.server.logger.ERROR("[database] {?s}", .{err.message});
+ try server.logger.ERROR("[database] {?s}", .{err.message});
}
}
diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig
index b0a89ac..fd1773d 100644
--- a/src/jetzig/http/Request.zig
+++ b/src/jetzig/http/Request.zig
@@ -27,7 +27,6 @@ path: jetzig.http.Path,
method: Method,
headers: jetzig.http.Headers,
host: []const u8,
-server: *jetzig.http.Server,
httpz_request: *httpz.Request,
httpz_response: *httpz.Response,
response: *jetzig.http.Response,
@@ -53,8 +52,14 @@ rendered_view: ?jetzig.views.View = null,
start_time: i128,
store: RequestStore(jetzig.kv.Store.GeneralStore),
cache: RequestStore(jetzig.kv.Store.CacheStore),
+job_queue: RequestStore(jetzig.kv.Store.JobQueueStore),
+job_definitions: []const jetzig.JobDefinition,
+mailer_definitions: []const jetzig.MailerDefinition,
repo: *jetzig.database.Repo,
global: *jetzig.Global,
+env: jetzig.Environment,
+routes: []const *const jetzig.views.Route,
+logger: jetzig.loggers.Logger,
/// Wrapper for KV store that uses the request's arena allocator for fetching values.
pub fn RequestStore(T: type) type {
@@ -123,12 +128,20 @@ pub fn RequestStore(T: type) type {
pub fn init(
allocator: std.mem.Allocator,
- server: *jetzig.http.Server,
start_time: i128,
httpz_request: *httpz.Request,
httpz_response: *httpz.Response,
response: *jetzig.http.Response,
repo: *jetzig.database.Repo,
+ env: jetzig.Environment,
+ routes: []const *const jetzig.views.Route,
+ logger: jetzig.loggers.Logger,
+ store: *jetzig.kv.Store.GeneralStore,
+ cache: *jetzig.kv.Store.CacheStore,
+ job_queue: *jetzig.kv.Store.JobQueueStore,
+ job_definitions: []const jetzig.JobDefinition,
+ mailer_definitions: []const jetzig.MailerDefinition,
+ global: *anyopaque,
) !Request {
const path = jetzig.http.Path.init(httpz_request.url.raw);
@@ -156,19 +169,24 @@ pub fn init(
.method = method,
.headers = headers,
.host = host,
- .server = server,
.response = response,
.response_data = response_data,
.httpz_request = httpz_request,
.httpz_response = httpz_response,
.start_time = start_time,
- .store = .{ .store = server.store, .allocator = allocator },
- .cache = .{ .store = server.cache, .allocator = allocator },
+ .store = .{ .store = store, .allocator = allocator },
+ .cache = .{ .store = cache, .allocator = allocator },
+ .job_queue = .{ .store = job_queue, .allocator = allocator },
+ .job_definitions = job_definitions,
+ .mailer_definitions = mailer_definitions,
+ .env = env,
+ .routes = routes,
+ .logger = logger,
.repo = repo,
.global = if (@hasField(jetzig.Global, "__jetzig_default"))
undefined
else
- @ptrCast(@alignCast(server.global)),
+ @ptrCast(@alignCast(global)),
};
}
@@ -501,19 +519,19 @@ pub fn cookies(self: *Request) !*jetzig.http.Cookies {
/// `jetzig.http.Session`.
pub fn session(self: *Request) !*jetzig.http.Session {
if (self._session) |capture| return capture;
- const cookie_name = self.server.env.vars.get("JETZIG_SESSION_COOKIE") orelse
+ const cookie_name = self.env.vars.get("JETZIG_SESSION_COOKIE") orelse
jetzig.http.Session.default_cookie_name;
const local_session = try self.allocator.create(jetzig.http.Session);
local_session.* = jetzig.http.Session.init(
self.allocator,
try self.cookies(),
- self.server.env.secret,
+ self.env.secret,
.{ .cookie_name = cookie_name },
);
local_session.parse() catch |err| {
switch (err) {
error.JetzigInvalidSessionCookie => {
- try self.server.logger.DEBUG("Invalid session cookie detected. Resetting session.", .{});
+ try self.logger.DEBUG("Invalid session cookie detected. Resetting session.", .{});
try local_session.reset();
},
else => return err,
@@ -565,11 +583,11 @@ pub fn job(self: *Request, job_name: []const u8) !*jetzig.Job {
const background_job = try self.allocator.create(jetzig.Job);
background_job.* = jetzig.Job.init(
self.allocator,
- self.server.store,
- self.server.job_queue,
- self.server.cache,
- self.server.logger,
- self.server.job_definitions,
+ self.store.store,
+ self.job_queue.store,
+ self.cache.store,
+ self.logger,
+ self.job_definitions,
job_name,
);
return background_job;
@@ -611,14 +629,14 @@ const RequestMail = struct {
self.request.allocator,
mail_job.params,
jetzig.jobs.JobEnv{
- .vars = self.request.server.env.vars,
- .environment = self.request.server.env.environment,
- .logger = self.request.server.logger,
- .routes = self.request.server.routes,
- .mailers = self.request.server.mailer_definitions,
- .jobs = self.request.server.job_definitions,
- .store = self.request.server.store,
- .cache = self.request.server.cache,
+ .vars = self.request.env.vars,
+ .environment = self.request.env.environment,
+ .logger = self.request.logger,
+ .routes = self.request.routes,
+ .mailers = self.request.mailer_definitions,
+ .jobs = self.request.job_definitions,
+ .store = self.request.store.store,
+ .cache = self.request.cache.store,
.mutex = undefined,
.repo = self.request.repo,
},
diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig
index ee09120..210b762 100644
--- a/src/jetzig/http/Server.zig
+++ b/src/jetzig/http/Server.zig
@@ -6,902 +6,916 @@ const zmpl = @import("zmpl");
const zmd = @import("zmd");
const httpz = @import("httpz");
-allocator: std.mem.Allocator,
-logger: jetzig.loggers.Logger,
-env: jetzig.Environment,
-routes: []const *const jetzig.views.Route,
-channel_routes: std.StaticStringMap(jetzig.channels.Route),
-custom_routes: []const jetzig.views.Route,
-job_definitions: []const jetzig.JobDefinition,
-mailer_definitions: []const jetzig.MailerDefinition,
-mime_map: *jetzig.http.mime.MimeMap,
-initialized: bool = false,
-store: *jetzig.kv.Store.GeneralStore,
-job_queue: *jetzig.kv.Store.JobQueueStore,
-cache: *jetzig.kv.Store.CacheStore,
-channels: *jetzig.kv.Store.ChannelStore,
-repo: *jetzig.database.Repo,
-global: *anyopaque,
-decoded_static_route_params: []const *jetzig.data.Value = &.{},
-debug_mutex: std.Thread.Mutex = .{},
-
-const Server = @This();
-
-pub fn init(
- allocator: std.mem.Allocator,
- env: jetzig.Environment,
- routes: []const *const jetzig.views.Route,
- channel_routes: std.StaticStringMap(jetzig.channels.Route),
- custom_routes: []const jetzig.views.Route,
- job_definitions: []const jetzig.JobDefinition,
- mailer_definitions: []const jetzig.MailerDefinition,
- mime_map: *jetzig.http.mime.MimeMap,
- store: *jetzig.kv.Store.GeneralStore,
- job_queue: *jetzig.kv.Store.JobQueueStore,
- cache: *jetzig.kv.Store.CacheStore,
- channels: *jetzig.kv.Store.ChannelStore,
- repo: *jetzig.database.Repo,
- global: *anyopaque,
-) Server {
- return .{
- .allocator = allocator,
- .logger = env.logger,
- .env = env,
- .routes = routes,
- .channel_routes = channel_routes,
- .custom_routes = custom_routes,
- .job_definitions = job_definitions,
- .mailer_definitions = mailer_definitions,
- .mime_map = mime_map,
- .store = store,
- .job_queue = job_queue,
- .cache = cache,
- .channels = channels,
- .repo = repo,
- .global = global,
- };
-}
-
-pub fn deinit(self: *Server) void {
- self.allocator.free(self.env.secret);
- self.allocator.free(self.env.bind);
-}
-
-const HttpzHandler = struct {
- server: *Server,
-
- pub const WebsocketHandler = jetzig.http.Websocket;
-
- pub fn handle(self: HttpzHandler, request: *httpz.Request, response: *httpz.Response) void {
- self.server.processNextRequest(request, response) catch |err| {
- self.server.errorHandlerFn(request, response, err) catch {};
- };
- }
-};
-
-pub fn listen(self: *Server) !void {
- try self.decodeStaticParams();
-
- const worker_count = jetzig.config.get(u16, "worker_count");
- const thread_count: u16 = jetzig.config.get(?u16, "thread_count") orelse @intCast(try std.Thread.getCpuCount());
-
- var httpz_server = try httpz.Server(HttpzHandler).init(
- self.allocator,
- .{
- .port = self.env.port,
- .address = self.env.bind,
- .thread_pool = .{
- .count = thread_count,
- .buffer_size = jetzig.config.get(usize, "buffer_size"),
- },
- .workers = .{
- .count = worker_count,
- .max_conn = jetzig.config.get(u16, "max_connections"),
- .retain_allocated_bytes = jetzig.config.get(usize, "arena_size"),
- },
- .request = .{
- .max_multiform_count = jetzig.config.get(usize, "max_multipart_form_fields"),
- .max_body_size = jetzig.config.get(usize, "max_bytes_request_body"),
- },
- },
- HttpzHandler{ .server = self },
- );
- defer httpz_server.deinit();
-
- try self.logger.INFO("Listening on http://{s}:{d} [{s}] [workers:{d} threads:{d}]", .{
- self.env.bind,
- self.env.port,
- @tagName(self.env.environment),
- worker_count,
- thread_count,
- });
-
- self.initialized = true;
-
- try jetzig.http.middleware.afterLaunch(self);
-
- return try httpz_server.listen();
-}
-
-pub fn errorHandlerFn(self: *Server, request: *httpz.Request, response: *httpz.Response, err: anyerror) !void {
- if (isBadHttpError(err)) return;
-
- self.logger.ERROR("Encountered error: {s} {s}", .{ @errorName(err), request.url.raw }) catch {};
- const stack = @errorReturnTrace();
- if (stack) |capture| {
- self.debug_mutex.lock();
- defer self.debug_mutex.unlock();
- self.logStackTrace(capture, request.arena) catch {};
- }
-
- response.body = "500 Internal Server Error";
-}
-
-pub fn processNextRequest(
- self: *Server,
- httpz_request: *httpz.Request,
- httpz_response: *httpz.Response,
-) !void {
- const start_time = std.time.nanoTimestamp();
-
- var repo = try self.repo.bindConnect(.{ .allocator = httpz_response.arena });
- defer repo.release();
-
- var response = try jetzig.http.Response.init(httpz_response.arena, httpz_response);
- var request = try jetzig.http.Request.init(
- httpz_response.arena,
- self,
- start_time,
- httpz_request,
- httpz_response,
- &response,
- &repo,
- );
-
- if (try self.upgradeWebsocket(httpz_request, httpz_response, &request)) {
- try self.logger.DEBUG("Websocket upgrade request successful.", .{});
- return;
- }
-
- try request.process();
-
- var middleware_data = try jetzig.http.middleware.afterRequest(&request);
- if (try maybeMiddlewareRender(&request, &response)) {
- try self.logger.logRequest(&request);
- return;
- }
-
- try self.renderResponse(&request);
- try request.response.headers.append("Content-Type", response.content_type);
-
- try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
- try request.respond();
- try jetzig.http.middleware.afterResponse(&middleware_data, &request);
- jetzig.http.middleware.deinit(&middleware_data, &request);
-
- try self.logger.logRequest(&request);
-}
-
-/// Attempt to match a channel name to a view with a Channel implementation.
-pub fn matchChannelRoute(self: *const Server, channel_name: []const u8) ?jetzig.channels.Route {
- return self.channel_routes.get(channel_name);
-}
-
-fn upgradeWebsocket(
- self: *const Server,
- httpz_request: *httpz.Request,
- httpz_response: *httpz.Response,
- request: *jetzig.http.Request,
-) !bool {
- const route = self.matchChannelRoute(request.path.view_name) orelse return false;
- const session = try request.session();
- const session_id = session.getT(.string, "_id") orelse {
- try self.logger.ERROR("Error fetching session ID for websocket, aborting.", .{});
- return false;
- };
-
- return try httpz.upgradeWebsocket(
- jetzig.http.Websocket,
- httpz_request,
- httpz_response,
- jetzig.http.Websocket.Context{
- .allocator = self.allocator,
- .route = route,
- .session_id = session_id,
- .server = self,
- },
- );
-}
-
-fn maybeMiddlewareRender(request: *jetzig.http.Request, response: *const jetzig.http.Response) !bool {
- if (request.middleware_rendered) |_| {
- // Request processing ends when a middleware renders or redirects.
- if (request.redirect_state) |state| {
- try request.renderRedirect(state);
- } else if (request.rendered_view) |rendered| {
- // TODO: Allow middleware to set content
- request.setResponse(.{ .view = rendered, .content = "" }, .{});
- }
- try request.response.headers.append("Content-Type", response.content_type);
- try request.respond();
- return true;
- } else return false;
-}
-
-fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
- const static_resource = self.matchStaticResource(request) catch |err| {
- if (isUnhandledError(err)) return err;
-
- const rendered = try self.renderInternalServerError(request, @errorReturnTrace(), err);
- request.setResponse(rendered, .{});
- return;
- };
-
- if (static_resource) |resource| {
- try renderStatic(resource, request);
- return;
- }
-
- if (matchMiddlewareRoute(request)) |route| {
- if (route.content) |content| {
- const rendered: RenderedView = .{
- .view = .{ .data = request.response_data, .status_code = route.status },
- .content = content,
- };
- request.setResponse(rendered, .{ .content_type = route.content_type });
- return;
- } else unreachable; // In future a MiddlewareRoute might provide a render function etc.
- }
-
- const maybe_route = self.matchCustomRoute(request) orelse try self.matchRoute(request, false);
-
- if (maybe_route) |route| {
- if (!route.validateFormat(request)) {
- return request.setResponse(try self.renderNotFound(request), .{});
- }
- }
-
- if (maybe_route) |route| {
- for (route.before_callbacks) |callback| {
- try callback(request, route);
- if (request.rendered_view) |view| {
- if (request.state == .failed) {
- request.setResponse(
- try self.renderError(request, view.status_code, .{}),
- .{},
- );
- } else if (request.state == .rendered) {
- // TODO: Allow callbacks to set content
- }
- return;
- }
- if (request.redirect_state) |state| {
- try request.renderRedirect(state);
- return;
- }
- }
- }
-
- switch (request.requestFormat()) {
- .HTML => try self.renderHTML(request, maybe_route),
- .JSON => try self.renderJSON(request, maybe_route),
- .UNKNOWN => try self.renderHTML(request, maybe_route),
- }
-
- if (maybe_route) |route| {
- for (route.after_callbacks) |callback| {
- try callback(request, request.response, route);
- }
- }
-
- if (request.redirect_state) |state| try request.renderRedirect(state);
-}
-
-fn renderStatic(resource: StaticResource, request: *jetzig.http.Request) !void {
- request.setResponse(
- .{ .view = .{ .data = request.response_data }, .content = resource.content },
- .{ .content_type = resource.mime_type },
- );
-}
-
-fn renderHTML(
- self: *Server,
- request: *jetzig.http.Request,
- route: ?jetzig.views.Route,
-) !void {
- if (route) |matched_route| {
- if (zmpl.findPrefixed("views", matched_route.template)) |template| {
- const rendered = self.renderView(matched_route, request, template) catch |err| {
- if (isUnhandledError(err)) return err;
- const rendered_error = try self.renderInternalServerError(
- request,
- @errorReturnTrace(),
- err,
- );
- return request.setResponse(rendered_error, .{});
- };
- return request.setResponse(rendered, .{});
- } else {
- // Try rendering without a template to see if we get a redirect or a template
- // assigned in a view.
- const rendered = self.renderView(matched_route, request, null) catch |err| {
- if (isUnhandledError(err)) return err;
- const rendered_error = try self.renderInternalServerError(request, @errorReturnTrace(), err);
- return request.setResponse(rendered_error, .{});
- };
-
- return if (request.state == .redirected or
- request.state == .failed or
- request.dynamic_assigned_template != null)
- request.setResponse(rendered, .{})
- else
- request.setResponse(try self.renderNotFound(request), .{});
- }
- } else {
- // If no matching route found, try to render a Markdown file in views directory.
- if (try self.renderMarkdown(request)) |rendered| {
- return request.setResponse(rendered, .{});
- } else {
- return request.setResponse(try self.renderNotFound(request), .{});
- }
- }
-}
-
-fn renderJSON(
- self: *Server,
- request: *jetzig.http.Request,
- route: ?jetzig.views.Route,
-) !void {
- if (route) |matched_route| {
- var rendered = try self.renderView(matched_route, request, null);
- var data = rendered.view.data;
-
- if (data.value) |_| {} else _ = try data.object();
-
- rendered.content = if (self.env.environment == .development)
- try data.toJsonOptions(.{ .pretty = true, .color = false })
- else
- try data.toJson();
-
- request.setResponse(rendered, .{});
- } else {
- request.setResponse(try self.renderNotFound(request), .{});
- }
-}
-
-fn renderMarkdown(self: *Server, request: *jetzig.http.Request) !?RenderedView {
- _ = self;
- // No route recognized, but we can still render a static markdown file if it matches the URI:
- if (request.method != .GET) return null;
- if (try jetzig.markdown.renderFile(request.allocator, request.path.base_path, .{})) |content| {
- return .{
- .view = jetzig.views.View{ .data = request.response_data, .status_code = .ok },
- .content = content,
- };
- } else {
- return null;
- }
-}
-
pub const RenderedView = struct { view: jetzig.views.View, content: []const u8 };
-fn renderView(
- self: *Server,
- route: jetzig.views.Route,
- request: *jetzig.http.Request,
- maybe_template: ?zmpl.Template,
-) !RenderedView {
- // View functions return a `View` to encourage users to return from a view function with
- // `return request.render(.ok)`, but the actual rendered view is stored in
- // `request.rendered_view`.
- _ = route.render(route, request) catch |err| {
- if (isUnhandledError(err)) return err;
- if (isBadRequest(err)) return try self.renderBadRequest(request);
- return try self.renderInternalServerError(request, @errorReturnTrace(), err);
- };
+pub fn RoutedServer(Routes: type) type {
+ _ = Routes;
+ return struct {
+ allocator: std.mem.Allocator,
+ logger: jetzig.loggers.Logger,
+ env: jetzig.Environment,
+ routes: []const *const jetzig.views.Route,
+ channel_routes: std.StaticStringMap(jetzig.channels.Route),
+ custom_routes: []const jetzig.views.Route,
+ job_definitions: []const jetzig.JobDefinition,
+ mailer_definitions: []const jetzig.MailerDefinition,
+ mime_map: *jetzig.http.mime.MimeMap,
+ initialized: bool = false,
+ store: *jetzig.kv.Store.GeneralStore,
+ job_queue: *jetzig.kv.Store.JobQueueStore,
+ cache: *jetzig.kv.Store.CacheStore,
+ channels: *jetzig.kv.Store.ChannelStore,
+ repo: *jetzig.database.Repo,
+ global: *anyopaque,
+ decoded_static_route_params: []const *jetzig.data.Value = &.{},
+ debug_mutex: std.Thread.Mutex = .{},
- if (request.state == .failed) {
- const view: jetzig.views.View = request.rendered_view orelse .{
- .data = request.response_data,
- .status_code = .internal_server_error,
- };
- return try self.renderError(request, view.status_code, .{});
- }
+ const Server = @This();
- const template: ?zmpl.Template = if (request.dynamic_assigned_template) |request_template|
- zmpl.findPrefixed("views", request_template) orelse maybe_template
- else
- maybe_template;
-
- if (request.rendered_multiple) return error.JetzigMultipleRenderError;
-
- if (request.rendered_view) |rendered_view| {
- if (request.state == .redirected) return .{ .view = rendered_view, .content = "" };
-
- if (template) |capture| {
+ pub fn init(
+ allocator: std.mem.Allocator,
+ env: jetzig.Environment,
+ routes: []const *const jetzig.views.Route,
+ channel_routes: std.StaticStringMap(jetzig.channels.Route),
+ custom_routes: []const jetzig.views.Route,
+ job_definitions: []const jetzig.JobDefinition,
+ mailer_definitions: []const jetzig.MailerDefinition,
+ mime_map: *jetzig.http.mime.MimeMap,
+ store: *jetzig.kv.Store.GeneralStore,
+ job_queue: *jetzig.kv.Store.JobQueueStore,
+ cache: *jetzig.kv.Store.CacheStore,
+ channels: *jetzig.kv.Store.ChannelStore,
+ repo: *jetzig.database.Repo,
+ global: *anyopaque,
+ ) Server {
return .{
- .view = rendered_view,
- .content = try self.renderTemplateWithLayout(request, capture, rendered_view, route),
- };
- } else {
- return switch (request.requestFormat()) {
- .HTML, .UNKNOWN => blk: {
- try self.logger.DEBUG(
- "Missing template for route `{s}.{s}`. Expected: `src/app/views/{s}.zmpl`.",
- .{ route.view_name, @tagName(route.action), route.template },
- );
- if (comptime jetzig.build_options.debug_console) {
- return error.ZmplTemplateNotFound;
- }
- break :blk try self.renderNotFound(request);
- },
- .JSON => .{ .view = rendered_view, .content = "" },
+ .allocator = allocator,
+ .logger = env.logger,
+ .env = env,
+ .routes = routes,
+ .channel_routes = channel_routes,
+ .custom_routes = custom_routes,
+ .job_definitions = job_definitions,
+ .mailer_definitions = mailer_definitions,
+ .mime_map = mime_map,
+ .store = store,
+ .job_queue = job_queue,
+ .cache = cache,
+ .channels = channels,
+ .repo = repo,
+ .global = global,
};
}
- } else {
- if (request.state == .processed) {
- try self.logger.WARN("`request.render` was not invoked. Rendering empty content.", .{});
+
+ pub fn deinit(self: *Server) void {
+ self.allocator.free(self.env.secret);
+ self.allocator.free(self.env.bind);
}
- request.response_data.reset();
- return .{
- .view = .{ .data = request.response_data, .status_code = .no_content },
- .content = "",
+
+ const HttpzHandler = struct {
+ server: *Server,
+
+ pub const WebsocketHandler = jetzig.http.Websocket;
+
+ pub fn handle(self: HttpzHandler, request: *httpz.Request, response: *httpz.Response) void {
+ self.server.processNextRequest(request, response) catch |err| {
+ self.server.errorHandlerFn(request, response, err) catch {};
+ };
+ }
};
- }
-}
-fn renderTemplateWithLayout(
- self: *Server,
- request: *jetzig.http.Request,
- template: zmpl.Template,
- view: jetzig.views.View,
- route: jetzig.views.Route,
-) ![]const u8 {
- try addTemplateConstants(view, route);
+ pub fn listen(self: *Server) !void {
+ try self.decodeStaticParams();
- const template_context = jetzig.TemplateContext{ .request = request };
+ const worker_count = jetzig.config.get(u16, "worker_count");
+ const thread_count: u16 = jetzig.config.get(?u16, "thread_count") orelse @intCast(try std.Thread.getCpuCount());
- if (request.getLayout(route)) |layout_name| {
- // TODO: Allow user to configure layouts directory other than src/app/views/layouts/
- const prefixed_name = try std.mem.concat(
- self.allocator,
- u8,
- &[_][]const u8{ "layouts", "/", layout_name },
- );
- defer self.allocator.free(prefixed_name);
-
- if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
- return try template.render(
- view.data,
- jetzig.TemplateContext,
- template_context,
- .{ .layout = layout },
+ var httpz_server = try httpz.Server(HttpzHandler).init(
+ self.allocator,
+ .{
+ .port = self.env.port,
+ .address = self.env.bind,
+ .thread_pool = .{
+ .count = thread_count,
+ .buffer_size = jetzig.config.get(usize, "buffer_size"),
+ },
+ .workers = .{
+ .count = worker_count,
+ .max_conn = jetzig.config.get(u16, "max_connections"),
+ .retain_allocated_bytes = jetzig.config.get(usize, "arena_size"),
+ },
+ .request = .{
+ .max_multiform_count = jetzig.config.get(usize, "max_multipart_form_fields"),
+ .max_body_size = jetzig.config.get(usize, "max_bytes_request_body"),
+ },
+ },
+ HttpzHandler{ .server = self },
);
- } else {
- try self.logger.WARN("Unknown layout: {s}", .{layout_name});
- return try template.render(
+ defer httpz_server.deinit();
+
+ try self.logger.INFO("Listening on http://{s}:{d} [{s}] [workers:{d} threads:{d}]", .{
+ self.env.bind,
+ self.env.port,
+ @tagName(self.env.environment),
+ worker_count,
+ thread_count,
+ });
+
+ self.initialized = true;
+
+ try jetzig.http.middleware.afterLaunch(self);
+
+ return try httpz_server.listen();
+ }
+
+ pub fn errorHandlerFn(self: *Server, request: *httpz.Request, response: *httpz.Response, err: anyerror) !void {
+ if (isBadHttpError(err)) return;
+
+ self.logger.ERROR("Encountered error: {s} {s}", .{ @errorName(err), request.url.raw }) catch {};
+ const stack = @errorReturnTrace();
+ if (stack) |capture| {
+ self.debug_mutex.lock();
+ defer self.debug_mutex.unlock();
+ self.logStackTrace(capture, request.arena) catch {};
+ }
+
+ response.body = "500 Internal Server Error";
+ }
+
+ pub fn processNextRequest(
+ self: *Server,
+ httpz_request: *httpz.Request,
+ httpz_response: *httpz.Response,
+ ) !void {
+ const start_time = std.time.nanoTimestamp();
+
+ var repo = try self.repo.bindConnect(.{ .allocator = httpz_response.arena });
+ defer repo.release();
+
+ var response = try jetzig.http.Response.init(httpz_response.arena, httpz_response);
+ var request = try jetzig.http.Request.init(
+ httpz_response.arena,
+ start_time,
+ httpz_request,
+ httpz_response,
+ &response,
+ &repo,
+ self.env,
+ self.routes,
+ self.logger,
+ self.store,
+ self.cache,
+ self.job_queue,
+ self.job_definitions,
+ self.mailer_definitions,
+ self.global,
+ );
+
+ if (try self.upgradeWebsocket(httpz_request, httpz_response, &request)) {
+ try self.logger.DEBUG("Websocket upgrade request successful.", .{});
+ return;
+ }
+
+ try request.process();
+
+ var middleware_data = try jetzig.http.middleware.afterRequest(&request);
+ if (try maybeMiddlewareRender(&request, &response)) {
+ try self.logger.logRequest(&request);
+ return;
+ }
+
+ try self.renderResponse(&request);
+ try request.response.headers.append("Content-Type", response.content_type);
+
+ try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
+ try request.respond();
+ try jetzig.http.middleware.afterResponse(&middleware_data, &request);
+ jetzig.http.middleware.deinit(&middleware_data, &request);
+
+ try self.logger.logRequest(&request);
+ }
+
+ /// Attempt to match a channel name to a view with a Channel implementation.
+ pub fn matchChannelRoute(self: *const Server, channel_name: []const u8) ?jetzig.channels.Route {
+ // TODO: Detect root path correctly.
+ return self.channel_routes.get(channel_name);
+ }
+
+ fn upgradeWebsocket(
+ self: *const Server,
+ httpz_request: *httpz.Request,
+ httpz_response: *httpz.Response,
+ request: *jetzig.http.Request,
+ ) !bool {
+ const route = self.matchChannelRoute(request.path.view_name) orelse return false;
+ const session = try request.session();
+ const session_id = session.getT(.string, "_id") orelse {
+ try self.logger.ERROR("Error fetching session ID for websocket, aborting.", .{});
+ return false;
+ };
+
+ return try httpz.upgradeWebsocket(
+ jetzig.http.Websocket,
+ httpz_request,
+ httpz_response,
+ jetzig.http.Websocket.Context{
+ .allocator = self.allocator,
+ .route = route,
+ .session_id = session_id,
+ .channels = self.channels,
+ },
+ );
+ }
+
+ fn maybeMiddlewareRender(request: *jetzig.http.Request, response: *const jetzig.http.Response) !bool {
+ if (request.middleware_rendered) |_| {
+ // Request processing ends when a middleware renders or redirects.
+ if (request.redirect_state) |state| {
+ try request.renderRedirect(state);
+ } else if (request.rendered_view) |rendered| {
+ // TODO: Allow middleware to set content
+ request.setResponse(.{ .view = rendered, .content = "" }, .{});
+ }
+ try request.response.headers.append("Content-Type", response.content_type);
+ try request.respond();
+ return true;
+ } else return false;
+ }
+
+ fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
+ const static_resource = self.matchStaticResource(request) catch |err| {
+ if (isUnhandledError(err)) return err;
+
+ const rendered = try self.renderInternalServerError(request, @errorReturnTrace(), err);
+ request.setResponse(rendered, .{});
+ return;
+ };
+
+ if (static_resource) |resource| {
+ try renderStatic(resource, request);
+ return;
+ }
+
+ if (matchMiddlewareRoute(request)) |route| {
+ if (route.content) |content| {
+ const rendered: RenderedView = .{
+ .view = .{ .data = request.response_data, .status_code = route.status },
+ .content = content,
+ };
+ request.setResponse(rendered, .{ .content_type = route.content_type });
+ return;
+ } else unreachable; // In future a MiddlewareRoute might provide a render function etc.
+ }
+
+ const maybe_route = self.matchCustomRoute(request) orelse try self.matchRoute(request, false);
+
+ if (maybe_route) |route| {
+ if (!route.validateFormat(request)) {
+ return request.setResponse(try self.renderNotFound(request), .{});
+ }
+ }
+
+ if (maybe_route) |route| {
+ for (route.before_callbacks) |callback| {
+ try callback(request, route);
+ if (request.rendered_view) |view| {
+ if (request.state == .failed) {
+ request.setResponse(
+ try self.renderError(request, view.status_code, .{}),
+ .{},
+ );
+ } else if (request.state == .rendered) {
+ // TODO: Allow callbacks to set content
+ }
+ return;
+ }
+ if (request.redirect_state) |state| {
+ try request.renderRedirect(state);
+ return;
+ }
+ }
+ }
+
+ switch (request.requestFormat()) {
+ .HTML => try self.renderHTML(request, maybe_route),
+ .JSON => try self.renderJSON(request, maybe_route),
+ .UNKNOWN => try self.renderHTML(request, maybe_route),
+ }
+
+ if (maybe_route) |route| {
+ for (route.after_callbacks) |callback| {
+ try callback(request, request.response, route);
+ }
+ }
+
+ if (request.redirect_state) |state| try request.renderRedirect(state);
+ }
+
+ fn renderStatic(resource: StaticResource, request: *jetzig.http.Request) !void {
+ request.setResponse(
+ .{ .view = .{ .data = request.response_data }, .content = resource.content },
+ .{ .content_type = resource.mime_type },
+ );
+ }
+
+ fn renderHTML(
+ self: *Server,
+ request: *jetzig.http.Request,
+ route: ?jetzig.views.Route,
+ ) !void {
+ if (route) |matched_route| {
+ if (zmpl.findPrefixed("views", matched_route.template)) |template| {
+ const rendered = self.renderView(matched_route, request, template) catch |err| {
+ if (isUnhandledError(err)) return err;
+ const rendered_error = try self.renderInternalServerError(
+ request,
+ @errorReturnTrace(),
+ err,
+ );
+ return request.setResponse(rendered_error, .{});
+ };
+ return request.setResponse(rendered, .{});
+ } else {
+ // Try rendering without a template to see if we get a redirect or a template
+ // assigned in a view.
+ const rendered = self.renderView(matched_route, request, null) catch |err| {
+ if (isUnhandledError(err)) return err;
+ const rendered_error = try self.renderInternalServerError(request, @errorReturnTrace(), err);
+ return request.setResponse(rendered_error, .{});
+ };
+
+ return if (request.state == .redirected or
+ request.state == .failed or
+ request.dynamic_assigned_template != null)
+ request.setResponse(rendered, .{})
+ else
+ request.setResponse(try self.renderNotFound(request), .{});
+ }
+ } else {
+ // If no matching route found, try to render a Markdown file in views directory.
+ if (try self.renderMarkdown(request)) |rendered| {
+ return request.setResponse(rendered, .{});
+ } else {
+ return request.setResponse(try self.renderNotFound(request), .{});
+ }
+ }
+ }
+
+ fn renderJSON(
+ self: *Server,
+ request: *jetzig.http.Request,
+ route: ?jetzig.views.Route,
+ ) !void {
+ if (route) |matched_route| {
+ var rendered = try self.renderView(matched_route, request, null);
+ var data = rendered.view.data;
+
+ if (data.value) |_| {} else _ = try data.object();
+
+ rendered.content = if (self.env.environment == .development)
+ try data.toJsonOptions(.{ .pretty = true, .color = false })
+ else
+ try data.toJson();
+
+ request.setResponse(rendered, .{});
+ } else {
+ request.setResponse(try self.renderNotFound(request), .{});
+ }
+ }
+
+ fn renderMarkdown(self: *Server, request: *jetzig.http.Request) !?RenderedView {
+ _ = self;
+ // No route recognized, but we can still render a static markdown file if it matches the URI:
+ if (request.method != .GET) return null;
+ if (try jetzig.markdown.renderFile(request.allocator, request.path.base_path, .{})) |content| {
+ return .{
+ .view = jetzig.views.View{ .data = request.response_data, .status_code = .ok },
+ .content = content,
+ };
+ } else {
+ return null;
+ }
+ }
+
+ fn renderView(
+ self: *Server,
+ route: jetzig.views.Route,
+ request: *jetzig.http.Request,
+ maybe_template: ?zmpl.Template,
+ ) !RenderedView {
+ // View functions return a `View` to encourage users to return from a view function with
+ // `return request.render(.ok)`, but the actual rendered view is stored in
+ // `request.rendered_view`.
+ _ = route.render(route, request) catch |err| {
+ if (isUnhandledError(err)) return err;
+ if (isBadRequest(err)) return try self.renderBadRequest(request);
+ return try self.renderInternalServerError(request, @errorReturnTrace(), err);
+ };
+
+ if (request.state == .failed) {
+ const view: jetzig.views.View = request.rendered_view orelse .{
+ .data = request.response_data,
+ .status_code = .internal_server_error,
+ };
+ return try self.renderError(request, view.status_code, .{});
+ }
+
+ const template: ?zmpl.Template = if (request.dynamic_assigned_template) |request_template|
+ zmpl.findPrefixed("views", request_template) orelse maybe_template
+ else
+ maybe_template;
+
+ if (request.rendered_multiple) return error.JetzigMultipleRenderError;
+
+ if (request.rendered_view) |rendered_view| {
+ if (request.state == .redirected) return .{ .view = rendered_view, .content = "" };
+
+ if (template) |capture| {
+ return .{
+ .view = rendered_view,
+ .content = try self.renderTemplateWithLayout(request, capture, rendered_view, route),
+ };
+ } else {
+ return switch (request.requestFormat()) {
+ .HTML, .UNKNOWN => blk: {
+ try self.logger.DEBUG(
+ "Missing template for route `{s}.{s}`. Expected: `src/app/views/{s}.zmpl`.",
+ .{ route.view_name, @tagName(route.action), route.template },
+ );
+ if (comptime jetzig.build_options.debug_console) {
+ return error.ZmplTemplateNotFound;
+ }
+ break :blk try self.renderNotFound(request);
+ },
+ .JSON => .{ .view = rendered_view, .content = "" },
+ };
+ }
+ } else {
+ if (request.state == .processed) {
+ try self.logger.WARN("`request.render` was not invoked. Rendering empty content.", .{});
+ }
+ request.response_data.reset();
+ return .{
+ .view = .{ .data = request.response_data, .status_code = .no_content },
+ .content = "",
+ };
+ }
+ }
+
+ fn renderTemplateWithLayout(
+ self: *Server,
+ request: *jetzig.http.Request,
+ template: zmpl.Template,
+ view: jetzig.views.View,
+ route: jetzig.views.Route,
+ ) ![]const u8 {
+ try addTemplateConstants(view, route);
+
+ const template_context = jetzig.TemplateContext{ .request = request };
+
+ if (request.getLayout(route)) |layout_name| {
+ // TODO: Allow user to configure layouts directory other than src/app/views/layouts/
+ const prefixed_name = try std.mem.concat(
+ self.allocator,
+ u8,
+ &[_][]const u8{ "layouts", "/", layout_name },
+ );
+ defer self.allocator.free(prefixed_name);
+
+ if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
+ return try template.render(
+ view.data,
+ jetzig.TemplateContext,
+ template_context,
+ .{ .layout = layout },
+ );
+ } else {
+ try self.logger.WARN("Unknown layout: {s}", .{layout_name});
+ return try template.render(
+ view.data,
+ jetzig.TemplateContext,
+ template_context,
+ .{},
+ );
+ }
+ } else return try template.render(
view.data,
jetzig.TemplateContext,
template_context,
.{},
);
}
- } else return try template.render(
- view.data,
- jetzig.TemplateContext,
- template_context,
- .{},
- );
-}
-fn addTemplateConstants(view: jetzig.views.View, route: jetzig.views.Route) !void {
- const action = switch (route.action) {
- .custom => route.name,
- else => |tag| @tagName(tag),
- };
-
- try view.data.addConst("jetzig_action", view.data.string(action));
- try view.data.addConst("jetzig_view", view.data.string(route.view_name));
-}
-
-fn isBadRequest(err: anyerror) bool {
- return switch (err) {
- error.JetzigBodyParseError, error.JetzigQueryParseError => true,
- else => false,
- };
-}
-
-fn isUnhandledError(err: anyerror) bool {
- return switch (err) {
- error.OutOfMemory => true,
- else => false,
- };
-}
-
-fn isBadHttpError(err: anyerror) bool {
- return switch (err) {
- error.JetzigParseHeadError,
- error.UnknownHttpMethod,
- error.HttpHeadersInvalid,
- error.HttpHeaderContinuationsUnsupported,
- error.HttpTransferEncodingUnsupported,
- error.HttpConnectionHeaderUnsupported,
- error.InvalidContentLength,
- error.CompressionUnsupported,
- error.MissingFinalNewline,
- error.HttpConnectionClosing,
- error.ConnectionResetByPeer,
- error.BrokenPipe,
- => true,
- else => false,
- };
-}
-
-fn renderInternalServerError(
- self: *Server,
- request: *jetzig.http.Request,
- stack_trace: ?*std.builtin.StackTrace,
- err: anyerror,
-) !RenderedView {
- try self.logger.logError(stack_trace, err);
-
- const status = jetzig.http.StatusCode.internal_server_error;
-
- return try self.renderError(request, status, .{ .stack_trace = stack_trace, .err = err });
-}
-
-fn renderNotFound(self: *Server, request: *jetzig.http.Request) !RenderedView {
- request.response_data.reset();
-
- const status: jetzig.http.StatusCode = .not_found;
- return try self.renderError(request, status, .{});
-}
-
-fn renderBadRequest(self: *Server, request: *jetzig.http.Request) !RenderedView {
- request.response_data.reset();
-
- const status: jetzig.http.StatusCode = .bad_request;
- return try self.renderError(request, status, .{});
-}
-
-fn renderError(
- self: Server,
- request: *jetzig.http.Request,
- status_code: jetzig.http.StatusCode,
- error_info: jetzig.debug.ErrorInfo,
-) !RenderedView {
- if (comptime jetzig.build_options.debug_console) {
- return try self.renderDebugConsole(request, status_code, error_info);
- } else return try self.renderGeneralError(request, status_code);
-}
-
-fn renderGeneralError(
- self: Server,
- request: *jetzig.http.Request,
- status_code: jetzig.http.StatusCode,
-) !RenderedView {
- if (try self.renderErrorView(request, status_code)) |view| return view;
- if (try renderStaticErrorPage(request, status_code)) |view| return view;
-
- return try renderDefaultError(request, status_code);
-}
-
-fn renderDebugConsole(
- self: Server,
- request: *jetzig.http.Request,
- status_code: jetzig.http.StatusCode,
- error_info: jetzig.debug.ErrorInfo,
-) !RenderedView {
- if (comptime jetzig.build_options.debug_console) {
- var buf = std.ArrayList(u8).init(request.allocator);
- const writer = buf.writer();
-
- if (error_info.stack_trace) |stack_trace| {
- const debug_content = jetzig.debug.HtmlStackTrace{
- .allocator = request.allocator,
- .stack_trace = stack_trace,
+ fn addTemplateConstants(view: jetzig.views.View, route: jetzig.views.Route) !void {
+ const action = switch (route.action) {
+ .custom => route.name,
+ else => |tag| @tagName(tag),
};
- const error_name = if (error_info.err) |err| @errorName(err) else "[UnknownError]";
- try writer.print(
- jetzig.debug.console_template,
- .{
- error_name,
- debug_content,
- try request.response_data.toJsonOptions(.{ .pretty = true }),
- @embedFile("../../assets/debug.css"),
- },
- );
- } else return try self.renderGeneralError(request, status_code);
- const content = try buf.toOwnedSlice();
+ try view.data.addConst("jetzig_action", view.data.string(action));
+ try view.data.addConst("jetzig_view", view.data.string(route.view_name));
+ }
- return .{
- .view = .{ .data = request.response_data, .status_code = status_code },
- .content = if (content.len == 0) "" else content,
- };
- } else unreachable;
-}
+ fn isBadRequest(err: anyerror) bool {
+ return switch (err) {
+ error.JetzigBodyParseError, error.JetzigQueryParseError => true,
+ else => false,
+ };
+ }
-fn renderErrorView(
- self: Server,
- request: *jetzig.http.Request,
- status_code: jetzig.http.StatusCode,
-) !?RenderedView {
- for (self.routes) |route| {
- if (std.mem.eql(u8, route.view_name, "errors") and route.action == .index) {
+ fn isUnhandledError(err: anyerror) bool {
+ return switch (err) {
+ error.OutOfMemory => true,
+ else => false,
+ };
+ }
+
+ fn isBadHttpError(err: anyerror) bool {
+ return switch (err) {
+ error.JetzigParseHeadError,
+ error.UnknownHttpMethod,
+ error.HttpHeadersInvalid,
+ error.HttpHeaderContinuationsUnsupported,
+ error.HttpTransferEncodingUnsupported,
+ error.HttpConnectionHeaderUnsupported,
+ error.InvalidContentLength,
+ error.CompressionUnsupported,
+ error.MissingFinalNewline,
+ error.HttpConnectionClosing,
+ error.ConnectionResetByPeer,
+ error.BrokenPipe,
+ => true,
+ else => false,
+ };
+ }
+
+ fn renderInternalServerError(
+ self: *Server,
+ request: *jetzig.http.Request,
+ stack_trace: ?*std.builtin.StackTrace,
+ err: anyerror,
+ ) !RenderedView {
+ try self.logger.logError(stack_trace, err);
+
+ const status = jetzig.http.StatusCode.internal_server_error;
+
+ return try self.renderError(request, status, .{ .stack_trace = stack_trace, .err = err });
+ }
+
+ fn renderNotFound(self: *Server, request: *jetzig.http.Request) !RenderedView {
request.response_data.reset();
- request.status_code = status_code;
- _ = route.render(route.*, request) catch |err| {
- if (isUnhandledError(err)) return err;
- try self.logger.logError(@errorReturnTrace(), err);
- try self.logger.ERROR(
- "Unexepected error occurred while rendering error page: {s}",
- .{@errorName(err)},
- );
- return try renderDefaultError(request, status_code);
- };
+ const status: jetzig.http.StatusCode = .not_found;
+ return try self.renderError(request, status, .{});
+ }
- if (request.rendered_view) |view| {
- switch (request.requestFormat()) {
- .HTML, .UNKNOWN => {
- if (zmpl.findPrefixed("views", route.template)) |template| {
- try addTemplateConstants(view, route.*);
- return .{
- .view = view,
- .content = try template.render(
- request.response_data,
- jetzig.TemplateContext,
- .{ .request = request },
- .{},
- ),
- };
+ fn renderBadRequest(self: *Server, request: *jetzig.http.Request) !RenderedView {
+ request.response_data.reset();
+
+ const status: jetzig.http.StatusCode = .bad_request;
+ return try self.renderError(request, status, .{});
+ }
+
+ fn renderError(
+ self: Server,
+ request: *jetzig.http.Request,
+ status_code: jetzig.http.StatusCode,
+ error_info: jetzig.debug.ErrorInfo,
+ ) !RenderedView {
+ if (comptime jetzig.build_options.debug_console) {
+ return try self.renderDebugConsole(request, status_code, error_info);
+ } else return try self.renderGeneralError(request, status_code);
+ }
+
+ fn renderGeneralError(
+ self: Server,
+ request: *jetzig.http.Request,
+ status_code: jetzig.http.StatusCode,
+ ) !RenderedView {
+ if (try self.renderErrorView(request, status_code)) |view| return view;
+ if (try renderStaticErrorPage(request, status_code)) |view| return view;
+
+ return try renderDefaultError(request, status_code);
+ }
+
+ fn renderDebugConsole(
+ self: Server,
+ request: *jetzig.http.Request,
+ status_code: jetzig.http.StatusCode,
+ error_info: jetzig.debug.ErrorInfo,
+ ) !RenderedView {
+ if (comptime jetzig.build_options.debug_console) {
+ var buf = std.ArrayList(u8).init(request.allocator);
+ const writer = buf.writer();
+
+ if (error_info.stack_trace) |stack_trace| {
+ const debug_content = jetzig.debug.HtmlStackTrace{
+ .allocator = request.allocator,
+ .stack_trace = stack_trace,
+ };
+ const error_name = if (error_info.err) |err| @errorName(err) else "[UnknownError]";
+ try writer.print(
+ jetzig.debug.console_template,
+ .{
+ error_name,
+ debug_content,
+ try request.response_data.toJsonOptions(.{ .pretty = true }),
+ @embedFile("../../assets/debug.css"),
+ },
+ );
+ } else return try self.renderGeneralError(request, status_code);
+
+ const content = try buf.toOwnedSlice();
+
+ return .{
+ .view = .{ .data = request.response_data, .status_code = status_code },
+ .content = if (content.len == 0) "" else content,
+ };
+ } else unreachable;
+ }
+
+ fn renderErrorView(
+ self: Server,
+ request: *jetzig.http.Request,
+ status_code: jetzig.http.StatusCode,
+ ) !?RenderedView {
+ for (self.routes) |route| {
+ if (std.mem.eql(u8, route.view_name, "errors") and route.action == .index) {
+ request.response_data.reset();
+ request.status_code = status_code;
+
+ _ = route.render(route.*, request) catch |err| {
+ if (isUnhandledError(err)) return err;
+ try self.logger.logError(@errorReturnTrace(), err);
+ try self.logger.ERROR(
+ "Unexepected error occurred while rendering error page: {s}",
+ .{@errorName(err)},
+ );
+ return try renderDefaultError(request, status_code);
+ };
+
+ if (request.rendered_view) |view| {
+ switch (request.requestFormat()) {
+ .HTML, .UNKNOWN => {
+ if (zmpl.findPrefixed("views", route.template)) |template| {
+ try addTemplateConstants(view, route.*);
+ return .{
+ .view = view,
+ .content = try template.render(
+ request.response_data,
+ jetzig.TemplateContext,
+ .{ .request = request },
+ .{},
+ ),
+ };
+ }
+ },
+ .JSON => return .{ .view = view, .content = try request.response_data.toJson() },
}
- },
- .JSON => return .{ .view = view, .content = try request.response_data.toJson() },
- }
- }
- }
- }
-
- return null;
-}
-
-fn renderStaticErrorPage(request: *jetzig.http.Request, status_code: jetzig.http.StatusCode) !?RenderedView {
- if (request.requestFormat() == .JSON) return null;
-
- var dir = std.fs.cwd().openDir(
- jetzig.config.get([]const u8, "public_content_path"),
- .{ .iterate = false, .no_follow = true },
- ) catch |err| {
- switch (err) {
- error.FileNotFound => return null,
- else => return err,
- }
- };
- defer dir.close();
-
- const status = jetzig.http.status_codes.get(status_code);
- const content = dir.readFileAlloc(
- request.allocator,
- try std.mem.concat(request.allocator, u8, &.{ status.getCode(), ".html" }),
- jetzig.config.get(usize, "max_bytes_public_content"),
- ) catch |err| {
- switch (err) {
- error.FileNotFound => return null,
- else => return err,
- }
- };
-
- return .{
- .view = jetzig.views.View{ .data = request.response_data, .status_code = status_code },
- .content = content,
- };
-}
-
-fn renderDefaultError(
- request: *const jetzig.http.Request,
- status_code: jetzig.http.StatusCode,
-) !RenderedView {
- const content = try request.formatStatus(status_code);
- return .{
- .view = jetzig.views.View{ .data = request.response_data, .status_code = status_code },
- .content = content,
- };
-}
-
-fn logStackTrace(
- self: Server,
- stack: *std.builtin.StackTrace,
- allocator: std.mem.Allocator,
-) !void {
- var buf = std.ArrayList(u8).init(allocator);
- defer buf.deinit();
- const writer = buf.writer();
- try stack.format("", .{}, writer);
- if (buf.items.len > 0) try self.logger.ERROR("{s}\n", .{buf.items});
-}
-
-fn matchCustomRoute(self: Server, request: *const jetzig.http.Request) ?jetzig.views.Route {
- for (self.custom_routes) |custom_route| {
- if (custom_route.match(request)) return custom_route;
- }
-
- return null;
-}
-
-fn matchMiddlewareRoute(request: *const jetzig.http.Request) ?jetzig.middleware.MiddlewareRoute {
- const middlewares = jetzig.config.get([]const type, "middleware");
-
- inline for (middlewares) |middleware| {
- if (@hasDecl(middleware, "routes")) {
- inline for (middleware.routes) |route| {
- if (route.match(request)) return route;
- }
- }
- }
-
- return null;
-}
-
-fn matchRoute(self: *Server, request: *jetzig.http.Request, static: bool) !?jetzig.views.Route {
- for (self.routes) |route| {
- // .index routes always take precedence.
- if (route.action == .index and try request.match(route.*)) {
- if (!jetzig.build_options.build_static) return route.*;
- if (route.static == static) return route.*;
- }
- }
-
- for (self.routes) |route| {
- if (try request.match(route.*)) {
- if (!jetzig.build_options.build_static) return route.*;
- if (route.static == static) return route.*;
- }
- }
-
- return null;
-}
-
-const StaticResource = struct {
- content: []const u8,
- mime_type: []const u8 = "application/octet-stream",
-};
-
-fn matchStaticResource(self: *Server, request: *jetzig.http.Request) !?StaticResource {
- if (comptime jetzig.build_options.debug_console) {
- if (std.mem.eql(u8, request.path.path, "/_jetzig_debug.js")) return .{
- .content = @embedFile("../../assets/debug.js"),
- .mime_type = "text/javascript",
- };
- }
-
- // TODO: Map public and static routes at launch to avoid accessing the file system when
- // matching any route - currently every request causes file system traversal.
- const public_resource = try self.matchPublicContent(request);
- if (public_resource) |resource| return resource;
-
- const static_content = try self.matchStaticContent(request);
- if (static_content) |content| return .{
- .content = content,
- .mime_type = switch (request.requestFormat()) {
- .HTML, .UNKNOWN => "text/html",
- .JSON => "application/json",
- },
- };
-
- return null;
-}
-
-fn matchPublicContent(self: *Server, request: *jetzig.http.Request) !?StaticResource {
- if (request.path.file_path.len <= 1) return null;
- if (request.method != .GET) return null;
-
- var iterable_dir = std.fs.cwd().openDir(
- jetzig.config.get([]const u8, "public_content_path"),
- .{ .iterate = true, .no_follow = true },
- ) catch |err| {
- switch (err) {
- error.FileNotFound => return null,
- else => return err,
- }
- };
- defer iterable_dir.close();
-
- var walker = try iterable_dir.walk(request.allocator);
- defer walker.deinit();
- var path_buffer: [256]u8 = undefined;
- while (try walker.next()) |file| {
- if (file.kind != .file) continue;
- const file_path = if (builtin.os.tag == .windows) blk: {
- _ = std.mem.replace(u8, file.path, std.fs.path.sep_str_windows, std.fs.path.sep_str_posix, &path_buffer);
- break :blk path_buffer[0..file.path.len];
- } else file.path;
- if (std.mem.eql(u8, file_path, request.path.file_path[1..])) {
- const content = try iterable_dir.readFileAlloc(
- request.allocator,
- file_path,
- jetzig.config.get(usize, "max_bytes_public_content"),
- );
- const extension = std.fs.path.extension(file_path);
- const mime_type = if (self.mime_map.get(extension)) |mime| mime else "application/octet-stream";
- return .{
- .content = content,
- .mime_type = mime_type,
- };
- }
- }
-
- return null;
-}
-
-fn matchStaticContent(self: *Server, request: *jetzig.http.Request) !?[]const u8 {
- const request_format = request.requestFormat();
- const matched_route = try self.matchRoute(request, true);
-
- if (matched_route) |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 (std.mem.eql(u8, static_output.route_id, route.id)) {
- const params = try request.params();
-
- 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;
}
- }
- return null;
-}
+ fn renderStaticErrorPage(request: *jetzig.http.Request, status_code: jetzig.http.StatusCode) !?RenderedView {
+ if (request.requestFormat() == .JSON) return null;
-pub fn decodeStaticParams(self: *Server) !void {
- if (comptime !@hasDecl(jetzig.root, "static")) return;
+ var dir = std.fs.cwd().openDir(
+ jetzig.config.get([]const u8, "public_content_path"),
+ .{ .iterate = false, .no_follow = true },
+ ) catch |err| {
+ switch (err) {
+ error.FileNotFound => return null,
+ else => return err,
+ }
+ };
+ defer dir.close();
- // 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| {
- const data = try self.allocator.create(jetzig.data.Data);
- data.* = jetzig.data.Data.init(self.allocator);
- try data.fromJson(compiled.output.params orelse "{}");
- try decoded.append(data.value.?);
- }
+ const status = jetzig.http.status_codes.get(status_code);
+ const content = dir.readFileAlloc(
+ request.allocator,
+ try std.mem.concat(request.allocator, u8, &.{ status.getCode(), ".html" }),
+ jetzig.config.get(usize, "max_bytes_public_content"),
+ ) catch |err| {
+ switch (err) {
+ error.FileNotFound => return null,
+ else => return err,
+ }
+ };
- self.decoded_static_route_params = try decoded.toOwnedSlice();
-}
+ return .{
+ .view = jetzig.views.View{ .data = request.response_data, .status_code = status_code },
+ .content = content,
+ };
+ }
-fn matchStaticOutput(
- maybe_expected_id: ?[]const u8,
- maybe_expected_params: ?*jetzig.data.Value,
- route: jetzig.views.Route,
- request: *const jetzig.http.Request,
- params: jetzig.data.Value,
-) bool {
- return if (maybe_expected_params) |expected_params| blk: {
- const params_match = expected_params.count() == 0 or expected_params.eql(params);
- break :blk switch (route.action) {
- .index, .post => params_match,
- inline else => if (maybe_expected_id) |expected_id|
- std.mem.eql(u8, expected_id, request.path.resource_id) and params_match
- else
- false,
+ fn renderDefaultError(
+ request: *const jetzig.http.Request,
+ status_code: jetzig.http.StatusCode,
+ ) !RenderedView {
+ const content = try request.formatStatus(status_code);
+ return .{
+ .view = jetzig.views.View{ .data = request.response_data, .status_code = status_code },
+ .content = content,
+ };
+ }
+
+ fn logStackTrace(
+ self: Server,
+ stack: *std.builtin.StackTrace,
+ allocator: std.mem.Allocator,
+ ) !void {
+ var buf = std.ArrayList(u8).init(allocator);
+ defer buf.deinit();
+ const writer = buf.writer();
+ try stack.format("", .{}, writer);
+ if (buf.items.len > 0) try self.logger.ERROR("{s}\n", .{buf.items});
+ }
+
+ fn matchCustomRoute(self: Server, request: *const jetzig.http.Request) ?jetzig.views.Route {
+ for (self.custom_routes) |custom_route| {
+ if (custom_route.match(request)) return custom_route;
+ }
+
+ return null;
+ }
+
+ fn matchMiddlewareRoute(request: *const jetzig.http.Request) ?jetzig.middleware.MiddlewareRoute {
+ const middlewares = jetzig.config.get([]const type, "middleware");
+
+ inline for (middlewares) |middleware| {
+ if (@hasDecl(middleware, "routes")) {
+ inline for (middleware.routes) |route| {
+ if (route.match(request)) return route;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ fn matchRoute(self: *Server, request: *jetzig.http.Request, static: bool) !?jetzig.views.Route {
+ for (self.routes) |route| {
+ // .index routes always take precedence.
+ if (route.action == .index and try request.match(route.*)) {
+ if (!jetzig.build_options.build_static) return route.*;
+ if (route.static == static) return route.*;
+ }
+ }
+
+ for (self.routes) |route| {
+ if (try request.match(route.*)) {
+ if (!jetzig.build_options.build_static) return route.*;
+ if (route.static == static) return route.*;
+ }
+ }
+
+ return null;
+ }
+
+ const StaticResource = struct {
+ content: []const u8,
+ mime_type: []const u8 = "application/octet-stream",
};
- } else if (maybe_expected_id) |id|
- std.mem.eql(u8, id, request.path.resource_id)
- else
- true; // We reached a params filter (possibly the default catch-all) with no params set.
+
+ fn matchStaticResource(self: *Server, request: *jetzig.http.Request) !?StaticResource {
+ if (comptime jetzig.build_options.debug_console) {
+ if (std.mem.eql(u8, request.path.path, "/_jetzig_debug.js")) return .{
+ .content = @embedFile("../../assets/debug.js"),
+ .mime_type = "text/javascript",
+ };
+ }
+
+ // TODO: Map public and static routes at launch to avoid accessing the file system when
+ // matching any route - currently every request causes file system traversal.
+ const public_resource = try self.matchPublicContent(request);
+ if (public_resource) |resource| return resource;
+
+ const static_content = try self.matchStaticContent(request);
+ if (static_content) |content| return .{
+ .content = content,
+ .mime_type = switch (request.requestFormat()) {
+ .HTML, .UNKNOWN => "text/html",
+ .JSON => "application/json",
+ },
+ };
+
+ return null;
+ }
+
+ fn matchPublicContent(self: *Server, request: *jetzig.http.Request) !?StaticResource {
+ if (request.path.file_path.len <= 1) return null;
+ if (request.method != .GET) return null;
+
+ var iterable_dir = std.fs.cwd().openDir(
+ jetzig.config.get([]const u8, "public_content_path"),
+ .{ .iterate = true, .no_follow = true },
+ ) catch |err| {
+ switch (err) {
+ error.FileNotFound => return null,
+ else => return err,
+ }
+ };
+ defer iterable_dir.close();
+
+ var walker = try iterable_dir.walk(request.allocator);
+ defer walker.deinit();
+ var path_buffer: [256]u8 = undefined;
+ while (try walker.next()) |file| {
+ if (file.kind != .file) continue;
+ const file_path = if (builtin.os.tag == .windows) blk: {
+ _ = std.mem.replace(u8, file.path, std.fs.path.sep_str_windows, std.fs.path.sep_str_posix, &path_buffer);
+ break :blk path_buffer[0..file.path.len];
+ } else file.path;
+ if (std.mem.eql(u8, file_path, request.path.file_path[1..])) {
+ const content = try iterable_dir.readFileAlloc(
+ request.allocator,
+ file_path,
+ jetzig.config.get(usize, "max_bytes_public_content"),
+ );
+ const extension = std.fs.path.extension(file_path);
+ const mime_type = if (self.mime_map.get(extension)) |mime| mime else "application/octet-stream";
+ return .{
+ .content = content,
+ .mime_type = mime_type,
+ };
+ }
+ }
+
+ return null;
+ }
+
+ fn matchStaticContent(self: *Server, request: *jetzig.http.Request) !?[]const u8 {
+ const request_format = request.requestFormat();
+ const matched_route = try self.matchRoute(request, true);
+
+ if (matched_route) |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 (std.mem.eql(u8, static_output.route_id, route.id)) {
+ const params = try request.params();
+
+ 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;
+ }
+ }
+
+ return null;
+ }
+
+ pub fn decodeStaticParams(self: *Server) !void {
+ if (comptime !@hasDecl(jetzig.root, "static")) return;
+
+ // 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| {
+ const data = try self.allocator.create(jetzig.data.Data);
+ data.* = jetzig.data.Data.init(self.allocator);
+ try data.fromJson(compiled.output.params orelse "{}");
+ try decoded.append(data.value.?);
+ }
+
+ self.decoded_static_route_params = try decoded.toOwnedSlice();
+ }
+
+ fn matchStaticOutput(
+ maybe_expected_id: ?[]const u8,
+ maybe_expected_params: ?*jetzig.data.Value,
+ route: jetzig.views.Route,
+ request: *const jetzig.http.Request,
+ params: jetzig.data.Value,
+ ) bool {
+ return if (maybe_expected_params) |expected_params| blk: {
+ const params_match = expected_params.count() == 0 or expected_params.eql(params);
+ break :blk switch (route.action) {
+ .index, .post => params_match,
+ inline else => if (maybe_expected_id) |expected_id|
+ std.mem.eql(u8, expected_id, request.path.resource_id) and params_match
+ else
+ false,
+ };
+ } else if (maybe_expected_id) |id|
+ std.mem.eql(u8, id, request.path.resource_id)
+ else
+ true; // We reached a params filter (possibly the default catch-all) with no params set.
+ }
+ };
}
diff --git a/src/jetzig/http/Websocket.zig b/src/jetzig/http/Websocket.zig
index 42ecbc0..9946f25 100644
--- a/src/jetzig/http/Websocket.zig
+++ b/src/jetzig/http/Websocket.zig
@@ -8,14 +8,14 @@ pub const Context = struct {
allocator: std.mem.Allocator,
route: jetzig.channels.Route,
session_id: []const u8,
- server: *const jetzig.http.Server,
+ channels: *jetzig.kv.Store.ChannelStore,
};
const Websocket = @This();
allocator: std.mem.Allocator,
connection: *httpz.websocket.Conn,
-server: *const jetzig.http.Server,
+channels: *jetzig.kv.Store.ChannelStore,
route: jetzig.channels.Route,
data: *jetzig.Data,
session_id: []const u8,
@@ -29,7 +29,7 @@ pub fn init(connection: *httpz.websocket.Conn, context: Context) !Websocket {
.connection = connection,
.route = context.route,
.session_id = context.session_id,
- .server = context.server,
+ .channels = context.channels,
.data = data,
};
}
@@ -70,15 +70,15 @@ pub fn syncState(websocket: *Websocket, channel: jetzig.channels.Channel) !void
const writer = write_buffer.writer();
// TODO: Make this really fast.
- try websocket.server.channels.put(websocket.session_id, channel.state);
+ try websocket.channels.put(websocket.session_id, channel.state);
try writer.print("__jetzig_channel_state__:{s}", .{try websocket.data.toJson()});
try write_buffer.flush();
}
pub fn getState(websocket: *Websocket) !*jetzig.data.Value {
- return try websocket.server.channels.get(websocket.data, websocket.session_id) orelse blk: {
+ return try websocket.channels.get(websocket.data, websocket.session_id) orelse blk: {
const root = try websocket.data.root(.object);
- try websocket.server.channels.put(websocket.session_id, root);
- break :blk try websocket.server.channels.get(websocket.data, websocket.session_id) orelse error.JetzigInvalidChannel;
+ try websocket.channels.put(websocket.session_id, root);
+ break :blk try websocket.channels.get(websocket.data, websocket.session_id) orelse error.JetzigInvalidChannel;
};
}
diff --git a/src/jetzig/http/middleware.zig b/src/jetzig/http/middleware.zig
index e0a4a15..ff07149 100644
--- a/src/jetzig/http/middleware.zig
+++ b/src/jetzig/http/middleware.zig
@@ -48,7 +48,7 @@ pub fn Type(comptime name: MiddlewareEnum()) type {
}
}
-pub fn afterLaunch(server: *jetzig.http.Server) !void {
+pub fn afterLaunch(server: *jetzig.http.Server.RoutedServer(@import("root").routes)) !void {
inline for (middlewares) |middleware| {
if (comptime @hasDecl(middleware, "afterLaunch")) {
try middleware.afterLaunch(server);
diff --git a/src/jetzig/middleware/AntiCsrfMiddleware.zig b/src/jetzig/middleware/AntiCsrfMiddleware.zig
index 0e73406..d1d716d 100644
--- a/src/jetzig/middleware/AntiCsrfMiddleware.zig
+++ b/src/jetzig/middleware/AntiCsrfMiddleware.zig
@@ -29,7 +29,7 @@ pub fn beforeRender(request: *jetzig.http.Request, route: jetzig.views.Route) !v
fn logFailure(request: *jetzig.http.Request) !void {
_ = request.fail(.forbidden);
- try request.server.logger.DEBUG("Anti-CSRF token validation failed. Request aborted.", .{});
+ try request.logger.DEBUG("Anti-CSRF token validation failed. Request aborted.", .{});
}
fn verifyCsrfToken(request: *jetzig.http.Request) !void {
diff --git a/src/jetzig/testing/App.zig b/src/jetzig/testing/App.zig
index 2617fcc..5863230 100644
--- a/src/jetzig/testing/App.zig
+++ b/src/jetzig/testing/App.zig
@@ -15,7 +15,7 @@ channels: *MemoryStore,
job_queue: *MemoryStore,
multipart_boundary: ?[]const u8 = null,
logger: jetzig.loggers.Logger,
-server: Server,
+server: *jetzig.http.Server.RoutedServer(@import("root").routes),
repo: *jetzig.database.Repo,
cookies: *jetzig.http.Cookies,
session: *jetzig.http.Session,
@@ -58,6 +58,28 @@ pub fn init(allocator: std.mem.Allocator, routes_module: type) !App {
const session = try alloc.create(jetzig.http.Session);
session.* = jetzig.http.Session.init(alloc, cookies, jetzig.testing.secret, .{});
+ const server = try alloc.create(jetzig.http.Server.RoutedServer(@import("root").routes));
+ // This server is only used by database logging - we create a more lifelike server when we
+ // process the actual test request.
+ server.* = .{
+ .logger = logger,
+ .allocator = undefined,
+ .env = undefined,
+ .routes = undefined,
+ .channel_routes = undefined,
+ .custom_routes = undefined,
+ .job_definitions = undefined,
+ .mailer_definitions = undefined,
+ .mime_map = undefined,
+ .initialized = undefined,
+ .store = undefined,
+ .job_queue = undefined,
+ .cache = undefined,
+ .channels = undefined,
+ .repo = undefined,
+ .global = undefined,
+ };
+
app.* = App{
.arena = arena,
.allocator = allocator,
@@ -67,7 +89,7 @@ pub fn init(allocator: std.mem.Allocator, routes_module: type) !App {
.channels = try createStore(arena.allocator(), logger, .channels),
.job_queue = try createStore(arena.allocator(), logger, .jobs),
.logger = logger,
- .server = .{ .logger = logger },
+ .server = server,
.repo = repo,
.cookies = cookies,
.session = session,
@@ -133,7 +155,7 @@ pub fn request(
.env_map = std.process.EnvMap.init(allocator),
.env_file = null,
};
- var server = jetzig.http.Server{
+ var server = jetzig.http.Server.RoutedServer(@import("root").routes){
.allocator = allocator,
.logger = self.logger,
.env = .{
@@ -150,6 +172,7 @@ pub fn request(
.secret = jetzig.testing.secret,
},
.routes = routes,
+ .channel_routes = std.StaticStringMap(jetzig.channels.Route).initComptime(.{}),
.custom_routes = &.{},
.mailer_definitions = &.{},
.job_definitions = &.{},
diff --git a/src/test_runner.zig b/src/test_runner.zig
index 0982b82..7c26ca2 100644
--- a/src/test_runner.zig
+++ b/src/test_runner.zig
@@ -8,6 +8,7 @@ pub const std_options = std.Options{
};
pub const jetzig_options = @import("main").jetzig_options;
+pub const routes = @import("main").routes;
pub fn log(
comptime message_level: std.log.Level,