Refactor Server into generic type

We need to have routes available within the server if we are going to do
any kind of dynamic dispatch for Channel Actions.
This commit is contained in:
Bob Farrell 2025-04-23 12:58:41 +01:00
parent d3b3ae63cf
commit 8c2d6806b5
16 changed files with 1059 additions and 993 deletions

View File

@ -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);
}

View File

@ -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);
}

View File

@ -57,15 +57,15 @@
<div id="party-container"></div>
<div class="board" id="board">
<div class="cell" id="tic-tac-toe-cell-0" data-cell="0"></div>
<div class="cell" id="tic-tac-toe-cell-1" data-cell="1"></div>
<div class="cell" id="tic-tac-toe-cell-2" data-cell="2"></div>
<div class="cell" id="tic-tac-toe-cell-3" data-cell="3"></div>
<div class="cell" id="tic-tac-toe-cell-4" data-cell="4"></div>
<div class="cell" id="tic-tac-toe-cell-5" data-cell="5"></div>
<div class="cell" id="tic-tac-toe-cell-6" data-cell="6"></div>
<div class="cell" id="tic-tac-toe-cell-7" data-cell="7"></div>
<div class="cell" id="tic-tac-toe-cell-8" data-cell="8"></div>
<div class="cell" jetzig-connect="$.cells.0" id="tic-tac-toe-cell-0" data-cell="0"></div>
<div class="cell" jetzig-connect="$.cells.1" id="tic-tac-toe-cell-1" data-cell="1"></div>
<div class="cell" jetzig-connect="$.cells.2" id="tic-tac-toe-cell-2" data-cell="2"></div>
<div class="cell" jetzig-connect="$.cells.3" id="tic-tac-toe-cell-3" data-cell="3"></div>
<div class="cell" jetzig-connect="$.cells.4" id="tic-tac-toe-cell-4" data-cell="4"></div>
<div class="cell" jetzig-connect="$.cells.5" id="tic-tac-toe-cell-5" data-cell="5"></div>
<div class="cell" jetzig-connect="$.cells.6" id="tic-tac-toe-cell-6" data-cell="6"></div>
<div class="cell" jetzig-connect="$.cells.7" id="tic-tac-toe-cell-7" data-cell="7"></div>
<div class="cell" jetzig-connect="$.cells.8" id="tic-tac-toe-cell-8" data-cell="8"></div>
</div>
<button id="reset-button">Reset Game</button>
@ -86,7 +86,7 @@
Object.entries(state.cells).forEach(([cell, state]) => {
const element = document.querySelector(`#tic-tac-toe-cell-${cell}`);
element.innerHTML = { player: "&#9992;", cpu: "&#129422;" }[state] || "";
element.innerHTML = { player: "&#9992;&#65039;", cpu: "&#129422;" }[state] || "";
});
});

View File

@ -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.* },
);
}
}

View File

@ -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,

View File

@ -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");

View File

@ -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,
};

View File

@ -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");

View File

@ -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});
}
}

View File

@ -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,
},

View File

