1
0
Fork 0
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:
renerocksai 2024-01-08 19:26:16 +01:00
parent 3d651229f8
commit 551d033edc
10 changed files with 181 additions and 234 deletions

View file

@ -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);

View file

@ -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,
}),
};

View file

@ -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,

View file

@ -43,15 +43,12 @@ pub const EndpointSettings = struct {
/// The main thread usually continues at the instructions after the call to zap.start().
///
/// ```zig
/// // file: stopendpoint.zig
///
/// pub const Self = @This();
///
/// const StopEndpoint = struct {
/// ep: zap.Endpoint = undefined,
///
/// pub fn init(
/// path: []const u8,
/// ) Self {
/// ) StopEndpoint {
/// return .{
/// .ep = zap.Endpoint.init(.{
/// .path = path,
@ -61,17 +58,17 @@ pub const EndpointSettings = struct {
/// }
///
/// // access the internal Endpoint
/// pub fn endpoint(self: *Self) *zap.Endpoint {
/// pub fn endpoint(self: *StopEndpoint) *zap.Endpoint {
/// return &self.ep;
/// }
///
/// fn get(e: *zap.Endpoint, r: zap.Request) void {
/// const self: *Self = @fieldParentPtr(Self, "ep", e);
/// const self: *StopEndpoint = @fieldParentPtr(StopEndpoint, "ep", e);
/// _ = self;
/// _ = e;
/// _ = r;
/// zap.stop();
/// }
/// };
/// ```
pub const Endpoint = struct {
settings: EndpointSettings,

View file

@ -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 => {

View file

@ -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 {

View file

@ -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});
}

View file

@ -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;
// }

View file

@ -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.

View file

@ -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");