Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support dependency injection #10

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 28 additions & 15 deletions examples/http/tls/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
\\ <!DOCTYPE html>
\\ <html>
\\ <head>
\\ <link rel="stylesheet" href="/embed/pico.min.css"/>
\\ </head>
\\ <body>
\\ <h1>Hello, World!</h1>
\\ </body>
\\ </html>
;

pub fn handler_fn(_: http.Request, response: *http.Response, ctx: http.Context) void {
// const body =
// \\ <!DOCTYPE html>
// \\ <html>
// \\ <head>
// \\ <link rel="stylesheet" href="/embed/pico.min.css"/>
// \\ </head>
// \\ <body>
// \\ <h1>Hello, World!</h1>
// \\ </body>
// \\ </html>
// ;
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));
Expand Down
5 changes: 4 additions & 1 deletion src/http/context.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
};
122 changes: 122 additions & 0 deletions src/http/injector.zig
Original file line number Diff line number Diff line change
@@ -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;
}
70 changes: 70 additions & 0 deletions src/http/meta.zig
Original file line number Diff line number Diff line change
@@ -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,
};
}
12 changes: 10 additions & 2 deletions src/http/router.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/http/server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down