mirror of
https://github.com/zigzap/zap.git
synced 2025-10-20 15:14:08 +00:00
API cleanup #2
-------------- - Middleware: no more MixContexts - zig structs are fine - more documentation (comments -> autodoc). - websocket docs
This commit is contained in:
parent
3d651229f8
commit
551d033edc
10 changed files with 181 additions and 234 deletions
|
@ -10,29 +10,12 @@ 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 },
|
||||
});
|
||||
const Context = struct {
|
||||
user: ?UserMiddleWare.User = null,
|
||||
session: ?SessionMiddleWare.Session = null,
|
||||
};
|
||||
```
|
||||
|
||||
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 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 the example above.
|
||||
|
||||
`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
|
||||
|
@ -62,10 +45,10 @@ Have a look at an excerpt of the example:
|
|||
|
||||
```zig
|
||||
// create a combined context struct
|
||||
const Context = zap.Middleware.MixContexts(.{
|
||||
.{ .name = "?user", .type = UserMiddleWare.User },
|
||||
.{ .name = "?session", .type = SessionMiddleWare.Session },
|
||||
});
|
||||
const Context = struct {
|
||||
user: ?UserMiddleWare.User = null,
|
||||
session: ?SessionMiddleWare.Session = null,
|
||||
};
|
||||
|
||||
// we create a Handler type based on our Context
|
||||
const Handler = zap.Middleware.Handler(Context);
|
||||
|
|
|
@ -20,10 +20,11 @@ const SharedAllocator = struct {
|
|||
};
|
||||
|
||||
// create a combined context struct
|
||||
const Context = zap.Middleware.MixContexts(.{
|
||||
.{ .name = "?user", .type = UserMiddleWare.User },
|
||||
.{ .name = "?session", .type = SessionMiddleWare.Session },
|
||||
});
|
||||
// NOTE: context struct members need to be optionals which default to null!!!
|
||||
const Context = struct {
|
||||
user: ?UserMiddleWare.User = null,
|
||||
session: ?SessionMiddleWare.Session = null,
|
||||
};
|
||||
|
||||
// we create a Handler type based on our Context
|
||||
const Handler = zap.Middleware.Handler(Context);
|
||||
|
@ -143,7 +144,7 @@ const HtmlEndpoint = struct {
|
|||
pub fn init() Self {
|
||||
return .{
|
||||
.ep = zap.Endpoint.init(.{
|
||||
.path = "/doesn'tmatter",
|
||||
.path = "/doesn't+matter",
|
||||
.get = get,
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -114,6 +114,7 @@ fn on_close_websocket(context: ?*Context, uuid: isize) void {
|
|||
std.log.info("websocket closed: {s}", .{message});
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_websocket_message(
|
||||
context: ?*Context,
|
||||
handle: WebSockets.WsHandle,
|
||||
|
|
|
@ -43,35 +43,32 @@ pub const EndpointSettings = struct {
|
|||
/// The main thread usually continues at the instructions after the call to zap.start().
|
||||
///
|
||||
/// ```zig
|
||||
/// // file: stopendpoint.zig
|
||||
/// const StopEndpoint = struct {
|
||||
/// ep: zap.Endpoint = undefined,
|
||||
///
|
||||
/// pub const Self = @This();
|
||||
/// pub fn init(
|
||||
/// path: []const u8,
|
||||
/// ) StopEndpoint {
|
||||
/// return .{
|
||||
/// .ep = zap.Endpoint.init(.{
|
||||
/// .path = path,
|
||||
/// .get = get,
|
||||
/// }),
|
||||
/// };
|
||||
/// }
|
||||
///
|
||||
/// ep: zap.Endpoint = undefined,
|
||||
/// // access the internal Endpoint
|
||||
/// pub fn endpoint(self: *StopEndpoint) *zap.Endpoint {
|
||||
/// return &self.ep;
|
||||
/// }
|
||||
///
|
||||
/// pub fn init(
|
||||
/// path: []const u8,
|
||||
/// ) Self {
|
||||
/// return .{
|
||||
/// .ep = zap.Endpoint.init(.{
|
||||
/// .path = path,
|
||||
/// .get = get,
|
||||
/// }),
|
||||
/// };
|
||||
/// }
|
||||
///
|
||||
/// // access the internal Endpoint
|
||||
/// pub fn endpoint(self: *Self) *zap.Endpoint {
|
||||
/// return &self.ep;
|
||||
/// }
|
||||
///
|
||||
/// fn get(e: *zap.Endpoint, r: zap.Request) void {
|
||||
/// const self: *Self = @fieldParentPtr(Self, "ep", e);
|
||||
/// _ = self;
|
||||
/// _ = e;
|
||||
/// _ = r;
|
||||
/// zap.stop();
|
||||
/// }
|
||||
/// fn get(e: *zap.Endpoint, r: zap.Request) void {
|
||||
/// const self: *StopEndpoint = @fieldParentPtr(StopEndpoint, "ep", e);
|
||||
/// _ = self;
|
||||
/// _ = r;
|
||||
/// zap.stop();
|
||||
/// }
|
||||
/// };
|
||||
/// ```
|
||||
pub const Endpoint = struct {
|
||||
settings: EndpointSettings,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const std = @import("std");
|
||||
const zap = @import("zap.zig");
|
||||
|
||||
/// Authentication Scheme enum: Basic or Bearer.
|
||||
pub const AuthScheme = enum {
|
||||
Basic,
|
||||
Bearer,
|
||||
|
@ -27,6 +28,7 @@ pub const AuthScheme = enum {
|
|||
}
|
||||
};
|
||||
|
||||
/// Used internally: check for presence of the requested auth header.
|
||||
pub fn checkAuthHeader(scheme: AuthScheme, auth_header: []const u8) bool {
|
||||
return switch (scheme) {
|
||||
.Basic => |b| std.mem.startsWith(u8, auth_header, b.str()) and auth_header.len > b.str().len,
|
||||
|
@ -34,6 +36,7 @@ pub fn checkAuthHeader(scheme: AuthScheme, auth_header: []const u8) bool {
|
|||
};
|
||||
}
|
||||
|
||||
/// Used internally: return the requested auth header.
|
||||
pub fn extractAuthHeader(scheme: AuthScheme, r: *const zap.Request) ?[]const u8 {
|
||||
return switch (scheme) {
|
||||
.Basic => |b| r.getHeader(b.headerFieldStrFio()),
|
||||
|
@ -41,6 +44,7 @@ pub fn extractAuthHeader(scheme: AuthScheme, r: *const zap.Request) ?[]const u8
|
|||
};
|
||||
}
|
||||
|
||||
/// Decoding Strategy for Basic Authentication
|
||||
const BasicAuthStrategy = enum {
|
||||
/// decode into user and pass, then check pass
|
||||
UserPass,
|
||||
|
@ -48,20 +52,21 @@ const BasicAuthStrategy = enum {
|
|||
Token68,
|
||||
};
|
||||
|
||||
/// Authentication result
|
||||
pub const AuthResult = enum {
|
||||
/// authentication / authorization was successful
|
||||
AuthOK,
|
||||
/// authentication / authorization failed
|
||||
AuthFailed,
|
||||
/// the authenticator handled the request that didn't pass authentication /
|
||||
/// authorization .
|
||||
/// this is used to implement authenticators that redirect to a login
|
||||
/// The authenticator handled the request that didn't pass authentication /
|
||||
/// authorization.
|
||||
/// This is used to implement authenticators that redirect to a login
|
||||
/// page. An AuthenticatingEndpoint will not do the default, which is trying
|
||||
/// to call the `unauthorized` callback or.
|
||||
/// to call the `unauthorized` callback if one exists orelse ignore the request.
|
||||
Handled,
|
||||
};
|
||||
|
||||
/// HTTP Basic Authentication RFC 7617
|
||||
/// HTTP Basic Authentication RFC 7617.
|
||||
/// "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
|
||||
/// user-pass strings: "$username:$password" -> base64
|
||||
///
|
||||
|
@ -73,10 +78,9 @@ pub const AuthResult = enum {
|
|||
/// Errors:
|
||||
/// WWW-Authenticate: Basic realm="this"
|
||||
///
|
||||
/// T : any kind of map that implements get([]const u8) -> []const u8
|
||||
/// Lookup : any kind of map that implements get([]const u8) -> []const u8
|
||||
pub fn BasicAuth(comptime Lookup: type, comptime kind: BasicAuthStrategy) type {
|
||||
return struct {
|
||||
// kind: BasicAuthStrategy,
|
||||
allocator: std.mem.Allocator,
|
||||
realm: ?[]const u8,
|
||||
lookup: *Lookup,
|
||||
|
@ -87,23 +91,24 @@ pub fn BasicAuth(comptime Lookup: type, comptime kind: BasicAuthStrategy) type {
|
|||
/// different implementations can
|
||||
/// - either decode, lookup and compare passwords
|
||||
/// - or just check for existence of the base64-encoded user:pass combination
|
||||
/// if realm is provided (not null), a copy is taken -> call deinit() to clean up
|
||||
/// if realm is provided (not null), a copy of it is taken -> call deinit() to clean up
|
||||
pub fn init(allocator: std.mem.Allocator, lookup: *Lookup, realm: ?[]const u8) !Self {
|
||||
return .{
|
||||
// .kind = kind,
|
||||
.allocator = allocator,
|
||||
.lookup = lookup,
|
||||
.realm = if (realm) |the_realm| try allocator.dupe(u8, the_realm) else null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Deinit the authenticator.
|
||||
pub fn deinit(self: *Self) void {
|
||||
if (self.realm) |the_realm| {
|
||||
self.allocator.free(the_realm);
|
||||
}
|
||||
}
|
||||
|
||||
/// Use this to decode the auth_header into user:pass, lookup pass in lookup
|
||||
/// Use this to decode the auth_header into user:pass, lookup pass in lookup.
|
||||
/// Note: usually, you don't want to use this; you'd go for `authenticateRequest()`.
|
||||
pub fn authenticateUserPass(self: *Self, auth_header: []const u8) AuthResult {
|
||||
zap.debug("AuthenticateUserPass\n", .{});
|
||||
const encoded = auth_header[AuthScheme.Basic.str().len..];
|
||||
|
@ -165,21 +170,27 @@ pub fn BasicAuth(comptime Lookup: type, comptime kind: BasicAuthStrategy) type {
|
|||
return .AuthFailed;
|
||||
}
|
||||
|
||||
/// Use this to just look up if the base64-encoded auth_header exists in lookup
|
||||
/// Use this to just look up if the base64-encoded auth_header exists in lookup.
|
||||
/// Note: usually, you don't want to use this; you'd go for `authenticateRequest()`.
|
||||
pub fn authenticateToken68(self: *Self, auth_header: []const u8) AuthResult {
|
||||
const token = auth_header[AuthScheme.Basic.str().len..];
|
||||
return if (self.lookup.*.contains(token)) .AuthOK else .AuthFailed;
|
||||
}
|
||||
|
||||
// dispatch based on kind
|
||||
/// dispatch based on kind (.UserPass / .Token689) and try to authenticate based on the header.
|
||||
/// Note: usually, you don't want to use this; you'd go for `authenticateRequest()`.
|
||||
pub fn authenticate(self: *Self, auth_header: []const u8) AuthResult {
|
||||
zap.debug("AUTHENTICATE\n", .{});
|
||||
// switch (self.kind) {
|
||||
switch (kind) {
|
||||
.UserPass => return self.authenticateUserPass(auth_header),
|
||||
.Token68 => return self.authenticateToken68(auth_header),
|
||||
}
|
||||
}
|
||||
|
||||
/// The zap authentication request handler.
|
||||
///
|
||||
/// Tries to extract the authentication header and perform the authentication.
|
||||
/// If no authentication header is found, an authorization header is tried.
|
||||
pub fn authenticateRequest(self: *Self, r: *const zap.Request) AuthResult {
|
||||
zap.debug("AUTHENTICATE REQUEST\n", .{});
|
||||
if (extractAuthHeader(.Basic, r)) |auth_header| {
|
||||
|
@ -215,10 +226,9 @@ pub const BearerAuthSingle = struct {
|
|||
|
||||
const Self = @This();
|
||||
|
||||
/// Creates a Single-Token Bearer Authenticator
|
||||
/// takes a copy of the token
|
||||
/// if realm is provided (not null), a copy is taken
|
||||
/// call deinit() to clean up
|
||||
/// Creates a Single-Token Bearer Authenticator.
|
||||
/// Takes a copy of the token.
|
||||
/// If realm is provided (not null), a copy is taken call deinit() to clean up.
|
||||
pub fn init(allocator: std.mem.Allocator, token: []const u8, realm: ?[]const u8) !Self {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
|
@ -226,6 +236,9 @@ pub const BearerAuthSingle = struct {
|
|||
.realm = if (realm) |the_realm| try allocator.dupe(u8, the_realm) else null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Try to authenticate based on the header.
|
||||
/// Note: usually, you don't want to use this; you'd go for `authenticateRequest()`.
|
||||
pub fn authenticate(self: *Self, auth_header: []const u8) AuthResult {
|
||||
if (checkAuthHeader(.Bearer, auth_header) == false) {
|
||||
return .AuthFailed;
|
||||
|
@ -234,6 +247,9 @@ pub const BearerAuthSingle = struct {
|
|||
return if (std.mem.eql(u8, token, self.token)) .AuthOK else .AuthFailed;
|
||||
}
|
||||
|
||||
/// The zap authentication request handler.
|
||||
///
|
||||
/// Tries to extract the authentication header and perform the authentication.
|
||||
pub fn authenticateRequest(self: *Self, r: *const zap.Request) AuthResult {
|
||||
if (extractAuthHeader(.Bearer, r)) |auth_header| {
|
||||
return self.authenticate(auth_header);
|
||||
|
@ -241,6 +257,7 @@ pub const BearerAuthSingle = struct {
|
|||
return .AuthFailed;
|
||||
}
|
||||
|
||||
/// Deinits the authenticator.
|
||||
pub fn deinit(self: *Self) void {
|
||||
if (self.realm) |the_realm| {
|
||||
self.allocator.free(the_realm);
|
||||
|
@ -267,9 +284,9 @@ pub fn BearerAuthMulti(comptime Lookup: type) type {
|
|||
|
||||
const Self = @This();
|
||||
|
||||
/// Creates a BasicAuth. `lookup` must implement `.get([]const u8) -> []const u8`
|
||||
/// to look up tokens
|
||||
/// if realm is provided (not null), a copy is taken -> call deinit() to clean up
|
||||
/// Creates a Multi Token Bearer Authenticator. `lookup` must implement
|
||||
/// `.get([]const u8) -> []const u8` to look up tokens.
|
||||
/// If realm is provided (not null), a copy of it is taken -> call deinit() to clean up.
|
||||
pub fn init(allocator: std.mem.Allocator, lookup: *Lookup, realm: ?[]const u8) !Self {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
|
@ -278,12 +295,16 @@ pub fn BearerAuthMulti(comptime Lookup: type) type {
|
|||
};
|
||||
}
|
||||
|
||||
/// Deinit the authenticator. Only required if a realm was provided at
|
||||
/// init() time.
|
||||
pub fn deinit(self: *Self) void {
|
||||
if (self.realm) |the_realm| {
|
||||
self.allocator.free(the_realm);
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to authenticate based on the header.
|
||||
/// Note: usually, you don't want to use this; you'd go for `authenticateRequest()`.
|
||||
pub fn authenticate(self: *Self, auth_header: []const u8) AuthResult {
|
||||
if (checkAuthHeader(.Bearer, auth_header) == false) {
|
||||
return .AuthFailed;
|
||||
|
@ -292,6 +313,9 @@ pub fn BearerAuthMulti(comptime Lookup: type) type {
|
|||
return if (self.lookup.*.contains(token)) .AuthOK else .AuthFailed;
|
||||
}
|
||||
|
||||
/// The zap authentication request handler.
|
||||
///
|
||||
/// Tries to extract the authentication header and perform the authentication.
|
||||
pub fn authenticateRequest(self: *Self, r: *const zap.Request) AuthResult {
|
||||
if (extractAuthHeader(.Bearer, r)) |auth_header| {
|
||||
return self.authenticate(auth_header);
|
||||
|
@ -301,6 +325,7 @@ pub fn BearerAuthMulti(comptime Lookup: type) type {
|
|||
};
|
||||
}
|
||||
|
||||
/// Settings to initialize a UserPassSessionAuth authenticator.
|
||||
pub const UserPassSessionAuthArgs = struct {
|
||||
/// username body parameter
|
||||
usernameParam: []const u8,
|
||||
|
@ -308,7 +333,7 @@ pub const UserPassSessionAuthArgs = struct {
|
|||
passwordParam: []const u8,
|
||||
/// redirect to this page if auth fails
|
||||
loginPage: []const u8,
|
||||
/// name of the cookie
|
||||
/// name of the auth cookie
|
||||
cookieName: []const u8,
|
||||
/// cookie max age in seconds; 0 -> session cookie
|
||||
cookieMaxAge: u8 = 0,
|
||||
|
@ -330,8 +355,8 @@ pub const UserPassSessionAuthArgs = struct {
|
|||
///
|
||||
/// Please note the implications of this simple approach: IF YOU REUSE "username"
|
||||
/// and "password" body params for anything else in your application, then the
|
||||
/// mechanisms described above will kick in. For that reason: please know what you're
|
||||
/// doing.
|
||||
/// mechanisms described above will still kick in. For that reason: please know what
|
||||
/// you're doing.
|
||||
///
|
||||
/// See UserPassSessionAuthArgs:
|
||||
/// - username & password param names can be defined by you
|
||||
|
@ -357,7 +382,7 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
|
|||
lookup: *Lookup,
|
||||
settings: UserPassSessionAuthArgs,
|
||||
|
||||
// TODO: cookie store per user
|
||||
// TODO: cookie store per user?
|
||||
sessionTokens: SessionTokenMap,
|
||||
passwordLookupLock: std.Thread.Mutex = .{},
|
||||
tokenLookupLock: std.Thread.Mutex = .{},
|
||||
|
@ -368,6 +393,8 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
|
|||
|
||||
const Token = [Hash.digest_length * 2]u8;
|
||||
|
||||
/// Construct this authenticator. See above and related types for more
|
||||
/// information.
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
lookup: *Lookup,
|
||||
|
@ -390,6 +417,7 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
|
|||
return ret;
|
||||
}
|
||||
|
||||
/// De-init this authenticator.
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.allocator.free(self.settings.usernameParam);
|
||||
self.allocator.free(self.settings.passwordParam);
|
||||
|
@ -406,7 +434,8 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
|
|||
|
||||
/// Check for session token cookie, remove the token from the valid tokens
|
||||
pub fn logout(self: *Self, r: *const zap.Request) void {
|
||||
// we erase the list of valid tokens server-side
|
||||
// we erase the list of valid tokens server-side (later) and set the
|
||||
// cookie to "invalid" on the client side.
|
||||
if (r.setCookie(.{
|
||||
.name = self.settings.cookieName,
|
||||
.value = "invalid",
|
||||
|
@ -530,6 +559,9 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
|
|||
return .AuthFailed;
|
||||
}
|
||||
|
||||
/// The zap authentication request handler.
|
||||
///
|
||||
/// See above for how it works.
|
||||
pub fn authenticateRequest(self: *Self, r: *const zap.Request) AuthResult {
|
||||
switch (self._internal_authenticateRequest(r)) {
|
||||
.AuthOK => {
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
const std = @import("std");
|
||||
|
||||
// TODO: rework logging in zap
|
||||
|
||||
debugOn: bool,
|
||||
|
||||
/// Access to facil.io's logging facilities
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(comptime debug: bool) Self {
|
||||
|
|
|
@ -1,43 +1,14 @@
|
|||
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) &@as(fieldType, null) else null,
|
||||
.is_comptime = false,
|
||||
.alignment = 0,
|
||||
};
|
||||
}
|
||||
return @Type(.{
|
||||
.Struct = .{
|
||||
.layout = .Auto,
|
||||
.fields = fields[0..],
|
||||
.decls = &[_]std.builtin.Type.Declaration{},
|
||||
.is_tuple = false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// Your middleware components need to contain a handler
|
||||
/// Your middleware components need to contain a handler.
|
||||
///
|
||||
/// A Handler is one element in the chain of request handlers that will be tried
|
||||
/// by the listener when a request arrives. Handlers indicate to the previous
|
||||
/// handler whether they processed a request by returning `true` from their
|
||||
/// `on_request` function, in which case a typical request handler would stop
|
||||
/// trying to pass the request on to the next handler in the chain. See
|
||||
/// the `handle_other` function in this struct.
|
||||
pub fn Handler(comptime ContextType: anytype) type {
|
||||
return struct {
|
||||
other_handler: ?*Self = null,
|
||||
|
@ -56,7 +27,7 @@ pub fn Handler(comptime ContextType: anytype) type {
|
|||
};
|
||||
}
|
||||
|
||||
// example for handling request
|
||||
// example for handling a request request
|
||||
// which you can use in your components, e.g.:
|
||||
// return self.handler.handleOther(r, context);
|
||||
pub fn handleOther(self: *Self, r: zap.Request, context: *ContextType) bool {
|
||||
|
@ -89,6 +60,9 @@ pub fn EndpointHandler(comptime HandlerType: anytype, comptime ContextType: anyt
|
|||
|
||||
const Self = @This();
|
||||
|
||||
/// Create an endpointhandler from an endpoint and pass in the next (other) handler in the chain.
|
||||
/// If `breakOnFinish` is `true`, the handler will stop handing requests down the chain if
|
||||
/// the endpoint processed the request.
|
||||
pub fn init(endpoint: *zap.Endpoint, other: ?*HandlerType, breakOnFinish: bool) Self {
|
||||
return .{
|
||||
.handler = HandlerType.init(onRequest, other),
|
||||
|
@ -97,11 +71,16 @@ pub fn EndpointHandler(comptime HandlerType: anytype, comptime ContextType: anyt
|
|||
};
|
||||
}
|
||||
|
||||
// we need the handler as a common interface to chain stuff
|
||||
/// Provides the handler as a common interface to chain stuff
|
||||
pub fn getHandler(self: *Self) *HandlerType {
|
||||
return &self.handler;
|
||||
}
|
||||
|
||||
/// The Handler's request handling function. Gets called from the listener
|
||||
/// with the request and a context instance. Calls the endpoint.
|
||||
///
|
||||
/// If `breakOnFinish` is `true`, the handler will stop handing requests down the chain if
|
||||
/// the endpoint processed the request.
|
||||
pub fn onRequest(handler: *HandlerType, r: zap.Request, context: *ContextType) bool {
|
||||
var self = @fieldParentPtr(Self, "handler", handler);
|
||||
r.setUserContext(context);
|
||||
|
@ -117,6 +96,8 @@ pub fn EndpointHandler(comptime HandlerType: anytype, comptime ContextType: anyt
|
|||
}
|
||||
|
||||
pub const Error = error{
|
||||
/// The listener could not be created because the settings provided to its
|
||||
/// init() function contained an `on_request` callback that was not null.
|
||||
InitOnRequestIsNotNull,
|
||||
};
|
||||
|
||||
|
@ -134,8 +115,9 @@ pub fn Listener(comptime ContextType: anytype) type {
|
|||
|
||||
const Self = @This();
|
||||
|
||||
/// initialize the middleware handler
|
||||
/// the passed in settings must have on_request set to null
|
||||
/// Construct and initialize a middleware handler.
|
||||
/// The passed in settings must have on_request set to null! If that is
|
||||
/// not the case, an InitOnRequestIsNotNull error will be returned.
|
||||
pub fn init(settings: zap.HttpListenerSettings, initial_handler: *Handler(ContextType), request_alloc: ?RequestAllocatorFn) Error!Self {
|
||||
// override on_request with ourselves
|
||||
if (settings.on_request != null) {
|
||||
|
@ -147,20 +129,25 @@ pub fn Listener(comptime ContextType: anytype) type {
|
|||
var ret: Self = .{
|
||||
.settings = settings,
|
||||
};
|
||||
|
||||
ret.settings.on_request = onRequest;
|
||||
ret.listener = zap.HttpListener.init(ret.settings);
|
||||
handler = initial_handler;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// Start listening.
|
||||
pub fn listen(self: *Self) !void {
|
||||
try self.listener.listen();
|
||||
}
|
||||
|
||||
// this is just a reference implementation
|
||||
// but it's actually used obviously. Create your own listener if you
|
||||
// want different behavior.
|
||||
// Didn't want to make this a callback
|
||||
/// The listener's request handler, stepping through the chain of Handlers
|
||||
/// by calling the initial one which takes it from there.
|
||||
///
|
||||
/// This is just a reference implementation that you can use by default.
|
||||
/// Create your own listener if you want different behavior.
|
||||
/// (Didn't want to make this a callback. Submit an issue if you really
|
||||
/// think that's an issue).
|
||||
pub fn onRequest(r: zap.Request) void {
|
||||
// we are the 1st handler in the chain, so we create a context
|
||||
var context: ContextType = .{};
|
||||
|
@ -182,60 +169,3 @@ pub fn Listener(comptime ContextType: anytype) type {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
const 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
|
||||
const nonOpts: NonOpts = .{
|
||||
.user = &user,
|
||||
.session = &session,
|
||||
};
|
||||
std.debug.print("nonOpts = {any}\n", .{nonOpts});
|
||||
}
|
||||
|
|
62
src/util.zig
62
src/util.zig
|
@ -1,9 +1,10 @@
|
|||
const std = @import("std");
|
||||
const fio = @import("fio.zig");
|
||||
|
||||
/// Used internally: convert a FIO object into its string representation.
|
||||
/// note: since this is called from within request functions, we don't make
|
||||
/// copies. Also, we return temp memory from fio. -> don't hold on to it outside
|
||||
/// of a request function
|
||||
/// of a request function. FIO temp memory strings do not need to be freed.
|
||||
pub fn fio2str(o: fio.FIOBJ) ?[]const u8 {
|
||||
if (o == 0) return null;
|
||||
const x: fio.fio_str_info_s = fio.fiobj_obj2cstr(o);
|
||||
|
@ -12,6 +13,12 @@ pub fn fio2str(o: fio.FIOBJ) ?[]const u8 {
|
|||
return x.data[0..x.len];
|
||||
}
|
||||
|
||||
/// A "string" type used internally that carries a flag whether its buffer needs
|
||||
/// to be freed or not - and honors it in `deinit()`. That way, it's always
|
||||
/// safe to call deinit().
|
||||
/// For instance, slices taken directly from the zap.Request need not be freed.
|
||||
/// But the ad-hoc created string representation of a float parameter must be
|
||||
/// freed after use.
|
||||
pub const FreeOrNot = struct {
|
||||
str: []const u8,
|
||||
freeme: bool,
|
||||
|
@ -24,6 +31,9 @@ pub const FreeOrNot = struct {
|
|||
}
|
||||
};
|
||||
|
||||
/// Used internally: convert a FIO object into its string representation.
|
||||
/// Depending on the type of the object, a buffer will be created. Hence a
|
||||
/// FreeOrNot type is used as the return type.
|
||||
pub fn fio2strAllocOrNot(o: fio.FIOBJ, a: std.mem.Allocator, always_alloc: bool) !FreeOrNot {
|
||||
if (o == 0) return .{ .str = "null", .freeme = false };
|
||||
if (o == fio.FIOBJ_INVALID) return .{ .str = "invalid", .freeme = false };
|
||||
|
@ -38,6 +48,8 @@ pub fn fio2strAllocOrNot(o: fio.FIOBJ, a: std.mem.Allocator, always_alloc: bool)
|
|||
else => .{ .str = "unknown_type", .freeme = false },
|
||||
};
|
||||
}
|
||||
|
||||
/// Used internally: convert a zig slice into a FIO string.
|
||||
pub fn str2fio(s: []const u8) fio.fio_str_info_s {
|
||||
return .{
|
||||
.data = toCharPtr(s),
|
||||
|
@ -46,6 +58,7 @@ pub fn str2fio(s: []const u8) fio.fio_str_info_s {
|
|||
};
|
||||
}
|
||||
|
||||
/// Used internally: convert a zig slice into a C pointer
|
||||
pub fn toCharPtr(s: []const u8) [*c]u8 {
|
||||
return @as([*c]u8, @ptrFromInt(@intFromPtr(s.ptr)));
|
||||
}
|
||||
|
@ -54,7 +67,8 @@ pub fn toCharPtr(s: []const u8) [*c]u8 {
|
|||
// JSON helpers
|
||||
//
|
||||
|
||||
/// provide your own buf, NOT mutex-protected!
|
||||
/// Concenience: format an arbitrary value into a JSON string buffer.
|
||||
/// Provide your own buf; this function is NOT mutex-protected!
|
||||
pub fn stringifyBuf(
|
||||
buffer: []u8,
|
||||
value: anytype,
|
||||
|
@ -68,47 +82,3 @@ pub fn stringifyBuf(
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// deprecated:
|
||||
|
||||
// 1MB JSON buffer
|
||||
// var jsonbuf: [1024 * 1024]u8 = undefined;
|
||||
// var mutex: std.Thread.Mutex = .{};
|
||||
|
||||
// use default 1MB buffer, mutex-protected
|
||||
// pub fn stringify(
|
||||
// value: anytype,
|
||||
// options: std.json.StringifyOptions,
|
||||
// ) ?[]const u8 {
|
||||
// mutex.lock();
|
||||
// defer mutex.unlock();
|
||||
// var fba = std.heap.FixedBufferAllocator.init(&jsonbuf);
|
||||
// var string = std.ArrayList(u8).init(fba.allocator());
|
||||
// if (std.json.stringify(value, options, string.writer())) {
|
||||
// return string.items;
|
||||
// } else |_| { // error
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
|
||||
// use default 1MB buffer, mutex-protected
|
||||
// pub fn stringifyArrayList(
|
||||
// comptime T: anytype,
|
||||
// list: *std.ArrayList(T),
|
||||
// options: std.json.StringifyOptions,
|
||||
// ) !?[]const u8 {
|
||||
// mutex.lock();
|
||||
// defer mutex.unlock();
|
||||
// var fba = std.heap.FixedBufferAllocator.init(&jsonbuf);
|
||||
// var string = std.ArrayList(u8).init(fba.allocator());
|
||||
// var writer = string.writer();
|
||||
// try writer.writeByte('[');
|
||||
// var first: bool = true;
|
||||
// for (list.items) |user| {
|
||||
// if (!first) try writer.writeByte(',');
|
||||
// first = false;
|
||||
// try std.json.stringify(user, options, string.writer());
|
||||
// }
|
||||
// try writer.writeByte(']');
|
||||
// return string.items;
|
||||
// }
|
||||
|
|
|
@ -3,10 +3,15 @@ const zap = @import("zap.zig");
|
|||
const fio = @import("fio.zig");
|
||||
const util = @import("util.zig");
|
||||
|
||||
/// The Handle type used for WebSocket connections. Do not mess with this.
|
||||
pub const WsHandle = ?*fio.ws_s;
|
||||
|
||||
/// WebSocket Handler. Pass in a Context type and it will give you a struct that
|
||||
/// contains all the types and functions you need. See the websocket example
|
||||
/// for more details.
|
||||
pub fn Handler(comptime ContextType: type) type {
|
||||
return struct {
|
||||
/// OnMessage Callback on a websocket
|
||||
/// OnMessage Callback on a websocket, type.
|
||||
pub const WsOnMessageFn = *const fn (
|
||||
/// user-provided context, passed in from websocketHttpUpgrade()
|
||||
context: ?*ContextType,
|
||||
|
@ -18,14 +23,16 @@ pub fn Handler(comptime ContextType: type) type {
|
|||
is_text: bool,
|
||||
) void;
|
||||
|
||||
/// Callback when websocket is closed. uuid is a connection identifier,
|
||||
/// Callback (type) when websocket is closed. uuid is a connection identifier,
|
||||
/// it is -1 if a connection could not be established
|
||||
pub const WsOnCloseFn = *const fn (context: ?*ContextType, uuid: isize) void;
|
||||
|
||||
/// A websocket callback function. provides the context passed in at
|
||||
/// A websocket callback function type. provides the context passed in at
|
||||
/// websocketHttpUpgrade().
|
||||
pub const WsFn = *const fn (context: ?*ContextType, handle: WsHandle) void;
|
||||
|
||||
/// Websocket connection handler creation settings. Provide the callbacks you need,
|
||||
/// and an optional context.
|
||||
pub const WebSocketSettings = struct {
|
||||
/// on_message(context, handle, message, is_text)
|
||||
on_message: ?WsOnMessageFn = null,
|
||||
|
@ -102,12 +109,13 @@ pub fn Handler(comptime ContextType: type) type {
|
|||
}
|
||||
}
|
||||
|
||||
const WebSocketError = error{
|
||||
pub const WebSocketError = error{
|
||||
WriteError,
|
||||
UpgradeError,
|
||||
SubscribeError,
|
||||
};
|
||||
|
||||
/// Write to the websocket identified by the handle.
|
||||
pub inline fn write(handle: WsHandle, message: []const u8, is_text: bool) WebSocketError!void {
|
||||
if (fio.websocket_write(
|
||||
handle,
|
||||
|
@ -118,21 +126,26 @@ pub fn Handler(comptime ContextType: type) type {
|
|||
}
|
||||
}
|
||||
|
||||
/// The context pointer is stored in facilio's udata pointer. Use
|
||||
/// this function to turn that pointer into a pointer to your
|
||||
/// ContextType.
|
||||
pub fn udataToContext(udata: *anyopaque) *ContextType {
|
||||
return @as(*ContextType, @ptrCast(@alignCast(udata)));
|
||||
}
|
||||
|
||||
/// Close the websocket connection.
|
||||
pub inline fn close(handle: WsHandle) void {
|
||||
fio.websocket_close(handle);
|
||||
}
|
||||
|
||||
/// Settings for publishing a message in a channel.
|
||||
const PublishArgs = struct {
|
||||
channel: []const u8,
|
||||
message: []const u8,
|
||||
is_json: bool = false,
|
||||
};
|
||||
|
||||
/// publish a message in a channel
|
||||
/// Publish a message in a channel.
|
||||
pub inline fn publish(args: PublishArgs) void {
|
||||
fio.fio_publish(.{
|
||||
.channel = util.str2fio(args.channel),
|
||||
|
@ -141,12 +154,19 @@ pub fn Handler(comptime ContextType: type) type {
|
|||
});
|
||||
}
|
||||
|
||||
/// Type for callback on subscription message.
|
||||
pub const SubscriptionOnMessageFn = *const fn (context: ?*ContextType, handle: WsHandle, channel: []const u8, message: []const u8) void;
|
||||
|
||||
/// Type for callback on unsubscribe message.
|
||||
pub const SubscriptionOnUnsubscribeFn = *const fn (context: ?*ContextType) void;
|
||||
|
||||
/// Settings for subscribing to a channel.
|
||||
pub const SubscribeArgs = struct {
|
||||
/// channel name
|
||||
channel: []const u8,
|
||||
/// on message callback
|
||||
on_message: ?SubscriptionOnMessageFn = null,
|
||||
/// on unsubscribe callback
|
||||
on_unsubscribe: ?SubscriptionOnUnsubscribeFn = null,
|
||||
/// this is not wrapped nicely yet
|
||||
match: fio.fio_match_fn = null,
|
||||
|
@ -162,9 +182,11 @@ pub fn Handler(comptime ContextType: type) type {
|
|||
/// above ~32Kb might be assumed to be binary rather than tested. force_binary has
|
||||
/// precedence over force_text.
|
||||
force_text: bool = false,
|
||||
/// your provided arbitrary context
|
||||
context: ?*ContextType = null,
|
||||
};
|
||||
|
||||
/// Subscribe to a channel.
|
||||
/// Returns a subscription ID on success and 0 on failure.
|
||||
/// we copy the pointer so make sure the struct stays valid.
|
||||
/// we need it to look up the ziggified callbacks.
|
||||
|
|
|
@ -13,7 +13,15 @@ pub usingnamespace @import("util.zig");
|
|||
pub usingnamespace @import("http.zig");
|
||||
pub usingnamespace @import("mustache.zig");
|
||||
pub usingnamespace @import("http_auth.zig");
|
||||
|
||||
/// Middleware support.
|
||||
/// Contains a special Listener and a Handler struct that support chaining
|
||||
/// requests handlers, with an optional stop once a handler indicates it
|
||||
/// processed the request. Also sports an EndpointHandler for using regular zap
|
||||
/// Endpoints as Handlers.
|
||||
pub const Middleware = @import("middleware.zig");
|
||||
|
||||
/// Websocket API
|
||||
pub const WebSockets = @import("websockets.zig");
|
||||
|
||||
pub const Log = @import("log.zig");
|
||||
|
|
Loading…
Add table
Reference in a new issue