1
0
Fork 0
mirror of https://github.com/zigzap/zap.git synced 2025-10-20 23:24:09 +00:00

zap.Middleware.EndpointHandler, r.get/setContext()

This commit is contained in:
Rene Schallner 2023-05-20 03:16:02 +02:00
parent e8c9e866cd
commit 35144b195e
9 changed files with 358 additions and 2 deletions

View file

@ -58,6 +58,8 @@ Here's what works:
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.
- **[MIDDLEWARE with endpoint support](examples/middleware_with_endpoint/middleware_with_endpoint.zig)**: Same as the example above, but this time we use an endpoint at the end of the chain, by wrapping it via `zap.Middleware.EndpointHandler`. Mixing endpoints in your middleware chain allows for usage of Zap's authenticated endpoints and your custom endpoints. Since Endpoints use a simpler API, you have to use `r.setUserContext()` and `r.getUserContext()` with the request if you want to access the middleware context from a wrapped endpoint. Since this mechanism uses an `*anyopaque` pointer underneath (to not break the Endpoint API), it is less type-safe than `zap.Middleware`'s use of contexts.
- **Per Request Contexts** : With the introduction of `setContext()` and `getContext()`, you can, of course use those two in projects that don't use `zap.SimpleEndpoint` or `zap.Middleware`, too, if you really, really, absolutely don't find another way to solve your context problem. **We recommend using a `zap.SimpleEndpoint`** inside of a struct that can provide all the context you need **instead**. You get access to your struct in the callbacks via the `@fieldParentPtr()` trick that is used extensively in Zap's examples, like the [endpoint example](examples/endpoint/endpoint.zig).
I'll continue wrapping more of facil.io's functionality and adding stuff to zap

View file

@ -56,6 +56,7 @@ pub fn build(b: *std.build.Builder) !void {
.{ .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" },
.{ .name = "middleware_with_endpoint", .src = "examples/middleware_with_endpoint/middleware_with_endpoint.zig" },
}) |excfg| {
const ex_name = excfg.name;
const ex_src = excfg.src;

View file

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

View file

@ -164,6 +164,7 @@ const HtmlMiddleWare = struct {
if (context.session) |session| {
sessionFound = true;
std.debug.assert(r.isFinished() == false);
const message = std.fmt.bufPrint(&buf, "User: {s} / {s}, Session: {s} / {s}", .{
user.name,
user.email,
@ -172,6 +173,7 @@ const HtmlMiddleWare = struct {
}) catch unreachable;
r.setContentType(.TEXT) catch unreachable;
r.sendBody(message) catch unreachable;
std.debug.assert(r.isFinished() == true);
return true;
}
}

View file

@ -0,0 +1,246 @@
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);
//
// 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
// 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 session 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);
}
};
//
// !!!! ENDPOINT !!!
//
// We define an endpoint as we usually would.
// NO ROUTING IS PERFORMED
// as we are just going to wrap it in a bunch of Middleware components
// and therefore NOT using an endpoint listener that would the routing for us
//
// Hence, the endpoint should check r.path in its on_request to check wether
// it is adressed or not.
//
// N.B. the EndpointHandler checks if the endpoint turned the request into
// "finished" state, e.g. by sending anything. If the endpoint didn't finish the
// request, the EndpointHandler will pass the request on to the next handler in
// the chain if there is one. See also the EndpointHandler's `breakOnFinish`
// parameter.
//
const HtmlEndpoint = struct {
endpoint: zap.SimpleEndpoint = undefined,
const Self = @This();
pub fn init() Self {
return .{
.endpoint = zap.SimpleEndpoint.init(.{
.path = "/doesn'tmatter",
.get = get,
}),
};
}
pub fn getEndpoint(self: *Self) *zap.SimpleEndpoint {
return &self.endpoint;
}
pub fn get(ep: *zap.SimpleEndpoint, r: zap.SimpleRequest) void {
var self = @fieldParentPtr(Self, "endpoint", ep);
_ = self;
var buf: [1024]u8 = undefined;
var userFound: bool = false;
var sessionFound: bool = false;
// the EndpointHandler set this for us!
// we got middleware context!!!
const maybe_context: ?*Context = r.getUserContext(Context);
if (maybe_context) |context| {
std.debug.print("\n\nHtmlEndpoint: handling request with context: {any}\n\n", .{context});
if (context.user) |user| {
userFound = true;
if (context.session) |session| {
sessionFound = true;
std.debug.assert(r.isFinished() == false);
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;
std.debug.assert(r.isFinished() == true);
return;
}
}
}
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;
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{
.thread_safe = true,
}){};
var allocator = gpa.allocator();
SharedAllocator.init(allocator);
// create the endpoint
var htmlEndpoint = HtmlEndpoint.init();
// we wrap the endpoint with a middleware handler
var htmlHandler = zap.Middleware.EndpointHandler(Handler, Context).init(
htmlEndpoint.getEndpoint(), // the endpoint
null, // no other handler (we are the last in the chain)
true, // break on finish. See EndpointHandler for this. Not applicable here.
);
// 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,
});
}

View file

@ -66,7 +66,9 @@ pub const http_s = extern struct {
params: FIOBJ,
body: FIOBJ,
udata: ?*anyopaque,
}; // zig-cache/i/e0c8a6e617497ade13de512cbe191f23/include/http.h:153:12: warning: struct demoted to opaque type - has bitfield
};
// zig-cache/i/e0c8a6e617497ade13de512cbe191f23/include/http.h:153:12: warning: struct demoted to opaque type - has bitfield
// typedef struct {
// /** The cookie's name (Symbol). */

