mirror of
https://github.com/ziex-dev/ziex.git
synced 2026-07-22 11:39:34 -06:00
feat: web standard Request/Response
This commit is contained in:
parent
d300fb9b7f
commit
4f44aad385
@ -63,11 +63,11 @@ pub const routes = [_]zx.App.Meta.Route{
|
||||
.page = wrapPage(@import(".zx/pages/examples/streaming/page.zig").Page),
|
||||
.page_opts = getOptions(@import(".zx/pages/examples/streaming/page.zig"), zx.PageOptions),
|
||||
},
|
||||
.{
|
||||
.path = "/examples/wasm/simple",
|
||||
.page = wrapPage(@import(".zx/pages/examples/wasm/simple/page.zig").Page),
|
||||
.page_opts = getOptions(@import(".zx/pages/examples/wasm/simple/page.zig"), zx.PageOptions),
|
||||
},
|
||||
// .{
|
||||
// .path = "/examples/wasm/simple",
|
||||
// .page = wrapPage(@import(".zx/pages/examples/wasm/simple/page.zig").Page),
|
||||
// .page_opts = getOptions(@import(".zx/pages/examples/wasm/simple/page.zig"), zx.PageOptions),
|
||||
// },
|
||||
.{
|
||||
.path = "/examples/overview",
|
||||
.page = wrapPage(@import(".zx/pages/examples/overview/page.zig").Page),
|
||||
|
||||
@ -482,7 +482,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
<li><code>site/pages/[category]/[item]/page.zx</code> → <code>/:category/:item</code></li>
|
||||
</ul>
|
||||
<h3>Accessing Route and Query Parameters</h3>
|
||||
<p>Use <code>ctx.request.param("name")</code> to access dynamic route parameters and <code>ctx.request.query("name")</code> for query strings:</p>
|
||||
<p>Use <code>ctx.request.getParam("name")</code> to access dynamic route parameters and <code>ctx.request.query("name")</code> for query strings:</p>
|
||||
<ExampleBlock id="dynamic-route" zx_code={zx_example_dynamic} zig_code={zig_example_dynamic} html_code={html_example_dynamic} filename="user_profile.zx" />
|
||||
<p>Both methods return <code>?[]const u8</code>, so use <code>orelse</code> to provide a default value.</p>
|
||||
</section>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
var count = zx.Signal(i32).init(0);
|
||||
var doubled = zx.Computed(i32, i32).init(&count, doubleValue);
|
||||
var count: zx.Signal(i32) = .init(0);
|
||||
var doubled: zx.Computed(i32, i32) = .init(&count, doubleValue);
|
||||
|
||||
pub fn CounterComponent(allocator: zx.Allocator) zx.Component {
|
||||
return (
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
pub fn UserProfile(ctx: zx.PageContext) zx.Component {
|
||||
const user_id = ctx.request.param("id") orelse "unknown";
|
||||
const user_id = ctx.request.getParam("id") orelse "unknown";
|
||||
|
||||
return (
|
||||
<main @allocator={ctx.arena}>
|
||||
|
||||
@ -16,7 +16,7 @@ const RequestInfo = struct {
|
||||
|
||||
pub fn handleRequest(ctx: zx.PageContext) RequestInfo {
|
||||
// const fd = ctx.request.formData() catch @panic("OOM");
|
||||
const qs = ctx.request.query() catch @panic("OOM");
|
||||
const qs = ctx.request.searchParams;
|
||||
|
||||
const is_reset = qs.get("reset") != null;
|
||||
const is_delete = qs.get("delete") != null;
|
||||
@ -42,7 +42,7 @@ pub fn handleRequest(ctx: zx.PageContext) RequestInfo {
|
||||
const filtered_users = filterUsers(ctx.arena, search_opt);
|
||||
|
||||
if (is_delete or is_add or is_reset) {
|
||||
ctx.response.header("Location", "/examples/form");
|
||||
ctx.response.setHeader("Location", "/examples/form");
|
||||
ctx.response.setStatus(.found);
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
const allocator = ctx.arena;
|
||||
|
||||
return (
|
||||
<div @allocator={allocator} class="todo-container">
|
||||
<TodoApp @rendering={.client} />
|
||||
@ -12,6 +13,29 @@ pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
);
|
||||
}
|
||||
|
||||
pub const CounterPage = @import("./simple/page.zx").Page;
|
||||
|
||||
fn Counter(allocator: zx.Allocator) zx.Component {
|
||||
const ctx = zx.PageContext{
|
||||
.request = .{
|
||||
.url = "",
|
||||
.method = .GET,
|
||||
.pathname = "",
|
||||
.headers = .{},
|
||||
.arena = allocator,
|
||||
},
|
||||
.response = .{ .arena = allocator },
|
||||
.allocator = allocator,
|
||||
.arena = allocator,
|
||||
};
|
||||
const counter_page = CounterPage(ctx);
|
||||
return (
|
||||
<div @allocator={allocator}>
|
||||
{counter_page}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Todo = struct { id: u32, text: []const u8, completed: bool };
|
||||
var todos = std.ArrayList(Todo).empty;
|
||||
|
||||
@ -22,6 +46,7 @@ pub fn TodoApp(allocator: zx.Allocator) zx.Component {
|
||||
<div @allocator={allocator} class="todo-app">
|
||||
<div class="todo-header">
|
||||
<h1 class="todo-title">ZX + WASM</h1>
|
||||
<Counter />
|
||||
<p class="todo-subtitle">A demo todo app built with ZX Client Side Rendering</p>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
return (
|
||||
<div @allocator={ctx.arena}>
|
||||
<CounterComponent @rendering={.client} />
|
||||
<CounterComponent />
|
||||
|
||||
<script type="module" src="/assets/main.js" />
|
||||
</div>
|
||||
|
||||
@ -1,10 +1,24 @@
|
||||
pub fn Layout(ctx: zx.LayoutContext, children: zx.Component) zx.Component {
|
||||
const path = ctx.request.pathname;
|
||||
const description = "A full-stack web framework for Zig. Write declarative UI components using familiar JSX patterns, transpiled to efficient Zig code.";
|
||||
const title = "ZX - Full-Stack Web Framework for Zig";
|
||||
const site_url = "https://ziex.dev";
|
||||
const keywords = "zig, zx, web framework, jsx, full-stack, ssr, ssg, wasm, components, server-side rendering, static site generation";
|
||||
const js_href = std.fmt.allocPrint(ctx.arena, "/assets/docs.js?v={s}", .{zx.info.version_string}) catch unreachable;
|
||||
const needs_js = !std.mem.eql(u8, ctx.request.url.path, "/");
|
||||
const needs_js = !std.mem.eql(u8, path, "/");
|
||||
|
||||
if (std.mem.eql(u8, path, "/overview") or std.mem.eql(u8, path, "/time")) {
|
||||
return (
|
||||
<html lang="en" @allocator={ctx.arena}>
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang="en" @allocator={ctx.arena}>
|
||||
@ -13,7 +27,7 @@ pub fn Layout(ctx: zx.LayoutContext, children: zx.Component) zx.Component {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
|
||||
<meta name="keywords" content={keywords} />
|
||||
<meta name="author" content="Nurul Huda" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
@ -48,9 +62,13 @@ pub fn Layout(ctx: zx.LayoutContext, children: zx.Component) zx.Component {
|
||||
}
|
||||
|
||||
fn getCss(ctx: zx.LayoutContext) []const u8 {
|
||||
const path = ctx.request.url.path;
|
||||
return if (std.mem.eql(u8, path, "/"))
|
||||
const req_path = ctx.request.pathname;
|
||||
return if (std.mem.eql(u8, req_path, "/"))
|
||||
@embedFile("../assets/home.css")
|
||||
else if (std.mem.eql(u8, req_path, "/overview"))
|
||||
""
|
||||
else if (std.mem.eql(u8, req_path, "/time"))
|
||||
""
|
||||
else
|
||||
@embedFile("../assets/docs.css");
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// site/pages/user/[id]/page.zx
|
||||
pub fn UserProfile(ctx: zx.PageContext) zx.Component {
|
||||
const user_id = ctx.request.param("id") orelse "unknown";
|
||||
const user_id = ctx.request.getParam("id") orelse "unknown";
|
||||
|
||||
var _zx = zx.allocInit(ctx.arena);
|
||||
return _zx.ele(
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// site/pages/user/[id]/page.zx
|
||||
pub fn UserProfile(ctx: zx.PageContext) zx.Component {
|
||||
const user_id = ctx.request.param("id") orelse "unknown";
|
||||
const user_id = ctx.request.getParam("id") orelse "unknown";
|
||||
|
||||
return (
|
||||
<main @allocator={ctx.arena}>
|
||||
|
||||
@ -3,7 +3,7 @@ pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
<main @allocator={ctx.arena}>
|
||||
<h1>About Us</h1>
|
||||
<p>Welcome to our website!</p>
|
||||
<p>Path: {ctx.request.url.path}</p>
|
||||
<p>Path: {ctx.request.pathname}</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@ -567,7 +567,7 @@ fn PagesAndLayouts(allocator: zx.Allocator, props: PagesAndLayoutsProps) zx.Comp
|
||||
<li><code>site/pages/user/[id]/page.zx</code> → <code>/user/:id</code></li>
|
||||
<li><code>site/pages/blog/[slug]/page.zx</code> → <code>/blog/:slug</code></li>
|
||||
</ul>
|
||||
<p>Access the parameter value using <code>ctx.request.param("name")</code>:</p>
|
||||
<p>Access the parameter value using <code>ctx.request.getParam("name")</code>:</p>
|
||||
<ExampleBlock id="dynamic-route" zx_code={props.zx_dynamic} zig_code={props.zig_dynamic} html_code={props.html_dynamic} filename="user/[id]/user_profile.zx" />
|
||||
|
||||
<h3>Creating layouts</h3>
|
||||
|
||||
162
src/app/Headers.zig
Normal file
162
src/app/Headers.zig
Normal file
@ -0,0 +1,162 @@
|
||||
//! MDN Web API compliant Headers abstraction.
|
||||
//! This module is backend-agnostic. The actual implementation is provided via vtable.
|
||||
//! https://developer.mozilla.org/en-US/docs/Web/API/Headers
|
||||
|
||||
const std = @import("std");
|
||||
const common = @import("common.zig");
|
||||
|
||||
pub const Headers = @This();
|
||||
|
||||
// Re-export types from std.http
|
||||
pub const Header = common.Header;
|
||||
/// @deprecated Use `Header` instead.
|
||||
pub const Entry = Header;
|
||||
|
||||
/// HTTP header iterator for parsing raw header bytes.
|
||||
/// Re-exported from std.http.HeaderIterator for convenience.
|
||||
///
|
||||
/// This is useful when you have raw HTTP protocol bytes and need to parse headers.
|
||||
/// For iterating over headers from a backend, use the `entries()` method which
|
||||
/// returns the vtable-based `Iterator`.
|
||||
///
|
||||
/// [Zig std.http Reference](https://ziglang.org/documentation/master/std/#std.http.HeaderIterator)
|
||||
pub const HeaderIterator = std.http.HeaderIterator;
|
||||
|
||||
// --- Headers Data --- //
|
||||
|
||||
/// Backend-specific context pointer (null for WASM/client-side)
|
||||
backend_ctx: ?*anyopaque = null,
|
||||
|
||||
/// VTable for backend-specific operations
|
||||
vtable: ?*const VTable = null,
|
||||
|
||||
/// Whether this Headers instance is read-only (from request)
|
||||
read_only: bool = true,
|
||||
|
||||
pub const VTable = struct {
|
||||
get: *const fn (ctx: *anyopaque, name: []const u8) ?[]const u8,
|
||||
has: *const fn (ctx: *anyopaque, name: []const u8) bool,
|
||||
set: *const fn (ctx: *anyopaque, name: []const u8, value: []const u8) void,
|
||||
append: *const fn (ctx: *anyopaque, name: []const u8, value: []const u8) void,
|
||||
iterate: *const fn (ctx: *anyopaque) ?Iterator,
|
||||
};
|
||||
|
||||
// --- Headers Methods --- //
|
||||
|
||||
/// Returns true if this Headers instance is read-only (from request).
|
||||
pub fn isReadOnly(self: *const Headers) bool {
|
||||
return self.read_only;
|
||||
}
|
||||
|
||||
/// Returns the value of the header with the specified name.
|
||||
pub fn get(self: *const Headers, name: []const u8) ?[]const u8 {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
return vt.get(ctx, name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns whether a header with the specified name exists.
|
||||
pub fn has(self: *const Headers, name: []const u8) bool {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
return vt.has(ctx, name);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Appends a new value onto an existing header (response only, no-op for request).
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Headers/append
|
||||
pub fn append(self: *Headers, name: []const u8, value: []const u8) void {
|
||||
if (self.read_only) return;
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
vt.append(ctx, name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a header value (response only, no-op for request).
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Headers/set
|
||||
pub fn set(self: *Headers, name: []const u8, value: []const u8) void {
|
||||
if (self.read_only) return;
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
vt.set(ctx, name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a header. Note: Most backends don't support header deletion.
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Headers/delete
|
||||
pub fn delete(self: *Headers, name: []const u8) void {
|
||||
_ = self;
|
||||
_ = name;
|
||||
// Most backends don't support deletion - no-op
|
||||
}
|
||||
|
||||
/// Returns an iterator over all key/value pairs.
|
||||
pub fn entries(self: *const Headers) ?Iterator {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
return vt.iterate(ctx);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Executes a provided function once for each header.
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Headers/forEach
|
||||
pub fn forEach(self: *const Headers, callback: *const fn (value: []const u8, key: []const u8) void) void {
|
||||
if (self.entries()) |*iter| {
|
||||
var it = iter.*;
|
||||
while (it.next()) |entry| {
|
||||
callback(entry.value, entry.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Iterator (vtable-based, for backend abstraction) --- //
|
||||
|
||||
/// Backend-agnostic iterator for iterating over headers.
|
||||
///
|
||||
/// Example:
|
||||
/// ```zig
|
||||
/// if (headers.entries()) |*iter| {
|
||||
/// while (iter.next()) |header| {
|
||||
/// std.debug.print("{s}: {s}\n", .{ header.name, header.value });
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub const Iterator = struct {
|
||||
backend_ctx: ?*anyopaque = null,
|
||||
nextFn: ?*const fn (ctx: *anyopaque) ?Header = null,
|
||||
|
||||
/// Returns the next header, or null if iteration is complete.
|
||||
pub fn next(self: *Iterator) ?Header {
|
||||
if (self.nextFn) |nextFunc| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
return nextFunc(ctx);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Builder --- //
|
||||
pub const Builder = struct {
|
||||
backend_ctx: ?*anyopaque = null,
|
||||
vtable: ?*const VTable = null,
|
||||
read_only: bool = true,
|
||||
|
||||
pub fn build(self: Builder) Headers {
|
||||
return .{
|
||||
.backend_ctx = self.backend_ctx,
|
||||
.vtable = self.vtable,
|
||||
.read_only = self.read_only,
|
||||
};
|
||||
}
|
||||
};
|
||||
309
src/app/Request.zig
Normal file
309
src/app/Request.zig
Normal file
@ -0,0 +1,309 @@
|
||||
//! The Request interface of the Fetch API represents a resource request.
|
||||
//!
|
||||
//! You can create a new Request object using the Request.Builder, but you are more likely
|
||||
//! to encounter a Request object being returned as part of another API operation, such as
|
||||
//! a page handler context receiving an incoming HTTP request.
|
||||
//!
|
||||
//! This module is backend-agnostic. The actual implementation is provided via vtable.
|
||||
//!
|
||||
//! https://developer.mozilla.org/en-US/docs/Web/API/Request
|
||||
|
||||
const std = @import("std");
|
||||
const common = @import("common.zig");
|
||||
|
||||
pub const Request = @This();
|
||||
|
||||
// Re-export common types for convenience
|
||||
pub const Method = common.Method;
|
||||
pub const Version = common.Version;
|
||||
pub const Protocol = common.Protocol; // Deprecated alias for Version
|
||||
pub const Cookies = common.Cookies;
|
||||
pub const Header = common.Header;
|
||||
/// @deprecated Use `Header` instead.
|
||||
pub const Entry = Header;
|
||||
pub const MultiFormEntry = common.MultiFormEntry;
|
||||
|
||||
// --- Instance Properties --- //
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Request#instance_properties
|
||||
|
||||
/// Contains the URL of the request.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Request/url
|
||||
url: []const u8,
|
||||
|
||||
/// Contains the request's method (GET, POST, etc.).
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Request/method
|
||||
///
|
||||
/// **Zig Note:** In the web standard, this is a string. In this implementation,
|
||||
/// it is represented as a `Method` enum for type safety. The original string
|
||||
/// is available via `method_str`.
|
||||
method: Method,
|
||||
|
||||
/// Contains the request's method as a string.
|
||||
///
|
||||
/// **Zig Note:** This is an extension field. In the web standard, `method` is
|
||||
/// already a string. This field preserves the original string representation.
|
||||
method_str: []const u8 = "",
|
||||
|
||||
/// Contains the pathname portion of the URL.
|
||||
///
|
||||
/// **Zig Note:** This is an extension field not present in the web standard Request.
|
||||
/// In the web standard, you would parse the URL to get the pathname.
|
||||
/// Example: https://foo.com/bar/baz -> /bar/baz
|
||||
pathname: []const u8,
|
||||
|
||||
/// Contains the referrer of the request (e.g., client, no-referrer, or a URL).
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Request/referrer
|
||||
///
|
||||
/// **Zig Note:** In the web standard, this defaults to "about:client". In this
|
||||
/// implementation, it defaults to an empty string.
|
||||
referrer: []const u8 = "",
|
||||
|
||||
/// Contains the search/query string portion of the URL.
|
||||
///
|
||||
/// **Zig Note:** This is an extension field not present in the web standard Request.
|
||||
/// In the web standard, you would parse the URL to get the search string.
|
||||
/// Example: https://foo.com/bar?q=qux -> q=qux
|
||||
search: []const u8 = "",
|
||||
|
||||
/// Contains the associated Headers object of the request.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Request/headers
|
||||
headers: Headers,
|
||||
|
||||
/// Cookie accessor for parsing cookies from the Cookie header.
|
||||
///
|
||||
/// **Zig Note:** This is an extension field not present in the web standard Request.
|
||||
/// In the web standard, you would access cookies via `document.cookie` or parse
|
||||
/// the Cookie header manually.
|
||||
cookies: Cookies = .{ .header_value = "" },
|
||||
|
||||
/// URL search parameters accessor.
|
||||
///
|
||||
/// **Zig Note:** This is an extension field. In the web standard, you would use
|
||||
/// `new URL(request.url).searchParams` to access search parameters.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
|
||||
searchParams: URLSearchParams = .{},
|
||||
|
||||
/// HTTP protocol version (HTTP/1.0 or HTTP/1.1).
|
||||
///
|
||||
/// **Zig Note:** This is an extension field not present in the web standard Request.
|
||||
/// The web standard Request doesn't expose the HTTP protocol version.
|
||||
/// Uses `std.http.Version` from the standard library.
|
||||
protocol: Version = .@"HTTP/1.1",
|
||||
|
||||
// --- Internal Fields (not part of web standard) --- //
|
||||
|
||||
/// Arena allocator for request-scoped allocations.
|
||||
///
|
||||
/// **Zig Note:** This is an internal field not present in the web standard.
|
||||
/// Used for memory management in the Zig implementation.
|
||||
arena: std.mem.Allocator,
|
||||
|
||||
/// Backend-specific context pointer (null for WASM/client-side).
|
||||
///
|
||||
/// **Zig Note:** This is an internal field not present in the web standard.
|
||||
/// Provides the vtable pattern for backend abstraction.
|
||||
backend_ctx: ?*anyopaque = null,
|
||||
|
||||
/// VTable for backend-specific operations.
|
||||
///
|
||||
/// **Zig Note:** This is an internal field not present in the web standard.
|
||||
/// Allows different HTTP server backends to implement request operations.
|
||||
vtable: ?*const VTable = null,
|
||||
|
||||
/// VTable interface for backend-specific request operations.
|
||||
///
|
||||
/// **Zig Note:** This is an internal type not present in the web standard.
|
||||
pub const VTable = struct {
|
||||
/// Returns the request body as text.
|
||||
text: *const fn (ctx: *anyopaque) ?[]const u8 = &defaultText,
|
||||
/// Returns a URL parameter by name (from route matching).
|
||||
getParam: *const fn (ctx: *anyopaque, name: []const u8) ?[]const u8 = &defaultGetParam,
|
||||
|
||||
fn defaultText(_: *anyopaque) ?[]const u8 {
|
||||
return null;
|
||||
}
|
||||
fn defaultGetParam(_: *anyopaque, _: []const u8) ?[]const u8 {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Instance Methods --- //
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Request#instance_methods
|
||||
|
||||
/// Returns a promise that resolves with a text representation of the request body.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Request/text
|
||||
///
|
||||
/// **Zig Note:** In the web standard, this returns a Promise<string>. In this
|
||||
/// implementation, it returns `?[]const u8` synchronously (null if no body).
|
||||
pub fn text(self: *const Request) ?[]const u8 {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
return vt.text(ctx);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns a URL parameter by name (from route matching).
|
||||
///
|
||||
/// **Zig Note:** This is an extension method not present in the web standard Request.
|
||||
/// Used for accessing dynamic route parameters (e.g., /users/:id -> getParam("id")).
|
||||
pub fn getParam(self: *const Request, name: []const u8) ?[]const u8 {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
return vt.getParam(ctx, name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- URLSearchParams --- //
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
|
||||
|
||||
/// The URLSearchParams interface defines utility methods to work with the
|
||||
/// query string of a URL.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
|
||||
///
|
||||
/// **Zig Note:** This is a simplified implementation that delegates to the backend.
|
||||
/// Not all methods from the web standard URLSearchParams interface are implemented.
|
||||
pub const URLSearchParams = struct {
|
||||
backend_ctx: ?*anyopaque = null,
|
||||
vtable: ?*const URLSearchParamsVTable = null,
|
||||
|
||||
pub const URLSearchParamsVTable = struct {
|
||||
get: *const fn (ctx: *anyopaque, name: []const u8) ?[]const u8,
|
||||
has: *const fn (ctx: *anyopaque, name: []const u8) bool,
|
||||
};
|
||||
|
||||
/// Returns the first value associated with the given search parameter.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/get
|
||||
///
|
||||
/// **Zig Note:** Returns `?[]const u8` instead of `string | null`.
|
||||
pub fn get(self: *const URLSearchParams, name: []const u8) ?[]const u8 {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
return vt.get(ctx, name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns a boolean indicating if such a given parameter exists.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/has
|
||||
pub fn has(self: *const URLSearchParams, name: []const u8) bool {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
return vt.has(ctx, name);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Headers --- //
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Headers
|
||||
|
||||
/// The Headers interface of the Fetch API allows you to perform various actions
|
||||
/// on HTTP request and response headers.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Headers
|
||||
///
|
||||
/// **Zig Note:** This is a simplified read-only implementation for request headers.
|
||||
/// Not all methods from the web standard Headers interface are implemented.
|
||||
pub const Headers = struct {
|
||||
backend_ctx: ?*anyopaque = null,
|
||||
vtable: ?*const HeadersVTable = null,
|
||||
|
||||
pub const HeadersVTable = struct {
|
||||
get: *const fn (ctx: *anyopaque, name: []const u8) ?[]const u8,
|
||||
has: *const fn (ctx: *anyopaque, name: []const u8) bool,
|
||||
};
|
||||
|
||||
/// Returns a String sequence of all the values of a header within a Headers
|
||||
/// object with a given name.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Headers/get
|
||||
///
|
||||
/// **Zig Note:** Returns `?[]const u8` instead of a string, returning `null`
|
||||
/// if the header is not found.
|
||||
pub fn get(self: *const Headers, name: []const u8) ?[]const u8 {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
return vt.get(ctx, name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns a boolean stating whether a Headers object contains a certain header.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Headers/has
|
||||
pub fn has(self: *const Headers, name: []const u8) bool {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
return vt.has(ctx, name);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Builder (not part of web standard) --- //
|
||||
|
||||
/// Builder for creating Request objects.
|
||||
///
|
||||
/// **Zig Note:** This is an internal type not present in the web standard.
|
||||
/// In the web standard, you would use the `new Request(input, init)` constructor.
|
||||
/// This builder pattern is used for backend implementations to construct
|
||||
/// Request objects with the appropriate vtable and context.
|
||||
pub const Builder = struct {
|
||||
url: []const u8 = "",
|
||||
method: Method = .GET,
|
||||
method_str: []const u8 = "GET",
|
||||
pathname: []const u8 = "/",
|
||||
referrer: []const u8 = "",
|
||||
search: []const u8 = "",
|
||||
protocol: Version = .@"HTTP/1.1",
|
||||
arena: std.mem.Allocator,
|
||||
backend_ctx: ?*anyopaque = null,
|
||||
vtable: ?*const VTable = null,
|
||||
headers_ctx: ?*anyopaque = null,
|
||||
headers_vtable: ?*const Headers.HeadersVTable = null,
|
||||
cookie_header: []const u8 = "",
|
||||
search_params_ctx: ?*anyopaque = null,
|
||||
search_params_vtable: ?*const URLSearchParams.URLSearchParamsVTable = null,
|
||||
|
||||
/// Builds the Request object with all configured values.
|
||||
pub fn build(self: Builder) Request {
|
||||
return .{
|
||||
.url = self.url,
|
||||
.method = self.method,
|
||||
.method_str = self.method_str,
|
||||
.pathname = self.pathname,
|
||||
.referrer = self.referrer,
|
||||
.search = self.search,
|
||||
.protocol = self.protocol,
|
||||
.arena = self.arena,
|
||||
.backend_ctx = self.backend_ctx,
|
||||
.vtable = self.vtable,
|
||||
.headers = .{
|
||||
.backend_ctx = self.headers_ctx,
|
||||
.vtable = self.headers_vtable,
|
||||
},
|
||||
.cookies = .{ .header_value = self.cookie_header },
|
||||
.searchParams = .{
|
||||
.backend_ctx = self.search_params_ctx,
|
||||
.vtable = self.search_params_vtable,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
350
src/app/Response.zig
Normal file
350
src/app/Response.zig
Normal file
@ -0,0 +1,350 @@
|
||||
//! The Response interface of the Fetch API represents the response to a request.
|
||||
//!
|
||||
//! You can create a new Response object using the Response.Builder, but you are more likely
|
||||
//! to encounter a Response object being returned as the result of another API operation—for
|
||||
//! example, a page handler context or a fetch() call.
|
||||
//!
|
||||
//! This module is backend-agnostic. The actual implementation is provided via vtable.
|
||||
//!
|
||||
//! https://developer.mozilla.org/en-US/docs/Web/API/Response
|
||||
|
||||
const std = @import("std");
|
||||
const common = @import("common.zig");
|
||||
|
||||
pub const Response = @This();
|
||||
|
||||
// Re-export common types for convenience
|
||||
pub const CookieOptions = common.CookieOptions;
|
||||
pub const ContentType = common.ContentType;
|
||||
pub const HttpStatus = common.HttpStatus;
|
||||
|
||||
/// The type of the response.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Response/type
|
||||
///
|
||||
/// **Values:**
|
||||
/// - `basic`: Normal, same origin response, with all headers exposed except "Set-Cookie".
|
||||
/// - `cors`: Response was received from a valid cross-origin request.
|
||||
/// - `default`: Default response type (used when response type is not explicitly set).
|
||||
/// - `error`: Network error. No useful information describing the error is available.
|
||||
/// - `opaque`: Response for "no-cors" request to cross-origin resource.
|
||||
/// - `opaqueredirect`: The fetch request was made with redirect: "manual".
|
||||
pub const ResponseType = enum {
|
||||
basic,
|
||||
cors,
|
||||
default,
|
||||
@"error",
|
||||
@"opaque",
|
||||
opaqueredirect,
|
||||
};
|
||||
|
||||
// --- Instance Properties --- //
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Response#instance_properties
|
||||
|
||||
/// A ReadableStream of the body contents.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Response/body
|
||||
///
|
||||
/// **Zig Note:** In the web standard, this is a ReadableStream. In this implementation,
|
||||
/// it is represented as `[]const u8` (a byte slice) for simplicity.
|
||||
body: []const u8 = "",
|
||||
|
||||
/// Stores a boolean value that declares whether the body has been used in a response yet.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Response/bodyUsed
|
||||
bodyUsed: bool = false,
|
||||
|
||||
/// The Headers object associated with the response.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Response/headers
|
||||
headers: Headers = .{},
|
||||
|
||||
/// A boolean indicating whether the response was successful (status in the range 200–299) or not.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Response/ok
|
||||
ok: bool = true,
|
||||
|
||||
/// Indicates whether or not the response is the result of a redirect
|
||||
/// (that is, its URL list has more than one entry).
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Response/redirected
|
||||
redirected: bool = false,
|
||||
|
||||
/// The status code of the response (e.g., 200 for a success).
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Response/status
|
||||
status: u16 = 200,
|
||||
|
||||
/// The status message corresponding to the status code (e.g., "OK" for 200).
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Response/statusText
|
||||
statusText: []const u8 = "OK",
|
||||
|
||||
/// The type of the response (e.g., basic, cors).
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Response/type
|
||||
///
|
||||
/// **Zig Note:** In the web standard, this is a string. In this implementation,
|
||||
/// it is represented as a `ResponseType` enum for type safety.
|
||||
type: ResponseType = .default,
|
||||
|
||||
/// The URL of the response.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Response/url
|
||||
url: []const u8 = "",
|
||||
|
||||
// --- Internal Fields (not part of web standard) --- //
|
||||
|
||||
/// Arena allocator for response-scoped allocations.
|
||||
///
|
||||
/// **Zig Note:** This is an internal field not present in the web standard.
|
||||
/// Used for memory management in the Zig implementation.
|
||||
arena: std.mem.Allocator,
|
||||
|
||||
/// Backend-specific context pointer (null for WASM/client-side).
|
||||
///
|
||||
/// **Zig Note:** This is an internal field not present in the web standard.
|
||||
/// Provides the vtable pattern for backend abstraction.
|
||||
backend_ctx: ?*anyopaque = null,
|
||||
|
||||
/// VTable for backend-specific operations.
|
||||
///
|
||||
/// **Zig Note:** This is an internal field not present in the web standard.
|
||||
/// Allows different HTTP server backends to implement response operations.
|
||||
vtable: ?*const VTable = null,
|
||||
|
||||
/// VTable interface for backend-specific response operations.
|
||||
///
|
||||
/// **Zig Note:** This is an internal type not present in the web standard.
|
||||
pub const VTable = struct {
|
||||
/// Sets the response status code.
|
||||
setStatus: *const fn (ctx: *anyopaque, code: u16) void,
|
||||
/// Sets the response body.
|
||||
setBody: *const fn (ctx: *anyopaque, content: []const u8) void,
|
||||
/// Sets a header.
|
||||
setHeader: *const fn (ctx: *anyopaque, name: []const u8, value: []const u8) void,
|
||||
/// Gets a writer for streaming.
|
||||
getWriter: *const fn (ctx: *anyopaque) ?std.io.AnyWriter,
|
||||
/// Writes a chunk for chunked transfer.
|
||||
writeChunk: *const fn (ctx: *anyopaque, data: []const u8) anyerror!void,
|
||||
/// Clears the response writer/buffer.
|
||||
clearWriter: *const fn (ctx: *anyopaque) void,
|
||||
};
|
||||
|
||||
// --- Methods --- //
|
||||
|
||||
/// Sets the HTTP status code using an HttpStatus enum.
|
||||
///
|
||||
/// **Zig Note:** This is an extension method not present in the web standard Response
|
||||
/// interface (which has read-only status). This method updates the backend response.
|
||||
/// The local `status`, `statusText`, and `ok` fields reflect the initial state;
|
||||
/// use the backend for the source of truth after mutation.
|
||||
pub fn setStatus(self: *const Response, stat: HttpStatus) void {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
vt.setStatus(ctx, @intFromEnum(stat));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the HTTP status code using a raw u16 value.
|
||||
///
|
||||
/// **Zig Note:** This is an extension method not present in the web standard Response
|
||||
/// interface (which has read-only status). This method updates the backend response.
|
||||
pub fn setStatusCode(self: *const Response, code: u16) void {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
vt.setStatus(ctx, code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the response body directly.
|
||||
///
|
||||
/// **Zig Note:** This is an extension method not present in the web standard Response
|
||||
/// interface (which has read-only body). This method updates the backend response.
|
||||
pub fn setBody(self: *const Response, content: []const u8) void {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
vt.setBody(ctx, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a header on the response.
|
||||
///
|
||||
/// **Zig Note:** This is an extension method. In the web standard, you would use
|
||||
/// `response.headers.set(name, value)` on the Headers object instead.
|
||||
pub fn setHeader(self: *const Response, name: []const u8, value: []const u8) void {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
vt.setHeader(ctx, name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the Content-Type header.
|
||||
///
|
||||
/// **Zig Note:** This is a convenience method not present in the web standard.
|
||||
/// Equivalent to `setHeader("Content-Type", content_type.toString())`.
|
||||
pub fn setContentType(self: *const Response, content_type: ContentType) void {
|
||||
self.setHeader("Content-Type", content_type.toString());
|
||||
}
|
||||
|
||||
/// Creates a redirect response by setting the Location header and status code.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static
|
||||
///
|
||||
/// **Zig Note:** In the web standard, `Response.redirect()` is a static method that
|
||||
/// creates a new Response object. In this implementation, it modifies the current
|
||||
/// response by setting the Location header and status code on the backend.
|
||||
///
|
||||
/// **Parameters:**
|
||||
/// - `location`: The URL to redirect to.
|
||||
/// - `redirect_status`: Optional status code (default: 302 Found).
|
||||
pub fn redirect(self: *const Response, location: []const u8, redirect_status: ?u16) void {
|
||||
const code = redirect_status orelse 302;
|
||||
self.setStatusCode(code);
|
||||
self.setHeader("Location", location);
|
||||
}
|
||||
|
||||
/// Gets the response writer for streaming content.
|
||||
///
|
||||
/// **Zig Note:** This is an extension method not present in the web standard.
|
||||
/// Used for server-side streaming responses.
|
||||
pub fn writer(self: *const Response) ?std.io.AnyWriter {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
return vt.getWriter(ctx);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Writes a chunk for chunked transfer encoding.
|
||||
///
|
||||
/// **Zig Note:** This is an extension method not present in the web standard.
|
||||
/// Used for server-side streaming/chunked responses.
|
||||
pub fn chunk(self: *const Response, data: []const u8) !void {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
try vt.writeChunk(ctx, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the response writer/buffer.
|
||||
///
|
||||
/// **Zig Note:** This is an extension method not present in the web standard.
|
||||
/// Used for server-side response management.
|
||||
pub fn clearWriter(self: *const Response) void {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
vt.clearWriter(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Headers --- //
|
||||
// https://developer.mozilla.org/en-US/API/Headers
|
||||
|
||||
/// The Headers interface of the Fetch API allows you to perform various actions
|
||||
/// on HTTP request and response headers.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Headers
|
||||
///
|
||||
/// **Zig Note:** This is a simplified implementation that delegates to the backend.
|
||||
/// Not all methods from the web standard Headers interface are implemented.
|
||||
pub const Headers = struct {
|
||||
backend_ctx: ?*anyopaque = null,
|
||||
vtable: ?*const HeadersVTable = null,
|
||||
|
||||
pub const HeadersVTable = struct {
|
||||
get: *const fn (ctx: *anyopaque, name: []const u8) ?[]const u8,
|
||||
set: *const fn (ctx: *anyopaque, name: []const u8, value: []const u8) void,
|
||||
add: *const fn (ctx: *anyopaque, name: []const u8, value: []const u8) void,
|
||||
};
|
||||
|
||||
/// Returns a String sequence of all the values of a header within a Headers
|
||||
/// object with a given name.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Headers/get
|
||||
///
|
||||
/// **Zig Note:** Returns `?[]const u8` instead of a string, returning `null`
|
||||
/// if the header is not found.
|
||||
pub fn get(self: *const Headers, name: []const u8) ?[]const u8 {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
return vt.get(ctx, name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Sets a new value for an existing header inside a Headers object,
|
||||
/// or adds the header if it does not already exist.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Headers/set
|
||||
pub fn set(self: *const Headers, name: []const u8, value: []const u8) void {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
vt.set(ctx, name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends a new value onto an existing header inside a Headers object,
|
||||
/// or adds the header if it does not already exist.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Headers/append
|
||||
///
|
||||
/// **Zig Note:** Named `add` instead of `append` in this implementation.
|
||||
pub fn add(self: *const Headers, name: []const u8, value: []const u8) void {
|
||||
if (self.vtable) |vt| {
|
||||
if (self.backend_ctx) |ctx| {
|
||||
vt.add(ctx, name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- Builder (not part of web standard) --- //
|
||||
|
||||
/// Builder for creating Response objects.
|
||||
///
|
||||
/// **Zig Note:** This is an internal type not present in the web standard.
|
||||
/// In the web standard, you would use the `new Response(body, init)` constructor.
|
||||
/// This builder pattern is used for backend implementations to construct
|
||||
/// Response objects with the appropriate vtable and context.
|
||||
pub const Builder = struct {
|
||||
status: u16 = 200,
|
||||
redirected: bool = false,
|
||||
url: []const u8 = "",
|
||||
response_type: ResponseType = .default,
|
||||
arena: std.mem.Allocator,
|
||||
backend_ctx: ?*anyopaque = null,
|
||||
vtable: ?*const VTable = null,
|
||||
headers_ctx: ?*anyopaque = null,
|
||||
headers_vtable: ?*const Headers.HeadersVTable = null,
|
||||
|
||||
/// Builds the Response object with all configured values.
|
||||
pub fn build(self: Builder) Response {
|
||||
return .{
|
||||
.body = "",
|
||||
.bodyUsed = false,
|
||||
.ok = self.status >= 200 and self.status <= 299,
|
||||
.redirected = self.redirected,
|
||||
.status = self.status,
|
||||
.statusText = common.statusCodeToText(self.status),
|
||||
.type = self.response_type,
|
||||
.url = self.url,
|
||||
.arena = self.arena,
|
||||
.backend_ctx = self.backend_ctx,
|
||||
.vtable = self.vtable,
|
||||
.headers = .{
|
||||
.backend_ctx = self.headers_ctx,
|
||||
.vtable = self.headers_vtable,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
291
src/app/adapter.zig
Normal file
291
src/app/adapter.zig
Normal file
@ -0,0 +1,291 @@
|
||||
//! httpz backend adapter.
|
||||
//! Creates abstract Request/Response from httpz types.
|
||||
//!
|
||||
//! This adapter converts between httpz-specific types and std.http types
|
||||
//! used in the abstract Request/Response layer.
|
||||
|
||||
const std = @import("std");
|
||||
const httpz = @import("httpz");
|
||||
const Request = @import("Request.zig");
|
||||
const Response = @import("Response.zig");
|
||||
const Headers = @import("Headers.zig");
|
||||
const common = @import("common.zig");
|
||||
|
||||
// --- Type Conversion Helpers --- //
|
||||
|
||||
/// Converts httpz.Method to std.http.Method.
|
||||
/// Note: httpz has OTHER for unknown methods, std.http.Method doesn't.
|
||||
/// OTHER is mapped to GET as a fallback (the original method string is preserved in method_str).
|
||||
fn convertMethod(method: httpz.Method) std.http.Method {
|
||||
return switch (method) {
|
||||
.GET => .GET,
|
||||
.HEAD => .HEAD,
|
||||
.POST => .POST,
|
||||
.PUT => .PUT,
|
||||
.DELETE => .DELETE,
|
||||
.CONNECT => .CONNECT,
|
||||
.OPTIONS => .OPTIONS,
|
||||
.PATCH => .PATCH,
|
||||
// Default to CONNECT but the original method string is preserved in method_str
|
||||
.OTHER => .CONNECT, //TODO: figure out better way to handle this
|
||||
};
|
||||
}
|
||||
|
||||
/// Converts httpz.Protocol to std.http.Version.
|
||||
fn convertProtocol(protocol: httpz.Protocol) std.http.Version {
|
||||
return switch (protocol) {
|
||||
.HTTP10 => .@"HTTP/1.0",
|
||||
.HTTP11 => .@"HTTP/1.1",
|
||||
};
|
||||
}
|
||||
|
||||
// --- Request Adapter --- //
|
||||
|
||||
/// Creates an abstract Request from an httpz.Request
|
||||
pub fn createRequest(inner: *httpz.Request) Request {
|
||||
return (Request.Builder{
|
||||
.url = inner.url.raw,
|
||||
.method = convertMethod(inner.method),
|
||||
.method_str = inner.method_string,
|
||||
.pathname = inner.url.path,
|
||||
.referrer = inner.headers.get("referer") orelse "",
|
||||
.search = inner.url.query,
|
||||
.protocol = convertProtocol(inner.protocol),
|
||||
.arena = inner.arena,
|
||||
.backend_ctx = @ptrCast(inner),
|
||||
.vtable = &request_vtable,
|
||||
.headers_ctx = @ptrCast(inner),
|
||||
.headers_vtable = &request_headers_vtable,
|
||||
.cookie_header = inner.headers.get("cookie") orelse "",
|
||||
.search_params_ctx = @ptrCast(inner),
|
||||
.search_params_vtable = &search_params_vtable,
|
||||
}).build();
|
||||
}
|
||||
|
||||
const request_vtable = Request.VTable{
|
||||
.text = &requestText,
|
||||
.getParam = &requestGetParam,
|
||||
};
|
||||
|
||||
fn requestText(ctx: *anyopaque) ?[]const u8 {
|
||||
const req: *httpz.Request = @ptrCast(@alignCast(ctx));
|
||||
return req.body();
|
||||
}
|
||||
|
||||
fn requestGetParam(ctx: *anyopaque, name: []const u8) ?[]const u8 {
|
||||
const req: *httpz.Request = @ptrCast(@alignCast(ctx));
|
||||
return req.param(name);
|
||||
}
|
||||
|
||||
const request_headers_vtable = Request.Headers.HeadersVTable{
|
||||
.get = &requestHeadersGet,
|
||||
.has = &requestHeadersHas,
|
||||
};
|
||||
|
||||
fn requestHeadersGet(ctx: *anyopaque, name: []const u8) ?[]const u8 {
|
||||
const req: *httpz.Request = @ptrCast(@alignCast(ctx));
|
||||
return req.headers.get(name);
|
||||
}
|
||||
|
||||
fn requestHeadersHas(ctx: *anyopaque, name: []const u8) bool {
|
||||
const req: *httpz.Request = @ptrCast(@alignCast(ctx));
|
||||
return req.headers.has(name);
|
||||
}
|
||||
|
||||
const search_params_vtable = Request.URLSearchParams.URLSearchParamsVTable{
|
||||
.get = &searchParamsGet,
|
||||
.has = &searchParamsHas,
|
||||
};
|
||||
|
||||
fn searchParamsGet(ctx: *anyopaque, name: []const u8) ?[]const u8 {
|
||||
const req: *httpz.Request = @ptrCast(@alignCast(ctx));
|
||||
const query = req.query() catch return null;
|
||||
return query.get(name);
|
||||
}
|
||||
|
||||
fn searchParamsHas(ctx: *anyopaque, name: []const u8) bool {
|
||||
const req: *httpz.Request = @ptrCast(@alignCast(ctx));
|
||||
const query = req.query() catch return false;
|
||||
return query.has(name);
|
||||
}
|
||||
|
||||
// --- Response Adapter --- //
|
||||
|
||||
/// Creates an abstract Response from an httpz.Response
|
||||
pub fn createResponse(inner: *httpz.Response, arena: std.mem.Allocator) Response {
|
||||
return (Response.Builder{
|
||||
.status = inner.status,
|
||||
.arena = arena,
|
||||
.backend_ctx = @ptrCast(inner),
|
||||
.vtable = &response_vtable,
|
||||
.headers_ctx = @ptrCast(inner),
|
||||
.headers_vtable = &response_headers_vtable,
|
||||
}).build();
|
||||
}
|
||||
|
||||
const response_vtable = Response.VTable{
|
||||
.setStatus = &responseSetStatus,
|
||||
.setBody = &responseSetBody,
|
||||
.setHeader = &responseSetHeader,
|
||||
.getWriter = &responseGetWriter,
|
||||
.writeChunk = &responseWriteChunk,
|
||||
.clearWriter = &responseClearWriter,
|
||||
};
|
||||
|
||||
fn responseSetStatus(ctx: *anyopaque, status: u16) void {
|
||||
const res: *httpz.Response = @ptrCast(@alignCast(ctx));
|
||||
res.status = status;
|
||||
}
|
||||
|
||||
fn responseSetBody(ctx: *anyopaque, content: []const u8) void {
|
||||
const res: *httpz.Response = @ptrCast(@alignCast(ctx));
|
||||
res.body = content;
|
||||
}
|
||||
|
||||
fn responseSetHeader(ctx: *anyopaque, name: []const u8, value: []const u8) void {
|
||||
const res: *httpz.Response = @ptrCast(@alignCast(ctx));
|
||||
res.header(name, value);
|
||||
}
|
||||
|
||||
fn responseGetWriter(ctx: *anyopaque) ?std.io.AnyWriter {
|
||||
const res: *httpz.Response = @ptrCast(@alignCast(ctx));
|
||||
return .{ .context = @ptrCast(res), .writeFn = httpzWriteFn };
|
||||
}
|
||||
|
||||
fn httpzWriteFn(context: *const anyopaque, buffer: []const u8) anyerror!usize {
|
||||
const res: *httpz.Response = @ptrCast(@alignCast(@constCast(context)));
|
||||
res.writer().writeAll(buffer) catch |err| return err;
|
||||
return buffer.len;
|
||||
}
|
||||
|
||||
fn responseWriteChunk(ctx: *anyopaque, data: []const u8) anyerror!void {
|
||||
const res: *httpz.Response = @ptrCast(@alignCast(ctx));
|
||||
try res.chunk(data);
|
||||
}
|
||||
|
||||
fn responseClearWriter(ctx: *anyopaque) void {
|
||||
const res: *httpz.Response = @ptrCast(@alignCast(ctx));
|
||||
res.clearWriter();
|
||||
}
|
||||
|
||||
const response_headers_vtable = Response.Headers.HeadersVTable{
|
||||
.get = &responseHeadersGet,
|
||||
.set = &responseHeadersSet,
|
||||
.add = &responseHeadersAdd,
|
||||
};
|
||||
|
||||
fn responseHeadersGet(ctx: *anyopaque, name: []const u8) ?[]const u8 {
|
||||
const res: *httpz.Response = @ptrCast(@alignCast(ctx));
|
||||
return res.headers.get(name);
|
||||
}
|
||||
|
||||
fn responseHeadersSet(ctx: *anyopaque, name: []const u8, value: []const u8) void {
|
||||
const res: *httpz.Response = @ptrCast(@alignCast(ctx));
|
||||
res.header(name, value);
|
||||
}
|
||||
|
||||
fn responseHeadersAdd(ctx: *anyopaque, name: []const u8, value: []const u8) void {
|
||||
const res: *httpz.Response = @ptrCast(@alignCast(ctx));
|
||||
res.headers.add(name, value);
|
||||
}
|
||||
|
||||
// --- Headers Adapter (standalone, for use with both request/response) --- //
|
||||
|
||||
pub fn createRequestHeaders(inner: *httpz.Request) Headers {
|
||||
return (Headers.Builder{
|
||||
.backend_ctx = @ptrCast(inner),
|
||||
.vtable = &headers_request_vtable,
|
||||
.read_only = true,
|
||||
}).build();
|
||||
}
|
||||
|
||||
pub fn createResponseHeaders(inner: *httpz.Response) Headers {
|
||||
return (Headers.Builder{
|
||||
.backend_ctx = @ptrCast(inner),
|
||||
.vtable = &headers_response_vtable,
|
||||
.read_only = false,
|
||||
}).build();
|
||||
}
|
||||
|
||||
const headers_request_vtable = Headers.VTable{
|
||||
.get = &headersRequestGet,
|
||||
.has = &headersRequestHas,
|
||||
.set = &headersNoOpSet,
|
||||
.append = &headersNoOpAppend,
|
||||
.iterate = &headersRequestIterate,
|
||||
};
|
||||
|
||||
const headers_response_vtable = Headers.VTable{
|
||||
.get = &headersResponseGet,
|
||||
.has = &headersResponseHas,
|
||||
.set = &headersResponseSet,
|
||||
.append = &headersResponseAppend,
|
||||
.iterate = &headersResponseIterate,
|
||||
};
|
||||
|
||||
fn headersRequestGet(ctx: *anyopaque, name: []const u8) ?[]const u8 {
|
||||
const req: *httpz.Request = @ptrCast(@alignCast(ctx));
|
||||
return req.headers.get(name);
|
||||
}
|
||||
|
||||
fn headersRequestHas(ctx: *anyopaque, name: []const u8) bool {
|
||||
const req: *httpz.Request = @ptrCast(@alignCast(ctx));
|
||||
return req.headers.has(name);
|
||||
}
|
||||
|
||||
fn headersNoOpSet(_: *anyopaque, _: []const u8, _: []const u8) void {}
|
||||
fn headersNoOpAppend(_: *anyopaque, _: []const u8, _: []const u8) void {}
|
||||
|
||||
fn headersRequestIterate(ctx: *anyopaque) ?Headers.Iterator {
|
||||
const req: *httpz.Request = @ptrCast(@alignCast(ctx));
|
||||
_ = req;
|
||||
// Note: Would need to store iterator state somewhere accessible
|
||||
// For now, return null - iteration not supported via vtable
|
||||
return null;
|
||||
}
|
||||
|
||||
fn headersResponseGet(ctx: *anyopaque, name: []const u8) ?[]const u8 {
|
||||
const res: *httpz.Response = @ptrCast(@alignCast(ctx));
|
||||
return res.headers.get(name);
|
||||
}
|
||||
|
||||
fn headersResponseHas(ctx: *anyopaque, name: []const u8) bool {
|
||||
const res: *httpz.Response = @ptrCast(@alignCast(ctx));
|
||||
return res.headers.has(name);
|
||||
}
|
||||
|
||||
fn headersResponseSet(ctx: *anyopaque, name: []const u8, value: []const u8) void {
|
||||
const res: *httpz.Response = @ptrCast(@alignCast(ctx));
|
||||
res.header(name, value);
|
||||
}
|
||||
|
||||
fn headersResponseAppend(ctx: *anyopaque, name: []const u8, value: []const u8) void {
|
||||
const res: *httpz.Response = @ptrCast(@alignCast(ctx));
|
||||
res.headers.add(name, value);
|
||||
}
|
||||
|
||||
fn headersResponseIterate(ctx: *anyopaque) ?Headers.Iterator {
|
||||
const res: *httpz.Response = @ptrCast(@alignCast(ctx));
|
||||
_ = res;
|
||||
// Note: Would need to store iterator state somewhere accessible
|
||||
// For now, return null - iteration not supported via vtable
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Utility: Get underlying httpz types (for code that still needs them) --- //
|
||||
|
||||
/// Get the underlying httpz.Request from an abstract Request
|
||||
pub fn getHttpzRequest(request: *const Request) ?*httpz.Request {
|
||||
if (request.backend_ctx) |ctx| {
|
||||
return @ptrCast(@alignCast(ctx));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get the underlying httpz.Response from an abstract Response
|
||||
pub fn getHttpzResponse(response: *const Response) ?*httpz.Response {
|
||||
if (response.backend_ctx) |ctx| {
|
||||
return @ptrCast(@alignCast(ctx));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
220
src/app/common.zig
Normal file
220
src/app/common.zig
Normal file
@ -0,0 +1,220 @@
|
||||
//! Common types for the app module.
|
||||
//! These types are backend-independent and can be used with any HTTP server.
|
||||
//!
|
||||
//! This module re-exports types from `std.http` where available for consistency
|
||||
//! with Zig's standard library.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
// --- HTTP Header (from std.http) --- //
|
||||
|
||||
/// HTTP header name/value pair - re-exported from std.http.Header for consistency.
|
||||
///
|
||||
/// Fields:
|
||||
/// - `name`: The header name (e.g., "Content-Type", "Authorization")
|
||||
/// - `value`: The header value
|
||||
///
|
||||
/// **Zig Note:** This uses `name` (as per RFC 7230) rather than `key`.
|
||||
pub const Header = std.http.Header;
|
||||
|
||||
/// Alias for backward compatibility.
|
||||
/// @deprecated Use `Header` instead.
|
||||
pub const Entry = Header;
|
||||
|
||||
/// HTTP header iterator for parsing raw header bytes.
|
||||
/// Re-exported from std.http.HeaderIterator for convenience.
|
||||
///
|
||||
/// Useful for parsing HTTP headers from raw bytes. Initializes with `init(bytes)`
|
||||
/// and iterates via `next()` returning `?Header`.
|
||||
pub const HeaderIterator = std.http.HeaderIterator;
|
||||
|
||||
/// Entry type for multipart form data (includes optional filename).
|
||||
pub const MultiFormEntry = struct {
|
||||
key: []const u8,
|
||||
value: []const u8,
|
||||
filename: ?[]const u8,
|
||||
};
|
||||
|
||||
// --- HTTP Method (from std.http) --- //
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
|
||||
|
||||
/// HTTP request methods - re-exported from std.http.Method for convenience.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
|
||||
///
|
||||
/// Includes useful methods:
|
||||
/// - `requestHasBody()`: Returns true if request of this method can have a body
|
||||
/// - `responseHasBody()`: Returns true if response to this method can have a body
|
||||
/// - `safe()`: Returns true if this method doesn't alter server state
|
||||
/// - `idempotent()`: Returns true if identical requests have the same effect
|
||||
/// - `cacheable()`: Returns true if response can be cached
|
||||
///
|
||||
/// **Note:** Unlike some HTTP libraries, std.http.Method does not have an "OTHER"
|
||||
/// variant for unknown methods. All standard HTTP methods are supported.
|
||||
pub const Method = std.http.Method;
|
||||
|
||||
// --- HTTP Version (from std.http) --- //
|
||||
|
||||
/// HTTP protocol versions - re-exported from std.http.Version for convenience.
|
||||
///
|
||||
/// Values:
|
||||
/// - `@"HTTP/1.0"`: HTTP/1.0 protocol
|
||||
/// - `@"HTTP/1.1"`: HTTP/1.1 protocol
|
||||
pub const Version = std.http.Version;
|
||||
|
||||
/// Alias for backward compatibility.
|
||||
/// @deprecated Use `Version` instead.
|
||||
pub const Protocol = Version;
|
||||
|
||||
// --- Cookie Types --- //
|
||||
|
||||
/// Cookie accessor - parses cookies from the Cookie header.
|
||||
///
|
||||
/// **Zig Note:** This is an extension type not present in the web standard.
|
||||
/// In browsers, cookies are accessed via `document.cookie`.
|
||||
pub const Cookies = struct {
|
||||
header_value: []const u8,
|
||||
|
||||
/// Get a cookie value by name.
|
||||
pub fn get(self: Cookies, name: []const u8) ?[]const u8 {
|
||||
var it = std.mem.splitScalar(u8, self.header_value, ';');
|
||||
while (it.next()) |kv| {
|
||||
const trimmed = std.mem.trimLeft(u8, kv, " ");
|
||||
if (name.len >= trimmed.len) continue;
|
||||
if (!std.mem.startsWith(u8, trimmed, name)) continue;
|
||||
if (trimmed[name.len] != '=') continue;
|
||||
return trimmed[name.len + 1 ..];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/// Options for setting cookies.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
|
||||
pub const CookieOptions = struct {
|
||||
/// Specifies the URL path that must exist in the requested URL.
|
||||
path: []const u8 = "",
|
||||
/// Specifies allowed hosts to receive the cookie.
|
||||
domain: []const u8 = "",
|
||||
/// Indicates the maximum lifetime of the cookie in seconds.
|
||||
max_age: ?i32 = null,
|
||||
/// Indicates that the cookie is sent only over HTTPS.
|
||||
secure: bool = false,
|
||||
/// Forbids JavaScript from accessing the cookie.
|
||||
http_only: bool = false,
|
||||
/// Indicates the cookie should be stored using partitioned storage.
|
||||
partitioned: bool = false,
|
||||
/// Controls whether the cookie is sent with cross-site requests.
|
||||
same_site: ?SameSite = null,
|
||||
|
||||
pub const SameSite = enum {
|
||||
lax,
|
||||
strict,
|
||||
none,
|
||||
};
|
||||
};
|
||||
|
||||
// --- HTTP Status (from std.http) --- //
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
|
||||
|
||||
/// HTTP status codes - re-exported from std.http.Status for convenience.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
|
||||
///
|
||||
/// Includes useful methods:
|
||||
/// - `phrase()`: Returns the status message (e.g., "OK", "Not Found")
|
||||
/// - `class()`: Returns the status class (.informational, .success, .redirect, .client_error, .server_error)
|
||||
pub const HttpStatus = std.http.Status;
|
||||
|
||||
/// Returns the status message (phrase) for an HTTP status code.
|
||||
/// Uses the standard library's Status.phrase() method.
|
||||
pub fn statusCodeToText(code: u16) []const u8 {
|
||||
const status: HttpStatus = @enumFromInt(code);
|
||||
return status.phrase() orelse "Unknown";
|
||||
}
|
||||
|
||||
// --- Content Types (MIME types) --- //
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
|
||||
|
||||
/// Common MIME content types.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||
///
|
||||
/// **Zig Note:** The standard library does not provide a ContentType enum.
|
||||
/// This enum uses the actual MIME type string as the tag name for convenience.
|
||||
pub const ContentType = enum {
|
||||
// Application types
|
||||
@"application/gzip",
|
||||
@"application/javascript",
|
||||
@"application/json",
|
||||
@"application/octet-stream",
|
||||
@"application/pdf",
|
||||
@"application/wasm",
|
||||
@"application/xhtml+xml",
|
||||
@"application/xml",
|
||||
@"application/x-www-form-urlencoded",
|
||||
|
||||
// Audio types
|
||||
@"audio/aac",
|
||||
@"audio/mpeg",
|
||||
@"audio/ogg",
|
||||
@"audio/wav",
|
||||
@"audio/webm",
|
||||
|
||||
// Font types
|
||||
@"font/otf",
|
||||
@"font/ttf",
|
||||
@"font/woff",
|
||||
@"font/woff2",
|
||||
|
||||
// Image types
|
||||
@"image/avif",
|
||||
@"image/bmp",
|
||||
@"image/gif",
|
||||
@"image/jpeg",
|
||||
@"image/png",
|
||||
@"image/svg+xml",
|
||||
@"image/tiff",
|
||||
@"image/webp",
|
||||
|
||||
// Multipart types
|
||||
@"multipart/form-data",
|
||||
|
||||
// Text types
|
||||
@"text/css",
|
||||
@"text/csv",
|
||||
@"text/html",
|
||||
@"text/javascript",
|
||||
@"text/plain",
|
||||
@"text/xml",
|
||||
|
||||
// Video types
|
||||
@"video/mp4",
|
||||
@"video/mpeg",
|
||||
@"video/ogg",
|
||||
@"video/webm",
|
||||
@"video/x-msvideo",
|
||||
|
||||
/// Returns the MIME type string.
|
||||
pub fn toString(self: ContentType) []const u8 {
|
||||
return @tagName(self);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Re-exports from std.http for convenience --- //
|
||||
|
||||
/// HTTP content encoding - re-exported from std.http.ContentEncoding.
|
||||
///
|
||||
/// Values: zstd, gzip, deflate, compress, identity
|
||||
pub const ContentEncoding = std.http.ContentEncoding;
|
||||
|
||||
/// HTTP transfer encoding - re-exported from std.http.TransferEncoding.
|
||||
///
|
||||
/// Values: chunked, none
|
||||
pub const TransferEncoding = std.http.TransferEncoding;
|
||||
|
||||
/// HTTP connection type - re-exported from std.http.Connection.
|
||||
///
|
||||
/// Values: keep_alive, close
|
||||
pub const Connection = std.http.Connection;
|
||||
@ -281,7 +281,9 @@ pub const Handler = struct {
|
||||
res.status = 404;
|
||||
res.content_type = .HTML;
|
||||
|
||||
const notfoundctx = zx.NotFoundContext.init(req, res, self.allocator);
|
||||
const abstract_req = httpz_adapter.createRequest(req);
|
||||
const abstract_res = httpz_adapter.createResponse(res, req.arena);
|
||||
const notfoundctx = zx.NotFoundContext.init(abstract_req, abstract_res, self.allocator);
|
||||
|
||||
// First try to get notfound from route_data if available
|
||||
var notfound_fn: ?*const fn (zx.NotFoundContext) Component = null;
|
||||
@ -317,7 +319,9 @@ pub const Handler = struct {
|
||||
res.status = 500;
|
||||
res.content_type = .HTML;
|
||||
|
||||
const errorctx = zx.ErrorContext.init(req, res, self.allocator, err);
|
||||
const abstract_req = httpz_adapter.createRequest(req);
|
||||
const abstract_res = httpz_adapter.createResponse(res, req.arena);
|
||||
const errorctx = zx.ErrorContext.init(abstract_req, abstract_res, self.allocator, err);
|
||||
|
||||
// Find the closest route with error handler
|
||||
var error_fn: ?*const fn (zx.ErrorContext) Component = null;
|
||||
@ -352,7 +356,9 @@ pub const Handler = struct {
|
||||
opts: RenderErrorPageOptions,
|
||||
) void {
|
||||
const path = req.url.path;
|
||||
const layoutctx = zx.LayoutContext.init(req, res, self.allocator);
|
||||
const abstract_req = httpz_adapter.createRequest(req);
|
||||
const abstract_res = httpz_adapter.createResponse(res, req.arena);
|
||||
const layoutctx = zx.LayoutContext.init(abstract_req, abstract_res, self.allocator);
|
||||
const is_dev_mode = self.meta.cli_command == .dev;
|
||||
|
||||
var component = page_component;
|
||||
@ -564,8 +570,10 @@ pub const Handler = struct {
|
||||
}
|
||||
}
|
||||
|
||||
const pagectx = zx.PageContext.init(req, res, allocator);
|
||||
const layoutctx = zx.LayoutContext.init(req, res, allocator);
|
||||
const abstract_req = httpz_adapter.createRequest(req);
|
||||
const abstract_res = httpz_adapter.createResponse(res, req.arena);
|
||||
const pagectx = zx.PageContext.init(abstract_req, abstract_res, allocator);
|
||||
const layoutctx = zx.LayoutContext.init(abstract_req, abstract_res, allocator);
|
||||
|
||||
// log.debug("cli command: {s}", .{@tagName(meta.cli_command orelse .serve)});
|
||||
|
||||
@ -690,7 +698,7 @@ pub const Handler = struct {
|
||||
try self.renderStreaming(res, &page_component, pagectx.arena);
|
||||
} else {
|
||||
// Normal mode: render everything at once
|
||||
const writer = &layoutctx.response.buffer.writer;
|
||||
const writer = &res.buffer.writer;
|
||||
_ = writer.write("<!DOCTYPE html>\n") catch |err| {
|
||||
std.debug.print("Error writing HTML: {}\n", .{err});
|
||||
break :blk;
|
||||
@ -908,8 +916,11 @@ const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const cachez = @import("cachez");
|
||||
const zx = @import("../root.zig");
|
||||
const httpz_adapter = @import("adapter.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Component = zx.Component;
|
||||
const Printer = zx.Printer;
|
||||
const App = zx.App;
|
||||
const Request = @import("Request.zig");
|
||||
const Response = @import("Response.zig");
|
||||
|
||||
@ -1788,3 +1788,6 @@ const ClientComponentOptions = struct {
|
||||
path: []const u8,
|
||||
id: []const u8,
|
||||
};
|
||||
pub const Headers = @import("app/Headers.zig");
|
||||
pub const Request = @import("app/Request.zig");
|
||||
pub const Response = @import("app/Response.zig");
|
||||
|
||||
@ -1,33 +1,42 @@
|
||||
const httpz = @import("httpz");
|
||||
//! 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("app/Request.zig");
|
||||
const Response = @import("app/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.
|
||||
const BaseContext = struct {
|
||||
/// The HTTP request object containing all request data (headers, body, query params, form data, etc.)
|
||||
request: *httpz.Request,
|
||||
/// The HTTP response object used to write the response body and set headers
|
||||
response: *httpz.Response,
|
||||
pub const BaseContext = struct {
|
||||
/// 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: ?*BaseContext = null,
|
||||
|
||||
/// Initialize a new BaseContext with the given request, response, and allocator.
|
||||
/// The arena allocator is automatically set from the request's arena.
|
||||
pub fn init(request: *httpz.Request, response: *httpz.Response, allocator: std.mem.Allocator) BaseContext {
|
||||
pub fn init(request: Request, response: Response, alloc: std.mem.Allocator) BaseContext {
|
||||
return .{
|
||||
.request = request,
|
||||
.response = response,
|
||||
.allocator = allocator,
|
||||
.allocator = alloc,
|
||||
.arena = request.arena,
|
||||
};
|
||||
}
|
||||
@ -46,8 +55,9 @@ const BaseContext = struct {
|
||||
/// ```zig
|
||||
/// pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
/// const allocator = ctx.arena; // Use arena for temporary allocations
|
||||
/// // Access request data
|
||||
/// const query = ctx.request.query() catch unreachable;
|
||||
/// // Access request data via MDN-compliant API
|
||||
/// const method = ctx.request.method;
|
||||
/// const url = ctx.request.url;
|
||||
/// // Render component
|
||||
/// return <div>Hello</div>;
|
||||
/// }
|
||||
@ -70,18 +80,24 @@ pub const PageContext = BaseContext;
|
||||
/// ```
|
||||
pub const LayoutContext = BaseContext;
|
||||
pub const NotFoundContext = BaseContext;
|
||||
|
||||
pub const ErrorContext = struct {
|
||||
request: *httpz.Request,
|
||||
response: *httpz.Response,
|
||||
/// The HTTP request object (backend-agnostic)
|
||||
request: Request,
|
||||
/// The HTTP response object (backend-agnostic)
|
||||
response: Response,
|
||||
/// Global allocator
|
||||
allocator: std.mem.Allocator,
|
||||
/// Arena allocator for request-scoped allocations
|
||||
arena: std.mem.Allocator,
|
||||
/// The error that occurred
|
||||
err: anyerror,
|
||||
|
||||
pub fn init(request: *httpz.Request, response: *httpz.Response, allocator: std.mem.Allocator, err: anyerror) ErrorContext {
|
||||
pub fn init(request: Request, response: Response, alloc: std.mem.Allocator, err: anyerror) ErrorContext {
|
||||
return .{
|
||||
.request = request,
|
||||
.response = response,
|
||||
.allocator = allocator,
|
||||
.allocator = alloc,
|
||||
.arena = request.arena,
|
||||
.err = err,
|
||||
};
|
||||
|
||||
125
test/app/common.zig
Normal file
125
test/app/common.zig
Normal file
@ -0,0 +1,125 @@
|
||||
const std = @import("std");
|
||||
// Access common types through Request/Response which re-export them
|
||||
const Request = @import("zx").Request;
|
||||
const Response = @import("zx").Response;
|
||||
|
||||
// --- Cookies (accessed via Request) --- //
|
||||
|
||||
test "Cookies: get returns value" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const req = (Request.Builder{
|
||||
.arena = fba.allocator(),
|
||||
.cookie_header = "session=abc123; user=john",
|
||||
}).build();
|
||||
|
||||
try std.testing.expectEqualStrings("abc123", req.cookies.get("session").?);
|
||||
try std.testing.expectEqualStrings("john", req.cookies.get("user").?);
|
||||
}
|
||||
|
||||
test "Cookies: get returns null for missing cookie" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const req = (Request.Builder{
|
||||
.arena = fba.allocator(),
|
||||
.cookie_header = "session=abc123",
|
||||
}).build();
|
||||
|
||||
try std.testing.expectEqual(null, req.cookies.get("missing"));
|
||||
}
|
||||
|
||||
test "Cookies: handles empty header" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const req = (Request.Builder{
|
||||
.arena = fba.allocator(),
|
||||
.cookie_header = "",
|
||||
}).build();
|
||||
|
||||
try std.testing.expectEqual(null, req.cookies.get("session"));
|
||||
}
|
||||
|
||||
test "Cookies: handles spaces after semicolon" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const req = (Request.Builder{
|
||||
.arena = fba.allocator(),
|
||||
.cookie_header = "a=1; b=2; c=3",
|
||||
}).build();
|
||||
|
||||
try std.testing.expectEqualStrings("1", req.cookies.get("a").?);
|
||||
try std.testing.expectEqualStrings("2", req.cookies.get("b").?);
|
||||
try std.testing.expectEqualStrings("3", req.cookies.get("c").?);
|
||||
}
|
||||
|
||||
// --- CookieOptions (accessed via Response) --- //
|
||||
|
||||
test "CookieOptions: defaults" {
|
||||
const opts = Response.CookieOptions{};
|
||||
try std.testing.expectEqualStrings("", opts.path);
|
||||
try std.testing.expectEqualStrings("", opts.domain);
|
||||
try std.testing.expectEqual(null, opts.max_age);
|
||||
try std.testing.expect(!opts.secure);
|
||||
try std.testing.expect(!opts.http_only);
|
||||
try std.testing.expect(!opts.partitioned);
|
||||
try std.testing.expectEqual(null, opts.same_site);
|
||||
}
|
||||
|
||||
test "CookieOptions: with all values" {
|
||||
const opts = Response.CookieOptions{
|
||||
.path = "/api",
|
||||
.domain = "example.com",
|
||||
.max_age = 3600,
|
||||
.secure = true,
|
||||
.http_only = true,
|
||||
.partitioned = true,
|
||||
.same_site = .strict,
|
||||
};
|
||||
try std.testing.expectEqualStrings("/api", opts.path);
|
||||
try std.testing.expectEqualStrings("example.com", opts.domain);
|
||||
try std.testing.expectEqual(@as(?i32, 3600), opts.max_age);
|
||||
try std.testing.expect(opts.secure);
|
||||
try std.testing.expect(opts.http_only);
|
||||
try std.testing.expect(opts.partitioned);
|
||||
try std.testing.expectEqual(Response.CookieOptions.SameSite.strict, opts.same_site.?);
|
||||
}
|
||||
|
||||
test "CookieOptions.SameSite: all values" {
|
||||
const values = [_]Response.CookieOptions.SameSite{ .lax, .strict, .none };
|
||||
try std.testing.expectEqual(3, values.len);
|
||||
}
|
||||
|
||||
// --- ContentType (accessed via Response) --- //
|
||||
|
||||
test "ContentType: toString returns MIME type" {
|
||||
try std.testing.expectEqualStrings("text/html", Response.ContentType.@"text/html".toString());
|
||||
try std.testing.expectEqualStrings("application/json", Response.ContentType.@"application/json".toString());
|
||||
try std.testing.expectEqualStrings("application/wasm", Response.ContentType.@"application/wasm".toString());
|
||||
try std.testing.expectEqualStrings("image/png", Response.ContentType.@"image/png".toString());
|
||||
}
|
||||
|
||||
// --- MultiFormEntry (accessed via Request) --- //
|
||||
|
||||
test "MultiFormEntry: fields" {
|
||||
const entry = Request.MultiFormEntry{
|
||||
.key = "file",
|
||||
.value = "binary data",
|
||||
.filename = "photo.jpg",
|
||||
};
|
||||
try std.testing.expectEqualStrings("file", entry.key);
|
||||
try std.testing.expectEqualStrings("binary data", entry.value);
|
||||
try std.testing.expectEqualStrings("photo.jpg", entry.filename.?);
|
||||
}
|
||||
|
||||
test "MultiFormEntry: filename can be null" {
|
||||
const entry = Request.MultiFormEntry{
|
||||
.key = "name",
|
||||
.value = "John",
|
||||
.filename = null,
|
||||
};
|
||||
try std.testing.expectEqual(null, entry.filename);
|
||||
}
|
||||
87
test/app/headers.zig
Normal file
87
test/app/headers.zig
Normal file
@ -0,0 +1,87 @@
|
||||
const std = @import("std");
|
||||
const Headers = @import("zx").Headers;
|
||||
|
||||
// --- Type Re-exports --- //
|
||||
|
||||
test "Headers.Header: is std.http.Header" {
|
||||
try std.testing.expect(Headers.Header == std.http.Header);
|
||||
}
|
||||
|
||||
test "Headers.HeaderIterator: is std.http.HeaderIterator" {
|
||||
try std.testing.expect(Headers.HeaderIterator == std.http.HeaderIterator);
|
||||
}
|
||||
|
||||
// --- HeaderIterator Usage --- //
|
||||
|
||||
test "Headers.HeaderIterator: parses raw HTTP headers" {
|
||||
const raw = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nX-Custom: value\r\n\r\n";
|
||||
var iter = Headers.HeaderIterator.init(raw);
|
||||
|
||||
const h1 = iter.next().?;
|
||||
try std.testing.expectEqualStrings("Content-Type", h1.name);
|
||||
try std.testing.expectEqualStrings("text/html", h1.value);
|
||||
|
||||
const h2 = iter.next().?;
|
||||
try std.testing.expectEqualStrings("X-Custom", h2.name);
|
||||
try std.testing.expectEqualStrings("value", h2.value);
|
||||
|
||||
try std.testing.expect(iter.next() == null);
|
||||
}
|
||||
|
||||
// --- Headers Instance (without backend) --- //
|
||||
|
||||
test "Headers: default is read-only" {
|
||||
const headers = Headers{};
|
||||
try std.testing.expect(headers.isReadOnly());
|
||||
}
|
||||
|
||||
test "Headers: can be set to writable" {
|
||||
const headers = Headers{ .read_only = false };
|
||||
try std.testing.expect(!headers.isReadOnly());
|
||||
}
|
||||
|
||||
test "Headers: get returns null without backend" {
|
||||
const headers = Headers{};
|
||||
try std.testing.expect(headers.get("Content-Type") == null);
|
||||
}
|
||||
|
||||
test "Headers: has returns false without backend" {
|
||||
const headers = Headers{};
|
||||
try std.testing.expect(!headers.has("Content-Type"));
|
||||
}
|
||||
|
||||
test "Headers: entries returns null without backend" {
|
||||
const headers = Headers{};
|
||||
try std.testing.expect(headers.entries() == null);
|
||||
}
|
||||
|
||||
test "Headers: write methods are no-op without backend" {
|
||||
var headers = Headers{ .read_only = false };
|
||||
// These should not crash
|
||||
headers.append("X-Test", "value");
|
||||
headers.set("X-Test", "value");
|
||||
headers.delete("X-Test");
|
||||
}
|
||||
|
||||
test "Headers: write methods are no-op when read-only" {
|
||||
var headers = Headers{}; // read_only = true by default
|
||||
// These should not crash and should be no-ops
|
||||
headers.append("X-Test", "value");
|
||||
headers.set("X-Test", "value");
|
||||
}
|
||||
|
||||
// --- Builder --- //
|
||||
|
||||
test "Headers.Builder: builds with defaults" {
|
||||
const headers = (Headers.Builder{}).build();
|
||||
try std.testing.expect(headers.read_only);
|
||||
try std.testing.expect(headers.backend_ctx == null);
|
||||
try std.testing.expect(headers.vtable == null);
|
||||
}
|
||||
|
||||
test "Headers.Builder: builds with custom values" {
|
||||
const headers = (Headers.Builder{
|
||||
.read_only = false,
|
||||
}).build();
|
||||
try std.testing.expect(!headers.read_only);
|
||||
}
|
||||
126
test/app/request.zig
Normal file
126
test/app/request.zig
Normal file
@ -0,0 +1,126 @@
|
||||
const std = @import("std");
|
||||
const Request = @import("zx").Request;
|
||||
|
||||
// --- Type Re-exports --- //
|
||||
|
||||
test "Request.Method: is std.http.Method" {
|
||||
try std.testing.expect(Request.Method == std.http.Method);
|
||||
}
|
||||
|
||||
test "Request.Version: is std.http.Version" {
|
||||
try std.testing.expect(Request.Version == std.http.Version);
|
||||
}
|
||||
|
||||
test "Request.Protocol: is deprecated alias for Version" {
|
||||
try std.testing.expect(Request.Protocol == Request.Version);
|
||||
}
|
||||
|
||||
test "Request.Header: is std.http.Header" {
|
||||
try std.testing.expect(Request.Header == std.http.Header);
|
||||
}
|
||||
|
||||
// --- Request Instance (without backend) --- //
|
||||
|
||||
test "Request: Builder default values" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const req = (Request.Builder{ .arena = fba.allocator() }).build();
|
||||
|
||||
try std.testing.expectEqualStrings("", req.url);
|
||||
try std.testing.expectEqualStrings("/", req.pathname);
|
||||
try std.testing.expectEqualStrings("", req.search);
|
||||
try std.testing.expectEqualStrings("", req.referrer);
|
||||
try std.testing.expectEqual(Request.Method.GET, req.method);
|
||||
try std.testing.expectEqual(Request.Version.@"HTTP/1.1", req.protocol);
|
||||
}
|
||||
|
||||
test "Request: text returns null without backend" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const req = (Request.Builder{ .arena = fba.allocator() }).build();
|
||||
try std.testing.expect(req.text() == null);
|
||||
}
|
||||
|
||||
test "Request: getParam returns null without backend" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const req = (Request.Builder{ .arena = fba.allocator() }).build();
|
||||
try std.testing.expect(req.getParam("id") == null);
|
||||
}
|
||||
|
||||
test "Request: cookies field returns Cookies accessor" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const req = (Request.Builder{
|
||||
.arena = fba.allocator(),
|
||||
.cookie_header = "session=abc123",
|
||||
}).build();
|
||||
|
||||
try std.testing.expectEqualStrings("abc123", req.cookies.get("session").?);
|
||||
}
|
||||
|
||||
// --- Headers --- //
|
||||
|
||||
test "Request.headers: get returns null without backend" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const req = (Request.Builder{ .arena = fba.allocator() }).build();
|
||||
try std.testing.expect(req.headers.get("Content-Type") == null);
|
||||
}
|
||||
|
||||
test "Request.headers: has returns false without backend" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const req = (Request.Builder{ .arena = fba.allocator() }).build();
|
||||
try std.testing.expect(!req.headers.has("Content-Type"));
|
||||
}
|
||||
|
||||
// --- URLSearchParams --- //
|
||||
|
||||
test "Request.searchParams: get returns null without backend" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const req = (Request.Builder{ .arena = fba.allocator() }).build();
|
||||
try std.testing.expect(req.searchParams.get("q") == null);
|
||||
}
|
||||
|
||||
test "Request.searchParams: has returns false without backend" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const req = (Request.Builder{ .arena = fba.allocator() }).build();
|
||||
try std.testing.expect(!req.searchParams.has("q"));
|
||||
}
|
||||
|
||||
// --- Builder --- //
|
||||
|
||||
test "Request.Builder: builds with custom values" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const req = (Request.Builder{
|
||||
.url = "/api/users",
|
||||
.method = .POST,
|
||||
.pathname = "/api/users",
|
||||
.search = "?page=1",
|
||||
.referrer = "https://example.com",
|
||||
.protocol = .@"HTTP/1.0",
|
||||
.cookie_header = "session=xyz",
|
||||
.arena = fba.allocator(),
|
||||
}).build();
|
||||
|
||||
try std.testing.expectEqualStrings("/api/users", req.url);
|
||||
try std.testing.expectEqual(Request.Method.POST, req.method);
|
||||
try std.testing.expectEqualStrings("/api/users", req.pathname);
|
||||
try std.testing.expectEqualStrings("?page=1", req.search);
|
||||
try std.testing.expectEqualStrings("https://example.com", req.referrer);
|
||||
try std.testing.expectEqual(Request.Version.@"HTTP/1.0", req.protocol);
|
||||
try std.testing.expectEqualStrings("xyz", req.cookies.get("session").?);
|
||||
}
|
||||
177
test/app/response.zig
Normal file
177
test/app/response.zig
Normal file
@ -0,0 +1,177 @@
|
||||
const std = @import("std");
|
||||
const Response = @import("zx").Response;
|
||||
|
||||
// --- Type Re-exports --- //
|
||||
|
||||
test "Response.HttpStatus: is std.http.Status" {
|
||||
try std.testing.expect(Response.HttpStatus == std.http.Status);
|
||||
}
|
||||
|
||||
test "Response.ContentType: is common.ContentType" {
|
||||
// Verify it exists and has expected variants
|
||||
try std.testing.expectEqualStrings("text/html", Response.ContentType.@"text/html".toString());
|
||||
}
|
||||
|
||||
test "Response.CookieOptions: is common.CookieOptions" {
|
||||
const opts = Response.CookieOptions{};
|
||||
try std.testing.expectEqualStrings("", opts.path);
|
||||
}
|
||||
|
||||
test "Response.Headers: exists and has expected methods" {
|
||||
const headers = Response.Headers{};
|
||||
// Response.Headers has get, set, add methods
|
||||
try std.testing.expect(headers.get("X-Test") == null);
|
||||
}
|
||||
|
||||
// --- Response Instance (Web Standard Properties) --- //
|
||||
|
||||
test "Response: default field values" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const res = Response{ .arena = fba.allocator() };
|
||||
|
||||
try std.testing.expectEqualStrings("", res.body);
|
||||
try std.testing.expect(!res.bodyUsed);
|
||||
try std.testing.expect(res.ok);
|
||||
try std.testing.expect(!res.redirected);
|
||||
try std.testing.expectEqual(@as(u16, 200), res.status);
|
||||
try std.testing.expectEqualStrings("OK", res.statusText);
|
||||
try std.testing.expectEqual(Response.ResponseType.default, res.type);
|
||||
try std.testing.expectEqualStrings("", res.url);
|
||||
}
|
||||
|
||||
test "Response: ok is true for 2xx status" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const res200 = (Response.Builder{ .status = 200, .arena = fba.allocator() }).build();
|
||||
const res201 = (Response.Builder{ .status = 201, .arena = fba.allocator() }).build();
|
||||
const res299 = (Response.Builder{ .status = 299, .arena = fba.allocator() }).build();
|
||||
|
||||
try std.testing.expect(res200.ok);
|
||||
try std.testing.expect(res201.ok);
|
||||
try std.testing.expect(res299.ok);
|
||||
}
|
||||
|
||||
test "Response: ok is false for non-2xx status" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const res199 = (Response.Builder{ .status = 199, .arena = fba.allocator() }).build();
|
||||
const res300 = (Response.Builder{ .status = 300, .arena = fba.allocator() }).build();
|
||||
const res404 = (Response.Builder{ .status = 404, .arena = fba.allocator() }).build();
|
||||
const res500 = (Response.Builder{ .status = 500, .arena = fba.allocator() }).build();
|
||||
|
||||
try std.testing.expect(!res199.ok);
|
||||
try std.testing.expect(!res300.ok);
|
||||
try std.testing.expect(!res404.ok);
|
||||
try std.testing.expect(!res500.ok);
|
||||
}
|
||||
|
||||
test "Response: statusText is set from status code" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const res200 = (Response.Builder{ .status = 200, .arena = fba.allocator() }).build();
|
||||
const res404 = (Response.Builder{ .status = 404, .arena = fba.allocator() }).build();
|
||||
const res500 = (Response.Builder{ .status = 500, .arena = fba.allocator() }).build();
|
||||
|
||||
try std.testing.expectEqualStrings("OK", res200.statusText);
|
||||
try std.testing.expectEqualStrings("Not Found", res404.statusText);
|
||||
try std.testing.expectEqualStrings("Internal Server Error", res500.statusText);
|
||||
}
|
||||
|
||||
// --- ResponseType --- //
|
||||
|
||||
test "Response.ResponseType: all values" {
|
||||
const types = [_]Response.ResponseType{
|
||||
.basic, .cors, .default, .@"error", .@"opaque", .opaqueredirect,
|
||||
};
|
||||
try std.testing.expectEqual(6, types.len);
|
||||
}
|
||||
|
||||
// --- Builder --- //
|
||||
|
||||
test "Response.Builder: builds with custom values" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const res = (Response.Builder{
|
||||
.status = 201,
|
||||
.url = "https://example.com/api",
|
||||
.response_type = .cors,
|
||||
.arena = fba.allocator(),
|
||||
}).build();
|
||||
|
||||
try std.testing.expectEqual(@as(u16, 201), res.status);
|
||||
try std.testing.expect(res.ok);
|
||||
try std.testing.expectEqualStrings("Created", res.statusText);
|
||||
try std.testing.expectEqualStrings("https://example.com/api", res.url);
|
||||
try std.testing.expectEqual(Response.ResponseType.cors, res.type);
|
||||
}
|
||||
|
||||
// --- Methods (without backend) --- //
|
||||
|
||||
test "Response.setStatus: no-op without backend" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const res = (Response.Builder{ .arena = fba.allocator() }).build();
|
||||
|
||||
// Should not crash, but local fields remain unchanged
|
||||
res.setStatus(.not_found);
|
||||
try std.testing.expectEqual(@as(u16, 200), res.status);
|
||||
}
|
||||
|
||||
test "Response.setStatusCode: no-op without backend" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const res = (Response.Builder{ .arena = fba.allocator() }).build();
|
||||
|
||||
res.setStatusCode(503);
|
||||
try std.testing.expectEqual(@as(u16, 200), res.status);
|
||||
}
|
||||
|
||||
test "Response.setBody: no-op without backend" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const res = (Response.Builder{ .arena = fba.allocator() }).build();
|
||||
|
||||
res.setBody("Hello");
|
||||
try std.testing.expectEqualStrings("", res.body);
|
||||
}
|
||||
|
||||
test "Response.redirect: no-op without backend" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const res = (Response.Builder{ .arena = fba.allocator() }).build();
|
||||
|
||||
res.redirect("/new", null);
|
||||
res.redirect("/permanent", 301);
|
||||
try std.testing.expectEqual(@as(u16, 200), res.status);
|
||||
try std.testing.expect(!res.redirected);
|
||||
}
|
||||
|
||||
// --- Headers --- //
|
||||
|
||||
test "Response.headers: get returns null without backend" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const res = Response{ .arena = fba.allocator() };
|
||||
try std.testing.expect(res.headers.get("Content-Type") == null);
|
||||
}
|
||||
|
||||
test "Response.headers: set and add are no-op without backend" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
|
||||
const res = Response{ .arena = fba.allocator() };
|
||||
// These should not crash (no-op without backend)
|
||||
res.headers.set("X-Test", "value");
|
||||
res.headers.add("X-Test", "value2");
|
||||
}
|
||||
136
test/app/routing.zig
Normal file
136
test/app/routing.zig
Normal file
@ -0,0 +1,136 @@
|
||||
const std = @import("std");
|
||||
const zx = @import("zx");
|
||||
const Request = zx.Request;
|
||||
const Response = zx.Response;
|
||||
const PageContext = zx.PageContext;
|
||||
const LayoutContext = zx.LayoutContext;
|
||||
const NotFoundContext = zx.NotFoundContext;
|
||||
const ErrorContext = zx.ErrorContext;
|
||||
|
||||
// --- Context Type Aliases --- //
|
||||
|
||||
test "PageContext: is same as LayoutContext" {
|
||||
try std.testing.expect(PageContext == LayoutContext);
|
||||
}
|
||||
|
||||
test "PageContext: is same as NotFoundContext" {
|
||||
try std.testing.expect(PageContext == NotFoundContext);
|
||||
}
|
||||
|
||||
// --- PageContext --- //
|
||||
|
||||
test "PageContext: has request field" {
|
||||
var buffer: [4096]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
const alloc = fba.allocator();
|
||||
|
||||
const req = (Request.Builder{
|
||||
.url = "/test",
|
||||
.method = .POST,
|
||||
.arena = alloc,
|
||||
}).build();
|
||||
|
||||
const res = (Response.Builder{
|
||||
.status = 200,
|
||||
.arena = alloc,
|
||||
}).build();
|
||||
|
||||
const ctx = PageContext.init(req, res, alloc);
|
||||
|
||||
try std.testing.expectEqualStrings("/test", ctx.request.url);
|
||||
try std.testing.expectEqual(Request.Method.POST, ctx.request.method);
|
||||
}
|
||||
|
||||
test "PageContext: has response field" {
|
||||
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{
|
||||
.status = 201,
|
||||
.arena = alloc,
|
||||
}).build();
|
||||
|
||||
const ctx = PageContext.init(req, res, alloc);
|
||||
|
||||
try std.testing.expectEqual(@as(u16, 201), ctx.response.status);
|
||||
try std.testing.expect(ctx.response.ok);
|
||||
try std.testing.expectEqualStrings("Created", ctx.response.statusText);
|
||||
}
|
||||
|
||||
test "PageContext: has allocator and arena fields" {
|
||||
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);
|
||||
|
||||
// Verify fields are accessible
|
||||
_ = ctx.allocator;
|
||||
_ = 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" {
|
||||
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 err = error.OutOfMemory;
|
||||
|
||||
const ctx = ErrorContext.init(req, res, alloc, err);
|
||||
|
||||
try std.testing.expectEqual(error.OutOfMemory, ctx.err);
|
||||
}
|
||||
|
||||
test "ErrorContext: has request and response fields" {
|
||||
var buffer: [4096]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
const alloc = fba.allocator();
|
||||
|
||||
const req = (Request.Builder{
|
||||
.url = "/error-page",
|
||||
.arena = alloc,
|
||||
}).build();
|
||||
|
||||
const res = (Response.Builder{
|
||||
.status = 500,
|
||||
.arena = alloc,
|
||||
}).build();
|
||||
|
||||
const ctx = ErrorContext.init(req, res, alloc, error.Unexpected);
|
||||
|
||||
try std.testing.expectEqualStrings("/error-page", ctx.request.url);
|
||||
try std.testing.expectEqual(@as(u16, 500), ctx.response.status);
|
||||
}
|
||||
|
||||
test "ErrorContext: has allocator and arena fields" {
|
||||
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 = ErrorContext.init(req, res, alloc, error.Unexpected);
|
||||
|
||||
_ = ctx.allocator;
|
||||
_ = ctx.arena;
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
test "init" {
|
||||
if (!test_util.shouldRunSlowTest()) return error.SkipZigTest;
|
||||
try test_cmd(.{
|
||||
.args = &.{"init"},
|
||||
.expected_exit_code = 0,
|
||||
@ -22,6 +23,7 @@ test "init" {
|
||||
}
|
||||
|
||||
test "init → init" {
|
||||
if (!test_util.shouldRunSlowTest()) return error.SkipZigTest;
|
||||
try test_cmd(.{
|
||||
.args = &.{"init"},
|
||||
.expected_exit_code = 0,
|
||||
@ -32,6 +34,7 @@ test "init → init" {
|
||||
}
|
||||
|
||||
test "init --force" {
|
||||
if (!test_util.shouldRunSlowTest()) return error.SkipZigTest;
|
||||
try test_cmd(.{
|
||||
.args = &.{ "init", "--force" },
|
||||
.expected_exit_code = 0,
|
||||
@ -52,6 +55,7 @@ test "init --force" {
|
||||
}
|
||||
|
||||
test "init -t react" {
|
||||
if (!test_util.shouldRunSlowTest()) return error.SkipZigTest;
|
||||
try test_cmd(.{
|
||||
.args = &.{ "init", "react", "--template", "react" },
|
||||
.expected_exit_code = 0,
|
||||
@ -137,7 +141,7 @@ test "init -t react" {
|
||||
// }
|
||||
|
||||
test "init → build" {
|
||||
if (!sholdRunSlowTest()) return error.SkipZigTest;
|
||||
if (!test_util.shouldRunSlowTest()) return error.SkipZigTest;
|
||||
|
||||
const test_dir_abs = try getTestDirPath();
|
||||
defer allocator.free(test_dir_abs);
|
||||
@ -155,7 +159,7 @@ test "init → build" {
|
||||
}
|
||||
|
||||
test "export" {
|
||||
if (builtin.os.tag == .windows or !sholdRunSlowTest()) return error.SkipZigTest; // Export doesn't work on Windows yet
|
||||
if (builtin.os.tag == .windows or !test_util.shouldRunSlowTest()) return error.SkipZigTest; // Export doesn't work on Windows yet
|
||||
try test_cmd(.{
|
||||
.args = &.{"export"},
|
||||
.expected_exit_code = 0,
|
||||
@ -177,7 +181,7 @@ test "export" {
|
||||
}
|
||||
|
||||
test "bundle" {
|
||||
if (!sholdRunSlowTest()) return error.SkipZigTest;
|
||||
if (!test_util.shouldRunSlowTest()) return error.SkipZigTest;
|
||||
try test_cmd(.{
|
||||
.args = &.{"bundle"},
|
||||
.expected_exit_code = 0,
|
||||
@ -197,7 +201,7 @@ test "bundle" {
|
||||
}
|
||||
|
||||
test "bundle --docker" {
|
||||
if (!sholdRunSlowTest()) return error.SkipZigTest;
|
||||
if (!test_util.shouldRunSlowTest()) return error.SkipZigTest;
|
||||
try test_cmd(.{
|
||||
.args = &.{ "bundle", "--docker" },
|
||||
.expected_exit_code = 0,
|
||||
@ -217,7 +221,7 @@ test "bundle --docker" {
|
||||
}
|
||||
|
||||
test "bundle --docker-compose" {
|
||||
if (!sholdRunSlowTest()) return error.SkipZigTest;
|
||||
if (!test_util.shouldRunSlowTest()) return error.SkipZigTest;
|
||||
try test_cmd(.{
|
||||
.args = &.{ "bundle", "--docker-compose" },
|
||||
.expected_exit_code = 0,
|
||||
@ -344,16 +348,6 @@ test "tests:afterAll" {
|
||||
// std.fs.cwd().deleteTree("test/tmp") catch {};
|
||||
}
|
||||
|
||||
fn sholdRunSlowTest() bool {
|
||||
// E2E environment variable is set
|
||||
const slow_tests = std.process.getEnvVarOwned(allocator, "E2E") catch {
|
||||
return false;
|
||||
};
|
||||
|
||||
defer allocator.free(slow_tests);
|
||||
return true;
|
||||
}
|
||||
|
||||
fn getZxPath() ![]const u8 {
|
||||
const zx_bin_rel = if (builtin.os.tag == .windows) "zig-out/bin/zx.exe" else "zig-out/bin/zx";
|
||||
const zx_bin_abs = try std.fs.cwd().realpathAlloc(allocator, zx_bin_rel);
|
||||
@ -397,6 +391,7 @@ fn killPort(port: []const u8) !void {
|
||||
}
|
||||
|
||||
const allocator = std.testing.allocator;
|
||||
const test_util = @import("./../test_util.zig");
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
@ -279,7 +279,7 @@ test "zx_comments" {
|
||||
}
|
||||
|
||||
test "performance > fmt" {
|
||||
// if (true) return error.Todo;
|
||||
if (!shouldRunSlowTest()) return;
|
||||
const MAX_TIME_MS = 50.0 * 8; // 50ms is on M1 Pro
|
||||
const MAX_TIME_PER_FILE_MS = 8.0 * 10; // 5ms is on M1 Pro
|
||||
|
||||
@ -349,8 +349,9 @@ fn expectLessThan(expected: f64, actual: f64) !void {
|
||||
|
||||
var test_file_cache: ?TestFileCache = null;
|
||||
var gpa_state: ?std.heap.GeneralPurposeAllocator(.{}) = null;
|
||||
|
||||
const TestFileCache = @import("./../test_util.zig").TestFileCache;
|
||||
const test_util = @import("./../test_util.zig");
|
||||
const TestFileCache = test_util.TestFileCache;
|
||||
const shouldRunSlowTest = test_util.shouldRunSlowTest;
|
||||
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
@ -4,6 +4,11 @@ test {
|
||||
_ = @import("zx/ast.zig");
|
||||
_ = @import("cli/fmt.zig");
|
||||
_ = @import("cli/cli.zig");
|
||||
_ = @import("app/headers.zig");
|
||||
_ = @import("app/request.zig");
|
||||
_ = @import("app/response.zig");
|
||||
_ = @import("app/common.zig");
|
||||
_ = @import("app/routing.zig");
|
||||
}
|
||||
|
||||
pub const std_options = std.Options{
|
||||
|
||||
@ -165,6 +165,18 @@ pub const TestFileCache = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub fn shouldRunSlowTest() bool {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// E2E environment variable is set
|
||||
const slow_tests = std.process.getEnvVarOwned(allocator, "E2E") catch {
|
||||
return false;
|
||||
};
|
||||
|
||||
defer allocator.free(slow_tests);
|
||||
return true;
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const zx = @import("zx");
|
||||
|
||||
@ -334,6 +334,7 @@ test "component_caching" {
|
||||
}
|
||||
|
||||
test "performance > transpile" {
|
||||
if (!test_util.shouldRunSlowTest()) return;
|
||||
const MAX_TIME_MS = 50.0 * 8; // 50ms is on M1 Pro
|
||||
const MAX_TIME_PER_FILE_MS = 8.0 * 10; // 5ms is on M1 Pro
|
||||
|
||||
@ -586,7 +587,8 @@ var test_file_cache: ?TestFileCache = null;
|
||||
var gpa_state: ?std.heap.GeneralPurposeAllocator(.{}) = null;
|
||||
|
||||
const native_os = @import("builtin").os.tag;
|
||||
const TestFileCache = @import("./../test_util.zig").TestFileCache;
|
||||
const test_util = @import("./../test_util.zig");
|
||||
const TestFileCache = test_util.TestFileCache;
|
||||
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user