diff --git a/examples/http/tls/main.zig b/examples/http/tls/main.zig
index c923d07..4ba501c 100644
--- a/examples/http/tls/main.zig
+++ b/examples/http/tls/main.zig
@@ -2,35 +2,48 @@ const std = @import("std");
const zzz = @import("zzz");
const http = zzz.HTTP;
const log = std.log.scoped(.@"examples/tls");
+
+pub const Post = struct {
+ id: u32,
+ title: []const u8,
+ body: []const u8,
+};
+
pub fn main() !void {
const host: []const u8 = "0.0.0.0";
const port: u16 = 9862;
const allocator = std.heap.c_allocator;
- var router = http.Router.init(allocator);
+ var user = Post{ .body = "testing injection", .title = "TEST", .id = 34 };
+ const cx = .{&user};
+
+ var router = http.Router.init(allocator, cx);
defer router.deinit();
try router.serve_embedded_file("/embed/pico.min.css", http.Mime.CSS, @embedFile("embed/pico.min.css"));
try router.serve_route("/", http.Route.init().get(struct {
- pub fn handler_fn(_: http.Request, response: *http.Response, _: http.Context) void {
- const body =
- \\
- \\
- \\
- \\
- \\
- \\
- \\ Hello, World!
- \\
- \\
- ;
-
+ pub fn handler_fn(_: http.Request, response: *http.Response, ctx: http.Context) void {
+ // const body =
+ // \\
+ // \\
+ // \\
+ // \\
+ // \\
+ // \\
+ // \\ Hello, World!
+ // \\
+ // \\
+ // ;
+ const post = try ctx.injector.get(*Post);
+ var out = try std.ArrayList(u8).init(ctx.allocator);
+ defer out.deinit();
+ try std.json.stringify(post, .{}, out.writer());
response.set(.{
.status = .OK,
.mime = http.Mime.HTML,
- .body = body[0..],
+ .body = out[0..],
});
}
}.handler_fn));
diff --git a/src/http/context.zig b/src/http/context.zig
index 52bae26..329e114 100644
--- a/src/http/context.zig
+++ b/src/http/context.zig
@@ -3,19 +3,22 @@ const log = std.log.scoped(.@"zzz/http/context");
const Capture = @import("routing_trie.zig").Capture;
const QueryMap = @import("routing_trie.zig").QueryMap;
+const Injector = @import("./injector.zig").Injector;
pub const Context = struct {
allocator: std.mem.Allocator,
path: []const u8,
captures: []Capture,
queries: *QueryMap,
+ injector: Injector,
- pub fn init(allocator: std.mem.Allocator, path: []const u8, captures: []Capture, queries: *QueryMap) Context {
+ pub fn init(allocator: std.mem.Allocator, path: []const u8, captures: []Capture, queries: *QueryMap, injector: Injector) Context {
return Context{
.allocator = allocator,
.path = path,
.captures = captures,
.queries = queries,
+ .injector = injector,
};
}
};
diff --git a/src/http/injector.zig b/src/http/injector.zig
new file mode 100644
index 0000000..9aa436e
--- /dev/null
+++ b/src/http/injector.zig
@@ -0,0 +1,122 @@
+const std = @import("std");
+const meta = @import("./meta.zig");
+const t = std.testing;
+// Source: https://github.com/cztomsik/tokamak/blob/main/src/injector.zig
+/// Injector serves as a custom runtime scope for retrieving dependencies.
+/// It can be passed around, enabling any code to request a value or reference
+/// to a given type. Additionally, it can invoke arbitrary functions and supply
+/// the necessary dependencies automatically.
+///
+/// Injectors can be nested. If a dependency is not found, the parent context
+/// is searched. If the dependency is still not found, an error is returned.
+pub const Injector = struct {
+ ctx: *anyopaque,
+ resolver: *const fn (*anyopaque, meta.TypeId) ?*anyopaque,
+ parent: ?*const Injector = null,
+
+ pub const empty: Injector = .{ .ctx = undefined, .resolver = resolveNull };
+
+ /// Create a new injector from a context ptr and an optional parent.
+ pub fn init(ctx: anytype, parent: ?*const Injector) Injector {
+ if (comptime !meta.isOnePtr(@TypeOf(ctx))) {
+ @compileError("Expected pointer to a context, got " ++ @typeName(@TypeOf(ctx)));
+ }
+
+ const H = struct {
+ fn resolve(ptr: *anyopaque, tid: meta.TypeId) ?*anyopaque {
+ var cx: @TypeOf(ctx) = @constCast(@ptrCast(@alignCast(ptr)));
+
+ inline for (std.meta.fields(@TypeOf(cx.*))) |f| {
+ const p = if (comptime meta.isOnePtr(f.type)) @field(cx, f.name) else &@field(cx, f.name);
+
+ if (tid == meta.tid(@TypeOf(p))) {
+ std.debug.assert(@intFromPtr(p) != 0xaaaaaaaaaaaaaaaa);
+ return @ptrCast(@constCast(p));
+ }
+ }
+
+ if (tid == meta.tid(@TypeOf(cx))) {
+ return ptr;
+ }
+
+ return null;
+ }
+ };
+
+ return .{
+ .ctx = @constCast(@ptrCast(ctx)), // resolver() casts back first, so this should be safe
+ .resolver = &H.resolve,
+ .parent = parent,
+ };
+ }
+
+ pub fn find(self: Injector, comptime T: type) ?T {
+ if (comptime T == Injector) {
+ return self;
+ }
+
+ if (comptime !meta.isOnePtr(T)) {
+ return if (self.find(*const T)) |p| p.* else null;
+ }
+
+ if (self.resolver(self.ctx, meta.tid(T))) |ptr| {
+ return @ptrCast(@constCast(@alignCast(ptr)));
+ }
+
+ if (comptime @typeInfo(T).Pointer.is_const) {
+ if (self.resolver(self.ctx, meta.tid(*@typeInfo(T).Pointer.child))) |ptr| {
+ return @ptrCast(@constCast(@alignCast(ptr)));
+ }
+ }
+
+ return if (self.parent) |p| p.find(T) else null;
+ }
+
+ /// Get a dependency from the context.
+ pub fn get(self: Injector, comptime T: type) !T {
+ return self.find(T) orelse {
+ std.log.debug("Missing dependency: {s}", .{@typeName(T)});
+ return error.MissingDependency;
+ };
+ }
+
+ test get {
+ var num: u32 = 123;
+ var cx = .{ .num = &num };
+ const inj = Injector.init(&cx, null);
+
+ try t.expectEqual(inj, inj.get(Injector));
+ try t.expectEqual(&num, inj.get(*u32));
+ try t.expectEqual(@as(*const u32, &num), inj.get(*const u32));
+ try t.expectEqual(123, inj.get(u32));
+ try t.expectEqual(error.MissingDependency, inj.get(u64));
+ }
+
+ /// Call a function with dependencies. The `extra_args` tuple is used to
+ /// pass additional arguments to the function. Function with anytype can
+ /// be called as long as the concrete value is provided in the `extra_args`.
+ pub fn call(self: Injector, comptime fun: anytype, extra_args: anytype) anyerror!meta.Result(fun) {
+ if (comptime @typeInfo(@TypeOf(extra_args)) != .@"struct") {
+ @compileError("Expected a tuple of arguments");
+ }
+
+ const params = @typeInfo(@TypeOf(fun)).@"fn".params;
+ const extra_start = params.len - extra_args.len;
+
+ const types = comptime brk: {
+ var types: [params.len]type = undefined;
+ for (0..extra_start) |i| types[i] = params[i].type orelse @compileError("reached anytype");
+ for (extra_start..params.len, 0..) |i, j| types[i] = @TypeOf(extra_args[j]);
+ break :brk &types;
+ };
+
+ var args: std.meta.Tuple(types) = undefined;
+ inline for (0..args.len) |i| args[i] = if (i < extra_start) try self.get(@TypeOf(args[i])) else extra_args[i - extra_start];
+
+ return @call(.auto, fun, args);
+ }
+};
+
+fn resolveNull(_: *anyopaque, _: meta.TypeId) ?*anyopaque {
+ return null;
+}
diff --git a/src/http/meta.zig b/src/http/meta.zig
new file mode 100644
index 0000000..9e09ab5
--- /dev/null
+++ b/src/http/meta.zig
@@ -0,0 +1,70 @@
+const std = @import("std");
+
+// https://github.com/ziglang/zig/issues/19858#issuecomment-2370673253
+pub const TypeId = *const struct {
+ _: u8 = undefined,
+};
+
+pub inline fn tid(comptime T: type) TypeId {
+ const H = struct {
+ comptime {
+ _ = T;
+ }
+ var id: Deref(TypeId) = .{};
+ };
+ return &H.id;
+}
+
+pub fn dupe(allocator: std.mem.Allocator, value: anytype) !@TypeOf(value) {
+ return switch (@typeInfo(@TypeOf(value))) {
+ .optional => try dupe(allocator, value orelse return null),
+ .@"struct" => |s| {
+ var res: @TypeOf(value) = undefined;
+ inline for (s.fields) |f| @field(res, f.name) = try dupe(allocator, @field(value, f.name));
+ return res;
+ },
+ .pointer => |p| switch (p.size) {
+ .Slice => if (p.child == u8) allocator.dupe(p.child, value) else error.NotSupported,
+ else => value,
+ },
+ else => value,
+ };
+}
+
+pub fn Return(comptime fun: anytype) type {
+ return switch (@typeInfo(@TypeOf(fun))) {
+ .@"fn" => |f| f.return_type.?,
+ else => @compileError("Expected a function, got " ++ @typeName(@TypeOf(fun))),
+ };
+}
+
+pub fn Result(comptime fun: anytype) type {
+ const R = Return(fun);
+
+ return switch (@typeInfo(R)) {
+ .error_union => |r| r.payload,
+ else => R,
+ };
+}
+
+pub fn isGeneric(comptime fun: anytype) bool {
+ return @typeInfo(@TypeOf(fun)).@"fn".is_generic;
+}
+
+pub fn isOnePtr(comptime T: type) bool {
+ return switch (@typeInfo(T)) {
+ .Pointer => |p| p.size == .One,
+ else => false,
+ };
+}
+
+pub fn Deref(comptime T: type) type {
+ return if (isOnePtr(T)) std.meta.Child(T) else T;
+}
+
+pub fn hasDecl(comptime T: type, comptime name: []const u8) bool {
+ return switch (@typeInfo(T)) {
+ .@"struct", .@"union", .@"enum", .@"opaque" => @hasDecl(T, name),
+ else => false,
+ };
+}
diff --git a/src/http/router.zig b/src/http/router.zig
index 515f465..a67748a 100644
--- a/src/http/router.zig
+++ b/src/http/router.zig
@@ -12,17 +12,25 @@ const Context = @import("context.zig").Context;
const RoutingTrie = @import("routing_trie.zig").RoutingTrie;
const QueryMap = @import("routing_trie.zig").QueryMap;
+const Injector = @import("injector.zig").Injector;
pub const Router = struct {
allocator: std.mem.Allocator,
routes: RoutingTrie,
+ injector: Injector,
/// This makes the router immutable, also making it
/// thread-safe when shared.
locked: bool = false,
- pub fn init(allocator: std.mem.Allocator) Router {
+ pub fn init(allocator: std.mem.Allocator, dep_ctx: anytype) Router {
const routes = RoutingTrie.init(allocator) catch unreachable;
- return Router{ .allocator = allocator, .routes = routes, .locked = false };
+ const injector = Injector.init(dep_ctx, null);
+ return Router{
+ .allocator = allocator,
+ .routes = routes,
+ .locked = false,
+ .injector = injector,
+ };
}
pub fn deinit(self: *Router) void {
diff --git a/src/http/server.zig b/src/http/server.zig
index 61a78d0..6c65cce 100644
--- a/src/http/server.zig
+++ b/src/http/server.zig
@@ -59,6 +59,7 @@ fn route_and_respond(p: *Provision, router: *const Router) !RecvStatus {
p.data.request.uri,
f.captures,
f.queries,
+ router.injector,
);
@call(.auto, func, .{ p.data.request, &p.data.response, context });