1
0
Fork 0
mirror of https://github.com/zigzap/zap.git synced 2025-10-20 15:14:08 +00:00
zap/src/endpoint.zig
2025-03-21 20:08:36 +01:00

349 lines
13 KiB
Zig

//! Endpoint and supporting types.
//!
//! An Endpoint can be any zig struct that defines all the callbacks lilsted
//! below.
//! Pass an instance of an Endpoint struct to zap.Endpoint.Listener.register()
//! function to register with the listener.
//!
//! **NOTE**: Endpoints must implement the following "interface":
//!
//! ```zig
//! /// The http request path / slug of the endpoint
//! path: []const u8,
//!
//! /// Handlers by request method:
//! pub fn get(_: *Self, _: zap.Request) void {}
//! pub fn post(_: *Self, _: zap.Request) void {}
//! pub fn put(_: *Self, _: zap.Request) void {}
//! pub fn delete(_: *Self, _: zap.Request) void {}
//! pub fn patch(_: *Self, _: zap.Request) void {}
//! pub fn options(_: *Self, _: zap.Request) void {}
//!
//! // optional, if auth stuff is used:
//! pub fn unauthorized(_: *Self, _: zap.Request) void {}
//! ```
//!
//! Example:
//! A simple endpoint listening on the /stop route that shuts down zap. The
//! main thread usually continues at the instructions after the call to
//! zap.start().
//!
//! ```zig
//! const StopEndpoint = struct {
//!
//! pub fn init( path: []const u8,) StopEndpoint {
//! return .{
//! .path = path,
//! };
//! }
//!
//! pub fn post(_: *StopEndpoint, _: zap.Request) void {}
//! pub fn put(_: *StopEndpoint, _: zap.Request) void {}
//! pub fn delete(_: *StopEndpoint, _: zap.Request) void {}
//! pub fn patch(_: *StopEndpoint, _: zap.Request) void {}
//! pub fn options(_: *StopEndpoint, _: zap.Request) void {}
//!
//! pub fn get(self: *StopEndpoint, r: zap.Request) void {
//! _ = self;
//! _ = r;
//! zap.stop();
//! }
//! };
//! ```
const std = @import("std");
const zap = @import("zap.zig");
const auth = @import("http_auth.zig");
/// Endpoint request error handling strategy
pub const ErrorStrategy = enum {
/// log errors to console
log_to_console,
/// log errors to console AND generate a HTML response
log_to_response,
/// raise errors -> TODO: clarify: where can they be caught? in App.run()
raise,
};
// zap types
const Request = zap.Request;
const ListenerSettings = zap.HttpListenerSettings;
const HttpListener = zap.HttpListener;
pub fn checkEndpointType(T: type) void {
if (@hasField(T, "path")) {
if (@FieldType(T, "path") != []const u8) {
@compileError(@typeName(@FieldType(T, "path")) ++ " has wrong type, expected: []const u8");
}
} else {
@compileError(@typeName(T) ++ " has no path field");
}
if (@hasField(T, "error_strategy")) {
if (@FieldType(T, "error_strategy") != ErrorStrategy) {
@compileError(@typeName(@FieldType(T, "error_strategy")) ++ " has wrong type, expected: zap.Endpoint.ErrorStrategy");
}
} else {
@compileError(@typeName(T) ++ " has no error_strategy field");
}
const methods_to_check = [_][]const u8{
"get",
"post",
"put",
"delete",
"patch",
"options",
};
inline for (methods_to_check) |method| {
if (@hasDecl(T, method)) {
if (@TypeOf(@field(T, method)) != fn (_: *T, _: Request) anyerror!void) {
@compileError(method ++ " method of " ++ @typeName(T) ++ " has wrong type:\n" ++ @typeName(@TypeOf(T.get)) ++ "\nexpected:\n" ++ @typeName(fn (_: *T, _: Request) anyerror!void));
}
} else {
@compileError(@typeName(T) ++ " has no method named `" ++ method ++ "`");
}
}
}
pub const Wrapper = struct {
pub const Interface = struct {
call: *const fn (*Interface, zap.Request) anyerror!void = undefined,
path: []const u8,
destroy: *const fn (allocator: std.mem.Allocator, *Interface) void = undefined,
};
pub fn Wrap(T: type) type {
return struct {
wrapped: *T,
wrapper: Interface,
const Wrapped = @This();
pub fn unwrap(wrapper: *Interface) *Wrapped {
const self: *Wrapped = @alignCast(@fieldParentPtr("wrapper", wrapper));
return self;
}
pub fn destroy(allocator: std.mem.Allocator, wrapper: *Interface) void {
const self: *Wrapped = @alignCast(@fieldParentPtr("wrapper", wrapper));
allocator.destroy(self);
}
pub fn onRequestWrapped(wrapper: *Interface, r: zap.Request) !void {
var self: *Wrapped = Wrapped.unwrap(wrapper);
try self.onRequest(r);
}
pub fn onRequest(self: *Wrapped, r: zap.Request) !void {
const ret = switch (r.methodAsEnum()) {
.GET => self.wrapped.*.get(r),
.POST => self.wrapped.*.post(r),
.PUT => self.wrapped.*.put(r),
.DELETE => self.wrapped.*.delete(r),
.PATCH => self.wrapped.*.patch(r),
.OPTIONS => self.wrapped.*.options(r),
else => error.UnsupportedHtmlRequestMethod,
};
if (ret) {
// handled without error
} else |err| {
switch (self.wrapped.*.error_strategy) {
.raise => return err,
.log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505),
.log_to_console => zap.debug("Error in {} {s} : {}", .{ Wrapped, r.method orelse "(no method)", err }),
}
}
}
};
}
pub fn init(T: type, value: *T) Wrapper.Wrap(T) {
checkEndpointType(T);
var ret: Wrapper.Wrap(T) = .{
.wrapped = value,
.wrapper = .{ .path = value.path },
};
ret.wrapper.call = Wrapper.Wrap(T).onRequestWrapped;
ret.wrapper.destroy = Wrapper.Wrap(T).destroy;
return ret;
}
};
/// Wrap an endpoint with an Authenticator
pub fn Authenticating(EndpointType: type, Authenticator: type) type {
return struct {
authenticator: *Authenticator,
ep: *EndpointType,
path: []const u8,
error_strategy: ErrorStrategy,
const AuthenticatingEndpoint = @This();
/// Init the authenticating endpoint. Pass in a pointer to the endpoint
/// you want to wrap, and the Authenticator that takes care of authenticating
/// requests.
pub fn init(e: *EndpointType, authenticator: *Authenticator) AuthenticatingEndpoint {
return .{
.authenticator = authenticator,
.ep = e,
.path = e.path,
.error_strategy = e.error_strategy,
};
}
/// Authenticates GET requests using the Authenticator.
pub fn get(self: *AuthenticatingEndpoint, r: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&r)) {
.AuthFailed => return self.ep.*.unauthorized(r),
.AuthOK => self.ep.*.get(r),
.Handled => {},
};
}
/// Authenticates POST requests using the Authenticator.
pub fn post(self: *AuthenticatingEndpoint, r: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&r)) {
.AuthFailed => return self.ep.*.unauthorized(r),
.AuthOK => self.ep.*.post(r),
.Handled => {},
};
}
/// Authenticates PUT requests using the Authenticator.
pub fn put(self: *AuthenticatingEndpoint, r: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&r)) {
.AuthFailed => return self.ep.*.unauthorized(r),
.AuthOK => self.ep.*.put(r),
.Handled => {},
};
}
/// Authenticates DELETE requests using the Authenticator.
pub fn delete(self: *AuthenticatingEndpoint, r: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&r)) {
.AuthFailed => return self.ep.*.unauthorized(r),
.AuthOK => self.ep.*.delete(r),
.Handled => {},
};
}
/// Authenticates PATCH requests using the Authenticator.
pub fn patch(self: *AuthenticatingEndpoint, r: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&r)) {
.AuthFailed => return self.ep.*.unauthorized(r),
.AuthOK => self.ep.*.patch(r),
.Handled => {},
};
}
/// Authenticates OPTIONS requests using the Authenticator.
pub fn options(self: *AuthenticatingEndpoint, r: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&r)) {
.AuthFailed => return self.ep.*.unauthorized(r),
.AuthOK => self.ep.*.put(r),
.Handled => {},
};
}
};
}
pub const EndpointListenerError = error{
/// Since we use .startsWith to check for matching paths, you cannot use
/// endpoint paths that overlap at the beginning. --> When trying to register
/// an endpoint whose path would shadow an already registered one, you will
/// receive this error.
EndpointPathShadowError,
};
/// The listener with endpoint support
///
/// NOTE: It switches on path.startsWith -> so use endpoints with distinctly starting names!!
pub const Listener = struct {
listener: HttpListener,
allocator: std.mem.Allocator,
/// Internal static interface struct of member endpoints
var endpoints: std.ArrayListUnmanaged(*Wrapper.Interface) = .empty;
/// Internal, static request handler callback. Will be set to the optional,
/// user-defined request callback that only gets called if no endpoints match
/// a request.
var on_request: ?zap.HttpRequestFn = null;
/// Initialize a new endpoint listener. Note, if you pass an `on_request`
/// callback in the provided ListenerSettings, this request callback will be
/// called every time a request arrives that no endpoint matches.
pub fn init(a: std.mem.Allocator, l: ListenerSettings) Listener {
// reset the global in case init is called multiple times, as is the
// case in the authentication tests
endpoints = .empty;
// take copy of listener settings before modifying the callback field
var ls = l;
// override the settings with our internal, actual callback function
// so that "we" will be called on request
ls.on_request = Listener.onRequest;
// store the settings-provided request callback for later use
on_request = l.on_request;
return .{
.listener = HttpListener.init(ls),
.allocator = a,
};
}
/// De-init the listener and free its resources.
/// Registered endpoints will not be de-initialized automatically; just removed
/// from the internal map.
pub fn deinit(self: *Listener) void {
for (endpoints.items) |endpoint_wrapper| {
endpoint_wrapper.destroy(self.allocator, endpoint_wrapper);
}
endpoints.deinit(self.allocator);
}
/// Call this to start listening. After this, no more endpoints can be
/// registered.
pub fn listen(self: *Listener) !void {
try self.listener.listen();
}
/// Register an endpoint with this listener.
/// NOTE: endpoint paths are matched with startsWith -> so use endpoints with distinctly starting names!!
/// If you try to register an endpoint whose path would shadow an already registered one, you will
/// receive an EndpointPathShadowError.
pub fn register(self: *Listener, e: anytype) !void {
for (endpoints.items) |other| {
if (std.mem.startsWith(
u8,
other.path,
e.path,
) or std.mem.startsWith(
u8,
e.path,
other.path,
)) {
return EndpointListenerError.EndpointPathShadowError;
}
}
const EndpointType = @typeInfo(@TypeOf(e)).pointer.child;
checkEndpointType(EndpointType);
const wrapper = try self.allocator.create(Wrapper.Wrap(EndpointType));
wrapper.* = Wrapper.init(EndpointType, e);
try endpoints.append(self.allocator, &wrapper.wrapper);
}
fn onRequest(r: Request) !void {
if (r.path) |p| {
for (endpoints.items) |wrapper| {
if (std.mem.startsWith(u8, p, wrapper.path)) {
return try wrapper.call(wrapper, r);
}
}
}
// if set, call the user-provided default callback
if (on_request) |foo| {
try foo(r);
}
}
};