mirror of
https://github.com/zigzap/zap.git
synced 2025-10-20 15:14:08 +00:00
First zap.App test SUCCESS!!!
This commit is contained in:
parent
51d17a1868
commit
458d6ee071
4 changed files with 211 additions and 65 deletions
|
@ -50,6 +50,7 @@ pub fn build(b: *std.Build) !void {
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
src: []const u8,
|
src: []const u8,
|
||||||
}{
|
}{
|
||||||
|
.{ .name = "app", .src = "examples/app/main.zig" },
|
||||||
.{ .name = "hello", .src = "examples/hello/hello.zig" },
|
.{ .name = "hello", .src = "examples/hello/hello.zig" },
|
||||||
.{ .name = "https", .src = "examples/https/https.zig" },
|
.{ .name = "https", .src = "examples/https/https.zig" },
|
||||||
.{ .name = "hello2", .src = "examples/hello2/hello2.zig" },
|
.{ .name = "hello2", .src = "examples/hello2/hello2.zig" },
|
||||||
|
|
89
examples/app/main.zig
Normal file
89
examples/app/main.zig
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const zap = @import("zap");
|
||||||
|
|
||||||
|
const MyContext = struct {
|
||||||
|
db_connection: []const u8,
|
||||||
|
|
||||||
|
pub fn init(connection: []const u8) MyContext {
|
||||||
|
return .{
|
||||||
|
.db_connection = connection,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const SimpleEndpoint = struct {
|
||||||
|
|
||||||
|
// Endpoint Interface part
|
||||||
|
path: []const u8,
|
||||||
|
error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response,
|
||||||
|
|
||||||
|
some_data: []const u8,
|
||||||
|
|
||||||
|
pub fn init(path: []const u8, data: []const u8) SimpleEndpoint {
|
||||||
|
return .{
|
||||||
|
.path = path,
|
||||||
|
.some_data = data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(e: *SimpleEndpoint, arena: Allocator, context: *MyContext, r: zap.Request) anyerror!void {
|
||||||
|
r.setStatus(.ok);
|
||||||
|
|
||||||
|
const thread_id = std.Thread.getCurrentId();
|
||||||
|
// look, we use the arena allocator here
|
||||||
|
// and we also just try it, not worrying about errors
|
||||||
|
const response_text = try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
\\Hello!
|
||||||
|
\\context.db_connection: {s}
|
||||||
|
\\endpoint.data: {s}
|
||||||
|
\\arena: {}
|
||||||
|
\\thread_id: {}
|
||||||
|
\\
|
||||||
|
,
|
||||||
|
.{ context.db_connection, e.some_data, arena.ptr, thread_id },
|
||||||
|
);
|
||||||
|
try r.sendBody(response_text);
|
||||||
|
std.time.sleep(std.time.ns_per_ms * 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty stubs for all other request methods
|
||||||
|
pub fn post(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) anyerror!void {}
|
||||||
|
pub fn put(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) anyerror!void {}
|
||||||
|
pub fn delete(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) anyerror!void {}
|
||||||
|
pub fn patch(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) anyerror!void {}
|
||||||
|
pub fn options(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) anyerror!void {}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var my_context = MyContext.init("db connection established!");
|
||||||
|
|
||||||
|
var gpa: std.heap.GeneralPurposeAllocator(.{
|
||||||
|
// just to be explicit
|
||||||
|
.thread_safe = true,
|
||||||
|
}) = .{};
|
||||||
|
defer std.debug.print("\n\nLeaks detected: {}\n\n", .{gpa.deinit() != .ok});
|
||||||
|
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
const App = zap.App.Create(MyContext);
|
||||||
|
var app = try App.init(allocator, &my_context, .{});
|
||||||
|
defer app.deinit();
|
||||||
|
|
||||||
|
var my_endpoint = SimpleEndpoint.init("/", "some endpoint specific data");
|
||||||
|
|
||||||
|
try app.register(&my_endpoint);
|
||||||
|
|
||||||
|
try app.listen(.{
|
||||||
|
.interface = "0.0.0.0",
|
||||||
|
.port = 3000,
|
||||||
|
});
|
||||||
|
std.debug.print("Listening on 0.0.0.0:3000\n", .{});
|
||||||
|
|
||||||
|
// start worker threads -- only 1 process!!!
|
||||||
|
zap.start(.{
|
||||||
|
.threads = 2,
|
||||||
|
.workers = 1,
|
||||||
|
});
|
||||||
|
}
|
184
src/App.zig
184
src/App.zig
|
@ -8,19 +8,19 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
const RwLock = std.Thread.RwLock;
|
const Thread = std.Thread;
|
||||||
|
const RwLock = Thread.RwLock;
|
||||||
|
|
||||||
const zap = @import("zap.zig");
|
const zap = @import("zap.zig");
|
||||||
const Request = zap.Request;
|
const Request = zap.Request;
|
||||||
|
const HttpListener = zap.HttpListener;
|
||||||
|
|
||||||
pub const Opts = struct {
|
pub const AppOpts = struct {
|
||||||
/// ErrorStrategy for (optional) request handler if no endpoint matches
|
/// ErrorStrategy for (optional) request handler if no endpoint matches
|
||||||
default_error_strategy: zap.Endpoint.ErrorStrategy = .log_to_console,
|
default_error_strategy: zap.Endpoint.ErrorStrategy = .log_to_console,
|
||||||
arena_retain_capacity: usize = 16 * 1024 * 1024,
|
arena_retain_capacity: usize = 16 * 1024 * 1024,
|
||||||
};
|
};
|
||||||
|
|
||||||
threadlocal var _arena: ?ArenaAllocator = null;
|
|
||||||
|
|
||||||
/// creates an App with custom app context
|
/// creates an App with custom app context
|
||||||
pub fn Create(comptime Context: type) type {
|
pub fn Create(comptime Context: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
|
@ -31,12 +31,22 @@ pub fn Create(comptime Context: type) type {
|
||||||
const InstanceData = struct {
|
const InstanceData = struct {
|
||||||
context: *Context = undefined,
|
context: *Context = undefined,
|
||||||
gpa: Allocator = undefined,
|
gpa: Allocator = undefined,
|
||||||
opts: Opts = undefined,
|
opts: AppOpts = undefined,
|
||||||
endpoints: std.StringArrayHashMapUnmanaged(*Endpoint.Interface) = .empty,
|
// endpoints: std.StringArrayHashMapUnmanaged(*Endpoint.Interface) = .empty,
|
||||||
|
endpoints: std.ArrayListUnmanaged(*Endpoint.Interface) = .empty,
|
||||||
|
|
||||||
there_can_be_only_one: bool = false,
|
there_can_be_only_one: bool = false,
|
||||||
track_arenas: std.ArrayListUnmanaged(*ArenaAllocator) = .empty,
|
track_arenas: std.AutoHashMapUnmanaged(Thread.Id, ArenaAllocator) = .empty,
|
||||||
track_arena_lock: RwLock = .{},
|
track_arena_lock: RwLock = .{},
|
||||||
|
|
||||||
|
/// the internal http listener
|
||||||
|
listener: HttpListener = undefined,
|
||||||
|
|
||||||
|
/// function pointer to handler for otherwise unhandled requests
|
||||||
|
/// Will automatically be set if your Context provides an unhandled
|
||||||
|
/// function of type `fn(*Context, Allocator, Request)`
|
||||||
|
///
|
||||||
|
unhandled: ?*const fn (*Context, Allocator, Request) anyerror!void = null,
|
||||||
};
|
};
|
||||||
var _static: InstanceData = .{};
|
var _static: InstanceData = .{};
|
||||||
|
|
||||||
|
@ -47,15 +57,16 @@ pub fn Create(comptime Context: type) type {
|
||||||
|
|
||||||
pub const Endpoint = struct {
|
pub const Endpoint = struct {
|
||||||
pub const Interface = struct {
|
pub const Interface = struct {
|
||||||
call: *const fn (*Interface, zap.Request) anyerror!void = undefined,
|
call: *const fn (*Interface, Request) anyerror!void = undefined,
|
||||||
path: []const u8,
|
path: []const u8,
|
||||||
destroy: *const fn (allocator: Allocator, *Interface) void = undefined,
|
destroy: *const fn (*Interface, Allocator) void = undefined,
|
||||||
};
|
};
|
||||||
pub fn Wrap(T: type) type {
|
pub fn Wrap(T: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
wrapped: *T,
|
wrapped: *T,
|
||||||
interface: Interface,
|
interface: Interface,
|
||||||
opts: Opts,
|
|
||||||
|
// tbh: unnecessary, since we have it in _static
|
||||||
app_context: *Context,
|
app_context: *Context,
|
||||||
|
|
||||||
const Wrapped = @This();
|
const Wrapped = @This();
|
||||||
|
@ -65,19 +76,19 @@ pub fn Create(comptime Context: type) type {
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn destroy(allocator: Allocator, wrapper: *Interface) void {
|
pub fn destroy(interface: *Interface, allocator: Allocator) void {
|
||||||
const self: *Wrapped = @alignCast(@fieldParentPtr("interface", wrapper));
|
const self: *Wrapped = @alignCast(@fieldParentPtr("interface", interface));
|
||||||
allocator.destroy(self);
|
allocator.destroy(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn onRequestWrapped(interface: *Interface, r: zap.Request) !void {
|
pub fn onRequestWrapped(interface: *Interface, r: Request) !void {
|
||||||
var self: *Wrapped = Wrapped.unwrap(interface);
|
var self: *Wrapped = Wrapped.unwrap(interface);
|
||||||
const arena = try get_arena();
|
var arena = try get_arena();
|
||||||
try self.onRequest(arena.allocator(), self.app_context, r);
|
try self.onRequest(arena.allocator(), self.app_context, r);
|
||||||
arena.reset(.{ .retain_capacity = self.opts.arena_retain_capacity });
|
_ = arena.reset(.{ .retain_with_limit = _static.opts.arena_retain_capacity });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn onRequest(self: *Wrapped, arena: Allocator, app_context: *Context, r: zap.Request) !void {
|
pub fn onRequest(self: *Wrapped, arena: Allocator, app_context: *Context, r: Request) !void {
|
||||||
const ret = switch (r.methodAsEnum()) {
|
const ret = switch (r.methodAsEnum()) {
|
||||||
.GET => self.wrapped.*.get(arena, app_context, r),
|
.GET => self.wrapped.*.get(arena, app_context, r),
|
||||||
.POST => self.wrapped.*.post(arena, app_context, r),
|
.POST => self.wrapped.*.post(arena, app_context, r),
|
||||||
|
@ -100,17 +111,17 @@ pub fn Create(comptime Context: type) type {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(T: type, value: *T, app_opts: Opts, app_context: *Context) Endpoint.Wrap(T) {
|
pub fn init(T: type, value: *T) Endpoint.Wrap(T) {
|
||||||
checkEndpointType(T);
|
checkEndpointType(T);
|
||||||
var ret: Endpoint.Wrap(T) = .{
|
return .{
|
||||||
.wrapped = value,
|
.wrapped = value,
|
||||||
.wrapper = .{ .path = value.path },
|
.interface = .{
|
||||||
.opts = app_opts,
|
.path = value.path,
|
||||||
.app_context = app_context,
|
.call = Endpoint.Wrap(T).onRequestWrapped,
|
||||||
|
.destroy = Endpoint.Wrap(T).destroy,
|
||||||
|
},
|
||||||
|
.app_context = _static.context,
|
||||||
};
|
};
|
||||||
ret.wrapper.call = Endpoint.Wrap(T).onRequestWrapped;
|
|
||||||
ret.wrapper.destroy = Endpoint.Wrap(T).destroy;
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn checkEndpointType(T: type) void {
|
pub fn checkEndpointType(T: type) void {
|
||||||
|
@ -140,8 +151,10 @@ pub fn Create(comptime Context: type) type {
|
||||||
};
|
};
|
||||||
inline for (methods_to_check) |method| {
|
inline for (methods_to_check) |method| {
|
||||||
if (@hasDecl(T, method)) {
|
if (@hasDecl(T, method)) {
|
||||||
if (@TypeOf(@field(T, method)) != fn (_: *T, _: Allocator, _: *Context, _: zap.Request) anyerror!void) {
|
const Method = @TypeOf(@field(T, method));
|
||||||
@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));
|
const Expected = fn (_: *T, _: Allocator, _: *Context, _: Request) anyerror!void;
|
||||||
|
if (Method != Expected) {
|
||||||
|
@compileError(method ++ " method of " ++ @typeName(T) ++ " has wrong type:\n" ++ @typeName(Method) ++ "\nexpected:\n" ++ @typeName(Expected));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@compileError(@typeName(T) ++ " has no method named `" ++ method ++ "`");
|
@compileError(@typeName(T) ++ " has no method named `" ++ method ++ "`");
|
||||||
|
@ -151,8 +164,10 @@ pub fn Create(comptime Context: type) type {
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const ListenerSettings = struct {
|
pub const ListenerSettings = struct {
|
||||||
port: usize,
|
/// IP interface, e.g. 0.0.0.0
|
||||||
interface: [*c]const u8 = null,
|
interface: [*c]const u8 = null,
|
||||||
|
/// IP port to listen on
|
||||||
|
port: usize,
|
||||||
public_folder: ?[]const u8 = null,
|
public_folder: ?[]const u8 = null,
|
||||||
max_clients: ?isize = null,
|
max_clients: ?isize = null,
|
||||||
max_body_size: ?usize = null,
|
max_body_size: ?usize = null,
|
||||||
|
@ -160,47 +175,79 @@ pub fn Create(comptime Context: type) type {
|
||||||
tls: ?zap.Tls = null,
|
tls: ?zap.Tls = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(gpa_: Allocator, context_: *Context, opts_: Opts) !App {
|
pub fn init(gpa_: Allocator, context_: *Context, opts_: AppOpts) !App {
|
||||||
if (App._static._there_can_be_only_one) {
|
if (_static.there_can_be_only_one) {
|
||||||
return error.OnlyOneAppAllowed;
|
return error.OnlyOneAppAllowed;
|
||||||
}
|
}
|
||||||
App._static.context = context_;
|
_static.context = context_;
|
||||||
App._static.gpa = gpa_;
|
_static.gpa = gpa_;
|
||||||
App._static.opts = opts_;
|
_static.opts = opts_;
|
||||||
App._static.there_can_be_only_one = true;
|
_static.there_can_be_only_one = true;
|
||||||
|
|
||||||
|
// set unhandled callback if provided by Context
|
||||||
|
if (@hasDecl(Context, "unhandled")) {
|
||||||
|
// try if we can use it
|
||||||
|
const Unhandled = @TypeOf(@field(Context, "unhandled"));
|
||||||
|
const Expected = fn (_: *Context, _: Allocator, _: Request) anyerror!void;
|
||||||
|
if (Unhandled != Expected) {
|
||||||
|
@compileError("`unhandled` method of " ++ @typeName(Context) ++ " has wrong type:\n" ++ @typeName(Unhandled) ++ "\nexpected:\n" ++ @typeName(Expected));
|
||||||
|
}
|
||||||
|
_static.unhandled = Context.unhandled;
|
||||||
|
}
|
||||||
return .{};
|
return .{};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit() void {
|
pub fn deinit(_: *App) void {
|
||||||
App._static.endpoints.deinit(_static.gpa);
|
// we created endpoint wrappers but only tracked their interfaces
|
||||||
|
// hence, we need to destroy the wrappers through their interfaces
|
||||||
|
if (false) {
|
||||||
|
var it = _static.endpoints.iterator();
|
||||||
|
while (it.next()) |kv| {
|
||||||
|
const interface = kv.value_ptr;
|
||||||
|
interface.*.destroy(_static.gpa);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (_static.endpoints.items) |interface| {
|
||||||
|
interface.destroy(interface, _static.gpa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_static.endpoints.deinit(_static.gpa);
|
||||||
|
|
||||||
App._static.track_arena_lock.lock();
|
_static.track_arena_lock.lock();
|
||||||
defer App._static.track_arena_lock.unlock();
|
defer _static.track_arena_lock.unlock();
|
||||||
for (App._static.track_arenas.items) |arena| {
|
|
||||||
|
var it = _static.track_arenas.valueIterator();
|
||||||
|
while (it.next()) |arena| {
|
||||||
|
// std.debug.print("deiniting arena: {*}\n", .{arena});
|
||||||
arena.deinit();
|
arena.deinit();
|
||||||
}
|
}
|
||||||
|
_static.track_arenas.deinit(_static.gpa);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_arena() !*ArenaAllocator {
|
pub fn get_arena() !*ArenaAllocator {
|
||||||
App._static.track_arena_lock.lockShared();
|
const thread_id = std.Thread.getCurrentId();
|
||||||
if (_arena == null) {
|
_static.track_arena_lock.lockShared();
|
||||||
App._static.track_arena_lock.unlockShared();
|
if (_static.track_arenas.getPtr(thread_id)) |arena| {
|
||||||
App._static.track_arena_lock.lock();
|
_static.track_arena_lock.unlockShared();
|
||||||
defer App._static.track_arena_lock.unlock();
|
return arena;
|
||||||
_arena = ArenaAllocator.init(App._static.gpa);
|
|
||||||
try App._static.track_arenas.append(App._static.gpa, &_arena.?);
|
|
||||||
} else {
|
} else {
|
||||||
App._static.track_arena_lock.unlockShared();
|
_static.track_arena_lock.unlockShared();
|
||||||
return &_arena.?;
|
_static.track_arena_lock.lock();
|
||||||
|
defer _static.track_arena_lock.unlock();
|
||||||
|
const arena = ArenaAllocator.init(_static.gpa);
|
||||||
|
try _static.track_arenas.put(_static.gpa, thread_id, arena);
|
||||||
|
return _static.track_arenas.getPtr(thread_id).?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register an endpoint with this listener.
|
/// Register an endpoint with this listener.
|
||||||
/// NOTE: endpoint paths are matched with startsWith -> so use endpoints with distinctly starting names!!
|
/// NOTE: endpoint paths are matched with startsWith
|
||||||
/// If you try to register an endpoint whose path would shadow an already registered one, you will
|
/// -> so use endpoints with distinctly starting names!!
|
||||||
/// receive an EndpointPathShadowError.
|
/// If you try to register an endpoint whose path would shadow an
|
||||||
pub fn register(self: *App, endpoint: anytype) !void {
|
/// already registered one, you will receive an
|
||||||
for (App._static.endpoints.items) |other| {
|
/// EndpointPathShadowError.
|
||||||
|
pub fn register(_: *App, endpoint: anytype) !void {
|
||||||
|
for (_static.endpoints.items) |other| {
|
||||||
if (std.mem.startsWith(
|
if (std.mem.startsWith(
|
||||||
u8,
|
u8,
|
||||||
other.path,
|
other.path,
|
||||||
|
@ -215,31 +262,38 @@ pub fn Create(comptime Context: type) type {
|
||||||
}
|
}
|
||||||
const EndpointType = @typeInfo(@TypeOf(endpoint)).pointer.child;
|
const EndpointType = @typeInfo(@TypeOf(endpoint)).pointer.child;
|
||||||
Endpoint.checkEndpointType(EndpointType);
|
Endpoint.checkEndpointType(EndpointType);
|
||||||
const wrapper = try self.gpa.create(Endpoint.Wrap(EndpointType));
|
const wrapper = try _static.gpa.create(Endpoint.Wrap(EndpointType));
|
||||||
wrapper.* = Endpoint.init(EndpointType, endpoint);
|
wrapper.* = Endpoint.init(EndpointType, endpoint);
|
||||||
try App._static.endpoints.append(self.gpa, &wrapper.wrapper);
|
try _static.endpoints.append(_static.gpa, &wrapper.interface);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn listen(self: *App, l: ListenerSettings) !void {
|
pub fn listen(_: *App, l: ListenerSettings) !void {
|
||||||
_ = self;
|
_static.listener = HttpListener.init(.{
|
||||||
_ = l;
|
.interface = l.interface,
|
||||||
// TODO: do it
|
.port = l.port,
|
||||||
|
.public_folder = l.public_folder,
|
||||||
|
.max_clients = l.max_clients,
|
||||||
|
.max_body_size = l.max_body_size,
|
||||||
|
.timeout = l.timeout,
|
||||||
|
.tls = l.tls,
|
||||||
|
|
||||||
|
.on_request = onRequest,
|
||||||
|
});
|
||||||
|
try _static.listener.listen();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn onRequest(r: Request) !void {
|
fn onRequest(r: Request) !void {
|
||||||
if (r.path) |p| {
|
if (r.path) |p| {
|
||||||
for (App._static.endpoints.items) |wrapper| {
|
for (_static.endpoints.items) |wrapper| {
|
||||||
if (std.mem.startsWith(u8, p, wrapper.path)) {
|
if (std.mem.startsWith(u8, p, wrapper.path)) {
|
||||||
return try wrapper.call(wrapper, r);
|
return try wrapper.call(wrapper, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (on_request) |foo| {
|
if (on_request) |foo| {
|
||||||
if (_arena == null) {
|
var arena = try get_arena();
|
||||||
_arena = ArenaAllocator.init(App._static.gpa);
|
foo(arena.allocator(), _static.context, r) catch |err| {
|
||||||
}
|
switch (_static.opts.default_error_strategy) {
|
||||||
foo(_arena.allocator(), App._static.context, r) catch |err| {
|
|
||||||
switch (App._static.opts.default_error_strategy) {
|
|
||||||
.raise => return err,
|
.raise => return err,
|
||||||
.log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505),
|
.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 }),
|
.log_to_console => zap.debug("Error in {} {s} : {}", .{ App, r.method orelse "(no method)", err }),
|
||||||
|
|
|
@ -13,6 +13,8 @@ pub const Endpoint = @import("endpoint.zig");
|
||||||
|
|
||||||
pub const Router = @import("router.zig");
|
pub const Router = @import("router.zig");
|
||||||
|
|
||||||
|
pub const App = @import("App.zig");
|
||||||
|
|
||||||
/// A struct to handle Mustache templating.
|
/// A struct to handle Mustache templating.
|
||||||
///
|
///
|
||||||
/// This is a wrapper around fiobj's mustache template handling.
|
/// This is a wrapper around fiobj's mustache template handling.
|
||||||
|
|
Loading…
Add table
Reference in a new issue