1
0
Fork 0
mirror of https://github.com/zigzap/zap.git synced 2025-10-20 15:14:08 +00:00

MIDDLEWARE support

This commit is contained in:
Rene Schallner 2023-05-19 02:47:12 +02:00
parent 7e6bda1b8a
commit d656452667
8 changed files with 538 additions and 4 deletions

View file

@ -54,6 +54,10 @@ Here's what works:
A convenience authenticator that redirects un-authenticated requests to a
login page and sends cookies containing session tokens based on
username/password pairs transmitted via POST request.
- **[MIDDLEWARE support](examples/middleware/middleware.zig)**: chain together
request handlers in middleware style. Provide custom context structs, totally
type-safe, using **[ZIG-CEPTION](doc/zig-ception.md)**. If you come from GO
this might appeal to you.
I'll continue wrapping more of facil.io's functionality and adding stuff to zap

View file

@ -55,6 +55,7 @@ pub fn build(b: *std.build.Builder) !void {
.{ .name = "websockets", .src = "examples/websockets/websockets.zig" },
.{ .name = "userpass_session", .src = "examples/userpass_session_auth/userpass_session_auth.zig" },
.{ .name = "sendfile", .src = "examples/sendfile/sendfile.zig" },
.{ .name = "middleware", .src = "examples/middleware/middleware.zig" },
}) |excfg| {
const ex_name = excfg.name;
const ex_src = excfg.src;

View file

@ -1,6 +1,6 @@
.{
.name = "zap",
.version = "0.0.20",
.version = "0.0.21",
.dependencies = .{
.@"facil.io" = .{

93
doc/zig-ception.md Normal file
View file

@ -0,0 +1,93 @@
# ZIG-CEPTION!
In ZAP, we have great zig-ception moment in the [middleware
example](../examples/middleware/middleware.zig). But first we need to introduce
one key function of `zap.Middleware`: **combining structs at comptime!**
## Combining structs at runtime
Here is how it is used in user-code:
```zig
// create a combined context struct
const Context = zap.Middleware.MixContexts(.{
.{ .name = "?user", .type = UserMiddleWare.User },
.{ .name = "?session", .type = SessionMiddleWare.Session },
});
```
The result of this function call is a struct that has a `user` field of type
`?UserMiddleWare.User`, which is the `User` struct inside of its containing
struct - and a `session` field of type `?SessionMiddleWare.Session`.
So `MixContexts` accepts a **tuple** of structs that each contain a
`name` field and a `type` field. As a hack, we support the `?` in the name to
indicate we want to the resulting struct field to be an optional.
A **tuple** means that we can "mix" as many structs as we like. Not just two
like in above example.
`MixContexts` inspects the passed-in `type` fields and **composes a new struct
type at comptime**! Have a look at its [source code](../src/middleware.zig).
You'll be blown away if this kind of metaprogramming stuff isn't what you do
everyday. I was totally blown away by trying it out and seeing it that it
_actually_ worked.
Why do we create combined structs? Because all our Middleware handler functions
need to receive a per-request context. But each wants their own data: the User
middleware might want to access a User struct, the Session middleware might want
a Session struct, and so on. So, which struct should we use in the prototype of
the "on_request" callback function? We could just use an `anyopaque` pointer.
That would solve the generic function prototype problem. But then everyone
implementing such a handler would need to cast this pointer back into - what?
Into the same type that the caller of the handler used. It gets really messy
when we continue this train of thought.
So, in ZAP, I opted for one Context type for all request handlers. Since ZAP is
a library, it cannot know what your preferred Context struct is. What it should
consist of. Therefore, it lets you combine all the structs your and maybe your
3rd parties's middleware components require - at comptime! And derive the
callback function prototype from that. If you look at the [middleware
example](../examples/middleware/middleware.zig), you'll notice, it's really
smooth to use.
**NOTE:** In your contexts, please also use OPTIONALS. They are set null at
context creation time. And will aid you in not shooting yourself in the foot
when accessing context fields that haven't been initialized - which may happen
when the order of your chain of components isn't perfect yet. 😉
## The zig-ception moment
Have a look at an excerpt of the example:
```zig
// we create a Handler type based on our Context
const Handler = zap.Middleware.Handler(Context);
//
// ZIG-CEPTION!!!
//
// Note how amazing zig is:
// - we create the "mixed" context based on the both middleware structs
// - we create the handler based on this context
// - we create the middleware structs based on the handler
// - which needs the context
// - which needs the middleware structs
// - ZIG-CEPTION!
// Example user middleware: puts user info into the context
const UserMiddleWare = struct {
handler: Handler,
// .. the UserMiddleWare depends on the handler
// which depends on the Context
// which depends on this UserMiddleWare struct
// ZIG-CEPTION!!!
```
## 🤯
The comments in the code say it all.
**Isn't ZIG AMAZING?**

View file

@ -0,0 +1,228 @@
const std = @import("std");
const zap = @import("zap");
// just a way to share our allocator via callback
const SharedAllocator = struct {
// static
var allocator: std.mem.Allocator = undefined;
const Self = @This();
// just a convenience function
pub fn init(a: std.mem.Allocator) void {
allocator = a;
}
// static function we can pass to the listener later
pub fn getAllocator() std.mem.Allocator {
return allocator;
}
};
// create a combined context struct
const Context = zap.Middleware.MixContexts(.{
.{ .name = "?user", .type = UserMiddleWare.User },
.{ .name = "?session", .type = SessionMiddleWare.Session },
});
// we create a Handler type based on our Context
const Handler = zap.Middleware.Handler(Context);
//
// ZIG-CEPTION!!!
//
// Note how amazing zig is:
// - we create the "mixed" context based on the both middleware structs
// - we create the handler based on this context
// - we create the middleware structs based on the handler
// - which needs the context
// - which needs the middleware structs
// - ZIG-CEPTION!
// Example user middleware: puts user info into the context
const UserMiddleWare = struct {
handler: Handler,
const Self = @This();
// Just some arbitrary struct we want in the per-request context
// note: it MUST have all default values!!!
// note: it MUST have all default values!!!
// note: it MUST have all default values!!!
// note: it MUST have all default values!!!
// This is so that it can be constructed via .{}
// as we can't expect the listener to know how to initialize our context structs
const User = struct {
name: []const u8 = undefined,
email: []const u8 = undefined,
};
pub fn init(other: ?*Handler) Self {
return .{
.handler = Handler.init(onRequest, other),
};
}
// we need the handler as a common interface to chain stuff
pub fn getHandler(self: *Self) *Handler {
return &self.handler;
}
// note that the first parameter is of type *Handler, not *Self !!!
pub fn onRequest(handler: *Handler, r: zap.SimpleRequest, context: *Context) bool {
// this is how we would get our self pointer
var self = @fieldParentPtr(Self, "handler", handler);
_ = self;
// do our work: fill in the user field of the context
context.user = User{
.name = "renerocksai",
.email = "supa@secret.org",
};
std.debug.print("\n\nUser Middleware: set user in context {any}\n\n", .{context.user});
// continue in the chain
return handler.handleOther(r, context);
}
};
// Example session middleware: puts user info into the context
const SessionMiddleWare = struct {
handler: Handler,
const Self = @This();
// Just some arbitrary struct we want in the per-request context
// note: it MUST have all default values!!!
const Session = struct {
info: []const u8 = undefined,
token: []const u8 = undefined,
};
pub fn init(other: ?*Handler) Self {
return .{
.handler = Handler.init(onRequest, other),
};
}
// we need the handler as a common interface to chain stuff
pub fn getHandler(self: *Self) *Handler {
return &self.handler;
}
// note that the first parameter is of type *Handler, not *Self !!!
pub fn onRequest(handler: *Handler, r: zap.SimpleRequest, context: *Context) bool {
// this is how we would get our self pointer
var self = @fieldParentPtr(Self, "handler", handler);
_ = self;
context.session = Session{
.info = "secret session",
.token = "rot47-asdlkfjsaklfdj",
};
std.debug.print("\n\nSessionMiddleware: set session in context {any}\n\n", .{context.session});
// continue in the chain
return handler.handleOther(r, context);
}
};
// Example html middleware: handles the request
const HtmlMiddleWare = struct {
handler: Handler,
const Self = @This();
pub fn init(other: ?*Handler) Self {
return .{
.handler = Handler.init(onRequest, other),
};
}
// we need the handler as a common interface to chain stuff
pub fn getHandler(self: *Self) *Handler {
return &self.handler;
}
// note that the first parameter is of type *Handler, not *Self !!!
pub fn onRequest(handler: *Handler, r: zap.SimpleRequest, context: *Context) bool {
// this is how we would get our self pointer
var self = @fieldParentPtr(Self, "handler", handler);
_ = self;
std.debug.print("\n\nHtmlMiddleware: handling request with context: {any}\n\n", .{context});
var buf: [1024]u8 = undefined;
var userFound: bool = false;
var sessionFound: bool = false;
if (context.user) |user| {
userFound = true;
if (context.session) |session| {
sessionFound = true;
const message = std.fmt.bufPrint(&buf, "User: {s} / {s}, Session: {s} / {s}", .{
user.name,
user.email,
session.info,
session.token,
}) catch unreachable;
r.setContentType(.TEXT) catch unreachable;
r.sendBody(message) catch unreachable;
return true;
}
}
const message = std.fmt.bufPrint(&buf, "User info found: {}, session info found: {}", .{ userFound, sessionFound }) catch unreachable;
r.setContentType(.TEXT) catch unreachable;
r.sendBody(message) catch unreachable;
return true;
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{
.thread_safe = true,
}){};
var allocator = gpa.allocator();
SharedAllocator.init(allocator);
// we create our HTML middleware component that handles the request
var htmlHandler = HtmlMiddleWare.init(null);
// we wrap it in the session Middleware component
var sessionHandler = SessionMiddleWare.init(htmlHandler.getHandler());
// we wrap that in the user Middleware component
var userHandler = UserMiddleWare.init(sessionHandler.getHandler());
// we create a listener with our combined context
// and pass it the initial handler: the user handler
var listener = try zap.Middleware.Listener(Context).init(
.{
.on_request = null, // must be null
.port = 3000,
.log = true,
.max_clients = 100000,
},
userHandler.getHandler(),
SharedAllocator.getAllocator,
);
zap.enableDebugLog();
listener.listen() catch |err| {
std.debug.print("\nLISTEN ERROR: {any}\n", .{err});
return;
};
std.debug.print("Visit me on http://127.0.0.1:3000\n", .{});
// start worker threads
zap.start(.{
.threads = 2,
.workers = 1,
});
}

201
src/middleware.zig Normal file
View file

@ -0,0 +1,201 @@
const std = @import("std");
const zap = @import("zap.zig");
pub const ContextDescriptor = struct {
name: []const u8,
type: type,
};
/// Provide a tuple of structs of type like ContextDescriptor
/// a name starting with '?', such as "?user" will be treated as Optional with default `null`.
pub fn MixContexts(comptime context_tuple: anytype) type {
var fields: [context_tuple.len]std.builtin.Type.StructField = undefined;
for (context_tuple, 0..) |t, i| {
var fieldType: type = t.type;
var fieldName: []const u8 = t.name[0..];
var isOptional: bool = false;
if (fieldName[0] == '?') {
fieldType = @Type(.{ .Optional = .{ .child = fieldType } });
fieldName = fieldName[1..];
isOptional = true;
}
fields[i] = .{
.name = fieldName,
.type = fieldType,
.default_value = if (isOptional) &null else null,
.is_comptime = false,
.alignment = 0,
};
}
return @Type(.{
.Struct = .{
.layout = .Auto,
.fields = fields[0..],
.decls = &[_]std.builtin.Type.Declaration{},
.is_tuple = false,
},
});
}
/// ContextType must implement .init(zap.SimpleRequest)
pub fn Handler(comptime ContextType: anytype) type {
return struct {
other_handler: ?*Self = null,
on_request: ?RequestFn = null,
// will be set
allocator: ?std.mem.Allocator = null,
pub const RequestFn = *const fn (*Self, zap.SimpleRequest, *ContextType) bool;
const Self = @This();
pub fn init(on_request: RequestFn, other: ?*Self) Self {
return .{
.other_handler = other,
.on_request = on_request,
};
}
// example for handling request
pub fn handleOther(self: *Self, r: zap.SimpleRequest, context: *ContextType) bool {
// in structs embedding a handler, we'd @fieldParentPtr the first
// param to get to the real self
// First, do our pre-other stuff
// ..
// then call the wrapped thing
var other_handler_finished = false;
if (self.other_handler) |other_handler| {
if (other_handler.on_request) |on_request| {
other_handler_finished = on_request(other_handler, r, context);
}
}
// now do our post stuff
return other_handler_finished;
}
};
}
pub const Error = error{
InitOnRequestIsNotNull,
};
pub const RequestAllocatorFn = *const fn () std.mem.Allocator;
pub fn Listener(comptime ContextType: anytype) type {
return struct {
listener: zap.SimpleHttpListener = undefined,
settings: zap.SimpleHttpListenerSettings,
// static initial handler
var handler: ?*Handler(ContextType) = undefined;
// static allocator getter
var requestAllocator: ?RequestAllocatorFn = null;
const Self = @This();
/// initialize the middleware handler
/// the passed in settings must have on_request set to null
///
pub fn init(settings: zap.SimpleHttpListenerSettings, initial_handler: *Handler(ContextType), request_alloc: ?RequestAllocatorFn) Error!Self {
// override on_request with ourselves
if (settings.on_request != null) {
return Error.InitOnRequestIsNotNull;
}
requestAllocator = request_alloc;
std.debug.assert(requestAllocator != null);
var ret: Self = .{
.settings = settings,
};
ret.settings.on_request = onRequest;
ret.listener = zap.SimpleHttpListener.init(ret.settings);
handler = initial_handler;
return ret;
}
pub fn listen(self: *Self) !void {
try self.listener.listen();
}
// this is just a reference implementation
pub fn onRequest(r: zap.SimpleRequest) void {
// we are the 1st handler in the chain, so we create a context
var context: ContextType = .{};
// handlers might need an allocator
// we CAN provide an allocator getter
var allocator: ?std.mem.Allocator = null;
if (requestAllocator) |foo| {
allocator = foo();
}
if (handler) |initial_handler| {
initial_handler.allocator = allocator;
if (initial_handler.on_request) |on_request| {
// we don't care about the return value at the top level
_ = on_request(initial_handler, r, &context);
}
}
}
};
}
test "it" {
// just some made-up struct
const User = struct {
name: []const u8,
email: []const u8,
};
// just some made-up struct
const Session = struct {
sessionType: []const u8,
token: []const u8,
valid: bool,
};
const Mixed = MixContexts(
.{
.{ .name = "?user", .type = *User },
.{ .name = "?session", .type = *Session },
},
);
std.debug.print("{any}\n", .{Mixed});
inline for (@typeInfo(Mixed).Struct.fields, 0..) |f, i| {
std.debug.print("field {} : name = {s} : type = {any}\n", .{ i, f.name, f.type });
}
var mixed: Mixed = .{
// it's all optionals which we made default to null in MixContexts
};
std.debug.print("mixed = {any}\n", .{mixed});
const NonOpts = MixContexts(
.{
.{ .name = "user", .type = *User },
.{ .name = "session", .type = *Session },
},
);
var user: User = .{
.name = "renerocksai",
.email = "secret",
};
var session: Session = .{
.sessionType = "bearerToken",
.token = "ABCDEFG",
.valid = false,
};
// this will fail if we don't specify
var nonOpts: NonOpts = .{
.user = &user,
.session = &session,
};
std.debug.print("nonOpts = {any}\n", .{nonOpts});
}

View file

@ -10,6 +10,7 @@ pub usingnamespace @import("util.zig");
pub usingnamespace @import("http.zig");
pub usingnamespace @import("mustache.zig");
pub usingnamespace @import("http_auth.zig");
pub const Middleware = @import("middleware.zig");
pub const WebSockets = @import("websockets.zig");
pub const Log = @import("log.zig");
@ -556,6 +557,7 @@ pub const SimpleHttpListener = struct {
var the_one_and_only_listener: ?*SimpleHttpListener = null;
pub fn init(settings: SimpleHttpListenerSettings) Self {
std.debug.assert(settings.on_request != null);
return .{
.settings = settings,
};
@ -574,7 +576,11 @@ pub const SimpleHttpListener = struct {
.method = util.fio2str(r.*.method),
.h = r,
};
l.settings.on_request.?(req);
std.debug.assert(l.settings.on_request != null);
if (l.settings.on_request) |on_request| {
// l.settings.on_request.?(req);
on_request(req);
}
}
}
@ -655,7 +661,8 @@ pub const SimpleHttpListener = struct {
// const result = try bufPrint(buf, fmt ++ "\x00", args);
// return result[0 .. result.len - 1 :0];
// }
if (fio.http_listen(printed_port.ptr, self.settings.interface, x) == -1) {
var ret = fio.http_listen(printed_port.ptr, self.settings.interface, x);
if (ret == -1) {
return error.ListenError;
}

View file

@ -13,4 +13,4 @@ cookies
websockets
userpass_session
sendfile
middleware