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:
parent
e8c9e866cd
commit
35144b195e
9 changed files with 358 additions and 2 deletions
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.{
|
||||
.name = "zap",
|
||||
.version = "0.0.22",
|
||||
.version = "0.0.23",
|
||||
|
||||
.dependencies = .{
|
||||
.@"facil.io" = .{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
246
examples/middleware_with_endpoint/middleware_with_endpoint.zig
Normal file
246
examples/middleware_with_endpoint/middleware_with_endpoint.zig
Normal 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,
|
||||
});
|
||||
}
|
|
@ -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). */
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
66
src/zap.zig
66
src/zap.zig
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,3 +15,4 @@ websockets
|
|||
userpass_session
|
||||
sendfile
|
||||
middleware
|
||||
middleware_with_endpoint
|
||||
|
|
Loading…
Add table
Reference in a new issue