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 });