universal-lambda-zig/src/flexilib.zig

162 lines
7.3 KiB
Zig

const std = @import("std");
const interface = @import("flexilib-interface");
const universal_lambda_interface = @import("universal_lambda_interface");
const testing = std.testing;
const log = std.log.scoped(.@"main-lib");
const Application = if (@import("builtin").is_test) @This() else @import("flexilib_handler");
// The main program will look for exports during the request lifecycle:
// zigInit (optional): called at the beginning of a request, includes pointer to an allocator
// handle_request: called with request data, expects response data
// request_deinit (optional): called at the end of a request to allow resource cleanup
//
// Setup for these is aided by the interface library as shown below
// zigInit is an optional export called at the beginning of a request. It will
// be passed an allocator (which...shh...is an arena allocator). Since the
// interface library provides a request handler that requires a built-in allocator,
// if you are using the interface's handleRequest function as shown above,
// you will need to also include this export. To customize, just do something
// like this:
//
// export fn zigInit(parent_allocator: *anyopaque) callconv(.C) void {
// // your code here, just include the next line
// interface.zigInit(parent_allocator);
// }
//
comptime {
@export(interface.zigInit, .{ .name = "zigInit", .linkage = .strong });
}
/// handle_request will be called on a single request, but due to the preservation
/// of restrictions imposed by the calling interface, it should generally be more
/// useful to call into the interface library to let it do the conversion work
/// on your behalf
export fn handle_request(request: *interface.Request) callconv(.C) ?*interface.Response {
// The interface library provides a handleRequest function that will handle
// marshalling data back and forth from the C format used for the interface
// to a more Zig friendly format. It also allows usage of zig errors. To
// use, pass in the request and the zig function used to handle the request
// (here called "handleRequest"). The function signature must be:
//
// fn (std.mem.Allocator, interface.ZigRequest, interface.ZigResponse) !void
//
return interface.handleRequest(request, handleRequest);
}
// request_deinit is an optional export and will be called a the end of the
// request. Useful for deallocating memory. Since this is zig code and the
// allocator used is an arena allocator, all allocated memory will be automatically
// cleaned up by the main program at the end of a request
//
// export fn request_deinit() void {
// }
// ************************************************************************
// Boilerplate ^^, Custom code vv
// ************************************************************************
//
// handleRequest function here is the last line of boilerplate and the
// entry to a request
fn handleRequest(allocator: std.mem.Allocator, response: *interface.ZigResponse) !void {
// setup
var response_writer = response.body.writer();
// dispatch to our actual handler
if (handler == null) _ = try Application.main();
std.debug.assert(handler != null);
// setup response
var ul_response = universal_lambda_interface.Response.init(allocator);
defer ul_response.deinit();
ul_response.request.target = response.request.target;
ul_response.request.headers = response.request.headers;
ul_response.request.method = std.meta.stringToEnum(std.http.Method, response.request.method) orelse std.http.Method.GET;
const builtin = @import("builtin");
const supports_getrusage = builtin.os.tag != .windows and @hasDecl(std.posix.system, "rusage"); // Is Windows it?
var rss: if (supports_getrusage) std.posix.rusage else void = undefined;
if (supports_getrusage and builtin.mode == .Debug)
rss = std.posix.getrusage(std.posix.rusage.SELF);
const response_content = try handler.?(
allocator,
response.request.content,
&ul_response,
);
if (supports_getrusage and builtin.mode == .Debug) { // and debug mode) {
const rusage = std.posix.getrusage(std.posix.rusage.SELF);
log.debug(
"Request complete, max RSS of process: {d}M. Incremental: {d}K, User: {d}μs, System: {d}μs",
.{
@divTrunc(rusage.maxrss, 1024),
rusage.maxrss - rss.maxrss,
(rusage.utime.tv_sec - rss.utime.tv_sec) * std.time.us_per_s +
rusage.utime.tv_usec - rss.utime.tv_usec,
(rusage.stime.tv_sec - rss.stime.tv_sec) * std.time.us_per_s +
rusage.stime.tv_usec - rss.stime.tv_usec,
},
);
}
response.headers = ul_response.headers;
// Anything manually written goes first
try response_writer.writeAll(ul_response.body.items);
// Now we right the official body (response from handler)
try response_writer.writeAll(response_content);
}
pub fn run(allocator: ?std.mem.Allocator, event_handler: universal_lambda_interface.HandlerFn) !u8 {
_ = allocator;
register(event_handler);
return 0;
}
var handler: ?universal_lambda_interface.HandlerFn = null;
/// Registers a handler function with flexilib
pub fn register(h: universal_lambda_interface.HandlerFn) void {
handler = h;
}
pub fn main() !u8 {
// should only be called under test!
// Flexilib runs under a DLL. So the plan is:
// 1. dll calls handle_request
// 2. handle_request discovers, through build, where it came from
// 3. handle_request calls main
// 4. main, in the application, calls run, thinking it's a console app
// 5. run, calls back to universal lambda, which then calls back here to register
// 6. register, registers the handler. It will need to be up to main() to recognize
// build_options and look for flexilib if they're doing something fancy
register(testHandler);
return 0;
}
fn testHandler(allocator: std.mem.Allocator, event_data: []const u8, context: @import("universal_lambda_interface").Context) ![]const u8 {
context.headers = &.{.{ .name = "X-custom-foo", .value = "bar" }};
try context.writeAll(event_data);
return std.fmt.allocPrint(allocator, "{d}", .{context.request.headers.len});
}
// Need to figure out how tests would work
test "handle_request" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var aa = arena.allocator();
interface.zigInit(&aa);
const headers: []interface.Header = @constCast(&[_]interface.Header{.{
.name_ptr = @ptrCast(@constCast("GET".ptr)),
.name_len = 3,
.value_ptr = @ptrCast(@constCast("GET".ptr)),
.value_len = 3,
}});
var req = interface.Request{
.method = @ptrCast(@constCast("GET".ptr)),
.method_len = 3,
.content = @ptrCast(@constCast(" ".ptr)),
.content_len = 1,
.headers = headers.ptr,
.headers_len = 1,
.target = @ptrCast(@constCast("/".ptr)),
.target_len = 1,
};
const response = handle_request(&req).?;
try testing.expectEqualStrings(" 1", response.ptr[0..response.len]);
try testing.expectEqual(@as(usize, 1), response.headers_len);
try testing.expectEqualStrings("X-custom-foo", response.headers[0].name_ptr[0..response.headers[0].name_len]);
try testing.expectEqualStrings("bar", response.headers[0].value_ptr[0..response.headers[0].value_len]);
}