rework context
All checks were successful
AWS-Zig Build / build-zig-0.11.0-amd64-host (push) Successful in 1m50s
All checks were successful
AWS-Zig Build / build-zig-0.11.0-amd64-host (push) Successful in 1m50s
This commit is a significant refactor that fixes a number of things. 1. Replaces the optional helpers import (which was always weird) with a mandatory interface import on behalf of the application. This is actually a good thing as it enables all things below. 2. Removes the severely awkward union that was the lambda context. Now, no matter how your handler runs, a single object with everything you need is fully populated and (nearly always) works as you would expect. There is a slight exception to this with AWS Lambda that is related to the service itself. It is also possible that not everything is passed in correctly for Cloudflare, which, if true, will be addressed later. 3. Allows writes to the context object. These will be added to the output, but is implementation dependent, and I'm not 100% sure I've got it right yet, but the infrastructure is there. 4. Allows proper tests throughout this project. 5. Allows proper tests in the application too. 6. Removes the need for the handler to be public under flexlib. Flexilib handler registration now works just like everything else. Note, however, that flexilib is unique in that your handler registration function will return before the program ends. If this is important for resource cleanup, @import("build_options").build_type is your friend. 7. Request method can now be passed into console applications using -m or --method
This commit is contained in:
parent
2915453c1b
commit
dcf00d8460
38
README.md
38
README.md
|
@ -62,24 +62,33 @@ try configureUniversalLambdaBuild(b, exe);
|
||||||
```
|
```
|
||||||
|
|
||||||
This will provide most of the magic functionality of the package, including
|
This will provide most of the magic functionality of the package, including
|
||||||
several new build steps to manage the system, and a new import to be used.
|
several new build steps to manage the system, and a new import to be used. For
|
||||||
|
testing, it is also advisable to add the modules to your tests by adding a line
|
||||||
|
like so:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
_ = try universal_lambda.addModules(b, main_tests);
|
||||||
|
```
|
||||||
|
|
||||||
**main.zig**
|
**main.zig**
|
||||||
|
|
||||||
The build changes above will add a module called 'universal_lambda_handler'.
|
The build changes above will add several modules:
|
||||||
Add an import:
|
|
||||||
|
* universal_lambda_handler: Main import, used to register your handler
|
||||||
|
* universal_lambda_interface: Contains the context type used in the handler function
|
||||||
|
* flexilib-interface: Used as a dependency of the handler. Not normally needed
|
||||||
|
|
||||||
|
Add imports for the handler registration and interface:
|
||||||
|
|
||||||
```zig
|
```zig
|
||||||
const universal_lambda = @import("universal_lambda_handler");
|
const universal_lambda = @import("universal_lambda_handler");
|
||||||
|
const universal_lambda_interface = @import("universal_lambda_interface");
|
||||||
```
|
```
|
||||||
|
|
||||||
Add a handler to be executed. **This must be public, and named 'handler'**.
|
Add a handler to be executed. The handler must follow this signature:
|
||||||
If you don't want to do that, name it whatever you want, and provide a public
|
|
||||||
const, e.g. `pub const handler=myActualFunctionName`. The handler must
|
|
||||||
follow this signature:
|
|
||||||
|
|
||||||
```zig
|
```zig
|
||||||
pub fn handler(allocator: std.mem.Allocator, event_data: []const u8, context: universal_lambda.Context) ![]const u8
|
pub fn handler(allocator: std.mem.Allocator, event_data: []const u8, context: universal_lambda_interface.Context) ![]const u8
|
||||||
```
|
```
|
||||||
|
|
||||||
Your main function should return `!u8`. Let the package know about your handler in your main function, like so:
|
Your main function should return `!u8`. Let the package know about your handler in your main function, like so:
|
||||||
|
@ -93,12 +102,8 @@ would like to use, you may specify it. Otherwise, an appropriate allocator
|
||||||
will be created and used. Currently this is an ArenaAllocator wrapped around
|
will be created and used. Currently this is an ArenaAllocator wrapped around
|
||||||
an appropriate base allocator, so your handler does not require deallocation.
|
an appropriate base allocator, so your handler does not require deallocation.
|
||||||
|
|
||||||
Note that for `flexilib` builds, the main function is ignored and the handler
|
|
||||||
is called directly. This is unique to flexilib.
|
|
||||||
|
|
||||||
A fully working example of usage is at https://git.lerch.org/lobo/universal-lambda-example/.
|
A fully working example of usage is at https://git.lerch.org/lobo/universal-lambda-example/.
|
||||||
|
|
||||||
|
|
||||||
Usage - Building
|
Usage - Building
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
@ -134,9 +139,8 @@ as a standalone web server.
|
||||||
Limitations
|
Limitations
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
This is currently a minimal viable product. The biggest current limitation
|
Limitations include standalone web server port customization and linux/aws cli requirements for Linux.
|
||||||
is that context is not currently implemented. This is important to see
|
|
||||||
command line arguments, http headers and the like.
|
|
||||||
|
|
||||||
Other limitations include standalone web server port customization, main
|
Also, within the context, AWS Lambda is unable to provide proper method, target,
|
||||||
function not called under flexilib, and linux/aws cli requirements for Linux.
|
and headers for the request. This may be important for routing purposes. Suggestion
|
||||||
|
here is to use API Gateway and pass these parameters through the event_data content.
|
||||||
|
|
56
build.zig
56
build.zig
|
@ -23,17 +23,9 @@ pub fn build(b: *std.Build) !void {
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
const flexilib_dep = b.dependency("flexilib", .{
|
const universal_lambda = @import("src/universal_lambda_build.zig");
|
||||||
.target = target,
|
universal_lambda.module_root = b.build_root.path;
|
||||||
.optimize = optimize,
|
_ = try universal_lambda.addModules(b, lib);
|
||||||
});
|
|
||||||
const flexilib_module = flexilib_dep.module("flexilib-interface");
|
|
||||||
lib.addModule("flexilib-interface", flexilib_module);
|
|
||||||
// Because we are...well, ourselves, we'll manually override the module
|
|
||||||
// root (we are not a module here).
|
|
||||||
const ulb = @import("src/universal_lambda_build.zig");
|
|
||||||
ulb.module_root = "";
|
|
||||||
_ = try ulb.createOptionsModule(b, lib);
|
|
||||||
|
|
||||||
// This declares intent for the library to be installed into the standard
|
// This declares intent for the library to be installed into the standard
|
||||||
// location when the user invokes the "install" step (the default step when
|
// location when the user invokes the "install" step (the default step when
|
||||||
|
@ -43,55 +35,27 @@ pub fn build(b: *std.Build) !void {
|
||||||
// Creates a step for unit testing. This only builds the test executable
|
// Creates a step for unit testing. This only builds the test executable
|
||||||
// but does not run it.
|
// but does not run it.
|
||||||
const main_tests = b.addTest(.{
|
const main_tests = b.addTest(.{
|
||||||
.root_source_file = .{ .path = "src/universal_lambda.zig" },
|
.root_source_file = .{ .path = "src/test.zig" },
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
_ = try ulb.createOptionsModule(b, main_tests);
|
_ = try universal_lambda.addModules(b, main_tests);
|
||||||
|
// _ = try ulb.createOptionsModule(b, main_tests);
|
||||||
|
|
||||||
main_tests.addModule("flexilib-interface", flexilib_module);
|
// main_tests.addModule("flexilib-interface", flexilib_module);
|
||||||
var run_main_tests = b.addRunArtifact(main_tests);
|
var run_main_tests = b.addRunArtifact(main_tests);
|
||||||
run_main_tests.skip_foreign_checks = true;
|
run_main_tests.skip_foreign_checks = true;
|
||||||
|
|
||||||
const helper_tests = b.addTest(.{
|
|
||||||
.root_source_file = .{ .path = "src/helpers.zig" },
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
_ = try ulb.createOptionsModule(b, helper_tests);
|
|
||||||
// Add module
|
|
||||||
helper_tests.addAnonymousModule("universal_lambda_handler", .{
|
|
||||||
// Source file can be anywhere on disk, does not need to be a subdirectory
|
|
||||||
.source_file = .{ .path = "src/universal_lambda.zig" },
|
|
||||||
// We alsso need the interface module available here
|
|
||||||
.dependencies = &[_]std.Build.ModuleDependency{
|
|
||||||
// Add options module so we can let our universal_lambda know what
|
|
||||||
// type of interface is necessary
|
|
||||||
.{
|
|
||||||
.name = "build_options",
|
|
||||||
.module = main_tests.modules.get("build_options").?,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.name = "flexilib-interface",
|
|
||||||
.module = flexilib_module,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
var run_helper_tests = b.addRunArtifact(helper_tests);
|
|
||||||
run_helper_tests.skip_foreign_checks = true;
|
|
||||||
|
|
||||||
// This creates a build step. It will be visible in the `zig build --help` menu,
|
// This creates a build step. It will be visible in the `zig build --help` menu,
|
||||||
// and can be selected like this: `zig build test`
|
// and can be selected like this: `zig build test`
|
||||||
// This will evaluate the `test` step rather than the default, which is "install".
|
// This will evaluate the `test` step rather than the default, which is "install".
|
||||||
const test_step = b.step("test", "Run library tests");
|
const test_step = b.step("test", "Run library tests");
|
||||||
test_step.dependOn(&run_main_tests.step);
|
test_step.dependOn(&run_main_tests.step);
|
||||||
test_step.dependOn(&run_helper_tests.step);
|
|
||||||
|
|
||||||
_ = b.addModule("universal_lambda_helpers", .{
|
|
||||||
.source_file = .{ .path = "src/helpers.zig" },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn configureBuild(b: *std.Build, cs: *std.Build.Step.Compile) !void {
|
pub fn configureBuild(b: *std.Build, cs: *std.Build.Step.Compile) !void {
|
||||||
try @import("src/universal_lambda_build.zig").configureBuild(b, cs);
|
try @import("src/universal_lambda_build.zig").configureBuild(b, cs);
|
||||||
}
|
}
|
||||||
|
pub fn addModules(b: *std.Build, cs: *std.Build.Step.Compile) ![]const u8 {
|
||||||
|
try @import("src/universal_lambda_build.zig").addModules(b, cs);
|
||||||
|
}
|
||||||
|
|
275
src/console.zig
Normal file
275
src/console.zig
Normal file
|
@ -0,0 +1,275 @@
|
||||||
|
//! This consists of helper functions to provide simple access using standard
|
||||||
|
//! patterns.
|
||||||
|
const std = @import("std");
|
||||||
|
const interface = @import("universal_lambda_interface");
|
||||||
|
|
||||||
|
const Option = struct {
|
||||||
|
short: []const u8,
|
||||||
|
long: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const target_option: Option = .{ .short = "t", .long = "target" };
|
||||||
|
const url_option: Option = .{ .short = "u", .long = "url" };
|
||||||
|
const header_option: Option = .{ .short = "h", .long = "header" };
|
||||||
|
const method_option: Option = .{ .short = "m", .long = "method" };
|
||||||
|
|
||||||
|
pub fn run(allocator: ?std.mem.Allocator, event_handler: interface.HandlerFn) !u8 {
|
||||||
|
var arena = std.heap.ArenaAllocator.init(allocator orelse std.heap.page_allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
|
||||||
|
const aa = arena.allocator();
|
||||||
|
|
||||||
|
const is_test = @import("builtin").is_test;
|
||||||
|
const data = if (is_test)
|
||||||
|
test_content
|
||||||
|
else
|
||||||
|
std.io.getStdIn().reader().readAllAlloc(aa, std.math.maxInt(usize));
|
||||||
|
// We're setting up an arena allocator. While we could use a gpa and get
|
||||||
|
// some additional safety, this is now "production" runtime, and those
|
||||||
|
// things are better handled by unit tests
|
||||||
|
var response = interface.Response.init(aa);
|
||||||
|
defer response.deinit();
|
||||||
|
var headers = try findHeaders(aa);
|
||||||
|
defer headers.deinit();
|
||||||
|
response.request.headers = headers.http_headers.*;
|
||||||
|
response.request.headers_owned = false;
|
||||||
|
response.request.target = try findTarget(aa);
|
||||||
|
response.request.method = try findMethod(aa);
|
||||||
|
// Note here we are throwing out the status and reason. This is to make
|
||||||
|
// the console experience less "webby" and more "consoly", at the potential
|
||||||
|
// cost of data loss for not outputting the http status/reason
|
||||||
|
const output = event_handler(aa, data, &response) catch |err| {
|
||||||
|
const err_writer = if (is_test)
|
||||||
|
test_output.writer()
|
||||||
|
else
|
||||||
|
std.io.getStdErr();
|
||||||
|
|
||||||
|
// Flush anything already written by the handler
|
||||||
|
try err_writer.writeAll(response.body.items);
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
|
const writer = if (is_test)
|
||||||
|
test_output.writer()
|
||||||
|
else if (response.status.class() == .success) std.io.getStdOut() else std.io.getStdErr();
|
||||||
|
|
||||||
|
// First flush anything written by the handler
|
||||||
|
try writer.writeAll(response.body.items);
|
||||||
|
|
||||||
|
// Now flush the result
|
||||||
|
try writer.writeAll(output);
|
||||||
|
try writer.writeAll("\n");
|
||||||
|
// We might have gotten an error message managed directly by the event handler
|
||||||
|
// If that's the case, we will need to report back an error code
|
||||||
|
return if (response.status.class() == .success) 0 else 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findMethod(allocator: std.mem.Allocator) !std.http.Method {
|
||||||
|
// without context, we have environment variables (but for this, I think not),
|
||||||
|
// possibly event data (API Gateway does this if so configured),
|
||||||
|
// or the command line. For now we'll just look at the command line
|
||||||
|
var argIterator = try std.process.argsWithAllocator(allocator);
|
||||||
|
_ = argIterator.next();
|
||||||
|
var is_target_option = false;
|
||||||
|
while (argIterator.next()) |arg| {
|
||||||
|
if (is_target_option) {
|
||||||
|
if (std.mem.startsWith(u8, arg, "-") or
|
||||||
|
std.mem.startsWith(u8, arg, "--"))
|
||||||
|
return error.CommandLineError;
|
||||||
|
return std.meta.stringToEnum(std.http.Method, arg) orelse return error.BadMethod;
|
||||||
|
}
|
||||||
|
if (std.mem.startsWith(u8, arg, "-" ++ method_option.short) or
|
||||||
|
std.mem.startsWith(u8, arg, "--" ++ method_option.long))
|
||||||
|
{
|
||||||
|
// We'll search for --target=blah style first
|
||||||
|
var split = std.mem.splitSequence(u8, arg, "=");
|
||||||
|
_ = split.next();
|
||||||
|
const rest = split.rest();
|
||||||
|
if (split.next()) |_|
|
||||||
|
return std.meta.stringToEnum(std.http.Method, rest) orelse return error.BadMethod;
|
||||||
|
is_target_option = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .GET;
|
||||||
|
}
|
||||||
|
fn findTarget(allocator: std.mem.Allocator) ![]const u8 {
|
||||||
|
// without context, we have environment variables (but for this, I think not),
|
||||||
|
// possibly event data (API Gateway does this if so configured),
|
||||||
|
// or the command line. For now we'll just look at the command line
|
||||||
|
var argIterator = try std.process.argsWithAllocator(allocator);
|
||||||
|
_ = argIterator.next();
|
||||||
|
var is_target_option = false;
|
||||||
|
var is_url_option = false;
|
||||||
|
while (argIterator.next()) |arg| {
|
||||||
|
if (is_target_option or is_url_option) {
|
||||||
|
if (std.mem.startsWith(u8, arg, "-") or
|
||||||
|
std.mem.startsWith(u8, arg, "--"))
|
||||||
|
return error.CommandLineError;
|
||||||
|
if (is_target_option)
|
||||||
|
return arg;
|
||||||
|
return (try std.Uri.parse(arg)).path;
|
||||||
|
}
|
||||||
|
if (std.mem.startsWith(u8, arg, "-" ++ target_option.short) or
|
||||||
|
std.mem.startsWith(u8, arg, "--" ++ target_option.long))
|
||||||
|
{
|
||||||
|
// We'll search for --target=blah style first
|
||||||
|
var split = std.mem.splitSequence(u8, arg, "=");
|
||||||
|
_ = split.next();
|
||||||
|
const rest = split.rest();
|
||||||
|
if (split.next()) |_| return rest; // found it
|
||||||
|
is_target_option = true;
|
||||||
|
}
|
||||||
|
if (std.mem.startsWith(u8, arg, "-" ++ url_option.short) or
|
||||||
|
std.mem.startsWith(u8, arg, "--" ++ url_option.long))
|
||||||
|
{
|
||||||
|
// We'll search for --target=blah style first
|
||||||
|
var split = std.mem.splitSequence(u8, arg, "=");
|
||||||
|
_ = split.next();
|
||||||
|
const rest = split.rest();
|
||||||
|
if (split.next()) |_| return (try std.Uri.parse(rest)).path; // found it
|
||||||
|
is_url_option = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Headers = struct {
|
||||||
|
http_headers: *std.http.Headers,
|
||||||
|
owned: bool,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, headers: *std.http.Headers, owned: bool) Self {
|
||||||
|
return .{
|
||||||
|
.http_headers = headers,
|
||||||
|
.owned = owned,
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
if (self.owned) {
|
||||||
|
self.http_headers.deinit();
|
||||||
|
self.allocator.destroy(self.http_headers);
|
||||||
|
self.http_headers = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get headers from request. Headers will be gathered from the command line
|
||||||
|
// and include all environment variables
|
||||||
|
pub fn findHeaders(allocator: std.mem.Allocator) !Headers {
|
||||||
|
var headers = try allocator.create(std.http.Headers);
|
||||||
|
errdefer allocator.destroy(headers);
|
||||||
|
headers.allocator = allocator;
|
||||||
|
headers.list = .{};
|
||||||
|
headers.index = .{};
|
||||||
|
headers.owned = true;
|
||||||
|
errdefer headers.deinit();
|
||||||
|
|
||||||
|
// without context, we have environment variables
|
||||||
|
// possibly event data (API Gateway does this if so configured),
|
||||||
|
// or the command line. For headers, we'll prioritize command line options
|
||||||
|
// with a fallback to environment variables
|
||||||
|
const is_test = @import("builtin").is_test;
|
||||||
|
var argIterator = if (is_test) test_args.iterator(0) else std.process.argsWithAllocator(allocator);
|
||||||
|
_ = argIterator.next();
|
||||||
|
var is_header_option = false;
|
||||||
|
while (argIterator.next()) |a| {
|
||||||
|
const arg = if (is_test) a.* else a;
|
||||||
|
if (is_header_option) {
|
||||||
|
if (std.mem.startsWith(u8, arg, "-") or
|
||||||
|
std.mem.startsWith(u8, arg, "--"))
|
||||||
|
return error.CommandLineError;
|
||||||
|
is_header_option = false;
|
||||||
|
var split = std.mem.splitSequence(u8, arg, "=");
|
||||||
|
const name = split.next().?;
|
||||||
|
try headers.append(name, split.rest());
|
||||||
|
}
|
||||||
|
if (std.mem.startsWith(u8, arg, "-" ++ header_option.short) or
|
||||||
|
std.mem.startsWith(u8, arg, "--" ++ header_option.long))
|
||||||
|
{
|
||||||
|
// header option forms on command line:
|
||||||
|
// -h name=value
|
||||||
|
// --header name=value
|
||||||
|
is_header_option = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (is_test) return Headers.init(allocator, headers, true);
|
||||||
|
|
||||||
|
// not found on command line. Let's check environment
|
||||||
|
var map = try std.process.getEnvMap(allocator);
|
||||||
|
defer map.deinit();
|
||||||
|
var it = map.iterator();
|
||||||
|
while (it.next()) |kvp| {
|
||||||
|
// Do not allow environment variables to interfere with command line
|
||||||
|
if (headers.getFirstValue(kvp.key_ptr.*) == null)
|
||||||
|
try headers.append(
|
||||||
|
kvp.key_ptr.*,
|
||||||
|
kvp.value_ptr.*,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Headers.init(allocator, headers, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
std.testing.refAllDecls(@This());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "can get headers" {
|
||||||
|
// const ll = std.testing.log_level;
|
||||||
|
// std.testing.log_level = .debug;
|
||||||
|
// defer std.testing.log_level = ll;
|
||||||
|
// This test complains about a leak in WASI, but in WASI, we're not running
|
||||||
|
// long processes (just command line stuff), so we don't really care about
|
||||||
|
// leaks. There doesn't seem to be a way to ignore leak detection
|
||||||
|
if (@import("builtin").os.tag == .wasi) return error.SkipZigTest;
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
test_args = .{};
|
||||||
|
defer test_args.deinit(allocator);
|
||||||
|
try test_args.append(allocator, "mainexe");
|
||||||
|
try test_args.append(allocator, "-h");
|
||||||
|
try test_args.append(allocator, "X-Foo=Bar");
|
||||||
|
var headers = try findHeaders(allocator);
|
||||||
|
defer headers.deinit();
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), headers.http_headers.list.items.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn testHandler(allocator: std.mem.Allocator, event_data: []const u8, context: interface.Context) ![]const u8 {
|
||||||
|
try context.headers.append("X-custom-foo", "bar");
|
||||||
|
try context.writeAll(event_data);
|
||||||
|
return std.fmt.allocPrint(allocator, "{d}", .{context.request.headers.list.items.len});
|
||||||
|
}
|
||||||
|
|
||||||
|
var test_args: std.SegmentedList([]const u8, 8) = undefined;
|
||||||
|
var test_content: []const u8 = undefined;
|
||||||
|
var test_output: std.ArrayList(u8) = undefined;
|
||||||
|
// 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();
|
||||||
|
test_args = .{};
|
||||||
|
defer test_args.deinit(aa);
|
||||||
|
try test_args.append(aa, "mainexe");
|
||||||
|
try test_args.append(aa, "-t");
|
||||||
|
try test_args.append(aa, "/hello");
|
||||||
|
|
||||||
|
try test_args.append(aa, "-m");
|
||||||
|
try test_args.append(aa, "PUT");
|
||||||
|
|
||||||
|
try test_args.append(aa, "-h");
|
||||||
|
try test_args.append(aa, "X-Foo=Bar");
|
||||||
|
|
||||||
|
test_content = " ";
|
||||||
|
test_output = std.ArrayList(u8).init(aa);
|
||||||
|
defer test_output.deinit();
|
||||||
|
const response = try run(aa, testHandler);
|
||||||
|
try std.testing.expectEqualStrings(" 1\n", test_output.items);
|
||||||
|
try std.testing.expectEqual(@as(u8, 0), response);
|
||||||
|
// Response headers won't be visible in a console app
|
||||||
|
// 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]);
|
||||||
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const interface = @import("flexilib-interface");
|
const interface = @import("flexilib-interface");
|
||||||
|
const universal_lambda_interface = @import("universal_lambda_interface");
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
|
||||||
const log = std.log.scoped(.@"main-lib");
|
const log = std.log.scoped(.@"main-lib");
|
||||||
|
|
||||||
const client_handler = @import("flexilib_handler");
|
const Application = if (@import("builtin").is_test) @This() else @import("flexilib_handler");
|
||||||
|
|
||||||
// The main program will look for exports during the request lifecycle:
|
// 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
|
// zigInit (optional): called at the beginning of a request, includes pointer to an allocator
|
||||||
|
@ -63,13 +64,57 @@ fn handleRequest(allocator: std.mem.Allocator, response: *interface.ZigResponse)
|
||||||
// setup
|
// setup
|
||||||
var response_writer = response.body.writer();
|
var response_writer = response.body.writer();
|
||||||
// dispatch to our actual handler
|
// dispatch to our actual handler
|
||||||
try response_writer.writeAll(try client_handler.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 response_content = try handler.?(
|
||||||
allocator,
|
allocator,
|
||||||
response.request.content,
|
response.request.content,
|
||||||
.{
|
&ul_response,
|
||||||
.flexilib = response,
|
);
|
||||||
},
|
// Copy any headers
|
||||||
));
|
for (ul_response.headers.list.items) |entry| {
|
||||||
|
try response.headers.append(entry.name, entry.value);
|
||||||
|
}
|
||||||
|
// 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 {
|
||||||
|
try context.headers.append("X-custom-foo", "bar");
|
||||||
|
try context.writeAll(event_data);
|
||||||
|
return std.fmt.allocPrint(allocator, "{d}", .{context.request.headers.list.items.len});
|
||||||
}
|
}
|
||||||
// Need to figure out how tests would work
|
// Need to figure out how tests would work
|
||||||
test "handle_request" {
|
test "handle_request" {
|
||||||
|
@ -86,13 +131,16 @@ test "handle_request" {
|
||||||
var req = interface.Request{
|
var req = interface.Request{
|
||||||
.method = @ptrCast(@constCast("GET".ptr)),
|
.method = @ptrCast(@constCast("GET".ptr)),
|
||||||
.method_len = 3,
|
.method_len = 3,
|
||||||
.content = @ptrCast(@constCast("GET".ptr)),
|
.content = @ptrCast(@constCast(" ".ptr)),
|
||||||
.content_len = 3,
|
.content_len = 1,
|
||||||
.headers = headers.ptr,
|
.headers = headers.ptr,
|
||||||
.headers_len = 1,
|
.headers_len = 1,
|
||||||
|
.target = @ptrCast(@constCast("/".ptr)),
|
||||||
|
.target_len = 1,
|
||||||
};
|
};
|
||||||
const response = handle_request(&req).?;
|
const response = handle_request(&req).?;
|
||||||
try testing.expectEqualStrings(" 1", response.ptr[0..response.len]);
|
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("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]);
|
try testing.expectEqualStrings("bar", response.headers[0].value_ptr[0..response.headers[0].value_len]);
|
||||||
}
|
}
|
||||||
|
|
223
src/helpers.zig
223
src/helpers.zig
|
@ -1,223 +0,0 @@
|
||||||
//! This consists of helper functions to provide simple access using standard
|
|
||||||
//! patterns.
|
|
||||||
const std = @import("std");
|
|
||||||
const universal_lambda = @import("universal_lambda_handler");
|
|
||||||
|
|
||||||
const Option = struct {
|
|
||||||
short: []const u8,
|
|
||||||
long: []const u8,
|
|
||||||
};
|
|
||||||
|
|
||||||
const target_option: Option = .{ .short = "t", .long = "target" };
|
|
||||||
const url_option: Option = .{ .short = "u", .long = "url" };
|
|
||||||
const header_option: Option = .{ .short = "h", .long = "header" };
|
|
||||||
|
|
||||||
/// Finds the "target" for this request. In a web request, this is the path
|
|
||||||
/// used for the request (e.g. "/" vs "/admin"). In a non-web environment,
|
|
||||||
/// this is determined by a command line option -t or --target. Note that
|
|
||||||
/// AWS API Gateway is not supported here...this is a configured thing in
|
|
||||||
/// API Gateway, and so is pretty situational. It also would be presented in
|
|
||||||
/// event data rather than context
|
|
||||||
pub fn findTarget(allocator: std.mem.Allocator, context: universal_lambda.Context) ![]const u8 {
|
|
||||||
switch (context) {
|
|
||||||
.web_request => |res| return res.request.target,
|
|
||||||
.flexilib => |ctx| return ctx.request.target,
|
|
||||||
.none => return findTargetWithoutContext(allocator),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setStatus(context: universal_lambda.Context, status: std.http.Status, reason: ?[]const u8) void {
|
|
||||||
switch (context) {
|
|
||||||
.web_request => |res| {
|
|
||||||
res.status = status;
|
|
||||||
res.reason = reason;
|
|
||||||
},
|
|
||||||
.flexilib => |res| {
|
|
||||||
res.status = status;
|
|
||||||
res.reason = reason;
|
|
||||||
},
|
|
||||||
.none => |res| {
|
|
||||||
res.status = status;
|
|
||||||
res.reason = reason;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn writeAll(context: universal_lambda.Context, data: []const u8) !void {
|
|
||||||
switch (context) {
|
|
||||||
.web_request => |res| return try res.writeAll(data),
|
|
||||||
.flexilib => |res| return try res.writeAll(data),
|
|
||||||
.none => |res| return try res.writeAll(data),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn writeFn(context: universal_lambda.Context, data: []const u8) WriteError!usize {
|
|
||||||
switch (context) {
|
|
||||||
.web_request => |res| return res.write(data),
|
|
||||||
.flexilib => |res| return res.write(data),
|
|
||||||
.none => |res| return res.write(data),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub const WriteError = error{OutOfMemory};
|
|
||||||
|
|
||||||
pub fn writer(context: universal_lambda.Context) std.io.Writer(universal_lambda.Context, WriteError, writeFn) {
|
|
||||||
return .{ .context = context };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn findTargetWithoutContext(allocator: std.mem.Allocator) ![]const u8 {
|
|
||||||
// without context, we have environment variables (but for this, I think not),
|
|
||||||
// possibly event data (API Gateway does this if so configured),
|
|
||||||
// or the command line. For now we'll just look at the command line
|
|
||||||
var argIterator = try std.process.argsWithAllocator(allocator);
|
|
||||||
_ = argIterator.next();
|
|
||||||
var is_target_option = false;
|
|
||||||
var is_url_option = false;
|
|
||||||
while (argIterator.next()) |arg| {
|
|
||||||
if (is_target_option or is_url_option) {
|
|
||||||
if (std.mem.startsWith(u8, arg, "-") or
|
|
||||||
std.mem.startsWith(u8, arg, "--"))
|
|
||||||
{
|
|
||||||
// bad user input, but we're not returning errors here
|
|
||||||
return "/";
|
|
||||||
}
|
|
||||||
if (is_target_option)
|
|
||||||
return arg;
|
|
||||||
return (try std.Uri.parse(arg)).path;
|
|
||||||
}
|
|
||||||
if (std.mem.startsWith(u8, arg, "-" ++ target_option.short) or
|
|
||||||
std.mem.startsWith(u8, arg, "--" ++ target_option.long))
|
|
||||||
{
|
|
||||||
// We'll search for --target=blah style first
|
|
||||||
var split = std.mem.splitSequence(u8, arg, "=");
|
|
||||||
_ = split.next();
|
|
||||||
const rest = split.rest();
|
|
||||||
if (split.next()) |_| return rest; // found it
|
|
||||||
is_target_option = true;
|
|
||||||
}
|
|
||||||
if (std.mem.startsWith(u8, arg, "-" ++ url_option.short) or
|
|
||||||
std.mem.startsWith(u8, arg, "--" ++ url_option.long))
|
|
||||||
{
|
|
||||||
// We'll search for --target=blah style first
|
|
||||||
var split = std.mem.splitSequence(u8, arg, "=");
|
|
||||||
_ = split.next();
|
|
||||||
const rest = split.rest();
|
|
||||||
if (split.next()) |_| return (try std.Uri.parse(rest)).path; // found it
|
|
||||||
is_url_option = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const Headers = struct {
|
|
||||||
http_headers: *std.http.Headers,
|
|
||||||
owned: bool,
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, headers: *std.http.Headers, owned: bool) Self {
|
|
||||||
return .{
|
|
||||||
.http_headers = headers,
|
|
||||||
.owned = owned,
|
|
||||||
.allocator = allocator,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
|
||||||
if (self.owned) {
|
|
||||||
self.http_headers.deinit();
|
|
||||||
self.allocator.destroy(self.http_headers);
|
|
||||||
self.http_headers = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Get headers from request. If Lambda is not in a web context, headers
|
|
||||||
/// will be gathered from the command line and include all environment variables
|
|
||||||
pub fn allHeaders(allocator: std.mem.Allocator, context: universal_lambda.Context) !Headers {
|
|
||||||
switch (context) {
|
|
||||||
.web_request => |res| return Headers.init(allocator, &res.request.headers, false),
|
|
||||||
.flexilib => |ctx| return Headers.init(allocator, &ctx.request.headers, false),
|
|
||||||
.none => return headersWithoutContext(allocator),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn headersWithoutContext(allocator: std.mem.Allocator) !Headers {
|
|
||||||
var headers = try allocator.create(std.http.Headers);
|
|
||||||
errdefer allocator.destroy(headers);
|
|
||||||
headers.allocator = allocator;
|
|
||||||
headers.list = .{};
|
|
||||||
headers.index = .{};
|
|
||||||
headers.owned = true;
|
|
||||||
errdefer headers.deinit();
|
|
||||||
|
|
||||||
// without context, we have environment variables
|
|
||||||
// possibly event data (API Gateway does this if so configured),
|
|
||||||
// or the command line. For headers, we'll prioritize command line options
|
|
||||||
// with a fallback to environment variables
|
|
||||||
var argIterator = try std.process.argsWithAllocator(allocator);
|
|
||||||
_ = argIterator.next();
|
|
||||||
var is_header_option = false;
|
|
||||||
while (argIterator.next()) |arg| {
|
|
||||||
if (is_header_option) {
|
|
||||||
if (std.mem.startsWith(u8, arg, "-") or
|
|
||||||
std.mem.startsWith(u8, arg, "--"))
|
|
||||||
{
|
|
||||||
return error.CommandLineError;
|
|
||||||
}
|
|
||||||
is_header_option = false;
|
|
||||||
var split = std.mem.splitSequence(u8, arg, "=");
|
|
||||||
const name = split.next().?;
|
|
||||||
try headers.append(name, split.rest());
|
|
||||||
}
|
|
||||||
if (std.mem.startsWith(u8, arg, "-" ++ header_option.short) or
|
|
||||||
std.mem.startsWith(u8, arg, "--" ++ header_option.long))
|
|
||||||
{
|
|
||||||
// header option forms on command line:
|
|
||||||
// -h name=value
|
|
||||||
// --header name=value
|
|
||||||
is_header_option = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// not found on command line. Let's check environment
|
|
||||||
var map = try std.process.getEnvMap(allocator);
|
|
||||||
defer map.deinit();
|
|
||||||
var it = map.iterator();
|
|
||||||
while (it.next()) |kvp| {
|
|
||||||
// Do not allow environment variables to interfere with command line
|
|
||||||
if (headers.getFirstValue(kvp.key_ptr.*) == null)
|
|
||||||
try headers.append(
|
|
||||||
kvp.key_ptr.*,
|
|
||||||
kvp.value_ptr.*,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Headers.init(allocator, headers, true); // nowhere to be found
|
|
||||||
}
|
|
||||||
|
|
||||||
test {
|
|
||||||
std.testing.refAllDecls(@This());
|
|
||||||
}
|
|
||||||
|
|
||||||
test "can get headers" {
|
|
||||||
// This test complains about a leak in WASI, but in WASI, we're not running
|
|
||||||
// long processes (just command line stuff), so we don't really care about
|
|
||||||
// leaks. There doesn't seem to be a way to ignore leak detection
|
|
||||||
if (@import("builtin").os.tag == .wasi) return error.SkipZigTest;
|
|
||||||
const allocator = std.testing.allocator;
|
|
||||||
var response = universal_lambda.Response.init(allocator);
|
|
||||||
const context = universal_lambda.Context{
|
|
||||||
.none = &response,
|
|
||||||
};
|
|
||||||
var headers = try allHeaders(allocator, context);
|
|
||||||
defer headers.deinit();
|
|
||||||
try std.testing.expect(headers.http_headers.list.items.len > 0);
|
|
||||||
}
|
|
||||||
test "can write" {
|
|
||||||
const allocator = std.testing.allocator;
|
|
||||||
var response = universal_lambda.Response.init(allocator);
|
|
||||||
const context = universal_lambda.Context{
|
|
||||||
.none = &response,
|
|
||||||
};
|
|
||||||
const my_writer = writer(context);
|
|
||||||
try my_writer.writeAll("hello");
|
|
||||||
try response.finish();
|
|
||||||
}
|
|
69
src/interface.zig
Normal file
69
src/interface.zig
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const HandlerFn = *const fn (std.mem.Allocator, []const u8, Context) anyerror![]const u8;
|
||||||
|
|
||||||
|
pub const Response = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
headers: std.http.Headers,
|
||||||
|
headers_owned: bool = true,
|
||||||
|
status: std.http.Status = .ok,
|
||||||
|
reason: ?[]const u8 = null,
|
||||||
|
/// client request. Note that in AWS lambda, all these are left at default.
|
||||||
|
/// It is currently up to you to work through a) if API Gateway is set up,
|
||||||
|
/// and b) how that gets parsed into event data. API Gateway has the ability
|
||||||
|
/// to severely muck with inbound data and we are unprepared to deal with
|
||||||
|
/// that here
|
||||||
|
request: struct {
|
||||||
|
target: []const u8 = "/",
|
||||||
|
headers: std.http.Headers,
|
||||||
|
headers_owned: bool = true,
|
||||||
|
method: std.http.Method = .GET,
|
||||||
|
},
|
||||||
|
body: std.ArrayList(u8),
|
||||||
|
|
||||||
|
// The problem we face is this:
|
||||||
|
//
|
||||||
|
// exe_run, cloudflare (wasi) are basically console apps
|
||||||
|
// flexilib is a web server (wierd one)
|
||||||
|
// standalone web server is a web server
|
||||||
|
// aws lambda is a web client
|
||||||
|
//
|
||||||
|
// void will work for exe_run/cloudflare
|
||||||
|
// ZigResponse works out of the box for flexilib - the lifecycle problem is
|
||||||
|
// handled in the interface
|
||||||
|
//
|
||||||
|
// aws lambda - need to investigate
|
||||||
|
// standalone web server...needs to spool
|
||||||
|
pub fn init(allocator: std.mem.Allocator) Response {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.headers = .{ .allocator = allocator },
|
||||||
|
.request = .{
|
||||||
|
.headers = .{ .allocator = allocator },
|
||||||
|
},
|
||||||
|
.body = std.ArrayList(u8).init(allocator),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pub fn write(res: *Response, bytes: []const u8) !usize {
|
||||||
|
return res.body.writer().write(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writeAll(res: *Response, bytes: []const u8) !void {
|
||||||
|
return res.body.writer().writeAll(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writer(res: *Response) std.io.Writer(*Response, error{OutOfMemory}, writeFn) {
|
||||||
|
return .{ .context = res };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeFn(context: *Response, data: []const u8) error{OutOfMemory}!usize {
|
||||||
|
return try context.write(data);
|
||||||
|
}
|
||||||
|
pub fn deinit(res: *Response) void {
|
||||||
|
res.body.deinit();
|
||||||
|
if (res.headers_owned) res.headers.deinit();
|
||||||
|
if (res.request.headers_owned) res.request.headers.deinit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Context = *Response;
|
|
@ -1,9 +1,10 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const HandlerFn = @import("universal_lambda.zig").HandlerFn;
|
const universal_lambda_interface = @import("universal_lambda_interface");
|
||||||
const Context = @import("universal_lambda.zig").Context;
|
const HandlerFn = universal_lambda_interface.HandlerFn;
|
||||||
const UniversalLambdaResponse = @import("universal_lambda.zig").Response;
|
const Context = universal_lambda_interface.Context;
|
||||||
|
const UniversalLambdaResponse = universal_lambda_interface.Response;
|
||||||
|
|
||||||
const log = std.log.scoped(.lambda);
|
const log = std.log.scoped(.lambda);
|
||||||
|
|
||||||
|
@ -70,15 +71,23 @@ pub fn run(allocator: ?std.mem.Allocator, event_handler: HandlerFn) !u8 { // TOD
|
||||||
defer ev.?.deinit();
|
defer ev.?.deinit();
|
||||||
// Lambda does not have context, just environment variables. API Gateway
|
// Lambda does not have context, just environment variables. API Gateway
|
||||||
// might be configured to pass in lots of context, but this comes through
|
// might be configured to pass in lots of context, but this comes through
|
||||||
// event data, not context.
|
// event data, not context. In this case, we lose:
|
||||||
|
//
|
||||||
|
// request headers
|
||||||
|
// request method
|
||||||
|
// request target
|
||||||
var response = UniversalLambdaResponse.init(req_allocator);
|
var response = UniversalLambdaResponse.init(req_allocator);
|
||||||
response.output_file = std.io.getStdOut();
|
defer response.deinit();
|
||||||
const event_response = event_handler(req_allocator, event.event_data, .{ .none = &response }) catch |err| {
|
const body_writer = std.io.getStdOut();
|
||||||
response.finish() catch unreachable;
|
const event_response = event_handler(req_allocator, event.event_data, &response) catch |err| {
|
||||||
|
body_writer.writeAll(response.body.items) catch unreachable;
|
||||||
event.reportError(@errorReturnTrace(), err, lambda_runtime_uri) catch unreachable;
|
event.reportError(@errorReturnTrace(), err, lambda_runtime_uri) catch unreachable;
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
response.finish() catch unreachable;
|
// No error during handler. Write anything sent to body to stdout instead
|
||||||
|
// I'm not totally sure this is the right behavior as it is a little inconsistent
|
||||||
|
// (flexilib and console both write to the same io stream as the main output)
|
||||||
|
body_writer.writeAll(response.body.items) catch unreachable;
|
||||||
event.postResponse(lambda_runtime_uri, event_response) catch |err| {
|
event.postResponse(lambda_runtime_uri, event_response) catch |err| {
|
||||||
event.reportError(@errorReturnTrace(), err, lambda_runtime_uri) catch unreachable;
|
event.reportError(@errorReturnTrace(), err, lambda_runtime_uri) catch unreachable;
|
||||||
continue;
|
continue;
|
||||||
|
|
6
src/test.zig
Normal file
6
src/test.zig
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
test {
|
||||||
|
std.testing.refAllDecls(@This());
|
||||||
|
std.testing.refAllDecls(@import("universal_lambda.zig"));
|
||||||
|
}
|
|
@ -1,120 +1,32 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const build_options = @import("build_options");
|
const build_options = @import("build_options");
|
||||||
const flexilib = @import("flexilib-interface");
|
const flexilib = @import("flexilib-interface");
|
||||||
pub const HandlerFn = *const fn (std.mem.Allocator, []const u8, Context) anyerror![]const u8;
|
const interface = @import("universal_lambda_interface");
|
||||||
|
|
||||||
const log = std.log.scoped(.universal_lambda);
|
const log = std.log.scoped(.universal_lambda);
|
||||||
|
|
||||||
pub const Response = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
headers: std.http.Headers,
|
|
||||||
output_file: ?std.fs.File = null,
|
|
||||||
status: std.http.Status = .ok,
|
|
||||||
reason: ?[]const u8 = null,
|
|
||||||
request: struct {
|
|
||||||
// TODO: We will likely end up needing method here at some point...
|
|
||||||
target: []const u8,
|
|
||||||
headers: std.http.Headers,
|
|
||||||
},
|
|
||||||
al: std.ArrayList(u8),
|
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator) Response {
|
|
||||||
return .{
|
|
||||||
.allocator = allocator,
|
|
||||||
.headers = .{ .allocator = allocator },
|
|
||||||
.request = .{
|
|
||||||
.target = "/",
|
|
||||||
.headers = .{ .allocator = allocator },
|
|
||||||
},
|
|
||||||
.al = std.ArrayList(u8).init(allocator),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
pub fn write(res: *Response, bytes: []const u8) !usize {
|
|
||||||
return res.al.writer().write(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn writeAll(res: *Response, bytes: []const u8) !void {
|
|
||||||
return res.al.writer().writeAll(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn writer(res: *Response) std.io.Writer {
|
|
||||||
return res.al.writer().writer();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn finish(res: *Response) !void {
|
|
||||||
if (res.output_file) |f| {
|
|
||||||
try f.writer().writeAll(res.al.items);
|
|
||||||
}
|
|
||||||
res.al.deinit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Context = union(enum) {
|
|
||||||
web_request: switch (build_options.build_type) {
|
|
||||||
.exe_run, .cloudflare => *Response,
|
|
||||||
else => *std.http.Server.Response,
|
|
||||||
},
|
|
||||||
flexilib: *flexilib.ZigResponse,
|
|
||||||
none: *Response,
|
|
||||||
};
|
|
||||||
|
|
||||||
const runFn = blk: {
|
const runFn = blk: {
|
||||||
switch (build_options.build_type) {
|
switch (build_options.build_type) {
|
||||||
.awslambda => break :blk @import("lambda.zig").run,
|
.awslambda => break :blk @import("lambda.zig").run,
|
||||||
.standalone_server => break :blk runStandaloneServer,
|
.standalone_server => break :blk runStandaloneServer,
|
||||||
.exe_run, .cloudflare => break :blk runExe,
|
.flexilib => break :blk @import("flexilib.zig").run,
|
||||||
else => @compileError("Provider interface for " ++ @tagName(build_options.build_type) ++ " has not yet been implemented"),
|
.exe_run, .cloudflare => break :blk @import("console.zig").run,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fn deinit() void {
|
|
||||||
// if (client) |*c| c.deinit();
|
|
||||||
// client = null;
|
|
||||||
}
|
|
||||||
/// Starts the universal lambda framework. Handler will be called when an event is processing.
|
/// Starts the universal lambda framework. Handler will be called when an event is processing.
|
||||||
/// Depending on the serverless system used, from a practical sense, this may not return.
|
/// Depending on the serverless system used, from a practical sense, this may not return.
|
||||||
///
|
///
|
||||||
/// If an allocator is not provided, an approrpriate allocator will be selected and used
|
/// If an allocator is not provided, an approrpriate allocator will be selected and used
|
||||||
/// This function is intended to loop infinitely. If not used in this manner,
|
/// This function is intended to loop infinitely. If not used in this manner,
|
||||||
/// make sure to call the deinit() function
|
/// make sure to call the deinit() function
|
||||||
pub fn run(allocator: ?std.mem.Allocator, event_handler: HandlerFn) !u8 { // TODO: remove inferred error set?
|
pub fn run(allocator: ?std.mem.Allocator, event_handler: interface.HandlerFn) !u8 { // TODO: remove inferred error set?
|
||||||
return try runFn(allocator, event_handler);
|
return try runFn(allocator, event_handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runExe(allocator: ?std.mem.Allocator, event_handler: HandlerFn) !u8 {
|
|
||||||
var arena = std.heap.ArenaAllocator.init(allocator orelse std.heap.page_allocator);
|
|
||||||
defer arena.deinit();
|
|
||||||
|
|
||||||
const aa = arena.allocator();
|
|
||||||
|
|
||||||
const data = try std.io.getStdIn().reader().readAllAlloc(aa, std.math.maxInt(usize));
|
|
||||||
// We're setting up an arena allocator. While we could use a gpa and get
|
|
||||||
// some additional safety, this is now "production" runtime, and those
|
|
||||||
// things are better handled by unit tests
|
|
||||||
var response = Response.init(aa);
|
|
||||||
|
|
||||||
// Note here we are throwing out the status and reason. This is to make
|
|
||||||
// the console experience less "webby" and more "consoly", at the potential
|
|
||||||
// cost of data loss for not outputting the http status/reason
|
|
||||||
const output = event_handler(aa, data, .{ .none = &response }) catch |err| {
|
|
||||||
response.output_file = std.io.getStdErr();
|
|
||||||
try response.finish();
|
|
||||||
return err;
|
|
||||||
};
|
|
||||||
|
|
||||||
response.output_file = if (response.status.class() == .success) std.io.getStdOut() else std.io.getStdErr();
|
|
||||||
const writer = response.output_file.?.writer();
|
|
||||||
try response.finish();
|
|
||||||
try writer.writeAll(output);
|
|
||||||
try writer.writeAll("\n");
|
|
||||||
// We might have gotten an error message managed directly by the event handler
|
|
||||||
// If that's the case, we will need to report back an error code
|
|
||||||
return if (response.status.class() == .success) 0 else 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Will create a web server and marshall all requests back to our event handler
|
/// Will create a web server and marshall all requests back to our event handler
|
||||||
/// To keep things simple, we'll have this on a single thread, at least for now
|
/// To keep things simple, we'll have this on a single thread, at least for now
|
||||||
fn runStandaloneServer(allocator: ?std.mem.Allocator, event_handler: HandlerFn) !u8 {
|
fn runStandaloneServer(allocator: ?std.mem.Allocator, event_handler: interface.HandlerFn) !u8 {
|
||||||
const alloc = allocator orelse std.heap.page_allocator;
|
const alloc = allocator orelse std.heap.page_allocator;
|
||||||
|
|
||||||
var arena = std.heap.ArenaAllocator.init(alloc);
|
var arena = std.heap.ArenaAllocator.init(alloc);
|
||||||
|
@ -152,7 +64,7 @@ fn runStandaloneServer(allocator: ?std.mem.Allocator, event_handler: HandlerFn)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn processRequest(aa: std.mem.Allocator, server: *std.http.Server, event_handler: HandlerFn) !void {
|
fn processRequest(aa: std.mem.Allocator, server: *std.http.Server, event_handler: interface.HandlerFn) !void {
|
||||||
var res = try server.accept(.{ .allocator = aa });
|
var res = try server.accept(.{ .allocator = aa });
|
||||||
defer {
|
defer {
|
||||||
_ = res.reset();
|
_ = res.reset();
|
||||||
|
@ -174,7 +86,7 @@ fn processRequest(aa: std.mem.Allocator, server: *std.http.Server, event_handler
|
||||||
// no need to free - will be handled by arena
|
// no need to free - will be handled by arena
|
||||||
|
|
||||||
response_bytes = event_handler(aa, body, .{ .web_request = &res }) catch |e| brk: {
|
response_bytes = event_handler(aa, body, .{ .web_request = &res }) catch |e| brk: {
|
||||||
res.status = .internal_server_error;
|
if (res.status.class() == .success) res.status = .internal_server_error;
|
||||||
// TODO: more about this particular request
|
// TODO: more about this particular request
|
||||||
log.err("Unexpected error from executor processing request: {any}", .{e});
|
log.err("Unexpected error from executor processing request: {any}", .{e});
|
||||||
if (@errorReturnTrace()) |trace| {
|
if (@errorReturnTrace()) |trace| {
|
||||||
|
@ -188,15 +100,19 @@ fn processRequest(aa: std.mem.Allocator, server: *std.http.Server, event_handler
|
||||||
_ = try res.writer().writeAll(response_bytes);
|
_ = try res.writer().writeAll(response_bytes);
|
||||||
try res.finish();
|
try res.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
std.testing.refAllDecls(@This()); // standalone, standalone web server
|
std.testing.refAllDecls(@This());
|
||||||
// if (builtin.os.tag == .wasi) return error.SkipZigTest;
|
// if (builtin.os.tag == .wasi) return error.SkipZigTest;
|
||||||
if (@import("builtin").os.tag != .wasi) {
|
if (@import("builtin").os.tag != .wasi) {
|
||||||
std.testing.refAllDecls(@import("lambda.zig")); // lambda
|
// these use http
|
||||||
|
std.testing.refAllDecls(@import("lambda.zig"));
|
||||||
std.testing.refAllDecls(@import("cloudflaredeploy.zig"));
|
std.testing.refAllDecls(@import("cloudflaredeploy.zig"));
|
||||||
std.testing.refAllDecls(@import("CloudflareDeployStep.zig"));
|
std.testing.refAllDecls(@import("CloudflareDeployStep.zig"));
|
||||||
}
|
}
|
||||||
// TODO: re-enable
|
std.testing.refAllDecls(@import("console.zig"));
|
||||||
// std.testing.refAllDecls(@import("flexilib.zig")); // flexilib
|
std.testing.refAllDecls(@import("flexilib.zig"));
|
||||||
|
|
||||||
|
// The following do not currently have tests
|
||||||
|
|
||||||
|
// TODO: Do we want build files here too?
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,17 +16,25 @@ pub var module_root: ?[]const u8 = null;
|
||||||
|
|
||||||
pub fn configureBuild(b: *std.Build, cs: *std.Build.Step.Compile) !void {
|
pub fn configureBuild(b: *std.Build, cs: *std.Build.Step.Compile) !void {
|
||||||
const function_name = b.option([]const u8, "function-name", "Function name for Lambda [zig-fn]") orelse "zig-fn";
|
const function_name = b.option([]const u8, "function-name", "Function name for Lambda [zig-fn]") orelse "zig-fn";
|
||||||
|
|
||||||
|
const file_location = addModules(b, cs);
|
||||||
|
|
||||||
|
// Add steps
|
||||||
|
try @import("lambda_build.zig").configureBuild(b, cs, function_name);
|
||||||
|
try @import("cloudflare_build.zig").configureBuild(b, cs, function_name);
|
||||||
|
try @import("flexilib_build.zig").configureBuild(b, cs, file_location);
|
||||||
|
try @import("standalone_server_build.zig").configureBuild(b, cs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add modules
|
||||||
|
///
|
||||||
|
/// We will create the following modules for downstream consumption:
|
||||||
|
///
|
||||||
|
/// * build_options
|
||||||
|
/// * flexilib-interface
|
||||||
|
/// * universal_lambda_handler
|
||||||
|
pub fn addModules(b: *std.Build, cs: *std.Build.Step.Compile) ![]const u8 {
|
||||||
const file_location = try findFileLocation(b);
|
const file_location = try findFileLocation(b);
|
||||||
/////////////////////////////////////////////////////////////////////////
|
|
||||||
// Add modules
|
|
||||||
//
|
|
||||||
// We will create the following modules for downstream consumption:
|
|
||||||
//
|
|
||||||
// * build_options
|
|
||||||
// * flexilib-interface
|
|
||||||
// * universal_lambda_handler
|
|
||||||
//
|
|
||||||
/////////////////////////////////////////////////////////////////////////
|
|
||||||
const options_module = try createOptionsModule(b, cs);
|
const options_module = try createOptionsModule(b, cs);
|
||||||
|
|
||||||
// We need to add the interface module here as well, so universal_lambda.zig
|
// We need to add the interface module here as well, so universal_lambda.zig
|
||||||
|
@ -43,6 +51,11 @@ pub fn configureBuild(b: *std.Build, cs: *std.Build.Step.Compile) !void {
|
||||||
const flexilib_module = flexilib_dep.module("flexilib-interface");
|
const flexilib_module = flexilib_dep.module("flexilib-interface");
|
||||||
// Make the interface available for consumption
|
// Make the interface available for consumption
|
||||||
cs.addModule("flexilib-interface", flexilib_module);
|
cs.addModule("flexilib-interface", flexilib_module);
|
||||||
|
cs.addAnonymousModule("universal_lambda_interface", .{
|
||||||
|
.source_file = .{ .path = b.pathJoin(&[_][]const u8{ file_location, "interface.zig" }) },
|
||||||
|
// We alsso need the interface module available here
|
||||||
|
.dependencies = &[_]std.Build.ModuleDependency{},
|
||||||
|
});
|
||||||
// Add module
|
// Add module
|
||||||
cs.addAnonymousModule("universal_lambda_handler", .{
|
cs.addAnonymousModule("universal_lambda_handler", .{
|
||||||
// Source file can be anywhere on disk, does not need to be a subdirectory
|
// Source file can be anywhere on disk, does not need to be a subdirectory
|
||||||
|
@ -59,36 +72,13 @@ pub fn configureBuild(b: *std.Build, cs: *std.Build.Step.Compile) !void {
|
||||||
.name = "flexilib-interface",
|
.name = "flexilib-interface",
|
||||||
.module = flexilib_module,
|
.module = flexilib_module,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
cs.addAnonymousModule("universal_lambda_helpers", .{
|
|
||||||
// Source file can be anywhere on disk, does not need to be a subdirectory
|
|
||||||
.source_file = .{ .path = b.pathJoin(&[_][]const u8{ file_location, "helpers.zig" }) },
|
|
||||||
// We alsso need the interface module available here
|
|
||||||
.dependencies = &[_]std.Build.ModuleDependency{
|
|
||||||
// Add options module so we can let our universal_lambda know what
|
|
||||||
// type of interface is necessary
|
|
||||||
.{
|
.{
|
||||||
.name = "build_options",
|
.name = "universal_lambda_interface",
|
||||||
.module = options_module,
|
.module = cs.modules.get("universal_lambda_interface").?,
|
||||||
},
|
|
||||||
.{
|
|
||||||
.name = "flexilib-interface",
|
|
||||||
.module = flexilib_module,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.name = "universal_lambda_handler",
|
|
||||||
.module = cs.modules.get("universal_lambda_handler").?,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
return file_location;
|
||||||
// Add steps
|
|
||||||
try @import("lambda_build.zig").configureBuild(b, cs, function_name);
|
|
||||||
try @import("cloudflare_build.zig").configureBuild(b, cs, function_name);
|
|
||||||
try @import("flexilib_build.zig").configureBuild(b, cs, file_location);
|
|
||||||
try @import("standalone_server_build.zig").configureBuild(b, cs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This function relies on internal implementation of the build runner
|
/// This function relies on internal implementation of the build runner
|
||||||
|
@ -116,7 +106,11 @@ pub fn configureBuild(b: *std.Build, cs: *std.Build.Step.Compile) !void {
|
||||||
/// to pull from a download location and update hashes every time we change
|
/// to pull from a download location and update hashes every time we change
|
||||||
fn findFileLocation(b: *std.Build) ![]const u8 {
|
fn findFileLocation(b: *std.Build) ![]const u8 {
|
||||||
if (module_root) |r| return b.pathJoin(&[_][]const u8{ r, "src" });
|
if (module_root) |r| return b.pathJoin(&[_][]const u8{ r, "src" });
|
||||||
const build_root = b.option([]const u8, "universal_lambda_build_root", "Build root for universal lambda (development of universal lambda only)");
|
const build_root = b.option(
|
||||||
|
[]const u8,
|
||||||
|
"universal_lambda_build_root",
|
||||||
|
"Build root for universal lambda (development of universal lambda only)",
|
||||||
|
);
|
||||||
if (build_root) |br| {
|
if (build_root) |br| {
|
||||||
return b.pathJoin(&[_][]const u8{ br, "src" });
|
return b.pathJoin(&[_][]const u8{ br, "src" });
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user