refactor: make context injection positional arg

This commit is contained in:
Nurul Huda (Apon) 2026-04-14 12:42:59 +06:00
parent 56532f7e04
commit 5a10a82d11
No known key found for this signature in database
GPG Key ID: 5D3F1DE2855A2F79
11 changed files with 179 additions and 418 deletions

View File

@ -1,6 +1,6 @@
pub fn Page(ctx: zx.PageCtx(void, AuthState)) zx.Component {
pub fn Page(ctx: zx.PageContext, state: AuthState) zx.Component {
const allocator = ctx.arena;
const auth = ctx.state;
const auth = state;
const msg = ctx.request.queries.get("msg");
// Handle login/logout POST actions
@ -35,7 +35,7 @@ pub fn Page(ctx: zx.PageCtx(void, AuthState)) zx.Component {
);
}
fn handleAction(ctx: zx.PageCtx(void, AuthState)) void {
fn handleAction(ctx: zx.PageContext) void {
if (ctx.request.method != .POST) return;
const fd = ctx.request.formData();

View File

@ -1,7 +1,7 @@
pub fn Page(ctx: zx.PageCtx(root.AppCtx, void)) zx.Component {
pub fn Page(ctx: zx.PageContext, app: root.AppCtx) zx.Component {
return (
<main @allocator={ctx.arena}>
<p>{ctx.app.port}</p>
<p>{app.port}</p>
<QuickExample />
</main>
);

View File

@ -1,10 +1,10 @@
pub fn Page(ctx: zx.PageCtx(root.AppCtx, UserState)) zx.Component {
pub fn Page(ctx: zx.PageContext, app: root.AppCtx, state: UserState) zx.Component {
const allocator = ctx.arena;
return (
<div @allocator={allocator}>
<h2>About ZX {ctx.app.port}</h2>
<h2>About State {ctx.state.count}</h2>
<h2>About ZX {app.port}</h2>
<h2>About State {state.count}</h2>
</div>
);
}

View File

@ -74,16 +74,11 @@ pub const SocketOptions = routing.SocketOptions;
/// --- Contexts --- //
pub const ProxyContext = ctxs.ProxyContext;
pub const AppCtx = routing.AppCtx;
pub const PageContext = routing.PageContext;
pub const PageCtx = routing.PageCtx;
pub const LayoutContext = routing.LayoutContext;
pub const LayoutCtx = routing.LayoutCtx;
pub const NotFoundContext = routing.NotFoundContext;
pub const NotFoundCtx = routing.NotFoundCtx;
pub const ErrorContext = routing.ErrorContext;
pub const RouteContext = routing.RouteContext;
pub const RouteCtx = routing.RouteCtx;
pub const SocketContext = routing.SocketContext;
pub const SocketCtx = routing.SocketCtx;
pub const SocketOpenContext = routing.SocketOpenContext;

View File

@ -82,13 +82,13 @@ pub fn handlePage(
proxy_state_ptr: ?*const anyopaque,
base_path: ?[]const u8,
) !PageResult {
var pagectx = zx.PageContext.initWithAppPtr(app_ctx, request, response, allocator);
pagectx._state_ptr = proxy_state_ptr;
const app_ptr: ?*const anyopaque = if (app_ctx) |p| @ptrCast(p) else null;
const pagectx = zx.PageContext.init(request, response, allocator);
const page_fn = route.page orelse return .not_found;
// -- Server action dispatch --
switch (try server_dispatch.dispatchAction(request, response, allocator, arena, route.path, pagectx, page_fn, base_path)) {
switch (try server_dispatch.dispatchAction(request, response, allocator, arena, route.path, pagectx, page_fn, app_ptr, proxy_state_ptr, base_path)) {
.not_triggered => {},
.ok => |r| return .{ .action_handled = .{ .body = r.body } },
.ok_native => {},
@ -97,7 +97,7 @@ pub fn handlePage(
}
// -- Server event dispatch --
switch (try server_dispatch.dispatchServerEvent(request, allocator, arena, route.path, pagectx, page_fn, base_path)) {
switch (try server_dispatch.dispatchServerEvent(request, allocator, arena, route.path, pagectx, page_fn, app_ptr, proxy_state_ptr, base_path)) {
.not_triggered => {},
.ok => |r| return .{ .event_handled = .{ .body = r.body } },
.ok_native => {},
@ -108,15 +108,14 @@ pub fn handlePage(
// -- Render page --
render.current_route_path = route.path;
var page_component = page_fn(pagectx) catch |err| {
var page_component = page_fn(pagectx, app_ptr, proxy_state_ptr) catch |err| {
render.current_route_path = null;
return .{ .page_error = err };
};
// -- Apply layout hierarchy --
var layoutctx = zx.LayoutContext.initWithAppPtr(app_ctx, request, response, allocator);
layoutctx._state_ptr = proxy_state_ptr;
page_component = Router.applyLayouts(route, request.pathname, layoutctx, page_component);
const layoutctx = zx.LayoutContext.init(request, response, allocator);
page_component = Router.applyLayouts(route, request.pathname, layoutctx, page_component, app_ptr, proxy_state_ptr);
// -- Inject build-time HTML (scripts, styles, etc.) --
injectZxInjections(arena, &page_component);
@ -148,14 +147,13 @@ pub fn handleApi(
// Resolve handler for HTTP method
const route_fn = Router.resolveCustomHandler(handlers, request.method, request.method_str) orelse return .not_found;
const app_ptr: ?*const anyopaque = if (app_ctx) |p| @ptrCast(p) else null;
if (handlers.socket != null) {
var routectx = zx.RouteContext.initWithAppPtrAndSocket(app_ctx, request, response, socket, allocator);
routectx._state_ptr = proxy_state_ptr;
route_fn(routectx) catch |err| return .{ .handler_error = err };
const routectx = zx.RouteContext.initWithSocket(request, response, socket, allocator);
route_fn(routectx, app_ptr, proxy_state_ptr) catch |err| return .{ .handler_error = err };
} else {
var routectx = zx.RouteContext.initWithAppPtr(app_ctx, request, response, allocator);
routectx._state_ptr = proxy_state_ptr;
route_fn(routectx) catch |err| return .{ .handler_error = err };
const routectx = zx.RouteContext.init(request, response, allocator);
route_fn(routectx, app_ptr, proxy_state_ptr) catch |err| return .{ .handler_error = err };
}
return .handled;

View File

@ -378,12 +378,14 @@ pub fn applyLayouts(
pathname: []const u8,
layoutctx: zx.LayoutContext,
page_component: Component,
app_ptr: ?*const anyopaque,
state_ptr: ?*const anyopaque,
) Component {
var component = page_component;
// Apply this route's own layout first
if (route.layout) |layout_fn| {
component = layout_fn(layoutctx, component);
component = layout_fn(layoutctx, component, app_ptr, state_ptr);
}
// Collect parent layouts (root to deepest, excluding current route)
@ -452,7 +454,7 @@ pub fn applyLayouts(
var j: usize = layout_count;
while (j > 0) {
j -= 1;
component = layouts[j](layoutctx, component);
component = layouts[j](layoutctx, component, app_ptr, state_ptr);
}
return component;
@ -464,6 +466,8 @@ pub fn applyLayoutsForPath(
path: []const u8,
layoutctx: zx.LayoutContext,
page_component: Component,
app_ptr: ?*const anyopaque,
state_ptr: ?*const anyopaque,
) Component {
var component = page_component;
@ -523,7 +527,7 @@ pub fn applyLayoutsForPath(
var j: usize = layout_count;
while (j > 0) {
j -= 1;
component = layouts[j](layoutctx, component);
component = layouts[j](layoutctx, component, app_ptr, state_ptr);
}
return component;
@ -550,7 +554,7 @@ pub fn renderErrorComponent(
};
var component = err_fn(errorctx);
component = applyLayoutsForPath(path, layoutctx, component);
component = applyLayoutsForPath(path, layoutctx, component, null, null);
return component;
}
@ -590,6 +594,6 @@ pub fn renderNotFoundComponent(
};
var component = nf_fn(notfoundctx);
component = applyLayoutsForPath(path, layoutctx, component);
component = applyLayoutsForPath(path, layoutctx, component, null, null);
return component;
}

View File

@ -1,137 +1,36 @@
//! Routing contexts for page, layout, error, and not-found handlers.
//! This module is backend-agnostic - no httpz dependency.
const std = @import("std");
const Request = @import("Request.zig");
const Response = @import("Response.zig");
/// Base context structure that provides access to request/response objects and allocators.
/// This is the foundation for both PageContext and LayoutContext, providing common functionality
/// for handling HTTP requests and managing memory allocation.
///
/// The type parameter `H` follows the httpz pattern and can be:
/// - `void`: No app context
/// - A struct type: App context stored by value (e.g., `BaseContext(AppCtx, void)`)
/// - A pointer type: App context stored by pointer (e.g., `BaseContext(*AppCtx, void)`)
///
/// The type parameter `S` is for per-request state set by proxy middleware:
/// - `void`: No state (default)
/// - A struct type: Typed state accessible via `ctx.state` (e.g., `BaseContext(AppCtx, MyState)`)
pub fn BaseContext(comptime H: type, comptime S: type) type {
// Extract the underlying app context type, following httpz pattern
// This is useful when H is a pointer - we get the child type
_ = switch (@typeInfo(H)) {
.@"struct" => H,
.pointer => |ptr| ptr.child,
.void => void,
else => @compileError("BaseContext app type must be a struct, pointer to struct, or void, got: " ++ @tagName(@typeInfo(H))),
};
pub const BaseContext = struct {
const Self = @This();
// Validate that State type is void or a struct
if (S != void) {
_ = switch (@typeInfo(S)) {
.@"struct" => S,
else => @compileError("BaseContext state type must be a struct or void, got: " ++ @tagName(@typeInfo(S))),
/// The HTTP request object (backend-agnostic)
request: Request,
/// The HTTP response object (backend-agnostic)
response: Response,
/// Global allocator passed from the app, only cleared when the app is deinitialized.
allocator: std.mem.Allocator,
/// Arena allocator cleared automatically after the request is processed.
arena: std.mem.Allocator,
pub fn init(request: Request, response: Response, alloc: std.mem.Allocator) Self {
return .{
.request = request,
.response = response,
.allocator = alloc,
.arena = request.arena,
};
}
const AppFieldType = if (H == void) ?*const anyopaque else H;
pub fn deinit(self: *Self) void {
self.allocator.destroy(self);
}
return struct {
const Self = @This();
/// Application context data:
/// - For void: type-erased pointer for internal routing
/// - For pointer types: stores the pointer directly
/// - For value types: stores the value directly
app: AppFieldType = if (H == void) null else undefined,
/// Per-request state set by proxy middleware.
/// - For void: field is omitted
/// - For struct types: typed state accessible via `ctx.state`
state: if (S == void) void else S = if (S == void) {} else undefined,
/// The HTTP request object (backend-agnostic)
/// Provides access to headers, body, query params, form data, cookies, etc.
request: Request,
/// The HTTP response object (backend-agnostic)
/// Used to set status, headers, body, and cookies.
response: Response,
/// Global allocator passed from the app, only cleared when the app is deinitialized.
/// Should be used for allocating memory that needs to persist across requests.
/// Make sure to free the memory on your own that is allocated with this allocator.
allocator: std.mem.Allocator,
/// Allocator for allocating memory that needs to be freed after the request is processed.
/// This allocator is cleared automatically when the request is processed, so you don't need
/// to manually free memory allocated with this allocator. Use this for temporary allocations
/// that are only needed during request processing.
arena: std.mem.Allocator,
/// Optional parent context, used for nested layouts or hierarchical context passing
parent_ctx: ?*Self = null,
//TODO: move these to single _inner ptr
_state_ptr: ?*const anyopaque = null,
/// Initialize a new BaseContext with the given request, response, and allocator.
/// For void types, app defaults to {}. For custom types, app is undefined until set.
pub fn init(request: Request, response: Response, alloc: std.mem.Allocator) Self {
return .{
.request = request,
.response = response,
.allocator = alloc,
.arena = request.arena,
};
}
/// Initialize a new BaseContext with a type-erased app context pointer.
/// Used by the handler to pass the app context to page functions.
pub fn initWithAppPtr(app_ptr: ?*const anyopaque, request: Request, response: Response, alloc: std.mem.Allocator) Self {
// Convert the type-erased pointer to the appropriate AppFieldType:
// - void: store the pointer directly for routing
// - pointer: cast to the pointer type
// - value: cast to pointer and dereference
const app: AppFieldType = if (H == void)
app_ptr
else if (@typeInfo(H) == .pointer)
@ptrCast(@alignCast(app_ptr))
else
(@as(*const H, @ptrCast(@alignCast(app_ptr)))).*;
return .{
.app = app,
.request = request,
.response = response,
.allocator = alloc,
.arena = request.arena,
};
}
/// Initialize a new BaseContext with app context data.
/// Use this when you need to pass custom application data to handlers.
pub fn initWithApp(app: H, request: Request, response: Response, alloc: std.mem.Allocator) Self {
return .{
.app = app,
.request = request,
.response = response,
.allocator = alloc,
.arena = request.arena,
};
}
/// Deinitialize the context, freeing any resources allocated with the global allocator.
/// Note: The arena allocator is automatically cleaned up by the request handler.
pub fn deinit(self: *Self) void {
self.allocator.destroy(self);
}
pub fn fmt(self: Self, comptime format: []const u8, args: anytype) ![]u8 {
return fmtInner(self.arena, format, args);
}
};
}
pub fn fmt(self: Self, comptime format: []const u8, args: anytype) ![]u8 {
return fmtInner(self.arena, format, args);
}
};
/// Context passed to page components. Provides access to the current HTTP request and response,
/// as well as allocators for memory management.
@ -147,10 +46,7 @@ pub fn BaseContext(comptime H: type, comptime S: type) type {
/// return <div>Hello</div>;
/// }
/// ```
pub const PageContext = BaseContext(void, void);
pub fn PageCtx(comptime AppType: type, comptime StateType: type) type {
return BaseContext(AppType, StateType);
}
pub const PageContext = BaseContext;
/// Context passed to layout components. Provides access to the current HTTP request and response,
/// as well as allocators for memory management. Layouts wrap page components and can be nested.
@ -166,14 +62,8 @@ pub fn PageCtx(comptime AppType: type, comptime StateType: type) type {
/// );
/// }
/// ```
pub const LayoutContext = BaseContext(void, void);
pub fn LayoutCtx(comptime AppType: type, comptime StateType: type) type {
return BaseContext(AppType, StateType);
}
pub const NotFoundContext = BaseContext(void, void);
pub fn NotFoundCtx(comptime AppType: type, comptime StateType: type) type {
return BaseContext(AppType, StateType);
}
pub const LayoutContext = BaseContext;
pub const NotFoundContext = BaseContext;
pub const ErrorContext = struct {
/// The HTTP request object (backend-agnostic)
@ -368,110 +258,41 @@ pub const Socket = struct {
}
};
pub const RouteContext = RouteCtx(void, void);
/// Route context. App context and proxy-set state are injected positionally
/// by the wrapRoute() wrapper, not exposed as fields here.
pub const RouteContext = struct {
const Self = @This();
/// Route context with typed app context and per-request state.
/// Use `RouteCtx(AppType, StateType)` to access typed app and state data.
/// - `AppType`: Application context type (or `void` for none)
/// - `StateType`: Per-request state type set by proxy middleware (or `void` for none)
pub fn RouteCtx(comptime H: type, comptime S: type) type {
// Validate State type
if (S != void) {
_ = switch (@typeInfo(S)) {
.@"struct" => S,
else => @compileError("RouteCtx state type must be a struct or void, got: " ++ @tagName(@typeInfo(S))),
request: Request,
response: Response,
socket: Socket,
allocator: std.mem.Allocator,
arena: std.mem.Allocator,
pub fn init(request: Request, response: Response, alloc: std.mem.Allocator) Self {
return .{
.request = request,
.response = response,
.socket = .{},
.allocator = alloc,
.arena = request.arena,
};
}
const AppFieldType = if (H == void) ?*const anyopaque else H;
pub fn initWithSocket(request: Request, response: Response, socket: Socket, alloc: std.mem.Allocator) Self {
return .{
.request = request,
.response = response,
.socket = socket,
.allocator = alloc,
.arena = request.arena,
};
}
return struct {
const Self = @This();
/// Application context data
/// - For void: type-erased pointer for internal routing
/// - For pointer types: stores the pointer directly
/// - For value types: stores the value directly
app: AppFieldType = if (H == void) null else undefined,
/// Per-request state set by proxy middleware
state: if (S == void) void else S = if (S == void) {} else undefined,
/// The HTTP request object (backend-agnostic)
request: Request,
/// The HTTP response object (backend-agnostic)
response: Response,
/// WebSocket interface for upgrading connections and sending/receiving messages
socket: Socket,
/// Global allocator
allocator: std.mem.Allocator,
/// Arena allocator for request-scoped allocations
arena: std.mem.Allocator,
//TODO: move these to single _inner ptr
_state_ptr: ?*const anyopaque = null,
pub fn init(request: Request, response: Response, alloc: std.mem.Allocator) Self {
return .{
.request = request,
.response = response,
.socket = .{},
.allocator = alloc,
.arena = request.arena,
};
}
pub fn initWithSocket(request: Request, response: Response, socket: Socket, alloc: std.mem.Allocator) Self {
return .{
.request = request,
.response = response,
.socket = socket,
.allocator = alloc,
.arena = request.arena,
};
}
/// Initialize with a type-erased app context pointer.
pub fn initWithAppPtr(app_ptr: ?*const anyopaque, request: Request, response: Response, alloc: std.mem.Allocator) Self {
const app: AppFieldType = if (H == void)
app_ptr
else if (@typeInfo(H) == .pointer)
@ptrCast(@alignCast(app_ptr))
else
(@as(*const H, @ptrCast(@alignCast(app_ptr)))).*;
return .{
.app = app,
.request = request,
.response = response,
.socket = .{},
.allocator = alloc,
.arena = request.arena,
};
}
/// Initialize with a type-erased app context pointer and a socket.
pub fn initWithAppPtrAndSocket(app_ptr: ?*const anyopaque, request: Request, response: Response, socket: Socket, alloc: std.mem.Allocator) Self {
const app: AppFieldType = if (H == void)
app_ptr
else if (@typeInfo(H) == .pointer)
@ptrCast(@alignCast(app_ptr))
else
(@as(*const H, @ptrCast(@alignCast(app_ptr)))).*;
return .{
.app = app,
.request = request,
.response = response,
.socket = socket,
.allocator = alloc,
.arena = request.arena,
};
}
pub fn fmt(self: Self, comptime format: []const u8, args: anytype) ![]u8 {
return fmtInner(self.arena, format, args);
}
};
}
pub fn fmt(self: Self, comptime format: []const u8, args: anytype) ![]u8 {
return fmtInner(self.arena, format, args);
}
};
/// Message type for WebSocket messages (text vs binary)
pub const SocketMessageType = enum {
@ -554,10 +375,6 @@ pub fn SocketCloseCtx(comptime DataType: type) type {
};
}
pub fn AppCtx(comptime DataType: type) type {
return DataType;
}
inline fn fmtInner(allocator: std.mem.Allocator, comptime format: []const u8, args: anytype) ![]u8 {
var aw: std.Io.Writer.Allocating = .init(allocator);
defer aw.deinit();

View File

@ -429,7 +429,7 @@ pub const ServerMeta = struct {
};
/// Route handler function type for API routes
pub const RouteHandler = *const fn (ctx: zx.RouteContext) anyerror!void;
pub const RouteHandler = *const fn (ctx: zx.RouteContext, app_ptr: ?*const anyopaque, state_ptr: ?*const anyopaque) anyerror!void;
/// Socket message handler function type for WebSocket connections
/// Called for each message received from the client
@ -660,54 +660,55 @@ pub const ServerMeta = struct {
}.wrapper;
}
fn injectApp(comptime AppType: type, app_ptr: ?*const anyopaque) AppType {
if (AppType == void) return {};
const ptr = app_ptr orelse return std.mem.zeroes(AppType);
if (@typeInfo(AppType) == .pointer) {
const loose: *align(1) const AppType = @ptrCast(ptr);
return loose;
} else {
const loose: *align(1) const AppType = @ptrCast(ptr);
return loose.*;
}
}
fn injectState(comptime StateType: type, state_ptr: ?*const anyopaque) StateType {
if (StateType == void) return {};
const ptr = state_ptr orelse return std.mem.zeroes(StateType);
if (@typeInfo(StateType) == .pointer) {
const loose: *align(1) const StateType = @ptrCast(ptr);
return loose;
} else {
const loose: *align(1) const StateType = @ptrCast(ptr);
return loose.*;
}
}
/// Wrapper to allow routes to return void or !void.
/// Handles both zx.RouteContext (void app/state) and zx.RouteCtx(AppCtx, State) (custom context).
/// Route function shape: `(ctx: zx.RouteContext, app?: App, state?: State)` positional.
fn wrapRoute(comptime routeFn: anytype) RouteHandler {
const FnInfo = @typeInfo(@TypeOf(routeFn)).@"fn";
const R = FnInfo.return_type.?;
const CtxType = FnInfo.params[0].type.?;
const n_params = FnInfo.params.len;
return struct {
fn wrapper(ctx: zx.RouteContext) anyerror!void {
if (CtxType == zx.RouteContext) {
// Standard RouteContext, pass directly
if (R == void) {
routeFn(ctx);
} else {
try routeFn(ctx);
}
} else {
// Custom context type - cast app pointer and state
const AppType = @TypeOf(@as(CtxType, undefined).app);
const app: AppType = if (AppType == void) {} else if (AppType == ?*const anyopaque)
ctx.app
else if (@typeInfo(AppType) == .pointer)
@ptrCast(@alignCast(ctx.app))
else
(@as(*const AppType, @ptrCast(@alignCast(ctx.app)))).*;
// Cast state from type-erased pointer
const StateType = @TypeOf(@as(CtxType, undefined).state);
const state: StateType = if (StateType == void) {} else if (ctx._state_ptr) |ptr|
(@as(*const StateType, @ptrCast(@alignCast(ptr)))).*
else
std.mem.zeroes(StateType);
const custom_ctx = CtxType{
.app = app,
.state = state,
.request = ctx.request,
.response = ctx.response,
.socket = ctx.socket,
.allocator = ctx.allocator,
.arena = ctx.arena,
};
if (R == void) {
routeFn(custom_ctx);
} else {
try routeFn(custom_ctx);
}
fn wrapper(ctx: zx.RouteContext, app_ptr: ?*const anyopaque, state_ptr: ?*const anyopaque) anyerror!void {
if (n_params == 1) {
if (R == void) routeFn(ctx) else try routeFn(ctx);
return;
}
if (n_params == 2) {
const app = injectApp(FnInfo.params[1].type.?, app_ptr);
if (R == void) routeFn(ctx, app) else try routeFn(ctx, app);
return;
}
if (n_params == 3) {
const app = injectApp(FnInfo.params[1].type.?, app_ptr);
const state = injectState(FnInfo.params[2].type.?, state_ptr);
if (R == void) routeFn(ctx, app, state) else try routeFn(ctx, app, state);
return;
}
@compileError("Route function must have 1-3 parameters: (ctx, app?, state?)");
}
}.wrapper;
}
@ -754,123 +755,75 @@ pub const ServerMeta = struct {
}
/// Page handler function type
pub const PageHandler = *const fn (ctx: zx.PageContext) anyerror!Component;
pub const PageHandler = *const fn (ctx: zx.PageContext, app_ptr: ?*const anyopaque, state_ptr: ?*const anyopaque) anyerror!Component;
/// Layout handler function type
pub const LayoutHandler = *const fn (ctx: zx.LayoutContext, component: Component) Component;
pub const LayoutHandler = *const fn (ctx: zx.LayoutContext, component: Component, app_ptr: ?*const anyopaque, state_ptr: ?*const anyopaque) Component;
/// Comptime function to wrap a page module's Page function.
/// Handles both zx.PageContext (void app/state) and zx.PageCtx(AppCtx, State) (custom context).
/// The app context and state are read from type-erased pointers and cast to the appropriate types.
/// The app context and state are read from type-erased pointers in ctx and cast to the appropriate types.
pub fn page(comptime T: type) PageHandler {
const is_md = @hasDecl(T, "_zx_md");
const pageFn = if (is_md) T._zx_md else T.Page;
const FnType = @TypeOf(pageFn);
const fn_info = @typeInfo(FnType).@"fn";
const CtxType = fn_info.params[0].type.?;
const R = fn_info.return_type.?;
const n_params = fn_info.params.len;
return struct {
fn wrapper(ctx: zx.PageContext) anyerror!Component {
const allocator = ctx.arena;
// If page expects standard PageContext, pass it directly
if (CtxType == zx.PageContext) {
if (R == Component) {
return pageFn(ctx);
} else {
return try pageFn(ctx);
}
} else if (is_md) {
fn wrapper(ctx: zx.PageContext, app_ptr: ?*const anyopaque, state_ptr: ?*const anyopaque) anyerror!Component {
if (is_md) {
// TODO: figure out a better design for page.mdzx file, we need a better way to use PageContext in md file without magic
const CtxType = fn_info.params[0].type.?;
const CCtxType = @typeInfo(CtxType).pointer.child;
const allocator = ctx.arena;
const cctx = (allocator.create(CCtxType) catch @panic("OOM"));
cctx.allocator = allocator;
const md_has_ctx = @hasDecl(T, "page_ctx");
if (md_has_ctx) {
T.page_ctx = ctx;
}
if (@hasDecl(T, "page_ctx")) T.page_ctx = ctx;
return pageFn(cctx);
} else {
// Page expects custom context type - cast app pointer and state to correct types
// ctx.app for void is ?*const anyopaque (type-erased pointer)
const AppType = @TypeOf(@as(CtxType, undefined).app);
const app: AppType = if (AppType == void) {} else if (AppType == ?*const anyopaque)
ctx.app
else if (@typeInfo(AppType) == .pointer)
@ptrCast(@alignCast(ctx.app))
else
(@as(*const AppType, @ptrCast(@alignCast(ctx.app)))).*;
// Cast state from type-erased pointer
const StateType = @TypeOf(@as(CtxType, undefined).state);
const state: StateType = if (StateType == void) {} else if (ctx._state_ptr) |ptr|
(@as(*const StateType, @ptrCast(@alignCast(ptr)))).*
else
std.mem.zeroes(StateType);
const custom_ctx = CtxType{
.app = app,
.state = state,
.request = ctx.request,
.response = ctx.response,
.allocator = ctx.allocator,
.arena = ctx.arena,
};
if (R == Component) {
return pageFn(custom_ctx);
} else {
return try pageFn(custom_ctx);
}
}
if (n_params == 1) {
if (R == Component) return pageFn(ctx) else return try pageFn(ctx);
}
if (n_params == 2) {
const app = injectApp(fn_info.params[1].type.?, app_ptr);
if (R == Component) return pageFn(ctx, app) else return try pageFn(ctx, app);
}
if (n_params == 3) {
const app = injectApp(fn_info.params[1].type.?, app_ptr);
const state = injectState(fn_info.params[2].type.?, state_ptr);
if (R == Component) return pageFn(ctx, app, state) else return try pageFn(ctx, app, state);
}
@compileError("Page function must have 1-3 parameters: (ctx, app?, state?)");
}
}.wrapper;
}
/// Comptime function to wrap a layout module's Layout function.
/// Handles both zx.LayoutContext (void app/state) and zx.LayoutCtx(AppCtx, State) (custom context).
/// Layout signature (positional): `(ctx: zx.LayoutContext, children: Component, app?, state?)`
pub fn layout(comptime T: type) LayoutHandler {
const layoutFn = T.Layout;
const FnType = @TypeOf(layoutFn);
const fn_info = @typeInfo(FnType).@"fn";
const CtxType = fn_info.params[0].type.?;
const n_params = fn_info.params.len;
return struct {
fn wrapper(ctx: zx.LayoutContext, component: Component) Component {
// If layout expects standard LayoutContext, pass it directly
if (CtxType == zx.LayoutContext) {
fn wrapper(ctx: zx.LayoutContext, component: Component, app_ptr: ?*const anyopaque, state_ptr: ?*const anyopaque) Component {
if (n_params == 2) {
return layoutFn(ctx, component);
} else {
// Layout expects custom context type - cast app pointer and state to correct types
// ctx.app for void is ?*const anyopaque (type-erased pointer)
const AppType = @TypeOf(@as(CtxType, undefined).app);
const app: AppType = if (AppType == void) {} else if (AppType == ?*const anyopaque)
ctx.app
else if (@typeInfo(AppType) == .pointer)
@ptrCast(@alignCast(ctx.app))
else
(@as(*const AppType, @ptrCast(@alignCast(ctx.app)))).*;
// Cast state from type-erased pointer
const StateType = @TypeOf(@as(CtxType, undefined).state);
const state: StateType = if (StateType == void) {} else if (ctx._state_ptr) |ptr|
(@as(*const StateType, @ptrCast(@alignCast(ptr)))).*
else
std.mem.zeroes(StateType);
const custom_ctx = CtxType{
.app = app,
.state = state,
.request = ctx.request,
.response = ctx.response,
.allocator = ctx.allocator,
.arena = ctx.arena,
};
return layoutFn(custom_ctx, component);
}
if (n_params == 3) {
const app = injectApp(fn_info.params[2].type.?, app_ptr);
return layoutFn(ctx, component, app);
}
if (n_params == 4) {
const app = injectApp(fn_info.params[2].type.?, app_ptr);
const state = injectState(fn_info.params[3].type.?, state_ptr);
return layoutFn(ctx, component, app, state);
}
@compileError("Layout function must have 2-4 parameters: (ctx, children, app?, state?)");
}
}.wrapper;
}

View File

@ -8,7 +8,7 @@ const zx = @import("../../root.zig");
const registry = @import("registry.zig");
const render = @import("render.zig");
pub const PageFn = *const fn (zx.PageContext) anyerror!zx.Component;
pub const PageFn = *const fn (zx.PageContext, ?*const anyopaque, ?*const anyopaque) anyerror!zx.Component;
pub const DispatchResult = union(enum) {
/// Request did not match this handler type continue normal handling.
@ -60,9 +60,11 @@ fn slowPathRender(
pagectx: zx.PageContext,
route_path: []const u8,
arena: std.mem.Allocator,
app_ptr: ?*const anyopaque,
state_ptr: ?*const anyopaque,
base_path: ?[]const u8,
) ?anyerror {
var page_component = page_fn(pagectx) catch |err| return err;
var page_component = page_fn(pagectx, app_ptr, state_ptr) catch |err| return err;
var discard = std.Io.Writer.Allocating.init(arena);
render.current_route_path = route_path;
page_component.render(&discard.writer, .{ .base_path = base_path }) catch {};
@ -81,6 +83,8 @@ pub fn dispatchAction(
route_path: []const u8,
pagectx: zx.PageContext,
page_fn: ?PageFn,
app_ptr: ?*const anyopaque,
state_ptr: ?*const anyopaque,
base_path: ?[]const u8,
) !DispatchResult {
if (!isActionRequest(request)) return .not_triggered;
@ -119,7 +123,7 @@ pub fn dispatchAction(
// Slow path: render the page to populate the registry, then retry.
if (page_fn) |pfn| {
if (slowPathRender(pfn, pagectx, route_path, arena, base_path)) |err| {
if (slowPathRender(pfn, pagectx, route_path, arena, app_ptr, state_ptr, base_path)) |err| {
return .{ .page_error = err };
}
}
@ -150,6 +154,8 @@ pub fn dispatchServerEvent(
route_path: []const u8,
pagectx: zx.PageContext,
page_fn: ?PageFn,
app_ptr: ?*const anyopaque,
state_ptr: ?*const anyopaque,
base_path: ?[]const u8,
) !DispatchResult {
if (!request.headers.has("x-zx-server-event")) return .not_triggered;
@ -169,7 +175,7 @@ pub fn dispatchServerEvent(
// Slow path: render the page to populate the registry, then retry.
if (page_fn) |pfn| {
if (slowPathRender(pfn, pagectx, route_path, arena, base_path)) |err| {
if (slowPathRender(pfn, pagectx, route_path, arena, app_ptr, state_ptr, base_path)) |err| {
return .{ .page_error = err };
}
}

View File

@ -1,10 +1,10 @@
pub const options = zx.PageOptions{ .methods = &.{ .GET, .POST } };
const Context = @import("../Context.zig");
pub fn Page(ctx: zx.PageCtx(Context, void)) zx.Component {
pub fn Page(ctx: zx.PageContext, app: Context) zx.Component {
const query = ctx.request.queries;
count = ctx.app.db.getCount() catch -1;
count = app.db.getCount() catch -1;
if (query.get("reset")) |_|
count = 0
@ -15,7 +15,7 @@ pub fn Page(ctx: zx.PageCtx(Context, void)) zx.Component {
else
count += 1;
ctx.app.db.setCount(count) catch |err| {
app.db.setCount(count) catch |err| {
zx.log.err("setting {s}", .{@errorName(err)});
};

View File

@ -74,18 +74,6 @@ test "PageContext: has allocator and arena fields" {
_ = ctx.arena;
}
test "PageContext: parent_ctx is null by default" {
var buffer: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const alloc = fba.allocator();
const req = (Request.Builder{ .arena = alloc }).build();
const res = (Response.Builder{ .arena = alloc }).build();
const ctx = PageContext.init(req, res, alloc);
try std.testing.expect(ctx.parent_ctx == null);
}
// --- ErrorContext --- //
test "ErrorContext: has error field" {