Skip to content

Commit 3769458

Browse files
feat: remove in-built router (#43)
1 parent 94e5d65 commit 3769458

11 files changed

Lines changed: 759 additions & 457 deletions

File tree

README.md

Lines changed: 120 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Cross-platform • Zero allocations
2424
- 🌍 **Cross-platform** support via Zig std.Io backends
2525
- 💾 **Zero runtime allocations** - all memory allocated at compile time
2626
- 🔒 **Lock-free** atomic operations for maximum concurrency
27-
- 📦 **Simple API** - just configure and run
27+
- 🧩 **Bring your own router** - plug in any dispatcher or router you want
2828
-**HTTP/2 RFC 7540** compliant
2929

3030
## Quick Start
@@ -63,11 +63,13 @@ exe.root_module.addImport("http2", http2_dep.module("http2"));
6363
const std = @import("std");
6464
const http2 = @import("http2");
6565
66-
fn indexHandler(ctx: *const http2.Context) !http2.Response {
67-
return ctx.response.text(.ok, "hello from http2.zig\n");
68-
}
66+
fn handleRequest(ctx: *const http2.Context) !http2.Response {
67+
if (ctx.method == .get) {
68+
if (std.mem.eql(u8, ctx.path, "/")) {
69+
return ctx.response.text(.ok, "hello from http2.zig\n");
70+
}
71+
}
6972
70-
fn notFoundHandler(ctx: *const http2.Context) !http2.Response {
7173
return ctx.response.text(.not_found, "not found\n");
7274
}
7375
@@ -80,29 +82,112 @@ pub fn main() !void {
8082
try http2.init(allocator);
8183
defer http2.deinit();
8284
83-
// Configure the request router
84-
var router = http2.Router.init(allocator);
85-
defer router.deinit();
86-
87-
try router.get("/", indexHandler);
88-
router.setFallback(notFoundHandler);
89-
9085
// Configure and create server
9186
const config = http2.Server.Config{
9287
.address = try std.Io.net.IpAddress.parse("127.0.0.1", 3000),
93-
.router = &router,
88+
.dispatcher = http2.RequestDispatcher.fromHandler(handleRequest),
9489
};
9590
9691
var server = try http2.Server.init(allocator, config);
9792
defer server.deinit();
9893
99-
std.log.info("HTTP/2 server listening on {}", .{config.address});
94+
std.log.info("HTTP/2 server listening on {f}", .{config.address});
10095
10196
// Run the server
10297
try server.run();
10398
}
10499
```
105100

101+
### Bring Your Own Router With turboapi-core
102+
103+
The core library stays router-agnostic. If you want radix-tree routing, path params, and HTTP
104+
helpers, add `turboapi-core` to your application:
105+
106+
```bash
107+
zig fetch --save=turboapi_core "git+https://github.com/justrach/turboapi-core.git#main"
108+
```
109+
110+
Wire it into `build.zig`:
111+
112+
```zig
113+
const core_dep = b.dependency("turboapi_core", .{
114+
.target = target,
115+
.optimize = optimize,
116+
});
117+
const core_mod = core_dep.module("turboapi-core");
118+
exe.root_module.addImport("turboapi-core", core_mod);
119+
```
120+
121+
Then bridge your router into `http2.zig` with a typed dispatcher:
122+
123+
```zig
124+
const std = @import("std");
125+
const core = @import("turboapi-core");
126+
const http2 = @import("http2");
127+
128+
const App = struct {
129+
router: core.Router,
130+
131+
fn init(target: *App, allocator: std.mem.Allocator) !void {
132+
target.* = .{
133+
.router = core.Router.init(allocator),
134+
};
135+
errdefer target.deinit();
136+
137+
try target.router.addRoute("GET", "/", "index");
138+
try target.router.addRoute("GET", "/users/{id}", "user_show");
139+
}
140+
141+
fn deinit(self: *App) void {
142+
self.router.deinit();
143+
}
144+
145+
fn dispatch(self: *const App, ctx: *const http2.Context) !http2.Response {
146+
if (self.router.findRoute(ctx.method.bytes(), ctx.path)) |match_result| {
147+
var match = match_result;
148+
defer match.deinit();
149+
150+
if (std.mem.eql(u8, match.handler_key, "index")) {
151+
return ctx.response.text(.ok, "hello\n");
152+
}
153+
if (std.mem.eql(u8, match.handler_key, "user_show")) {
154+
_ = match.params.get("id");
155+
return ctx.response.text(.ok, "user\n");
156+
}
157+
}
158+
159+
return ctx.response.text(.not_found, "not found\n");
160+
}
161+
};
162+
163+
pub fn main() !void {
164+
var gpa: std.heap.DebugAllocator(.{}) = .init;
165+
defer _ = gpa.deinit();
166+
const allocator = gpa.allocator();
167+
168+
try http2.init(allocator);
169+
defer http2.deinit();
170+
171+
var app: App = undefined;
172+
try App.init(&app, allocator);
173+
defer app.deinit();
174+
175+
const config = http2.Server.Config{
176+
.address = try std.Io.net.IpAddress.parse("127.0.0.1", 3000),
177+
.dispatcher = http2.RequestDispatcher.bind(App, &app, App.dispatch),
178+
};
179+
180+
var server = try http2.Server.init(allocator, config);
181+
defer server.deinit();
182+
try server.run();
183+
}
184+
```
185+
186+
Repository examples:
187+
188+
- `examples/basic_tls.zig` shows the same dispatcher API with a small custom Zig router.
189+
- `examples/turboapi.zig` shows the `turboapi-core` integration with TLS.
190+
106191
## Performance
107192

108193
TBD
@@ -116,8 +201,8 @@ pub const Server.Config = struct {
116201
/// Address to bind to
117202
address: std.Io.net.IpAddress,
118203
119-
/// Request router for handling HTTP requests
120-
router: *Router,
204+
/// Request dispatcher for application routing or request handling
205+
dispatcher: RequestDispatcher,
121206
122207
/// Maximum concurrent connections (default: 1000)
123208
max_connections: u32 = 1000,
@@ -127,25 +212,25 @@ pub const Server.Config = struct {
127212
};
128213
```
129214

130-
### Router
215+
### Request Dispatcher
216+
217+
`http2.zig` no longer ships with a built-in router. Instead, `Server.Config` takes a
218+
`RequestDispatcher`, which is just a function pointer plus optional typed state.
131219

132-
The server expects a router in `Server.Config`, and requests are dispatched through it.
220+
For stateless handling:
133221

134222
```zig
135-
try router.get("/", indexHandler);
136-
try router.post("/api/messages", createMessageHandler);
137-
try router.getPrefix("/assets", staticAssetsHandler);
138-
router.setFallback(notFoundHandler);
223+
.dispatcher = http2.RequestDispatcher.fromHandler(handleRequest),
139224
```
140225

141-
Current routing behavior:
226+
For stateful apps, middleware stacks, or third-party routers:
227+
228+
```zig
229+
.dispatcher = http2.RequestDispatcher.bind(App, &app, App.dispatch),
230+
```
142231

143-
- `get`, `post`, `put`, `delete`, `head`, `options`, and `patch` register exact routes.
144-
- `getPrefix` and `postPrefix` register prefix routes.
145-
- Prefix routes are ordered by longest path first.
146-
- Prefix matching is segment-aware: `/api` matches `/api` and `/api/users`, but not `/apix`.
147-
- A matching path with the wrong method returns `405 Method Not Allowed`.
148-
- A missing path falls through to the fallback handler when configured; otherwise it returns `404 Not Found`.
232+
This keeps transport, request parsing, and response building inside `http2.zig`, while letting the
233+
application decide how routing, params, middleware, and fallback behavior should work.
149234

150235
The request context passed to handlers exposes:
151236

@@ -207,11 +292,14 @@ zig build -Doptimize=ReleaseFast
207292
### Running Examples
208293

209294
```bash
210-
# Run the hello world example
211-
zig build run-hello
295+
# Run the basic TLS example with the local Zig router
296+
zig build run
297+
298+
# Run the turboapi-core example
299+
zig build run-turboapi
212300

213301
# Run the benchmark server
214-
cd benchmarks && zig build run
302+
zig build benchmark
215303
```
216304

217305
### Benchmarking

benchmarks/benchmark.zig

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@ pub const std_options: std.Options = .{
66
.log_level = .warn,
77
};
88

9-
/// Simple Hello World handler for benchmarking
10-
fn helloHandler(ctx: *const http2.Context) !http2.Response {
11-
return ctx.response.text(.ok, "Hello, World!");
9+
/// Simple request dispatcher for benchmarking.
10+
fn benchmarkHandler(ctx: *const http2.Context) !http2.Response {
11+
if (ctx.method == .get) {
12+
if (std.mem.eql(u8, ctx.path, "/")) {
13+
return ctx.response.text(.ok, "Hello, World!");
14+
}
15+
}
16+
17+
return ctx.response.text(.not_found, "Not Found");
1218
}
1319

1420
/// High-performance HTTP/2 over HTTPS benchmark server
@@ -35,16 +41,10 @@ pub fn main() !void {
3541
else
3642
(if (use_tls) @as(u16, 8443) else @as(u16, 3000));
3743

38-
// Set up simple router for benchmarking
39-
var router = http2.Router.init(allocator);
40-
defer router.deinit();
41-
42-
try router.get("/", helloHandler);
43-
4444
// Configure server for benchmarking with high concurrency
4545
const config = http2.Server.Config{
4646
.address = try std.Io.net.IpAddress.parse("127.0.0.1", port),
47-
.router = &router,
47+
.dispatcher = http2.RequestDispatcher.fromHandler(benchmarkHandler),
4848
.max_connections = http2.memory_budget.MemBudget.max_conns,
4949
.buffer_size = 32 * 1024,
5050
};

build.zig

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,34 @@ fn add_examples(
9090
run_basic.step.dependOn(b.getInstallStep());
9191
const run_step = b.step("run", "Run basic TLS server example");
9292
run_step.dependOn(&run_basic.step);
93+
94+
const turboapi_core_dep = b.dependency("turboapi_core", .{
95+
.target = target,
96+
.optimize = optimize,
97+
});
98+
const turboapi_core_module = turboapi_core_dep.module("turboapi-core");
99+
100+
// turboapi-core Example
101+
const turboapi_module = b.createModule(.{
102+
.root_source_file = b.path("examples/turboapi.zig"),
103+
.target = target,
104+
.optimize = optimize,
105+
});
106+
turboapi_module.addImport("http2", http2_module);
107+
turboapi_module.addImport("turboapi-core", turboapi_core_module);
108+
turboapi_module.addOptions("build_options", build_options);
109+
110+
const turboapi = b.addExecutable(.{
111+
.name = "turboapi_server",
112+
.root_module = turboapi_module,
113+
});
114+
linkBoringSsl(b, turboapi);
115+
b.installArtifact(turboapi);
116+
117+
const run_turboapi = b.addRunArtifact(turboapi);
118+
run_turboapi.step.dependOn(b.getInstallStep());
119+
const run_turboapi_step = b.step("run-turboapi", "Run turboapi-core example");
120+
run_turboapi_step.dependOn(&run_turboapi.step);
93121
}
94122

95123
/// Add benchmark application

build.zig.zon

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@
1414
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
1515
// Once all dependencies are fetched, `zig build` no longer requires
1616
// internet connectivity.
17-
.dependencies = .{},
18-
19-
// Specifies the set of files and directories that are included in this package.
20-
// Only files and directories listed here are included in the `hash` that
21-
// is computed for this package.
22-
// Paths are relative to the build root. Use the empty string (`""`) to refer to
23-
// the build root itself.
24-
// A directory listed here means that all files within, recursively, are included.
17+
.dependencies = .{
18+
.turboapi_core = .{
19+
.url = "git+https://github.com/justrach/turboapi-core.git?ref=main#ae5228141a805f6b8ff741a7534af3701b1ae912",
20+
.hash = "turboapi_core-0.1.0-DjdHgpCQAACVu14-9yq2RfaBbvNHBVk2qPeySXKnN23E",
21+
},
22+
},
2523
.paths = .{
2624
"build.zig",
2725
"build.zig.zon",
2826
"src",
27+
"examples",
28+
"benchmarks",
2929
"LICENSE",
3030
"README.md",
3131
},

0 commit comments

Comments
 (0)