Skip to content

Commit

Permalink
zap.Middleware.EndpointHandler, r.get/setContext()
Browse files Browse the repository at this point in the history
  • Loading branch information
renerocksai committed May 20, 2023
1 parent e8c9e86 commit 35144b1
Show file tree
Hide file tree
Showing 9 changed files with 358 additions and 2 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.{
.name = "zap",
.version = "0.0.22",
.version = "0.0.23",

.dependencies = .{
.@"facil.io" = .{
Expand Down
2 changes: 2 additions & 0 deletions examples/middleware/middleware.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}
}
Expand Down
246 changes: 246 additions & 0 deletions examples/middleware_with_endpoint/middleware_with_endpoint.zig
Original file line number Diff line number Diff line change
@@ -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 = "[email protected]",
};

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,
});
}
4 changes: 3 additions & 1 deletion src/fio.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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). */
Expand Down
36 changes: 36 additions & 0 deletions src/middleware.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
Loading

0 comments on commit 35144b1

Please sign in to comment.