mirror of
https://github.com/ziex-dev/ziex.git
synced 2026-07-20 02:29:36 -06:00
refactor: make context injection positional arg
This commit is contained in:
parent
56532f7e04
commit
5a10a82d11
@ -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();
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)});
|
||||
};
|
||||
|
||||
|
||||
@ -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" {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user