diff --git a/doc/zig-ception.md b/doc/zig-ception.md index 4d70551..cdea9d8 100644 --- a/doc/zig-ception.md +++ b/doc/zig-ception.md @@ -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); diff --git a/examples/middleware_with_endpoint/middleware_with_endpoint.zig b/examples/middleware_with_endpoint/middleware_with_endpoint.zig index e9b5e2d..9e4b6c8 100644 --- a/examples/middleware_with_endpoint/middleware_with_endpoint.zig +++ b/examples/middleware_with_endpoint/middleware_with_endpoint.zig @@ -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, }), }; diff --git a/examples/websockets/websockets.zig b/examples/websockets/websockets.zig index 06b1b17..fea0933 100644 --- a/examples/websockets/websockets.zig +++ b/examples/websockets/websockets.zig @@ -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, diff --git a/src/endpoint.zig b/src/endpoint.zig index 1d05091..e61c591 100644 --- a/src/endpoint.zig +++ b/src/endpoint.zig @@ -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, diff --git a/src/http_auth.zig b/src/http_auth.zig index 78e7fb8..3a49bf1 100644 --- a/src/http_auth.zig +++ b/src/http_auth.zig @@ -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 => { diff --git a/src/log.zig b/src/log.zig index 4c66c53..210eefc 100644 --- a/src/log.zig +++ b/src/log.zig @@ -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 { diff --git a/src/middleware.zig b/src/middleware.zig index 7b2dfdb..4c320dd 100644 --- a/src/middleware.zig +++ b/src/middleware.zig @@ -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 }); - } - - var 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 - var nonOpts: NonOpts = .{ - .user = &user, - .session = &session, - }; - std.debug.print("nonOpts = {any}\n", .{nonOpts}); -} diff --git a/src/util.zig b/src/util.zig index acd5022..71ebc2a 100644 --- a/src/util.zig +++ b/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; -// } diff --git a/src/websockets.zig b/src/websockets.zig index 1fef326..0e46270 100644 --- a/src/websockets.zig +++ b/src/websockets.zig @@ -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. diff --git a/src/zap.zig b/src/zap.zig index 49dc531..e070625 100644 --- a/src/zap.zig +++ b/src/zap.zig @@ -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");