diff --git a/.github/workflows/mastercheck.yml b/.github/workflows/mastercheck.yml index 58d766f..78d5f7b 100644 --- a/.github/workflows/mastercheck.yml +++ b/.github/workflows/mastercheck.yml @@ -26,5 +26,7 @@ jobs: run: zig version - name: Build simple endpoint example run: zig build endpoint - - name: Build tests - run: zig build test + - name: Build authentication tests + run: zig build test-authentication + - name: Build http parameter tests + run: zig build test-httpparams diff --git a/.gitignore b/.gitignore index 8421f2c..430aed5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ tmp/ **/target/* **/__pycache__/* wrk/go/main +scratch diff --git a/README.md b/README.md index 5c62ca1..8435859 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,9 @@ Here's what works: - **[mustache](examples/mustache/)**: a simple example using [mustache](https://mustache.github.io/) templating. - **[endpoint authentication](examples/endpoint_auth/)**: a simple authenticated - endpoint. Read more about authentication [here](./doc/authentication.md) + endpoint. Read more about authentication [here](./doc/authentication.md). +- **[http parameters](examples/http_parameters/)**: a simple example sending + itself query parameters of all supported types. I'll continue wrapping more of facil.io's functionality and adding stuff to zap diff --git a/build.zig b/build.zig index 90dae19..20fb821 100644 --- a/build.zig +++ b/build.zig @@ -50,6 +50,7 @@ pub fn build(b: *std.build.Builder) !void { .{ .name = "wrk_zigstd", .src = "wrk/zigstd/main.zig" }, .{ .name = "mustache", .src = "examples/mustache/mustache.zig" }, .{ .name = "endpoint_auth", .src = "examples/endpoint_auth/endpoint_auth.zig" }, + .{ .name = "http_params", .src = "examples/http_params/http_params.zig" }, }) |excfg| { const ex_name = excfg.name; const ex_src = excfg.src; @@ -93,11 +94,20 @@ pub fn build(b: *std.build.Builder) !void { // // TOOLS & TESTING // + // n.b.: tests run in parallel, so we need all tests that use the network + // to run sequentially, since zap doesn't like to be started multiple + // times on different threads + // + // TODO: for some reason, tests aren't run more than once unless + // dependencies have changed. + // So, for now, we just force the exe to be built, so in order that + // we can call it again when needed. - // tests + // authentication tests // const auth_tests = b.addTest(.{ - .root_source_file = .{ .path = "src/test_auth.zig" }, + .name = "auth_tests", + .root_source_file = .{ .path = "src/tests/test_auth.zig" }, .target = target, .optimize = optimize, }); @@ -105,15 +115,33 @@ pub fn build(b: *std.build.Builder) !void { auth_tests.addModule("zap", zap_module); const run_auth_tests = b.addRunArtifact(auth_tests); + const install_auth_tests = b.addInstallArtifact(auth_tests); + + // http paramters (qyery, body) tests + const httpparams_tests = b.addTest(.{ + .name = "http_params_tests", + .root_source_file = .{ .path = "src/tests/test_http_params.zig" }, + .target = target, + .optimize = optimize, + }); + + httpparams_tests.linkLibrary(facil_dep.artifact("facil.io")); + httpparams_tests.addModule("zap", zap_module); + const run_httpparams_tests = b.addRunArtifact(httpparams_tests); // TODO: for some reason, tests aren't run more than once unless // dependencies have changed. // So, for now, we just force the exe to be built, so in order that // we can call it again when needed. - const install_auth_tests = b.addInstallArtifact(auth_tests); + const install_httpparams_tests = b.addInstallArtifact(httpparams_tests); - const test_step = b.step("test", "Run unit tests [REMOVE zig-cache!]"); - test_step.dependOn(&run_auth_tests.step); - test_step.dependOn(&install_auth_tests.step); + // test commands + const run_auth_test_step = b.step("test-authentication", "Run auth unit tests [REMOVE zig-cache!]"); + run_auth_test_step.dependOn(&run_auth_tests.step); + run_auth_test_step.dependOn(&install_auth_tests.step); + + const run_httpparams_test_step = b.step("test-httpparams", "Run http param unit tests [REMOVE zig-cache!]"); + run_httpparams_test_step.dependOn(&run_httpparams_tests.step); + run_httpparams_test_step.dependOn(&install_httpparams_tests.step); // pkghash // diff --git a/build.zig.zon b/build.zig.zon index 2dadfb1..c922f76 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = "zap", - .version = "0.0.13", + .version = "0.0.14", .dependencies = .{ .@"facil.io" = .{ diff --git a/examples/http_params/http_params.zig b/examples/http_params/http_params.zig new file mode 100644 index 0000000..1bb739d --- /dev/null +++ b/examples/http_params/http_params.zig @@ -0,0 +1,120 @@ +const std = @import("std"); +const zap = @import("zap"); + +// We send ourselves a request +fn makeRequest(a: std.mem.Allocator, url: []const u8) !void { + const uri = try std.Uri.parse(url); + + var h = std.http.Headers{ .allocator = a }; + defer h.deinit(); + + var http_client: std.http.Client = .{ .allocator = a }; + defer http_client.deinit(); + + var req = try http_client.request(.GET, uri, h, .{}); + defer req.deinit(); + + try req.start(); + try req.wait(); +} + +fn makeRequestThread(a: std.mem.Allocator, url: []const u8) !std.Thread { + return try std.Thread.spawn(.{}, makeRequest, .{ a, url }); +} + +// here we go +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{ + .thread_safe = true, + }){}; + var allocator = gpa.allocator(); + + const Handler = struct { + var alloc: std.mem.Allocator = undefined; + + pub fn on_request(r: zap.SimpleRequest) void { + std.debug.print("\n=====================================================\n", .{}); + defer std.debug.print("=====================================================\n\n", .{}); + + // check for FORM parameters + r.parseBody() catch |err| { + std.log.err("Parse Body error: {any}. Expected if body is empty", .{err}); + }; + + // check for query parameters + r.parseQuery(); + + var param_count = r.getParamCount(); + std.log.info("param_count: {}", .{param_count}); + + // iterate over all params as strings + var strparams = r.parametersToOwnedStrList(alloc, false) catch unreachable; + defer strparams.deinit(); + std.debug.print("\n", .{}); + for (strparams.items) |kv| { + std.log.info("ParamStr `{s}` is `{s}`", .{ kv.key.str, kv.value.str }); + } + + std.debug.print("\n", .{}); + + // iterate over all params + const params = r.parametersToOwnedList(alloc, false) catch unreachable; + defer params.deinit(); + for (params.items) |kv| { + std.log.info("Param `{s}` is {any}", .{ kv.key.str, kv.value }); + } + + // let's get param "one" by name + std.debug.print("\n", .{}); + if (r.getParamStr("one", alloc, false)) |maybe_str| { + if (maybe_str) |*s| { + defer s.deinit(); + + std.log.info("Param one = {s}", .{s.str}); + } else { + std.log.info("Param one not found!", .{}); + } + } + // since we provided "false" for duplicating strings in the call + // to getParamStr(), there won't be an allocation error + else |err| { + std.log.err("cannot check for `one` param: {any}\n", .{err}); + } + + // check if we received a terminate=true parameter + if (r.getParamStr("terminate", alloc, false)) |maybe_str| { + if (maybe_str) |*s| { + defer s.deinit(); + if (std.mem.eql(u8, s.str, "true")) { + zap.fio_stop(); + } + } + } else |err| { + std.log.err("cannot check for terminate param: {any}\n", .{err}); + } + } + }; + + Handler.alloc = allocator; + + // setup listener + var listener = zap.SimpleHttpListener.init( + .{ + .port = 3000, + .on_request = Handler.on_request, + .log = false, + .max_clients = 10, + .max_body_size = 1 * 1024, + }, + ); + zap.enableDebugLog(); + try listener.listen(); + std.log.info("\n\nTerminate with CTRL+C or by sending query param terminate=true\n", .{}); + + const thread = try makeRequestThread(allocator, "http://127.0.0.1:3000/?one=1&two=2&string=hello+world&float=6.28&bool=true"); + defer thread.join(); + zap.start(.{ + .threads = 1, + .workers = 0, + }); +} diff --git a/src/fio.zig b/src/fio.zig index 6a79f8e..619db9f 100644 --- a/src/fio.zig +++ b/src/fio.zig @@ -74,10 +74,22 @@ pub const struct_fio_str_info_s = extern struct { }; pub const fio_str_info_s = struct_fio_str_info_s; pub extern fn http_send_body(h: [*c]http_s, data: ?*anyopaque, length: usize) c_int; +pub fn fiobj_each1(arg_o: FIOBJ, arg_start_at: usize, arg_task: ?*const fn (FIOBJ, ?*anyopaque) callconv(.C) c_int, arg_arg: ?*anyopaque) callconv(.C) usize { + var o = arg_o; + var start_at = arg_start_at; + var task = arg_task; + var arg = arg_arg; + if ((((o != 0) and ((o & @bitCast(c_ulong, @as(c_long, @as(c_int, 1)))) == @bitCast(c_ulong, @as(c_long, @as(c_int, 0))))) and ((o & @bitCast(c_ulong, @as(c_long, @as(c_int, 6)))) != @bitCast(c_ulong, @as(c_long, @as(c_int, 6))))) and (fiobj_type_vtable(o).*.each != null)) return fiobj_type_vtable(o).*.each.?(o, start_at, task, arg); + return 0; +} pub extern fn fiobj_hash_new() FIOBJ; pub extern fn fiobj_hash_set(hash: FIOBJ, key: FIOBJ, obj: FIOBJ) c_int; pub extern fn fiobj_hash_get(hash: FIOBJ, key: FIOBJ) FIOBJ; +pub extern fn fiobj_hash_pop(hash: FIOBJ, key: [*c]FIOBJ) FIOBJ; +pub extern fn fiobj_hash_count(hash: FIOBJ) usize; +pub extern fn fiobj_hash_key_in_loop() FIOBJ; +pub extern fn fiobj_hash_haskey(hash: FIOBJ, key: FIOBJ) c_int; pub extern fn fiobj_ary_push(ary: FIOBJ, obj: FIOBJ) void; pub extern fn fiobj_float_new(num: f64) FIOBJ; pub extern fn fiobj_num_new_bignum(num: isize) FIOBJ; @@ -210,6 +222,22 @@ pub fn fiobj_type_vtable(arg_o: FIOBJ) callconv(.C) [*c]const fiobj_object_vtabl } return null; } + +pub fn fiobj_obj2num(o: FIOBJ) callconv(.C) isize { + if ((o & @bitCast(c_ulong, @as(c_long, @as(c_int, 1)))) != 0) { + const sign: usize = if ((o & ~(~@bitCast(usize, @as(c_long, @as(c_int, 0))) >> @intCast(@import("std").math.Log2Int(usize), 1))) != 0) ~(~@bitCast(usize, @as(c_long, @as(c_int, 0))) >> @intCast(@import("std").math.Log2Int(usize), 1)) | (~(~@bitCast(usize, @as(c_long, @as(c_int, 0))) >> @intCast(@import("std").math.Log2Int(usize), 1)) >> @intCast(@import("std").math.Log2Int(usize), 1)) else @bitCast(c_ulong, @as(c_long, @as(c_int, 0))); + return @bitCast(isize, ((o & (~@bitCast(usize, @as(c_long, @as(c_int, 0))) >> @intCast(@import("std").math.Log2Int(usize), 1))) >> @intCast(@import("std").math.Log2Int(c_ulong), 1)) | sign); + } + if (!(o != 0) or !(((o != 0) and ((o & @bitCast(c_ulong, @as(c_long, @as(c_int, 1)))) == @bitCast(c_ulong, @as(c_long, @as(c_int, 0))))) and ((o & @bitCast(c_ulong, @as(c_long, @as(c_int, 6)))) != @bitCast(c_ulong, @as(c_long, @as(c_int, 6)))))) return @bitCast(isize, @as(c_long, @boolToInt(o == @bitCast(c_ulong, @as(c_long, FIOBJ_T_TRUE))))); + return fiobj_type_vtable(o).*.to_i.?(o); +} +pub fn fiobj_obj2float(o: FIOBJ) callconv(.C) f64 { + if ((o & @bitCast(c_ulong, @as(c_long, @as(c_int, 1)))) != 0) return @intToFloat(f64, fiobj_obj2num(o)); + // the below doesn't parse and we don't support ints here anyway + // if (!(o != 0) or ((o & @bitCast(c_ulong, @as(c_long, @as(c_int, 6)))) == @bitCast(c_ulong, @as(c_long, @as(c_int, 6))))) return @intToFloat(f64, o == @bitCast(c_ulong, @as(c_long, FIOBJ_T_TRUE))); + return fiobj_type_vtable(o).*.to_f.?(o); +} + pub extern fn fio_ltocstr(c_long) fio_str_info_s; pub fn fiobj_obj2cstr(o: FIOBJ) callconv(.C) fio_str_info_s { if (!(o != 0)) { diff --git a/src/test_auth.zig b/src/tests/test_auth.zig similarity index 98% rename from src/test_auth.zig rename to src/tests/test_auth.zig index 692af4a..6fc56bb 100644 --- a/src/test_auth.zig +++ b/src/tests/test_auth.zig @@ -1,9 +1,13 @@ const std = @import("std"); -const Authenticators = @import("http_auth.zig"); -const zap = @import("zap.zig"); -const Endpoints = @import("endpoint.zig"); -const fio = @import("fio.zig"); -const util = @import("util.zig"); +const zap = @import("zap"); +// const Authenticators = @import("http_auth.zig"); +const Authenticators = zap; +const Endpoints = zap; +// const Endpoints = @import("endpoint.zig"); +const fio = zap; +// const fio = @import("fio.zig"); +const util = zap; +// const util = @import("util.zig"); test "BearerAuthSingle authenticate" { const a = std.testing.allocator; diff --git a/src/tests/test_http_params.zig b/src/tests/test_http_params.zig new file mode 100644 index 0000000..14f8fdc --- /dev/null +++ b/src/tests/test_http_params.zig @@ -0,0 +1,166 @@ +const std = @import("std"); +const zap = @import("zap"); + +fn makeRequest(a: std.mem.Allocator, url: []const u8) !void { + const uri = try std.Uri.parse(url); + + var h = std.http.Headers{ .allocator = a }; + defer h.deinit(); + + var http_client: std.http.Client = .{ .allocator = a }; + defer http_client.deinit(); + + var req = try http_client.request(.GET, uri, h, .{}); + defer req.deinit(); + + try req.start(); + try req.wait(); + zap.fio_stop(); +} + +fn makeRequestThread(a: std.mem.Allocator, url: []const u8) !std.Thread { + return try std.Thread.spawn(.{}, makeRequest, .{ a, url }); +} + +test "http parameters" { + var allocator = std.testing.allocator; + + const Handler = struct { + var alloc: std.mem.Allocator = undefined; + var ran: bool = false; + var param_count: isize = 0; + + var strParams: ?zap.HttpParamStrKVList = null; + var params: ?zap.HttpParamKVList = null; + var paramOneStr: ?zap.FreeOrNot = null; + + pub fn on_request(r: zap.SimpleRequest) void { + ran = true; + r.parseQuery(); + param_count = r.getParamCount(); + + // true -> make copies of temp strings + strParams = r.parametersToOwnedStrList(alloc, true) catch unreachable; + + // true -> make copies of temp strings + params = r.parametersToOwnedList(alloc, true) catch unreachable; + + var maybe_str = r.getParamStr("one", alloc, true) catch unreachable; + if (maybe_str) |*s| { + paramOneStr = s.*; + } + } + }; + + Handler.alloc = allocator; + + // setup listener + var listener = zap.SimpleHttpListener.init( + .{ + .port = 3001, + .on_request = Handler.on_request, + .log = false, + .max_clients = 10, + .max_body_size = 1 * 1024, + }, + ); + zap.enableDebugLog(); + try listener.listen(); + + const thread = try makeRequestThread(allocator, "http://127.0.0.1:3001/?one=1&two=2&string=hello+world&float=6.28&bool=true"); + defer thread.join(); + zap.start(.{ + .threads = 1, + .workers = 0, + }); + + defer { + if (Handler.strParams) |*p| { + p.deinit(); + } + if (Handler.params) |*p| { + p.deinit(); + } + if (Handler.paramOneStr) |*p| { + // allocator.free(p); + p.deinit(); + } + } + + try std.testing.expectEqual(Handler.ran, true); + try std.testing.expectEqual(Handler.param_count, 5); + try std.testing.expect(Handler.paramOneStr != null); + try std.testing.expectEqualStrings(Handler.paramOneStr.?.str, "1"); + try std.testing.expect(Handler.strParams != null); + for (Handler.strParams.?.items, 0..) |kv, i| { + switch (i) { + 0 => { + try std.testing.expectEqualStrings(kv.key.str, "one"); + try std.testing.expectEqualStrings(kv.value.str, "1"); + }, + 1 => { + try std.testing.expectEqualStrings(kv.key.str, "two"); + try std.testing.expectEqualStrings(kv.value.str, "2"); + }, + 2 => { + try std.testing.expectEqualStrings(kv.key.str, "string"); + try std.testing.expectEqualStrings(kv.value.str, "hello world"); + }, + 3 => { + try std.testing.expectEqualStrings(kv.key.str, "float"); + try std.testing.expectEqualStrings(kv.value.str, "6.28"); + }, + 4 => { + try std.testing.expectEqualStrings(kv.key.str, "bool"); + try std.testing.expectEqualStrings(kv.value.str, "true"); + }, + else => return error.TooManyArgs, + } + } + + for (Handler.params.?.items, 0..) |kv, i| { + switch (i) { + 0 => { + try std.testing.expectEqualStrings(kv.key.str, "one"); + try std.testing.expect(kv.value != null); + switch (kv.value.?) { + .Int => |n| try std.testing.expectEqual(n, 1), + else => return error.InvalidHttpParamType, + } + }, + 1 => { + try std.testing.expectEqualStrings(kv.key.str, "two"); + try std.testing.expect(kv.value != null); + switch (kv.value.?) { + .Int => |n| try std.testing.expectEqual(n, 2), + else => return error.InvalidHttpParamType, + } + }, + 2 => { + try std.testing.expectEqualStrings(kv.key.str, "string"); + try std.testing.expect(kv.value != null); + switch (kv.value.?) { + .String => |s| try std.testing.expectEqualStrings(s.str, "hello world"), + else => return error.InvalidHttpParamType, + } + }, + 3 => { + try std.testing.expectEqualStrings(kv.key.str, "float"); + try std.testing.expect(kv.value != null); + switch (kv.value.?) { + .Float => |f| try std.testing.expectEqual(f, 6.28), + else => return error.InvalidHttpParamType, + } + }, + 4 => { + try std.testing.expectEqualStrings(kv.key.str, "bool"); + try std.testing.expect(kv.value != null); + switch (kv.value.?) { + .Bool => |b| try std.testing.expectEqual(b, true), + else => return error.InvalidHttpParamType, + } + }, + else => return error.TooManyArgs, + } + } +} diff --git a/src/util.zig b/src/util.zig index 01fc9cc..1a38f95 100644 --- a/src/util.zig +++ b/src/util.zig @@ -1,6 +1,9 @@ const std = @import("std"); const fio = @import("fio.zig"); +/// note: since this is called from within request functions, we don't make +/// copies. Also, we return temp memory from fio. -> don't hold on to it outside +/// of a request function pub fn fio2str(o: fio.FIOBJ) ?[]const u8 { if (o == 0) return null; const x: fio.fio_str_info_s = fio.fiobj_obj2cstr(o); @@ -9,6 +12,32 @@ pub fn fio2str(o: fio.FIOBJ) ?[]const u8 { return std.mem.span(x.data); } +pub const FreeOrNot = struct { + str: []const u8, + freeme: bool, + allocator: ?std.mem.Allocator = null, + + pub fn deinit(self: *const @This()) void { + if (self.freeme) { + self.allocator.?.free(self.str); + } + } +}; + +pub fn fio2strAllocOrNot(o: fio.FIOBJ, a: std.mem.Allocator, always_alloc: bool) !FreeOrNot { + if (o == 0) return .{ .str = "null", .freeme = false }; + if (o == fio.FIOBJ_INVALID) return .{ .str = "null", .freeme = false }; + return switch (fio.fiobj_type(o)) { + fio.FIOBJ_T_TRUE => .{ .str = "true", .freeme = false }, + fio.FIOBJ_T_FALSE => .{ .str = "false", .freeme = false }, + // according to fio2str above, the orelse should never happen + fio.FIOBJ_T_NUMBER => .{ .str = try a.dupe(u8, fio2str(o) orelse "null"), .freeme = true, .allocator = a }, + fio.FIOBJ_T_FLOAT => .{ .str = try a.dupe(u8, fio2str(o) orelse "null"), .freeme = true, .allocator = a }, + // the string comes out of the request, so it is safe to not make a copy + fio.FIOBJ_T_STRING => .{ .str = if (always_alloc) try a.dupe(u8, fio2str(o) orelse "") else fio2str(o) orelse "", .freeme = if (always_alloc) true else false, .allocator = a }, + else => .{ .str = "null", .freeme = false }, + }; +} pub fn str2fio(s: []const u8) fio.fio_str_info_s { return .{ .data = toCharPtr(s), diff --git a/src/zap.zig b/src/zap.zig index 9284b1b..f8ee503 100644 --- a/src/zap.zig +++ b/src/zap.zig @@ -48,11 +48,8 @@ pub const HttpError = error{ HttpSendBody, HttpSetContentType, HttpSetHeader, -}; - -pub const HttpParam = struct { - key: []const u8, - value: []const u8, + HttpParseBody, + HttpIterParams, }; pub const ContentType = enum { @@ -153,10 +150,6 @@ pub const SimpleRequest = struct { } debug("setHeader: ret = {}\n", .{ret}); - // Note to self: - // const new_fiobj_str = fio.fiobj_str_new(name.ptr, name.len); - // fio.fiobj_free(new_fiobj_str); - if (ret == 0) return; return error.HttpSetHeader; } @@ -169,20 +162,263 @@ pub const SimpleRequest = struct { self.h.*.status = @intCast(usize, @enumToInt(status)); } - pub fn nextParam(self: *const Self) ?HttpParam { + /// Attempts to decode the request's body. + /// This should be called BEFORE parseQuery + /// Result is accessible via parametersToOwnedSlice(), parametersToOwnedStrSlice() + /// + /// Supported body types: + /// - application/x-www-form-urlencoded + /// - application/json + /// - multipart/form-data + pub fn parseBody(self: *const Self) HttpError!void { + if (fio.http_parse_body(self.h) == -1) return error.HttpParseBody; + } + + /// Parses the query part of an HTTP request + /// This should be called AFTER parseBody(), just in case the body is a JSON + /// object that doesn't have a hash map at its root. + /// + /// Result is accessible via parametersToOwnedSlice(), parametersToOwnedStrSlice() + pub fn parseQuery(self: *const Self) void { + return fio.http_parse_query(self.h); + } + + /// not implemented. + pub fn parseCookies() !void {} + + /// not implemented. + pub fn getCookie(name: []const u8) ?[]const u8 { + _ = name; + } + + /// Returns the number of parameters after parsing. + /// + /// Parse with parseBody() and / or parseQuery() + pub fn getParamCount(self: *const Self) isize { + if (self.h.*.params == 0) return 0; + return fio.fiobj_obj2num(self.h.*.params); + } + + /// Returns the query / body parameters as key/value pairs, as strings. + /// Supported param types that will be converted: + /// + /// - Bool + /// - Int + /// - Float + /// - String + /// + /// At the moment, no fio ARRAYs are supported as well as HASH maps. + /// So, for JSON body payloads: parse the body instead. + /// + /// Requires parseBody() and/or parseQuery() have been called. + /// Returned list needs to be deinited. + pub fn parametersToOwnedStrList(self: *const Self, a: std.mem.Allocator, always_alloc: bool) anyerror!HttpParamStrKVList { + var params = try std.ArrayList(HttpParamStrKV).initCapacity(a, @intCast(usize, self.getParamCount())); + var context: _parametersToOwnedStrSliceContext = .{ + .params = ¶ms, + .allocator = a, + .always_alloc = always_alloc, + }; + const howmany = fio.fiobj_each1(self.h.*.params, 0, _each_nextParamStr, &context); + if (howmany != self.getParamCount()) { + return error.HttpIterParams; + } + return .{ .items = try params.toOwnedSlice(), .allocator = a }; + } + + const _parametersToOwnedStrSliceContext = struct { + allocator: std.mem.Allocator, + params: *std.ArrayList(HttpParamStrKV), + last_error: ?anyerror = null, + always_alloc: bool, + }; + + fn _each_nextParamStr(fiobj_value: fio.FIOBJ, context: ?*anyopaque) callconv(.C) c_int { + const ctx: *_parametersToOwnedStrSliceContext = @ptrCast(*_parametersToOwnedStrSliceContext, @alignCast(@alignOf(*_parametersToOwnedStrSliceContext), context)); + // this is thread-safe, guaranteed by fio + var fiobj_key: fio.FIOBJ = fio.fiobj_hash_key_in_loop(); + ctx.params.append(.{ + .key = util.fio2strAllocOrNot(fiobj_key, ctx.allocator, ctx.always_alloc) catch |err| { + ctx.last_error = err; + return -1; + }, + .value = util.fio2strAllocOrNot(fiobj_value, ctx.allocator, ctx.always_alloc) catch |err| { + ctx.last_error = err; + return -1; + }, + }) catch |err| { + // what to do? + // signal the caller that an error occured by returning -1 + // also, set the error + ctx.last_error = err; + return -1; + }; + return 0; + } + + /// Returns the query / body parameters as key/value pairs + /// Supported param types that will be converted: + /// + /// - Bool + /// - Int + /// - Float + /// - String + /// + /// At the moment, no fio ARRAYs are supported as well as HASH maps. + /// So, for JSON body payloads: parse the body instead. + /// + /// Requires parseBody() and/or parseQuery() have been called. + /// Returned slice needs to be freed. + pub fn parametersToOwnedList(self: *const Self, a: std.mem.Allocator, dupe_strings: bool) !HttpParamKVList { + var params = try std.ArrayList(HttpParamKV).initCapacity(a, @intCast(usize, self.getParamCount())); + var context: _parametersToOwnedSliceContext = .{ .params = ¶ms, .allocator = a, .dupe_strings = dupe_strings }; + const howmany = fio.fiobj_each1(self.h.*.params, 0, _each_nextParam, &context); + if (howmany != self.getParamCount()) { + return error.HttpIterParams; + } + return .{ .items = try params.toOwnedSlice(), .allocator = a }; + } + + const _parametersToOwnedSliceContext = struct { + params: *std.ArrayList(HttpParamKV), + last_error: ?anyerror = null, + allocator: std.mem.Allocator, + dupe_strings: bool, + }; + + fn _each_nextParam(fiobj_value: fio.FIOBJ, context: ?*anyopaque) callconv(.C) c_int { + const ctx: *_parametersToOwnedSliceContext = @ptrCast(*_parametersToOwnedSliceContext, @alignCast(@alignOf(*_parametersToOwnedSliceContext), context)); + // this is thread-safe, guaranteed by fio + var fiobj_key: fio.FIOBJ = fio.fiobj_hash_key_in_loop(); + ctx.params.append(.{ + .key = util.fio2strAllocOrNot(fiobj_key, ctx.allocator, ctx.dupe_strings) catch |err| { + ctx.last_error = err; + return -1; + }, + .value = Fiobj2HttpParam(fiobj_value, ctx.allocator, ctx.dupe_strings) catch |err| { + ctx.last_error = err; + return -1; + }, + }) catch |err| { + // what to do? + // signal the caller that an error occured by returning -1 + // also, set the error + ctx.last_error = err; + return -1; + }; + return 0; + } + + /// get named parameter as string + /// Supported param types that will be converted: + /// + /// - Bool + /// - Int + /// - Float + /// - String + /// + /// At the moment, no fio ARRAYs are supported as well as HASH maps. + /// So, for JSON body payloads: parse the body instead. + /// + /// Requires parseBody() and/or parseQuery() have been called. + /// The returned string needs to be deinited with .deinit() + pub fn getParamStr(self: *const Self, name: []const u8, a: std.mem.Allocator, always_alloc: bool) !?util.FreeOrNot { if (self.h.*.params == 0) return null; - var key: fio.FIOBJ = undefined; - const value = fio.fiobj_hash_pop(self.h.*.params, &key); + const key = fio.fiobj_str_new(name.ptr, name.len); + defer fio.fiobj_free_wrapped(key); + const value = fio.fiobj_hash_get(self.h.*.params, key); if (value == fio.FIOBJ_INVALID) { return null; } - return HttpParam{ - .key = util.fio2str(key).?, - .value = util.fio2str(value).?, - }; + return try util.fio2strAllocOrNot(value, a, always_alloc); } }; +/// Key value pair of strings from HTTP parameters +pub const HttpParamStrKV = struct { + key: util.FreeOrNot, + value: util.FreeOrNot, + pub fn deinit(self: *@This()) void { + self.key.deinit(); + self.value.deinit(); + } +}; + +pub const HttpParamStrKVList = struct { + items: []HttpParamStrKV, + allocator: std.mem.Allocator, + pub fn deinit(self: *@This()) void { + for (self.items) |*item| { + item.deinit(); + } + self.allocator.free(self.items); + } +}; + +pub const HttpParamKVList = struct { + items: []HttpParamKV, + allocator: std.mem.Allocator, + pub fn deinit(self: *const @This()) void { + for (self.items) |*item| { + item.deinit(); + } + self.allocator.free(self.items); + } +}; + +pub const HttpParamValueType = enum { + // Null, + Bool, + Int, + Float, + String, + Unsupported, + Unsupported_Hash, + Unsupported_Array, +}; + +pub const HttpParam = union(HttpParamValueType) { + Bool: bool, + Int: isize, + Float: f64, + /// we don't do writable strings here + String: util.FreeOrNot, + /// value will always be null + Unsupported: ?void, + /// value will always be null + Unsupported_Hash: ?void, + /// value will always be null + Unsupported_Array: ?void, +}; + +pub const HttpParamKV = struct { + key: util.FreeOrNot, + value: ?HttpParam, + pub fn deinit(self: *@This()) void { + self.key.deinit(); + if (self.value) |p| { + switch (p) { + .String => |*s| s.deinit(), + else => {}, + } + } + } +}; + +pub fn Fiobj2HttpParam(o: fio.FIOBJ, a: std.mem.Allocator, dupe_string: bool) !?HttpParam { + return switch (fio.fiobj_type(o)) { + fio.FIOBJ_T_NULL => null, + fio.FIOBJ_T_TRUE => .{ .Bool = true }, + fio.FIOBJ_T_FALSE => .{ .Bool = false }, + fio.FIOBJ_T_NUMBER => .{ .Int = fio.fiobj_obj2num(o) }, + fio.FIOBJ_T_FLOAT => .{ .Float = fio.fiobj_obj2float(o) }, + fio.FIOBJ_T_STRING => .{ .String = try util.fio2strAllocOrNot(o, a, dupe_string) }, + fio.FIOBJ_T_ARRAY => .{ .Unsupported_Array = null }, + fio.FIOBJ_T_HASH => .{ .Unsupported_Hash = null }, + else => .{ .Unsupported = null }, + }; +} + pub const HttpRequestFn = *const fn (r: [*c]fio.http_s) callconv(.C) void; pub const SimpleHttpRequestFn = *const fn (SimpleRequest) void;