mirror of
https://github.com/zigzap/zap.git
synced 2025-10-20 15:14:08 +00:00
349 lines
13 KiB
Zig
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);
|
|
}
|
|
}
|
|
};
|