View file

@ -80,6 +80,42 @@ pub fn Handler(comptime ContextType: anytype) type {
};
}
/// A convenience handler for artibrary zap.SimpleEndpoint
pub fn EndpointHandler(comptime HandlerType: anytype, comptime ContextType: anytype) type {
return struct {
handler: HandlerType,
endpoint: *zap.SimpleEndpoint,
breakOnFinish: bool,
const Self = @This();
pub fn init(endpoint: *zap.SimpleEndpoint, other: ?*HandlerType, breakOnFinish: bool) Self {
return .{
.handler = HandlerType.init(onRequest, other),
.endpoint = endpoint,
.breakOnFinish = breakOnFinish,
};
}
// we need the handler as a common interface to chain stuff
pub fn getHandler(self: *Self) *HandlerType {
return &self.handler;
}
pub fn onRequest(handler: *HandlerType, r: zap.SimpleRequest, context: *ContextType) bool {
var self = @fieldParentPtr(Self, "handler", handler);
r.setUserContext(context);
self.endpoint.onRequest(r);
// if the request was handled by the endpoint, we may break the chain here
if (r.isFinished() and self.breakOnFinish) {
return true;
}
return self.handler.handleOther(r, context);
}
};
}
pub const Error = error{
InitOnRequestIsNotNull,
};

View file

@ -69,8 +69,48 @@ pub const SimpleRequest = struct {
method: ?[]const u8,
h: [*c]fio.http_s,
/// NEVER touch this field!!!!
/// if you absolutely MUST, then you may provide context here
/// via setUserContext and getUserContext
_user_context: *UserContext,
/// NEVER touch this field!!!!
/// use markAsFinished() and isFinished() instead
/// this is a hack: the listener will put a pointer to this into the udata
/// field of `h`. So copies of the SimpleRequest will all have way to the
/// same instance of this field.
_is_finished_request_global: bool,
/// NEVER touch this field!!!!
/// this is part of the hack.
_is_finished: *bool = undefined,
const UserContext = struct {
user_context: ?*anyopaque = null,
};
const Self = @This();
pub fn markAsFinished(self: *const Self, finished: bool) void {
// we might be a copy
self._is_finished.* = finished;
}
pub fn isFinished(self: *const Self) bool {
// we might be a copy
return self._is_finished.*;
}
pub fn setUserContext(self: *const Self, context: *anyopaque) void {
self._user_context.*.user_context = context;
}
pub fn getUserContext(self: *const Self, comptime Context: type) ?*Context {
if (self._user_context.*.user_context) |ptr| {
return @ptrCast(*Context, @alignCast(@alignOf(*Context), ptr));
} else {
return null;
}
}
pub fn sendBody(self: *const Self, body: []const u8) HttpError!void {
const ret = fio.http_send_body(self.h, @intToPtr(
*anyopaque,
@ -78,6 +118,7 @@ pub const SimpleRequest = struct {
), body.len);
debug("SimpleRequest.sendBody(): ret = {}\n", .{ret});
if (ret == -1) return error.HttpSendBody;
self.markAsFinished(true);
}
pub fn sendJson(self: *const Self, json: []const u8) HttpError!void {
@ -86,6 +127,7 @@ pub const SimpleRequest = struct {
*anyopaque,
@ptrToInt(json.ptr),
), json.len) != 0) return error.HttpSendBody;
self.markAsFinished(true);
} else |err| return err;
}
@ -104,6 +146,7 @@ pub const SimpleRequest = struct {
self.setStatus(if (code) |status| status else .found);
try self.setHeader("Location", path);
try self.sendBody("moved");
self.markAsFinished(true);
}
/// shows how to use the logger
@ -187,6 +230,7 @@ pub const SimpleRequest = struct {
pub fn sendFile(self: *const Self, file_path: []const u8) !void {
if (fio.http_sendfile2(self.h, util.toCharPtr(file_path), file_path.len, null, 0) != 0)
return error.SendFile;
self.markAsFinished(true);
}
/// Attempts to decode the request's body.
@ -575,7 +619,15 @@ pub const SimpleHttpListener = struct {
.body = util.fio2str(r.*.body),
.method = util.fio2str(r.*.method),
.h = r,
._is_finished_request_global = false,
._user_context = undefined,
};
req._is_finished = &req._is_finished_request_global;
var user_context: SimpleRequest.UserContext = .{};
req._user_context = &user_context;
req.markAsFinished(false);
std.debug.assert(l.settings.on_request != null);
if (l.settings.on_request) |on_request| {
// l.settings.on_request.?(req);
@ -592,7 +644,14 @@ pub const SimpleHttpListener = struct {
.body = util.fio2str(r.*.body),
.method = util.fio2str(r.*.method),
.h = r,
._is_finished_request_global = false,
._user_context = undefined,
};
req._is_finished = &req._is_finished_request_global;
var user_context: SimpleRequest.UserContext = .{};
req._user_context = &user_context;
l.settings.on_response.?(req);
}
}
@ -605,8 +664,15 @@ pub const SimpleHttpListener = struct {
.body = util.fio2str(r.*.body),
.method = util.fio2str(r.*.method),
.h = r,
._is_finished_request_global = false,
._user_context = undefined,
};
var zigtarget: []u8 = target[0..target_len];
req._is_finished = &req._is_finished_request_global;
var user_context: SimpleRequest.UserContext = .{};
req._user_context = &user_context;
l.settings.on_upgrade.?(req, zigtarget);
}
}

View file

@ -15,3 +15,4 @@ websockets
userpass_session
sendfile
middleware
middleware_with_endpoint