@ -6,28 +6,33 @@ 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 = .{},
pub const RenderedView = struct { view: jetzig.views.View, content: []const u8 };
const Server = @This();
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 = .{},
pub fn init(
const Server = @This();
pub fn init(
allocator: std.mem.Allocator,
env: jetzig.Environment,
routes: []const *const jetzig.views.Route,
@ -42,7 +47,7 @@ pub fn init(
channels: *jetzig.kv.Store.ChannelStore,
repo: *jetzig.database.Repo,
global: *anyopaque,
) Server {
) Server {
return .{
.allocator = allocator,
.logger = env.logger,
@ -60,14 +65,14 @@ pub fn init(
.repo = repo,
.global = global,
};
}
}
pub fn deinit(self: *Server) void {
pub fn deinit(self: *Server) void {
self.allocator.free(self.env.secret);
self.allocator.free(self.env.bind);
}
}
const HttpzHandler = struct {
const HttpzHandler = struct {
server: *Server,
pub const WebsocketHandler = jetzig.http.Websocket;
@ -77,9 +82,9 @@ const HttpzHandler = struct {
self.server.errorHandlerFn(request, response, err) catch {};
};
}
};
};
pub fn listen(self: *Server) !void {
pub fn listen(self: *Server) !void {
try self.decodeStaticParams();
const worker_count = jetzig.config.get(u16, "worker_count");
@ -121,9 +126,9 @@ pub fn listen(self: *Server) !void {
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 {
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 {};
@ -135,13 +140,13 @@ pub fn errorHandlerFn(self: *Server, request: *httpz.Request, response: *httpz.R
}
response.body = "500 Internal Server Error";
}
}
pub fn processNextRequest(
pub fn processNextRequest(
self: *Server,
httpz_request: *httpz.Request,
httpz_response: *httpz.Response,
) !void {
) !void {
const start_time = std.time.nanoTimestamp();
var repo = try self.repo.bindConnect(.{ .allocator = httpz_response.arena });
@ -150,12 +155,20 @@ pub fn processNextRequest(
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,
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)) {
@ -180,19 +193,20 @@ pub fn processNextRequest(
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 {
/// 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(
fn upgradeWebsocket(
self: *const Server,
httpz_request: *httpz.Request,
httpz_response: *httpz.Response,
request: *jetzig.http.Request,
) !bool {
) !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 {
@ -208,12 +222,12 @@ fn upgradeWebsocket(
.allocator = self.allocator,
.route = route,
.session_id = session_id,
.server = self,
.channels = self.channels,
},
);
}
}
fn maybeMiddlewareRender(request: *jetzig.http.Request, response: *const jetzig.http.Response) !bool {
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| {
@ -226,9 +240,9 @@ fn maybeMiddlewareRender(request: *jetzig.http.Request, response: *const jetzig.
try request.respond();
return true;
} else return false;
}
}
fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
const static_resource = self.matchStaticResource(request) catch |err| {
if (isUnhandledError(err)) return err;
@ -295,20 +309,20 @@ fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
}
if (request.redirect_state) |state| try request.renderRedirect(state);
}
}
fn renderStatic(resource: StaticResource, request: *jetzig.http.Request) !void {
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(
fn renderHTML(
self: *Server,
request: *jetzig.http.Request,
route: ?jetzig.views.Route,
) !void {
) !void {
if (route) |matched_route| {
if (zmpl.findPrefixed("views", matched_route.template)) |template| {
const rendered = self.renderView(matched_route, request, template) catch |err| {
@ -345,13 +359,13 @@ fn renderHTML(
return request.setResponse(try self.renderNotFound(request), .{});
}
}
}
}
fn renderJSON(
fn renderJSON(
self: *Server,
request: *jetzig.http.Request,
route: ?jetzig.views.Route,
) !void {
) !void {
if (route) |matched_route| {
var rendered = try self.renderView(matched_route, request, null);
var data = rendered.view.data;
@ -367,9 +381,9 @@ fn renderJSON(
} else {
request.setResponse(try self.renderNotFound(request), .{});
}
}
}
fn renderMarkdown(self: *Server, request: *jetzig.http.Request) !?RenderedView {
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;
@ -381,16 +395,14 @@ fn renderMarkdown(self: *Server, request: *jetzig.http.Request) !?RenderedView {
} else {
return null;
}
}
}
pub const RenderedView = struct { view: jetzig.views.View, content: []const u8 };
fn renderView(
fn renderView(
self: *Server,
route: jetzig.views.Route,
request: *jetzig.http.Request,
maybe_template: ?zmpl.Template,
) !RenderedView {
) !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`.
@ -448,15 +460,15 @@ fn renderView(
.content = "",
};
}
}
}
fn renderTemplateWithLayout(
fn renderTemplateWithLayout(
self: *Server,
request: *jetzig.http.Request,
template: zmpl.Template,
view: jetzig.views.View,
route: jetzig.views.Route,
) ![]const u8 {
) ![]const u8 {
try addTemplateConstants(view, route);
const template_context = jetzig.TemplateContext{ .request = request };
@ -492,9 +504,9 @@ fn renderTemplateWithLayout(
template_context,
.{},
);
}
}
fn addTemplateConstants(view: jetzig.views.View, route: jetzig.views.Route) !void {
fn addTemplateConstants(view: jetzig.views.View, route: jetzig.views.Route) !void {
const action = switch (route.action) {
.custom => route.name,
else => |tag| @tagName(tag),
@ -502,23 +514,23 @@ fn addTemplateConstants(view: jetzig.views.View, route: jetzig.views.Route) !voi
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 {
fn isBadRequest(err: anyerror) bool {
return switch (err) {
error.JetzigBodyParseError, error.JetzigQueryParseError => true,
else => false,
};
}
}
fn isUnhandledError(err: anyerror) bool {
fn isUnhandledError(err: anyerror) bool {
return switch (err) {
error.OutOfMemory => true,
else => false,
};
}
}
fn isBadHttpError(err: anyerror) bool {
fn isBadHttpError(err: anyerror) bool {
return switch (err) {
error.JetzigParseHeadError,
error.UnknownHttpMethod,
@ -535,63 +547,63 @@ fn isBadHttpError(err: anyerror) bool {
=> true,
else => false,
};
}
}
fn renderInternalServerError(
fn renderInternalServerError(
self: *Server,
request: *jetzig.http.Request,
stack_trace: ?*std.builtin.StackTrace,
err: anyerror,
) !RenderedView {
) !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 {
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 {
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(
fn renderError(
self: Server,
request: *jetzig.http.Request,
status_code: jetzig.http.StatusCode,
error_info: jetzig.debug.ErrorInfo,
) !RenderedView {
) !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(
fn renderGeneralError(
self: Server,
request: *jetzig.http.Request,
status_code: jetzig.http.StatusCode,
) !RenderedView {
) !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(
fn renderDebugConsole(
self: Server,
request: *jetzig.http.Request,
status_code: jetzig.http.StatusCode,
error_info: jetzig.debug.ErrorInfo,
) !RenderedView {
) !RenderedView {
if (comptime jetzig.build_options.debug_console) {
var buf = std.ArrayList(u8).init(request.allocator);
const writer = buf.writer();
@ -620,13 +632,13 @@ fn renderDebugConsole(
.content = if (content.len == 0) "" else content,
};
} else unreachable;
}
}
fn renderErrorView(
fn renderErrorView(
self: Server,
request: *jetzig.http.Request,
status_code: jetzig.http.StatusCode,
) !?RenderedView {
) !?RenderedView {
for (self.routes) |route| {
if (std.mem.eql(u8, route.view_name, "errors") and route.action == .index) {
request.response_data.reset();
@ -665,9 +677,9 @@ fn renderErrorView(
}
return null;
}
}
fn renderStaticErrorPage(request: *jetzig.http.Request, status_code: jetzig.http.StatusCode) !?RenderedView {
fn renderStaticErrorPage(request: *jetzig.http.Request, status_code: jetzig.http.StatusCode) !?RenderedView {
if (request.requestFormat() == .JSON) return null;
var dir = std.fs.cwd().openDir(
@ -697,40 +709,40 @@ fn renderStaticErrorPage(request: *jetzig.http.Request, status_code: jetzig.http
.view = jetzig.views.View{ .data = request.response_data, .status_code = status_code },
.content = content,
};
}
}
fn renderDefaultError(
fn renderDefaultError(
request: *const jetzig.http.Request,
status_code: jetzig.http.StatusCode,
) !RenderedView {
) !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(
fn logStackTrace(
self: Server,
stack: *std.builtin.StackTrace,
allocator: std.mem.Allocator,
) !void {
) !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 {
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 {
fn matchMiddlewareRoute(request: *const jetzig.http.Request) ?jetzig.middleware.MiddlewareRoute {
const middlewares = jetzig.config.get([]const type, "middleware");
inline for (middlewares) |middleware| {
@ -742,9 +754,9 @@ fn matchMiddlewareRoute(request: *const jetzig.http.Request) ?jetzig.middleware.
}
return null;
}
}
fn matchRoute(self: *Server, request: *jetzig.http.Request, static: bool) !?jetzig.views.Route {
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.*)) {
@ -761,14 +773,14 @@ fn matchRoute(self: *Server, request: *jetzig.http.Request, static: bool) !?jetz
}
return null;
}
}
const StaticResource = struct {
const StaticResource = struct {
content: []const u8,
mime_type: []const u8 = "application/octet-stream",
};
};
fn matchStaticResource(self: *Server, request: *jetzig.http.Request) !?StaticResource {
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"),
@ -791,9 +803,9 @@ fn matchStaticResource(self: *Server, request: *jetzig.http.Request) !?StaticRes
};
return null;
}
}
fn matchPublicContent(self: *Server, request: *jetzig.http.Request) !?StaticResource {
fn matchPublicContent(self: *Server, request: *jetzig.http.Request) !?StaticResource {
if (request.path.file_path.len <= 1) return null;
if (request.method != .GET) return null;
@ -833,9 +845,9 @@ fn matchPublicContent(self: *Server, request: *jetzig.http.Request) !?StaticReso
}
return null;
}
}
fn matchStaticContent(self: *Server, request: *jetzig.http.Request) !?[]const u8 {
fn matchStaticContent(self: *Server, request: *jetzig.http.Request) !?[]const u8 {
const request_format = request.requestFormat();
const matched_route = try self.matchRoute(request, true);
@ -867,9 +879,9 @@ fn matchStaticContent(self: *Server, request: *jetzig.http.Request) !?[]const u8
}
return null;
}
}
pub fn decodeStaticParams(self: *Server) !void {
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.
@ -882,15 +894,15 @@ pub fn decodeStaticParams(self: *Server) !void {
}
self.decoded_static_route_params = try decoded.toOwnedSlice();
}
}
fn matchStaticOutput(
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 {
) 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) {
@ -904,4 +916,6 @@ fn matchStaticOutput(
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.
}
};
}

View File

@ -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;
};
}

View File

@ -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);

View File

@ -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 {

View File

@ -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 = &.{},

View File

@ -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,