mirror of
https://github.com/zigzap/zap.git
synced 2025-10-20 23:24:09 +00:00
continue working on zap.App
This commit is contained in:
parent
029c8ff069
commit
7da0a6fe4e
1 changed files with 232 additions and 13 deletions
245
src/App.zig
245
src/App.zig
|
@ -6,25 +6,244 @@
|
||||||
//! - automatic error catching & logging, optional report to HTML
|
//! - automatic error catching & logging, optional report to HTML
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
const RwLock = std.Thread.RwLock;
|
||||||
|
|
||||||
const zap = @import("zap.zig");
|
const zap = @import("zap.zig");
|
||||||
|
const Request = zap.Request;
|
||||||
|
|
||||||
pub const Opts = struct {
|
pub const Opts = struct {
|
||||||
request_error_strategy: enum {
|
/// ErrorStrategy for (optional) request handler if no endpoint matches
|
||||||
/// log errors to console
|
default_error_strategy: zap.Endpoint.ErrorStrategy = .log_to_console,
|
||||||
log_to_console,
|
arena_retain_capacity: usize = 16 * 1024 * 1024,
|
||||||
/// 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,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
threadlocal var arena: ?std.heap.ArenaAllocator = null;
|
threadlocal var _arena: ?ArenaAllocator = null;
|
||||||
|
|
||||||
pub fn create(comptime Context: type, context: *Context, opts: Opts) type {
|
/// creates an App with custom app context
|
||||||
|
pub fn Create(comptime Context: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
context: *Context = context,
|
const App = @This();
|
||||||
error_strategy: @TypeOf(opts.request_error_strategy) = opts.request_error_strategy,
|
|
||||||
endpoints: std.StringArrayHashMapUnmanaged(*zap.Endpoint.Wrapper.Internal) = .empty,
|
// we make the following fields static so we can access them from a
|
||||||
|
// context-free, pure zap request handler
|
||||||
|
const InstanceData = struct {
|
||||||
|
context: *Context = undefined,
|
||||||
|
gpa: Allocator = undefined,
|
||||||
|
opts: Opts = undefined,
|
||||||
|
endpoints: std.StringArrayHashMapUnmanaged(*Endpoint.Wrapper.Interface) = .empty,
|
||||||
|
|
||||||
|
there_can_be_only_one: bool = false,
|
||||||
|
track_arenas: std.ArrayListUnmanaged(*ArenaAllocator) = .empty,
|
||||||
|
track_arena_lock: RwLock = .{},
|
||||||
|
};
|
||||||
|
var _static: InstanceData = .{};
|
||||||
|
|
||||||
|
/// 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: ?*const fn (Allocator, *Context, Request) anyerror!void = null;
|
||||||
|
|
||||||
|
pub const Endpoint = struct {
|
||||||
|
pub const Wrapper = struct {
|
||||||
|
pub const Interface = struct {
|
||||||
|
call: *const fn (*Interface, zap.Request) anyerror!void = undefined,
|
||||||
|
path: []const u8,
|
||||||
|
destroy: *const fn (allocator: Allocator, *Interface) void = undefined,
|
||||||
|
};
|
||||||
|
pub fn Wrap(T: type) type {
|
||||||
|
return struct {
|
||||||
|
wrapped: *T,
|
||||||
|
wrapper: Interface,
|
||||||
|
opts: Opts,
|
||||||
|
app_context: *Context,
|
||||||
|
|
||||||
|
const Wrapped = @This();
|
||||||
|
|
||||||
|
pub fn unwrap(wrapper: *Interface) *Wrapped {
|
||||||
|
const self: *Wrapped = @alignCast(@fieldParentPtr("wrapper", wrapper));
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn destroy(allocator: 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);
|
||||||
|
const arena = try get_arena();
|
||||||
|
try self.onRequest(arena.allocator(), self.app_context, r);
|
||||||
|
arena.reset(.{ .retain_capacity = self.opts.arena_retain_capacity });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn onRequest(self: *Wrapped, arena: Allocator, app_context: *Context, r: zap.Request) !void {
|
||||||
|
const ret = switch (r.methodAsEnum()) {
|
||||||
|
.GET => self.wrapped.*.get(arena, app_context, r),
|
||||||
|
.POST => self.wrapped.*.post(arena, app_context, r),
|
||||||
|
.PUT => self.wrapped.*.put(arena, app_context, r),
|
||||||
|
.DELETE => self.wrapped.*.delete(arena, app_context, r),
|
||||||
|
.PATCH => self.wrapped.*.patch(arena, app_context, r),
|
||||||
|
.OPTIONS => self.wrapped.*.options(arena, app_context, 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, app_opts: Opts, app_context: *Context) Wrapper.Wrap(T) {
|
||||||
|
checkEndpointType(T);
|
||||||
|
var ret: Wrapper.Wrap(T) = .{
|
||||||
|
.wrapped = value,
|
||||||
|
.wrapper = .{ .path = value.path },
|
||||||
|
.opts = app_opts,
|
||||||
|
.app_context = app_context,
|
||||||
|
};
|
||||||
|
ret.wrapper.call = Wrapper.Wrap(T).onRequestWrapped;
|
||||||
|
ret.wrapper.destroy = Wrapper.Wrap(T).destroy;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
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") != zap.Endpoint.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, _: Allocator, _: *Context, _: zap.Request) anyerror!void) {
|
||||||
|
@compileError(method ++ " method of " ++ @typeName(T) ++ " has wrong type:\n" ++ @typeName(@TypeOf(T.get)) ++ "\nexpected:\n" ++ @typeName(fn (_: *T, _: Allocator, _: *Context, _: zap.Request) anyerror!void));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@compileError(@typeName(T) ++ " has no method named `" ++ method ++ "`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Listener = struct {
|
||||||
|
pub const Settings = struct {
|
||||||
|
//
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(gpa_: Allocator, context_: *Context, opts_: Opts) !App {
|
||||||
|
if (App._static._there_can_be_only_one) {
|
||||||
|
return error.OnlyOneAppAllowed;
|
||||||
|
}
|
||||||
|
App._static.context = context_;
|
||||||
|
App._static.gpa = gpa_;
|
||||||
|
App._static.opts = opts_;
|
||||||
|
App._static.there_can_be_only_one = true;
|
||||||
|
return .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit() void {
|
||||||
|
App._static.endpoints.deinit(_static.gpa);
|
||||||
|
|
||||||
|
App._static.track_arena_lock.lock();
|
||||||
|
defer App._static.track_arena_lock.unlock();
|
||||||
|
for (App._static.track_arenas.items) |arena| {
|
||||||
|
arena.deinit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_arena() !*ArenaAllocator {
|
||||||
|
App._static.track_arena_lock.lockShared();
|
||||||
|
if (_arena == null) {
|
||||||
|
App._static.track_arena_lock.unlockShared();
|
||||||
|
App._static.track_arena_lock.lock();
|
||||||
|
defer App._static.track_arena_lock.unlock();
|
||||||
|
_arena = ArenaAllocator.init(App._static.gpa);
|
||||||
|
try App._static.track_arenas.append(App._static.gpa, &_arena.?);
|
||||||
|
} else {
|
||||||
|
App._static.track_arena_lock.unlockShared();
|
||||||
|
return &_arena.?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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: *App, endpoint: anytype) !void {
|
||||||
|
for (App._static.endpoints.items) |other| {
|
||||||
|
if (std.mem.startsWith(
|
||||||
|
u8,
|
||||||
|
other.path,
|
||||||
|
endpoint.path,
|
||||||
|
) or std.mem.startsWith(
|
||||||
|
u8,
|
||||||
|
endpoint.path,
|
||||||
|
other.path,
|
||||||
|
)) {
|
||||||
|
return zap.Endpoint.EndpointListenerError.EndpointPathShadowError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const EndpointType = @typeInfo(@TypeOf(endpoint)).pointer.child;
|
||||||
|
Endpoint.Wrapper.checkEndpointType(EndpointType);
|
||||||
|
const wrapper = try self.gpa.create(Endpoint.Wrapper.Wrap(EndpointType));
|
||||||
|
wrapper.* = Endpoint.Wrapper.init(EndpointType, endpoint);
|
||||||
|
try App._static.endpoints.append(self.gpa, &wrapper.wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listen(self: *App, l: Listener.Settings) !void {
|
||||||
|
_ = self;
|
||||||
|
_ = l;
|
||||||
|
// TODO: do it
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onRequest(r: Request) !void {
|
||||||
|
if (r.path) |p| {
|
||||||
|
for (App._static.endpoints.items) |wrapper| {
|
||||||
|
if (std.mem.startsWith(u8, p, wrapper.path)) {
|
||||||
|
return try wrapper.call(wrapper, r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (on_request) |foo| {
|
||||||
|
if (_arena == null) {
|
||||||
|
_arena = ArenaAllocator.init(App._static.gpa);
|
||||||
|
}
|
||||||
|
foo(_arena.allocator(), App._static.context, r) catch |err| {
|
||||||
|
switch (App._static.opts.default_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} : {}", .{ App, r.method orelse "(no method)", err }),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue