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,