From 83805e20bec4f1102c752ef60b380507e931ccb7 Mon Sep 17 00:00:00 2001 From: Thom Dickson Date: Fri, 23 Aug 2024 22:58:39 -0400 Subject: [PATCH 01/57] add type introspection for router handle_func --- src/router.zig | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/router.zig b/src/router.zig index c9ca7d5..40ab1d3 100644 --- a/src/router.zig +++ b/src/router.zig @@ -74,6 +74,41 @@ pub fn handle_func_unbound(self: *Self, path: []const u8, h: zap.HttpRequestFn) pub fn handle_func(self: *Self, path: []const u8, instance: *anyopaque, handler: anytype) !void { // TODO: assert type of instance has handler + // Introspection checks on handler type + comptime { + const hand_info = @typeInfo(@TypeOf(handler)); + + // Need to check: + // 1) handler is function pointer + const f = blk: { + if (hand_info == .Pointer) { + const inner = @typeInfo(hand_info.Pointer.child); + if (inner == .Fn) { + break :blk inner.Fn; + } + } + @compileError("Expected handler to be a function pointer. Found " ++ + @typeName(@TypeOf(handler))); + }; + + // 2) snd arg is zap.Request + if (f.params.len != 2) { + @compileError("Expected handler to have two paramters"); + } + const arg_type = f.params[1].type.?; + if (arg_type != zap.Request) { + @compileError("Expected handler's second argument to be of type zap.Request. Found " ++ + @typeName(arg_type)); + } + + // 3) handler returns void + const ret_info = @typeInfo(f.return_type.?); + if (ret_info != .Void) { + @compileError("Expected handler's return type to be void. Found " ++ + @typeName(f.return_type.?)); + } + } + if (path.len == 0) { return RouterError.EmptyPath; } From 2bc3b7df894443c5d8fff04f540122880116ec1a Mon Sep 17 00:00:00 2001 From: Matt Iversen Date: Mon, 30 Sep 2024 01:51:28 +1000 Subject: [PATCH 02/57] Cleanup FAQ of README.md Deduplicate references to building with master, and remove outdated reference to future release of 0.13 --- README.md | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 054ca85..daf3da3 100644 --- a/README.md +++ b/README.md @@ -24,30 +24,28 @@ proved to be: Exactly the goals I set out to achieve! -## Most FAQ: - -### Zap uses the latest stable zig release (0.13.0) for a reason. So you don't -have to keep up with frequent breaking changes. It's an "LTS feature". If you -want to use zig master, use the `zig-master` branch but be aware that I don't -provide `build.zig.zon` snippets or tagged releases for it for the time being. -If you know what you are doing, that shouldn't stop you from using it with zig -master though. +## FAQ: +- Q: **What version of Zig does Zap support?** + - Zap uses the latest stable zig release (0.13.0), so you don't have to keep + up with frequent breaking changes. It's an "LTS feature". +- Q: **Can Zap build with Zig's master branch?** + - See the `zig-master` branch. An example of how to use it is + [here](https://github.com/zigzap/hello-master). Please note that the + zig-master branch is not the official master branch of ZAP. Be aware that + I don't provide `build.zig.zon` snippets or tagged releases for it for + the time being. If you know what you are doing, that shouldn't stop you + from using it with zig master though. - Q: **Where is the API documentation?** - - A: Docs are a work in progress. You can check them out + - Docs are a work in progress. You can check them out [here](https://zigzap.org/zap). - - A: Run `zig build run-docserver` to serve them locally. -- Q: **Zap doesn't build with Zig master?** - - A: See the zig-master branch. An example of how to use it is - [here](https://github.com/zigzap/hello-master). Please note that the - zig-master branch is not the official master branch of ZAP. Yet. Until zig - 0.13.0 is released. + - Run `zig build run-docserver` to serve them locally. - Q: **Does ZAP work on Windows?** - - A: No. This is due to the underlying facil.io C library. Future versions + - No. This is due to the underlying facil.io C library. Future versions of facil.io might support Windows but there is no timeline yet. Your best options on Windows are WSL2 or a docker container. - Q: **Does ZAP support TLS / HTTPS?** - - A: Yes, ZAP supports using the system's openssl. See the + - Yes, ZAP supports using the system's openssl. See the [https](./examples/https/https.zig) example and make sure to build with the `-Dopenssl` flag or the environment variable `ZAP_USE_OPENSSL=true`: - `.openssl = true,` (in dependent projects' build.zig, From ae5c9278335d8e1133cd6d22707323dda712e120 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Sun, 20 Oct 2024 23:30:16 +0200 Subject: [PATCH 03/57] Revert "EndpointHandler: de-morgan logic for useRoutes" This reverts commit 9543ede15fc9ad038ec4839628c886b124580983. --- src/middleware.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware.zig b/src/middleware.zig index 3fa734c..c094fee 100644 --- a/src/middleware.zig +++ b/src/middleware.zig @@ -99,7 +99,7 @@ pub fn EndpointHandler(comptime HandlerType: anytype, comptime ContextType: anyt pub fn onRequest(handler: *HandlerType, r: zap.Request, context: *ContextType) bool { const self: *Self = @fieldParentPtr("handler", handler); r.setUserContext(context); - if (self.options.checkPath and + if (!self.options.checkPath or std.mem.startsWith(u8, r.path orelse "", self.endpoint.settings.path)) { self.endpoint.onRequest(r); From 5b04c02e252c7357b7503e9bed3bbe81ed7cde28 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 20 Oct 2024 21:36:12 +0000 Subject: [PATCH 04/57] Update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ebc942..b090cc5 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ In your zig project folder (where `build.zig` is located), run: ``` -zig fetch --save "git+https://github.com/zigzap/zap#v0.9.0" +zig fetch --save "git+https://github.com/zigzap/zap#v0.9.1" ``` @@ -404,3 +404,4 @@ pub fn main() !void { + From 2bd450ac6074e658c690e4c0e881c46f5396d75d Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Sat, 22 Feb 2025 12:38:28 +0100 Subject: [PATCH 05/57] fix mustache.zig for current zig master (0.14.x) --- .gitignore | 1 + src/mustache.zig | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 6d2d37c..158e964 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ zig-out/ zig-cache/ +.zig-cache/ *.tar.gz flake.lock.bak tmp/ diff --git a/src/mustache.zig b/src/mustache.zig index 2670910..761b25f 100644 --- a/src/mustache.zig +++ b/src/mustache.zig @@ -213,7 +213,7 @@ fn fiobjectify( }, .error_set => return fiobjectify(@as([]const u8, @errorName(value))), .pointer => |ptr_info| switch (ptr_info.size) { - .One => switch (@typeInfo(ptr_info.child)) { + .one => switch (@typeInfo(ptr_info.child)) { .array => { const Slice = []const std.meta.Elem(ptr_info.child); return fiobjectify(@as(Slice, value)); @@ -224,7 +224,7 @@ fn fiobjectify( }, }, // TODO: .Many when there is a sentinel (waiting for https://github.com/ziglang/zig/pull/3972) - .Slice => { + .slice => { // std.debug.print("new slice\n", .{}); if (ptr_info.child == u8 and std.unicode.utf8ValidateSlice(value)) { return fio.fiobj_str_new(util.toCharPtr(value), value.len); From 2d3f8938a55a321447a4fbf0d6d5b70d8334eec6 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Tue, 25 Feb 2025 12:56:02 +0000 Subject: [PATCH 06/57] fix https example output's suggested curl command --- examples/https/https.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/https/https.zig b/examples/https/https.zig index aeb7930..2806179 100644 --- a/examples/https/https.zig +++ b/examples/https/https.zig @@ -64,9 +64,9 @@ pub fn main() !void { std.debug.print("", .{}); std.debug.print( \\ - \\ *********************************************** - \\ *** Try me with: curl -k -v localhost:4443/ *** - \\ *********************************************** + \\ ******************************************************* + \\ *** Try me with: curl -k -v https://localhost:4443/ *** + \\ ******************************************************* \\ \\Your browser may lie to you, indicate a non-secure connection because of the self-created certificate, and make you believe that HTTPS / TLS "does not work". \\ From 7867f32e4de7342a3eed03ef881f8f8e5ce59f5a Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Tue, 25 Feb 2025 13:15:35 +0000 Subject: [PATCH 07/57] updated flake.nix --- flake.lock | 107 +++++++++++++++++------------------------------------ flake.nix | 52 +++++++++++++------------- 2 files changed, 60 insertions(+), 99 deletions(-) diff --git a/flake.lock b/flake.lock index ffba236..3bc6ee6 100644 --- a/flake.lock +++ b/flake.lock @@ -1,6 +1,22 @@ { "nodes": { "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { "flake": false, "locked": { "lastModified": 1696426674, @@ -16,32 +32,16 @@ "type": "github" } }, - "flake-compat_2": { - "flake": false, - "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, "flake-utils": { "inputs": { "systems": "systems" }, "locked": { - "lastModified": 1701680307, - "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -55,11 +55,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1701680307, - "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", "owner": "numtide", "repo": "flake-utils", - "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", "type": "github" }, "original": { @@ -68,72 +68,34 @@ "type": "github" } }, - "flake-utils_3": { - "locked": { - "lastModified": 1659877975, - "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "neovim-flake": { - "inputs": { - "flake-utils": "flake-utils_2", - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "dir": "contrib", - "lastModified": 1704461694, - "narHash": "sha256-dQc9Bkh5uf0R4po3NWnCGx+3eqOZR7iSR4jmRvNNm+E=", - "owner": "neovim", - "repo": "neovim", - "rev": "c509f4907bf7405c9c2ae3f7eff76c5d552944cc", - "type": "github" - }, - "original": { - "dir": "contrib", - "owner": "neovim", - "repo": "neovim", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1704290814, - "narHash": "sha256-LWvKHp7kGxk/GEtlrGYV68qIvPHkU9iToomNFGagixU=", + "lastModified": 1740396192, + "narHash": "sha256-ATMHHrg3sG1KgpQA5x8I+zcYpp5Sf17FaFj/fN+8OoQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "70bdadeb94ffc8806c0570eb5c2695ad29f0e421", + "rev": "d9b69c3ec2a2e2e971c534065bdd53374bd68b97", "type": "github" }, "original": { "owner": "nixos", - "ref": "release-23.05", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, "nixpkgs_2": { "locked": { - "lastModified": 1702350026, - "narHash": "sha256-A+GNZFZdfl4JdDphYKBJ5Ef1HOiFsP18vQe9mqjmUis=", + "lastModified": 1708161998, + "narHash": "sha256-6KnemmUorCvlcAvGziFosAVkrlWZGIc6UNT9GUYr0jQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9463103069725474698139ab10f17a9d125da859", + "rev": "84d981bae8b5e783b3b548de505b22880559515f", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-23.05", + "ref": "nixos-23.11", "repo": "nixpkgs", "type": "github" } @@ -142,7 +104,6 @@ "inputs": { "flake-compat": "flake-compat", "flake-utils": "flake-utils", - "neovim-flake": "neovim-flake", "nixpkgs": "nixpkgs", "zig": "zig" } @@ -180,15 +141,15 @@ "zig": { "inputs": { "flake-compat": "flake-compat_2", - "flake-utils": "flake-utils_3", + "flake-utils": "flake-utils_2", "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1704888534, - "narHash": "sha256-douEXUiWCVL9NvWKYBc8ydq51qLLUwlBo6lJJoktkGw=", + "lastModified": 1740485530, + "narHash": "sha256-PjYwEHq51GVB4ND3z45cxGC59AcK4Yzh9bdfHW569cM=", "owner": "mitchellh", "repo": "zig-overlay", - "rev": "c69295c92a98947295755a9ac2d49a8d447cc04d", + "rev": "67d4ddec445dd3cb3f7396ca89f00c5f85ed201e", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 01f1b72..3fd3935 100644 --- a/flake.nix +++ b/flake.nix @@ -2,16 +2,16 @@ description = "zap dev shell"; inputs = { - # nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; // GLIBC problem! - nixpkgs.url = "github:nixos/nixpkgs/release-23.05"; + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + # nixpkgs.url = "github:nixos/nixpkgs/release-23.05"; flake-utils.url = "github:numtide/flake-utils"; # required for latest zig zig.url = "github:mitchellh/zig-overlay"; # required for latest neovim - neovim-flake.url = "github:neovim/neovim?dir=contrib"; - neovim-flake.inputs.nixpkgs.follows = "nixpkgs"; + # neovim-flake.url = "github:neovim/neovim?dir=contrib"; + # neovim-flake.inputs.nixpkgs.follows = "nixpkgs"; # Used for shell.nix flake-compat = { @@ -30,7 +30,7 @@ # Other overlays (final: prev: { zigpkgs = inputs.zig.packages.${prev.system}; - neovim-nightly-pkgs = inputs.neovim-flake.packages.${prev.system}; + # neovim-nightly-pkgs = inputs.neovim-flake.packages.${prev.system}; }) ]; @@ -44,18 +44,18 @@ devShells.default = pkgs.mkShell { nativeBuildInputs = with pkgs; [ # neovim-nightly-pkgs.neovim - zigpkgs."0.12.0" + zigpkgs."0.13.0" bat wrk - python310 - python310Packages.sanic - python310Packages.matplotlib + python3 + python3Packages.sanic + python3Packages.matplotlib poetry poetry - pkgs.rustc - pkgs.cargo - pkgs.gcc - pkgs.rustfmt + pkgs.rustc + pkgs.cargo + pkgs.gcc + pkgs.rustfmt pkgs.clippy pkgs.go pkgs.gotools @@ -74,14 +74,14 @@ buildInputs = with pkgs; [ # we need a version of bash capable of being interactive - # as opposed to a bash just used for building this flake + # as opposed to a bash just used for building this flake # in non-interactive mode - bashInteractive + bashInteractive ]; shellHook = '' - # once we set SHELL to point to the interactive bash, neovim will - # launch the correct $SHELL in its :terminal + # once we set SHELL to point to the interactive bash, neovim will + # launch the correct $SHELL in its :terminal export SHELL=${pkgs.bashInteractive}/bin/bash export LD_LIBRARY_PATH=${pkgs.zlib.out}/lib:${pkgs.icu.out}/lib:${pkgs.openssl.out}/lib:$LD_LIBRARY_PATH ''; @@ -89,20 +89,20 @@ devShells.build = pkgs.mkShell { nativeBuildInputs = with pkgs; [ - zigpkgs."0.12.0" + zigpkgs."0.13.0" pkgs.openssl ]; buildInputs = with pkgs; [ # we need a version of bash capable of being interactive - # as opposed to a bash just used for building this flake + # as opposed to a bash just used for building this flake # in non-interactive mode - bashInteractive + bashInteractive ]; shellHook = '' - # once we set SHELL to point to the interactive bash, neovim will - # launch the correct $SHELL in its :terminal + # once we set SHELL to point to the interactive bash, neovim will + # launch the correct $SHELL in its :terminal export SHELL=${pkgs.bashInteractive}/bin/bash export LD_LIBRARY_PATH=${pkgs.zlib.out}/lib:${pkgs.icu.out}/lib:${pkgs.openssl.out}/lib:$LD_LIBRARY_PATH ''; @@ -116,14 +116,14 @@ buildInputs = with pkgs; [ # we need a version of bash capable of being interactive - # as opposed to a bash just used for building this flake + # as opposed to a bash just used for building this flake # in non-interactive mode - bashInteractive + bashInteractive ]; shellHook = '' - # once we set SHELL to point to the interactive bash, neovim will - # launch the correct $SHELL in its :terminal + # once we set SHELL to point to the interactive bash, neovim will + # launch the correct $SHELL in its :terminal export SHELL=${pkgs.bashInteractive}/bin/bash export LD_LIBRARY_PATH=${pkgs.zlib.out}/lib:${pkgs.icu.out}/lib:${pkgs.openssl.out}/lib:$LD_LIBRARY_PATH ''; From 2529ae71057fd4fbf6c815c96b04f6a41bac08f3 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Tue, 25 Feb 2025 13:31:54 +0000 Subject: [PATCH 08/57] update python example; sanic deps in flake.nix, measure_all.sh --- flake.nix | 1 + wrk/measure_all.sh | 6 +++++- wrk/python/main.py | 11 +++++++---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/flake.nix b/flake.nix index 3fd3935..80b9232 100644 --- a/flake.nix +++ b/flake.nix @@ -49,6 +49,7 @@ wrk python3 python3Packages.sanic + python3Packages.setuptools python3Packages.matplotlib poetry poetry diff --git a/wrk/measure_all.sh b/wrk/measure_all.sh index 0f5bb5b..0e80a59 100755 --- a/wrk/measure_all.sh +++ b/wrk/measure_all.sh @@ -8,13 +8,17 @@ fi SUBJECTS="$1" -if [ "$SUBJECTS" = "README" ] ; then +if [ "$SUBJECTS" = "README" ] ; then rm -f wrk/*.perflog SUBJECTS="zig-zap go python-sanic rust-axum csharp cpp-beast" + # above targets csharp and cpp-beast are out of date! + SUBJECTS="zig-zap go python-sanic rust-axum" fi if [ -z "$SUBJECTS" ] ; then SUBJECTS="zig-zap go python python-sanic rust-bythebook rust-bythebook-improved rust-clean rust-axum csharp cpp-beast" + # above targets csharp and cpp-beast are out of date! + SUBJECTS="zig-zap go python python-sanic rust-bythebook rust-bythebook-improved rust-clean rust-axum" fi for S in $SUBJECTS; do diff --git a/wrk/python/main.py b/wrk/python/main.py index 0e180fa..f3c77d1 100644 --- a/wrk/python/main.py +++ b/wrk/python/main.py @@ -7,10 +7,13 @@ serverPort = 8080 class MyServer(BaseHTTPRequestHandler): def do_GET(self): - self.send_response(200) - self.send_header("Content-type", "text/html") - self.end_headers() - self.wfile.write(bytes("HI FROM PYTHON!!!", "utf-8")) + try: + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + self.wfile.write(bytes("HI FROM PYTHON!!!", "utf-8")) + except: + pass def log_message(self, format, *args): return From 6b268c5f6e0535e244522cda56a1c7fc43075d22 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Sun, 2 Mar 2025 11:01:16 +0100 Subject: [PATCH 09/57] migrate build.zig.zon to new format - name now is an enum literal. - added fingerprint --- build.zig.zon | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 56d0e04..089f248 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,10 +1,11 @@ .{ - .name = "zap", - .version = "0.8.0", + .name = .zap, + .version = "0.9.1", .paths = .{ "build.zig", "build.zig.zon", "src", "facil.io", }, + .fingerprint = 0x8ac41f4ef381871a, } From a56781df246f6ed9bbfc6adb41e95b7100772185 Mon Sep 17 00:00:00 2001 From: vctrmn Date: Mon, 3 Mar 2025 17:10:43 +0100 Subject: [PATCH 10/57] Fix deprecated warning for non-prototype function declaration in websocket.c --- .gitignore | 1 + facil.io/lib/facil/http/websockets.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6d2d37c..5ac9561 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ scratch .vs/ **/*.perflog wrk/*.png +.zig-cache/ \ No newline at end of file diff --git a/facil.io/lib/facil/http/websockets.c b/facil.io/lib/facil/http/websockets.c index 87353ef..1e882da 100644 --- a/facil.io/lib/facil/http/websockets.c +++ b/facil.io/lib/facil/http/websockets.c @@ -96,7 +96,7 @@ void free_ws_buffer(ws_s *owner, struct buffer_s buff) { Create/Destroy the websocket object (prototypes) */ -static ws_s *new_websocket(); +static ws_s *new_websocket(intptr_t uuid); static void destroy_ws(ws_s *ws); /******************************************************************************* From 7a131a8be4ab8c136cb0235c852a85d431c7d711 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Fri, 28 Jun 2024 12:39:54 +0200 Subject: [PATCH 11/57] fix hash --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e897d2f..a9c12b1 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,12 @@ Exactly the goals I set out to achieve! ## FAQ: - Q: **What version of Zig does Zap support?** - - Zap uses the latest stable zig release (0.13.0), so you don't have to keep - up with frequent breaking changes. It's an "LTS feature". + - Zap uses the latest stable zig release (0.13.0), so you don't have to keep + up with frequent breaking changes. It's an "LTS feature". - Q: **Can Zap build with Zig's master branch?** - - See the `zig-master` branch. An example of how to use it is - [here](https://github.com/zigzap/hello-master). Please note that the - zig-master branch is not the official master branch of ZAP. Be aware that + - See the `zig-master` branch. An example of how to use it is + [here](https://github.com/zigzap/hello-master). Please note that the + zig-master branch is not the official master branch of ZAP. Be aware that I don't provide `build.zig.zon` snippets or tagged releases for it for the time being. If you know what you are doing, that shouldn't stop you from using it with zig master though. From e9fa5687516760b3dd34e454098e5c3ab04f37f3 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Mon, 14 Oct 2024 22:26:18 +0200 Subject: [PATCH 12/57] update to zig master --- src/mustache.zig | 28 ++++++++++++++-------------- src/request.zig | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/mustache.zig b/src/mustache.zig index 5b75d88..2670910 100644 --- a/src/mustache.zig +++ b/src/mustache.zig @@ -139,7 +139,7 @@ pub const BuildResult = struct { // See `fiobjectify` for more information. pub fn build(self: *Self, data: anytype) BuildResult { const T = @TypeOf(data); - if (@typeInfo(T) != .Struct) { + if (@typeInfo(T) != .@"struct") { @compileError("No struct: '" ++ @typeName(T) ++ "'"); } @@ -157,29 +157,29 @@ fn fiobjectify( ) fio.FIOBJ { const T = @TypeOf(value); switch (@typeInfo(T)) { - .Float, .ComptimeFloat => { + .float, .comptime_float => { return fio.fiobj_float_new(value); }, - .Int, .ComptimeInt => { + .int, .comptime_int => { return fio.fiobj_num_new_bignum(value); }, - .Bool => { + .bool => { return if (value) fio.fiobj_true() else fio.fiobj_false(); }, - .Null => { + .null => { return 0; }, - .Optional => { + .optional => { if (value) |payload| { return fiobjectify(payload); } else { return fiobjectify(null); } }, - .Enum => { + .@"enum" => { return fio.fiobj_num_new_bignum(@intFromEnum(value)); }, - .Union => { + .@"union" => { const info = @typeInfo(T).Union; if (info.tag_type) |UnionTagType| { inline for (info.fields) |u_field| { @@ -191,7 +191,7 @@ fn fiobjectify( @compileError("Unable to fiobjectify untagged union '" ++ @typeName(T) ++ "'"); } }, - .Struct => |S| { + .@"struct" => |S| { // create a new fio hashmap const m = fio.fiobj_hash_new(); // std.debug.print("new struct\n", .{}); @@ -211,10 +211,10 @@ fn fiobjectify( } return m; }, - .ErrorSet => return fiobjectify(@as([]const u8, @errorName(value))), - .Pointer => |ptr_info| switch (ptr_info.size) { + .error_set => return fiobjectify(@as([]const u8, @errorName(value))), + .pointer => |ptr_info| switch (ptr_info.size) { .One => switch (@typeInfo(ptr_info.child)) { - .Array => { + .array => { const Slice = []const std.meta.Elem(ptr_info.child); return fiobjectify(@as(Slice, value)); }, @@ -239,8 +239,8 @@ fn fiobjectify( }, else => @compileError("Unable to fiobjectify type '" ++ @typeName(T) ++ "'"), }, - .Array => return fiobjectify(&value), - .Vector => |info| { + .array => return fiobjectify(&value), + .vector => |info| { const array: [info.len]info.child = value; return fiobjectify(&array); }, diff --git a/src/request.zig b/src/request.zig index e0d7668..07050ef 100644 --- a/src/request.zig +++ b/src/request.zig @@ -339,7 +339,7 @@ pub fn _internal_sendError(self: *const Self, err: anyerror, err_trace: ?std.bui if (err_trace) |trace| { const debugInfo = try std.debug.getSelfDebugInfo(); const ttyConfig: std.io.tty.Config = .no_color; - try std.debug.writeStackTrace(trace, writer, fba.allocator(), debugInfo, ttyConfig); + try std.debug.writeStackTrace(trace, writer, debugInfo, ttyConfig); } try self.sendBody(string.items); From 5808a02466f004f3019ab3b2e707bce24d92efac Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Sat, 22 Feb 2025 12:38:28 +0100 Subject: [PATCH 13/57] fix mustache.zig for current zig master (0.14.x) --- .gitignore | 1 + src/mustache.zig | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 6d2d37c..158e964 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ zig-out/ zig-cache/ +.zig-cache/ *.tar.gz flake.lock.bak tmp/ diff --git a/src/mustache.zig b/src/mustache.zig index 2670910..761b25f 100644 --- a/src/mustache.zig +++ b/src/mustache.zig @@ -213,7 +213,7 @@ fn fiobjectify( }, .error_set => return fiobjectify(@as([]const u8, @errorName(value))), .pointer => |ptr_info| switch (ptr_info.size) { - .One => switch (@typeInfo(ptr_info.child)) { + .one => switch (@typeInfo(ptr_info.child)) { .array => { const Slice = []const std.meta.Elem(ptr_info.child); return fiobjectify(@as(Slice, value)); @@ -224,7 +224,7 @@ fn fiobjectify( }, }, // TODO: .Many when there is a sentinel (waiting for https://github.com/ziglang/zig/pull/3972) - .Slice => { + .slice => { // std.debug.print("new slice\n", .{}); if (ptr_info.child == u8 and std.unicode.utf8ValidateSlice(value)) { return fio.fiobj_str_new(util.toCharPtr(value), value.len); From a2591bf7715cf52891e27f8f13f5f97e9ce0490f Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Sun, 2 Mar 2025 11:01:16 +0100 Subject: [PATCH 14/57] migrate build.zig.zon to new format - name now is an enum literal. - added fingerprint --- build.zig.zon | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 56d0e04..089f248 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,10 +1,11 @@ .{ - .name = "zap", - .version = "0.8.0", + .name = .zap, + .version = "0.9.1", .paths = .{ "build.zig", "build.zig.zon", "src", "facil.io", }, + .fingerprint = 0x8ac41f4ef381871a, } From 43542acd36a6a4e379adc815a7aef3738583fed4 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Thu, 6 Mar 2025 13:59:39 +0100 Subject: [PATCH 15/57] Update flake.nix and GH workflows for zig 0.14.0 --- .github/workflows/build-current-zig.yml | 10 +++++----- .github/workflows/release.yml | 11 +---------- flake.nix | 8 +------- 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build-current-zig.yml b/.github/workflows/build-current-zig.yml index dfb3ce8..0f9bffe 100644 --- a/.github/workflows/build-current-zig.yml +++ b/.github/workflows/build-current-zig.yml @@ -1,5 +1,5 @@ name: Works with Zig 0.13.0 -on: +on: push: branches: - master @@ -7,7 +7,7 @@ on: branches: - master workflow_dispatch: - + jobs: ci: strategy: @@ -18,14 +18,14 @@ jobs: - uses: actions/checkout@v3 - uses: goto-bus-stop/setup-zig@v2 with: - version: 0.13.0 + version: 0.14.0 - name: Check zig version run: zig version - name: Build all examples run: zig build all - # - name: Run all tests - # run: zig build test # Run tests separately so we can see more clearly which one fails + # Also, the test runner tries to run tests concurrently, which causes + # conflicts when port numbers are re-used in the tests - name: Run mustache tests run: zig build test-mustache - name: Run httpparams tests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 91ad13c..459f99c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ on: jobs: build: runs-on: ubuntu-latest - permissions: + permissions: id-token: write contents: write @@ -25,15 +25,6 @@ jobs: run: | echo "::set-output name=version::${GITHUB_REF#refs/tags/}" - # we don't build pkghash, we provide it in ./tools - # - uses: goto-bus-stop/setup-zig@v2 - # with: - # version: master - # - name: Check zig version - # run: zig version - # - name: Build pkghash tool - # run: zig build pkghash - - name: Generate release notes id: release_notes run: | diff --git a/flake.nix b/flake.nix index 80b9232..9b9d2b2 100644 --- a/flake.nix +++ b/flake.nix @@ -9,10 +9,6 @@ # required for latest zig zig.url = "github:mitchellh/zig-overlay"; - # required for latest neovim - # neovim-flake.url = "github:neovim/neovim?dir=contrib"; - # neovim-flake.inputs.nixpkgs.follows = "nixpkgs"; - # Used for shell.nix flake-compat = { url = github:edolstra/flake-compat; @@ -30,7 +26,6 @@ # Other overlays (final: prev: { zigpkgs = inputs.zig.packages.${prev.system}; - # neovim-nightly-pkgs = inputs.neovim-flake.packages.${prev.system}; }) ]; @@ -43,8 +38,7 @@ in rec { devShells.default = pkgs.mkShell { nativeBuildInputs = with pkgs; [ - # neovim-nightly-pkgs.neovim - zigpkgs."0.13.0" + zigpkgs."0.14.0" bat wrk python3 From 14a1760dec0f3bea71ae941a33850af830825f78 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Thu, 6 Mar 2025 13:24:34 +0000 Subject: [PATCH 16/57] really update flake --- flake.lock | 12 ++++++------ flake.nix | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/flake.lock b/flake.lock index 3bc6ee6..ee9c5fd 100644 --- a/flake.lock +++ b/flake.lock @@ -70,11 +70,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1740396192, - "narHash": "sha256-ATMHHrg3sG1KgpQA5x8I+zcYpp5Sf17FaFj/fN+8OoQ=", + "lastModified": 1741037377, + "narHash": "sha256-SvtvVKHaUX4Owb+PasySwZsoc5VUeTf1px34BByiOxw=", "owner": "nixos", "repo": "nixpkgs", - "rev": "d9b69c3ec2a2e2e971c534065bdd53374bd68b97", + "rev": "02032da4af073d0f6110540c8677f16d4be0117f", "type": "github" }, "original": { @@ -145,11 +145,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1740485530, - "narHash": "sha256-PjYwEHq51GVB4ND3z45cxGC59AcK4Yzh9bdfHW569cM=", + "lastModified": 1741263138, + "narHash": "sha256-qlX8tgtZMTSOEeAM8AmC7K6mixgYOguhl/xLj5xQrXc=", "owner": "mitchellh", "repo": "zig-overlay", - "rev": "67d4ddec445dd3cb3f7396ca89f00c5f85ed201e", + "rev": "627055069ee1409e8c9be7bcc533e8823fb87b18", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 9b9d2b2..659fec0 100644 --- a/flake.nix +++ b/flake.nix @@ -84,7 +84,7 @@ devShells.build = pkgs.mkShell { nativeBuildInputs = with pkgs; [ - zigpkgs."0.13.0" + zigpkgs."0.14.0" pkgs.openssl ]; From 3b06a336ef27e5ffe04075109d67e309b83a337a Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Thu, 6 Mar 2025 14:50:36 +0100 Subject: [PATCH 17/57] patched nix flake to use master instead of 0.14.0 for now --- flake.nix | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 659fec0..f6781a1 100644 --- a/flake.nix +++ b/flake.nix @@ -38,7 +38,8 @@ in rec { devShells.default = pkgs.mkShell { nativeBuildInputs = with pkgs; [ - zigpkgs."0.14.0" + # TODO: re-enable this once it is fixed: zigpkgs."0.14.0" + zigpkgs.master bat wrk python3 @@ -84,7 +85,8 @@ devShells.build = pkgs.mkShell { nativeBuildInputs = with pkgs; [ - zigpkgs."0.14.0" + # zigpkgs."0.14.0" + zigpkgs.master pkgs.openssl ]; From 8e80eecc3eca910c4da0c4243a00c27ab4f8b4cb Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 16 Mar 2025 04:45:31 +0100 Subject: [PATCH 18/57] endpoint simplification --- examples/endpoint/main.zig | 4 +- examples/endpoint/stopendpoint.zig | 23 +-- examples/endpoint/userweb.zig | 42 ++--- src/endpoint.zig | 282 ++++++++++------------------- 4 files changed, 121 insertions(+), 230 deletions(-) diff --git a/examples/endpoint/main.zig b/examples/endpoint/main.zig index dcfffda..d6b519b 100644 --- a/examples/endpoint/main.zig +++ b/examples/endpoint/main.zig @@ -41,8 +41,8 @@ pub fn main() !void { var stopEp = StopEndpoint.init("/stop"); // register endpoints with the listener - try listener.register(userWeb.endpoint()); - try listener.register(stopEp.endpoint()); + try listener.register(&userWeb); + try listener.register(&stopEp); // fake some users var uid: usize = undefined; diff --git a/examples/endpoint/stopendpoint.zig b/examples/endpoint/stopendpoint.zig index 9411d49..b9a7c46 100644 --- a/examples/endpoint/stopendpoint.zig +++ b/examples/endpoint/stopendpoint.zig @@ -5,25 +5,22 @@ const zap = @import("zap"); /// the main thread usually continues at the instructions after the call to zap.start(). pub const Self = @This(); -ep: zap.Endpoint = undefined, +path: []const u8, -pub fn init( - path: []const u8, -) Self { +pub fn init(path: []const u8) Self { return .{ - .ep = zap.Endpoint.init(.{ - .path = path, - .get = get, - }), + .path = path, }; } -pub fn endpoint(self: *Self) *zap.Endpoint { - return &self.ep; -} - -fn get(e: *zap.Endpoint, r: zap.Request) void { +pub fn get(e: *Self, r: zap.Request) void { _ = e; _ = r; zap.stop(); } + +pub fn post(_: *Self, _: zap.Request) void {} +pub fn put(_: *Self, _: zap.Request) void {} +pub fn delete(_: *Self, _: zap.Request) void {} +pub fn patch(_: *Self, _: zap.Request) void {} +pub fn options(_: *Self, _: zap.Request) void {} diff --git a/examples/endpoint/userweb.zig b/examples/endpoint/userweb.zig index 6c6775f..f95504a 100644 --- a/examples/endpoint/userweb.zig +++ b/examples/endpoint/userweb.zig @@ -8,9 +8,10 @@ const User = Users.User; pub const Self = @This(); alloc: std.mem.Allocator = undefined, -ep: zap.Endpoint = undefined, _users: Users = undefined, +path: []const u8, + pub fn init( a: std.mem.Allocator, user_path: []const u8, @@ -18,15 +19,7 @@ pub fn init( return .{ .alloc = a, ._users = Users.init(a), - .ep = zap.Endpoint.init(.{ - .path = user_path, - .get = getUser, - .post = postUser, - .put = putUser, - .patch = putUser, - .delete = deleteUser, - .options = optionsUser, - }), + .path = user_path, }; } @@ -38,27 +31,22 @@ pub fn users(self: *Self) *Users { return &self._users; } -pub fn endpoint(self: *Self) *zap.Endpoint { - return &self.ep; -} - fn userIdFromPath(self: *Self, path: []const u8) ?usize { - if (path.len >= self.ep.settings.path.len + 2) { - if (path[self.ep.settings.path.len] != '/') { + if (path.len >= self.path.len + 2) { + if (path[self.path.len] != '/') { return null; } - const idstr = path[self.ep.settings.path.len + 1 ..]; + const idstr = path[self.path.len + 1 ..]; return std.fmt.parseUnsigned(usize, idstr, 10) catch null; } return null; } -fn getUser(e: *zap.Endpoint, r: zap.Request) void { - const self: *Self = @fieldParentPtr("ep", e); - +pub fn put(_: *Self, _: zap.Request) void {} +pub fn get(self: *Self, r: zap.Request) void { if (r.path) |path| { // /users - if (path.len == e.settings.path.len) { + if (path.len == self.path.len) { return self.listUsers(r); } var jsonbuf: [256]u8 = undefined; @@ -81,8 +69,7 @@ fn listUsers(self: *Self, r: zap.Request) void { } } -fn postUser(e: *zap.Endpoint, r: zap.Request) void { - const self: *Self = @fieldParentPtr("ep", e); +pub fn post(self: *Self, r: zap.Request) void { if (r.body) |body| { const maybe_user: ?std.json.Parsed(User) = std.json.parseFromSlice(User, self.alloc, body, .{}) catch null; if (maybe_user) |u| { @@ -100,8 +87,7 @@ fn postUser(e: *zap.Endpoint, r: zap.Request) void { } } -fn putUser(e: *zap.Endpoint, r: zap.Request) void { - const self: *Self = @fieldParentPtr("ep", e); +pub fn patch(self: *Self, r: zap.Request) void { if (r.path) |path| { if (self.userIdFromPath(path)) |id| { if (self._users.get(id)) |_| { @@ -126,8 +112,7 @@ fn putUser(e: *zap.Endpoint, r: zap.Request) void { } } -fn deleteUser(e: *zap.Endpoint, r: zap.Request) void { - const self: *Self = @fieldParentPtr("ep", e); +pub fn delete(self: *Self, r: zap.Request) void { if (r.path) |path| { if (self.userIdFromPath(path)) |id| { var jsonbuf: [128]u8 = undefined; @@ -144,8 +129,7 @@ fn deleteUser(e: *zap.Endpoint, r: zap.Request) void { } } -fn optionsUser(e: *zap.Endpoint, r: zap.Request) void { - _ = e; +pub fn options(_: *Self, r: zap.Request) void { r.setHeader("Access-Control-Allow-Origin", "*") catch return; r.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") catch return; r.setStatus(zap.StatusCode.no_content); diff --git a/src/endpoint.zig b/src/endpoint.zig index 07cecc9..3bdaff3 100644 --- a/src/endpoint.zig +++ b/src/endpoint.zig @@ -9,222 +9,130 @@ const Request = zap.Request; const ListenerSettings = zap.HttpListenerSettings; const HttpListener = zap.HttpListener; -/// Type of the request function callbacks. -pub const RequestFn = *const fn (self: *Endpoint, r: Request) void; - -/// Settings to initialize an Endpoint -pub const Settings = struct { - /// path / slug of the endpoint - path: []const u8, - /// callback to GET request handler - get: ?RequestFn = null, - /// callback to POST request handler - post: ?RequestFn = null, - /// callback to PUT request handler - put: ?RequestFn = null, - /// callback to DELETE request handler - delete: ?RequestFn = null, - /// callback to PATCH request handler - patch: ?RequestFn = null, - /// callback to OPTIONS request handler - options: ?RequestFn = null, - /// Only applicable to Authenticating Endpoint: handler for unauthorized requests - unauthorized: ?RequestFn = null, -}; - -settings: Settings, - -/// Initialize the endpoint. -/// Set only the callbacks you need. Requests of HTTP methods without a -/// provided callback will be ignored. -pub fn init(s: Settings) Endpoint { - return .{ - .settings = .{ - .path = s.path, - .get = s.get orelse &nop, - .post = s.post orelse &nop, - .put = s.put orelse &nop, - .delete = s.delete orelse &nop, - .patch = s.patch orelse &nop, - .options = s.options orelse &nop, - .unauthorized = s.unauthorized orelse &nop, - }, +const EndpointWrapper = struct { + pub const Wrapper = struct { + call: *const fn (*Wrapper, zap.Request) void = undefined, + path: []const u8, + destroy: *const fn (allocator: std.mem.Allocator, *Wrapper) void = undefined, }; -} + pub fn Wrap(T: type) type { + return struct { + wrapped: *T, + wrapper: Wrapper, -// no operation. Dummy handler function for ignoring unset request types. -fn nop(self: *Endpoint, r: Request) void { - _ = self; - _ = r; -} + const Self = @This(); -/// The global request handler for this Endpoint, called by the listener. -pub fn onRequest(self: *Endpoint, r: zap.Request) void { - switch (r.methodAsEnum()) { - .GET => self.settings.get.?(self, r), - .POST => self.settings.post.?(self, r), - .PUT => self.settings.put.?(self, r), - .DELETE => self.settings.delete.?(self, r), - .PATCH => self.settings.patch.?(self, r), - .OPTIONS => self.settings.options.?(self, r), - else => return, + pub fn unwrap(wrapper: *Wrapper) *Self { + const self: *Self = @alignCast(@fieldParentPtr("wrapper", wrapper)); + return self; + } + + pub fn destroy(allocator: std.mem.Allocator, wrapper: *Wrapper) void { + const self: *Self = @alignCast(@fieldParentPtr("wrapper", wrapper)); + allocator.destroy(self); + } + + pub fn onRequestWrapped(wrapper: *Wrapper, r: zap.Request) void { + var self: *Self = Self.unwrap(wrapper); + self.onRequest(r); + } + + pub fn onRequest(self: *Self, r: zap.Request) void { + switch (r.methodAsEnum()) { + .GET => return self.wrapped.*.get(r), + .POST => return self.wrapped.*.post(r), + .PUT => return self.wrapped.*.put(r), + .DELETE => return self.wrapped.*.delete(r), + .PATCH => return self.wrapped.*.patch(r), + .OPTIONS => return self.wrapped.*.options(r), + else => {}, + } + // TODO: log that req fn is not implemented on this EP + } + }; } -} + + pub fn init(T: type, value: *T) EndpointWrapper.Wrap(T) { + var ret: EndpointWrapper.Wrap(T) = .{ + .wrapped = value, + .wrapper = .{ .path = value.path }, + }; + ret.wrapper.call = EndpointWrapper.Wrap(T).onRequestWrapped; + ret.wrapper.destroy = EndpointWrapper.Wrap(T).destroy; + return ret; + } +}; /// Wrap an endpoint with an Authenticator -> new Endpoint of type Endpoint /// is available via the `endpoint()` function. -pub fn Authenticating(comptime Authenticator: type) type { +pub fn Authenticating(EndpointType: type, Authenticator: type) type { return struct { authenticator: *Authenticator, - ep: *Endpoint, - auth_endpoint: Endpoint, + ep: *EndpointType, + path: []const u8, const Self = @This(); /// Init the authenticating endpoint. Pass in a pointer to the endpoint /// you want to wrap, and the Authenticator that takes care of authenticating /// requests. - pub fn init(e: *Endpoint, authenticator: *Authenticator) Self { + pub fn init(e: *EndpointType, authenticator: *Authenticator) Self { return .{ .authenticator = authenticator, .ep = e, - .auth_endpoint = Endpoint.init(.{ - .path = e.settings.path, - // we override only the set ones. the other ones - // are set to null anyway -> will be nopped out - .get = if (e.settings.get != null) get else null, - .post = if (e.settings.post != null) post else null, - .put = if (e.settings.put != null) put else null, - .delete = if (e.settings.delete != null) delete else null, - .patch = if (e.settings.patch != null) patch else null, - .options = if (e.settings.options != null) options else null, - .unauthorized = e.settings.unauthorized, - }), + .path = e.path, }; } - /// Get the auth endpoint struct of type Endpoint so it can be stored in the listener. - /// When the listener calls the auth_endpoint, onRequest will have - /// access to all of this via fieldParentPtr - pub fn endpoint(self: *Self) *Endpoint { - return &self.auth_endpoint; - } - - /// GET: here, the auth_endpoint will be passed in as endpoint. /// Authenticates GET requests using the Authenticator. - pub fn get(e: *Endpoint, r: zap.Request) void { - const authEp: *Self = @fieldParentPtr("auth_endpoint", e); - switch (authEp.authenticator.authenticateRequest(&r)) { - .AuthFailed => { - if (e.settings.unauthorized) |unauthorized| { - unauthorized(authEp.ep, r); - return; - } else { - r.setStatus(.unauthorized); - r.sendBody("UNAUTHORIZED") catch return; - return; - } - }, - .AuthOK => authEp.ep.settings.get.?(authEp.ep, r), + pub fn get(self: *Self, r: zap.Request) void { + switch (self.authenticator.authenticateRequest(&r)) { + .AuthFailed => return self.ep.*.unauthorized(r), + .AuthOK => self.ep.*.get(r), .Handled => {}, } } - /// POST: here, the auth_endpoint will be passed in as endpoint. /// Authenticates POST requests using the Authenticator. - pub fn post(e: *Endpoint, r: zap.Request) void { - const authEp: *Self = @fieldParentPtr("auth_endpoint", e); - switch (authEp.authenticator.authenticateRequest(&r)) { - .AuthFailed => { - if (e.settings.unauthorized) |unauthorized| { - unauthorized(authEp.ep, r); - return; - } else { - r.setStatus(.unauthorized); - r.sendBody("UNAUTHORIZED") catch return; - return; - } - }, - .AuthOK => authEp.ep.settings.post.?(authEp.ep, r), + pub fn post(self: *Self, r: zap.Request) void { + switch (self.authenticator.authenticateRequest(&r)) { + .AuthFailed => return self.ep.*.unauthorized(r), + .AuthOK => self.ep.*.post(r), .Handled => {}, } } - /// PUT: here, the auth_endpoint will be passed in as endpoint. /// Authenticates PUT requests using the Authenticator. - pub fn put(e: *Endpoint, r: zap.Request) void { - const authEp: *Self = @fieldParentPtr("auth_endpoint", e); - switch (authEp.authenticator.authenticateRequest(&r)) { - .AuthFailed => { - if (e.settings.unauthorized) |unauthorized| { - unauthorized(authEp.ep, r); - return; - } else { - r.setStatus(.unauthorized); - r.sendBody("UNAUTHORIZED") catch return; - return; - } - }, - .AuthOK => authEp.ep.settings.put.?(authEp.ep, r), + pub fn put(self: *Self, r: zap.Request) void { + switch (self.authenticator.authenticateRequest(&r)) { + .AuthFailed => return self.ep.*.unauthorized(r), + .AuthOK => self.ep.*.put(r), .Handled => {}, } } - /// DELETE: here, the auth_endpoint will be passed in as endpoint. /// Authenticates DELETE requests using the Authenticator. - pub fn delete(e: *Endpoint, r: zap.Request) void { - const authEp: *Self = @fieldParentPtr("auth_endpoint", e); - switch (authEp.authenticator.authenticateRequest(&r)) { - .AuthFailed => { - if (e.settings.unauthorized) |unauthorized| { - unauthorized(authEp.ep, r); - return; - } else { - r.setStatus(.unauthorized); - r.sendBody("UNAUTHORIZED") catch return; - return; - } - }, - .AuthOK => authEp.ep.settings.delete.?(authEp.ep, r), + pub fn delete(self: *Self, r: zap.Request) void { + switch (self.authenticator.authenticateRequest(&r)) { + .AuthFailed => return self.ep.*.unauthorized(r), + .AuthOK => self.ep.*.delete(r), .Handled => {}, } } - /// PATCH: here, the auth_endpoint will be passed in as endpoint. /// Authenticates PATCH requests using the Authenticator. - pub fn patch(e: *Endpoint, r: zap.Request) void { - const authEp: *Self = @fieldParentPtr("auth_endpoint", e); - switch (authEp.authenticator.authenticateRequest(&r)) { - .AuthFailed => { - if (e.settings.unauthorized) |unauthorized| { - unauthorized(authEp.ep, r); - return; - } else { - r.setStatus(.unauthorized); - r.sendBody("UNAUTHORIZED") catch return; - return; - } - }, - .AuthOK => authEp.ep.settings.patch.?(authEp.ep, r), + pub fn patch(self: *Self, r: zap.Request) void { + switch (self.authenticator.authenticateRequest(&r)) { + .AuthFailed => return self.ep.*.unauthorized(r), + .AuthOK => self.ep.*.patch(r), .Handled => {}, } } - /// OPTIONS: here, the auth_endpoint will be passed in as endpoint. /// Authenticates OPTIONS requests using the Authenticator. - pub fn options(e: *Endpoint, r: zap.Request) void { - const authEp: *Self = @fieldParentPtr("auth_endpoint", e); - switch (authEp.authenticator.authenticateRequest(&r)) { - .AuthFailed => { - if (e.settings.unauthorized) |unauthorized| { - unauthorized(authEp.ep, r); - return; - } else { - r.setStatus(.unauthorized); - r.sendBody("UNAUTHORIZED") catch return; - return; - } - }, - .AuthOK => authEp.ep.settings.put.?(authEp.ep, r), + pub fn options(self: *Self, r: zap.Request) void { + switch (self.authenticator.authenticateRequest(&r)) { + .AuthFailed => return self.ep.*.unauthorized(r), + .AuthOK => self.ep.*.put(r), .Handled => {}, } } @@ -249,7 +157,7 @@ pub const Listener = struct { const Self = @This(); /// Internal static struct of member endpoints - var endpoints: std.ArrayList(*Endpoint) = undefined; + var endpoints: std.ArrayListUnmanaged(*EndpointWrapper.Wrapper) = .empty; /// Internal, static request handler callback. Will be set to the optional, /// user-defined request callback that only gets called if no endpoints match @@ -260,12 +168,10 @@ pub const Listener = struct { /// callback in the provided ListenerSettings, this request callback will be /// called every time a request arrives that no endpoint matches. pub fn init(a: std.mem.Allocator, l: ListenerSettings) Self { - endpoints = std.ArrayList(*Endpoint).init(a); - // take copy of listener settings before modifying the callback field var ls = l; - // override the settings with our internal, actul callback function + // override the settings with our internal, actual callback function // so that "we" will be called on request ls.on_request = Listener.onRequest; @@ -281,8 +187,10 @@ pub const Listener = struct { /// Registered endpoints will not be de-initialized automatically; just removed /// from the internal map. pub fn deinit(self: *Self) void { - _ = self; - endpoints.deinit(); + for (endpoints.items) |endpoint_wrapper| { + endpoint_wrapper.destroy(self.allocator, endpoint_wrapper); + } + endpoints.deinit(self.allocator); } /// Call this to start listening. After this, no more endpoints can be @@ -295,29 +203,31 @@ pub const Listener = struct { /// NOTE: endpoint paths are matched with startsWith -> so use endpoints with distinctly starting names!! /// If you try to register an endpoint whose path would shadow an already registered one, you will /// receive an EndpointPathShadowError. - pub fn register(self: *Self, e: *Endpoint) !void { - _ = self; + pub fn register(self: *Self, e: anytype) !void { for (endpoints.items) |other| { if (std.mem.startsWith( u8, - other.settings.path, - e.settings.path, + other.path, + e.path, ) or std.mem.startsWith( u8, - e.settings.path, - other.settings.path, + e.path, + other.path, )) { return EndpointListenerError.EndpointPathShadowError; } } - try endpoints.append(e); + const EndpointType = @typeInfo(@TypeOf(e)).pointer.child; + const wrapper = try self.allocator.create(EndpointWrapper.Wrap(EndpointType)); + wrapper.* = EndpointWrapper.init(EndpointType, e); + try endpoints.append(self.allocator, &wrapper.wrapper); } fn onRequest(r: Request) void { if (r.path) |p| { - for (endpoints.items) |e| { - if (std.mem.startsWith(u8, p, e.settings.path)) { - e.onRequest(r); + for (endpoints.items) |wrapper| { + if (std.mem.startsWith(u8, p, wrapper.path)) { + wrapper.call(wrapper, r); return; } } From 0123e60059f1c41d872995ac64ccf5ad724de492 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 16 Mar 2025 13:39:01 +0100 Subject: [PATCH 19/57] endpoint type check with meaningful error messages --- src/endpoint.zig | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/endpoint.zig b/src/endpoint.zig index 3bdaff3..047f073 100644 --- a/src/endpoint.zig +++ b/src/endpoint.zig @@ -9,6 +9,34 @@ const Request = zap.Request; const ListenerSettings = zap.HttpListenerSettings; const HttpListener = zap.HttpListener; +pub fn checkEndpointType(T: type) void { + if (@hasField(T, "path")) { + if (@FieldType(T, "path") != []const u8) { + @compileError(@typeName(@FieldType(T, "path")) ++ " has wrong type, expected: []const u8"); + } + } else { + @compileError(@typeName(T) ++ " has no path field"); + } + + const methods_to_check = [_][]const u8{ + "get", + "post", + "put", + "delete", + "patch", + "options", + }; + inline for (methods_to_check) |method| { + if (@hasDecl(T, method)) { + if (@TypeOf(@field(T, method)) != fn (_: *T, _: Request) void) { + @compileError(method ++ " method of " ++ @typeName(T) ++ " has wrong type:\n" ++ @typeName(@TypeOf(T.get)) ++ "\nexpected:\n" ++ @typeName(fn (_: *T, _: Request) void)); + } + } else { + @compileError(@typeName(T) ++ " has no method named `" ++ method ++ "`"); + } + } +} + const EndpointWrapper = struct { pub const Wrapper = struct { call: *const fn (*Wrapper, zap.Request) void = undefined, @@ -53,6 +81,7 @@ const EndpointWrapper = struct { } pub fn init(T: type, value: *T) EndpointWrapper.Wrap(T) { + checkEndpointType(T); var ret: EndpointWrapper.Wrap(T) = .{ .wrapped = value, .wrapper = .{ .path = value.path }, @@ -218,6 +247,7 @@ pub const Listener = struct { } } const EndpointType = @typeInfo(@TypeOf(e)).pointer.child; + checkEndpointType(EndpointType); const wrapper = try self.allocator.create(EndpointWrapper.Wrap(EndpointType)); wrapper.* = EndpointWrapper.init(EndpointType, e); try endpoints.append(self.allocator, &wrapper.wrapper); From d0cb7520ac7638eac94ab0446195846ac141cc99 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 16 Mar 2025 13:39:23 +0100 Subject: [PATCH 20/57] fix endpoint_auth example for new Endpoint stuff --- examples/endpoint_auth/endpoint_auth.zig | 42 ++++++++++++++---------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/examples/endpoint_auth/endpoint_auth.zig b/examples/endpoint_auth/endpoint_auth.zig index 4b69d7e..f6f1e8a 100644 --- a/examples/endpoint_auth/endpoint_auth.zig +++ b/examples/endpoint_auth/endpoint_auth.zig @@ -10,18 +10,28 @@ const HTTP_RESPONSE: []const u8 = \\ ; -// authenticated requests go here -fn endpoint_http_get(e: *zap.Endpoint, r: zap.Request) void { - _ = e; - r.sendBody(HTTP_RESPONSE) catch return; -} +const Endpoint = struct { + // the slug + path: []const u8, -// just for fun, we also catch the unauthorized callback -fn endpoint_http_unauthorized(e: *zap.Endpoint, r: zap.Request) void { - _ = e; - r.setStatus(.unauthorized); - r.sendBody("UNAUTHORIZED ACCESS") catch return; -} + // authenticated requests go here + pub fn get(_: *Endpoint, r: zap.Request) void { + r.sendBody(HTTP_RESPONSE) catch return; + } + + // just for fun, we also catch the unauthorized callback + pub fn unauthorized(_: *Endpoint, r: zap.Request) void { + r.setStatus(.unauthorized); + r.sendBody("UNAUTHORIZED ACCESS") catch return; + } + + // not implemented, don't care + pub fn post(_: *Endpoint, _: zap.Request) void {} + pub fn put(_: *Endpoint, _: zap.Request) void {} + pub fn delete(_: *Endpoint, _: zap.Request) void {} + pub fn patch(_: *Endpoint, _: zap.Request) void {} + pub fn options(_: *Endpoint, _: zap.Request) void {} +}; pub fn main() !void { // setup listener @@ -38,11 +48,9 @@ pub fn main() !void { defer listener.deinit(); // create mini endpoint - var ep = zap.Endpoint.init(.{ + var ep: Endpoint = .{ .path = "/test", - .get = endpoint_http_get, - .unauthorized = endpoint_http_unauthorized, - }); + }; // create authenticator const Authenticator = zap.Auth.BearerSingle; @@ -50,10 +58,10 @@ pub fn main() !void { defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = zap.Endpoint.Authenticating(Authenticator); + const BearerAuthEndpoint = zap.Endpoint.Authenticating(Endpoint, Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); - try listener.register(auth_ep.endpoint()); + try listener.register(&auth_ep); listener.listen() catch {}; std.debug.print( From 7aae77b034290c4e8ead5a219d971ca694e83ec4 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 16 Mar 2025 13:39:52 +0100 Subject: [PATCH 21/57] fix doc comment for new Endpoint --- src/zap.zig | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/zap.zig b/src/zap.zig index 3cbe216..8a777ef 100644 --- a/src/zap.zig +++ b/src/zap.zig @@ -10,40 +10,38 @@ pub const fio = @import("fio.zig"); pub const Tls = @import("tls.zig"); /// Endpoint and supporting types. -/// Create one and pass in your callbacks. Then, -/// pass it to a HttpListener's `register()` function to register with the -/// listener. +/// Create one and define all callbacks. Then, pass it to a HttpListener's +/// `register()` function to register with the listener. /// -/// **NOTE**: A common endpoint pattern for zap is to create your own struct -/// that embeds an Endpoint, provides specific callbacks, and uses -/// `@fieldParentPtr` to get a reference to itself. +/// **NOTE**: Endpoints must define the following: +/// +/// ```zig +/// path: []const u8, +/// pub fn get(_: *Self, _: zap.Request) void {} +/// pub fn post(_: *Self, _: zap.Request) void {} +/// pub fn put(_: *Self, _: zap.Request) void {} +/// pub fn delete(_: *Self, _: zap.Request) void {} +/// pub fn patch(_: *Self, _: zap.Request) void {} +/// pub fn options(_: *Self, _: zap.Request) void {} +/// // optional, if auth stuff is used: +/// pub fn unauthorized(_: *Self, _: zap.Request) void {} +/// ``` /// /// Example: -/// A simple endpoint listening on the /stop route that shuts down zap. -/// The main thread usually continues at the instructions after the call to zap.start(). +/// A simple endpoint listening on the /stop route that shuts down zap. The +/// main thread usually continues at the instructions after the call to +/// zap.start(). /// /// ```zig /// const StopEndpoint = struct { -/// ep: zap.Endpoint = undefined, /// -/// pub fn init( -/// path: []const u8, -/// ) StopEndpoint { +/// pub fn init( path: []const u8,) StopEndpoint { /// return .{ -/// .ep = zap.Endpoint.init(.{ -/// .path = path, -/// .get = get, -/// }), +/// .path = path, /// }; /// } /// -/// // access the internal Endpoint -/// pub fn endpoint(self: *StopEndpoint) *zap.Endpoint { -/// return &self.ep; -/// } -/// -/// fn get(e: *zap.Endpoint, r: zap.Request) void { -/// const self: *StopEndpoint = @fieldParentPtr("ep", e); +/// pub fn get(self: *StopEndpoint, r: zap.Request) void { /// _ = self; /// _ = r; /// zap.stop(); From 7f82a6b35045f4da526c70ccc232d103ffe5555e Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 16 Mar 2025 14:13:54 +0100 Subject: [PATCH 22/57] fixed zap.Middleware + example for endpoint handlers to new zap.Endpoint --- .../middleware_with_endpoint.zig | 25 ++++++++----------- src/middleware.zig | 18 +++++++++---- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/examples/middleware_with_endpoint/middleware_with_endpoint.zig b/examples/middleware_with_endpoint/middleware_with_endpoint.zig index 3e94450..3bf9551 100644 --- a/examples/middleware_with_endpoint/middleware_with_endpoint.zig +++ b/examples/middleware_with_endpoint/middleware_with_endpoint.zig @@ -136,26 +136,23 @@ const SessionMiddleWare = struct { // `breakOnFinish` parameter. // const HtmlEndpoint = struct { - ep: zap.Endpoint = undefined, const Self = @This(); + path: []const u8 = "(undefined)", + pub fn init() Self { return .{ - .ep = zap.Endpoint.init(.{ - .path = "/doesn't+matter", - .get = get, - }), + .path = "/doesn't+matter", }; } - pub fn endpoint(self: *Self) *zap.Endpoint { - return &self.ep; - } - - pub fn get(ep: *zap.Endpoint, r: zap.Request) void { - const self: *Self = @fieldParentPtr("ep", ep); - _ = self; + pub fn post(_: *HtmlEndpoint, _: zap.Request) void {} + pub fn put(_: *HtmlEndpoint, _: zap.Request) void {} + pub fn delete(_: *HtmlEndpoint, _: zap.Request) void {} + pub fn patch(_: *HtmlEndpoint, _: zap.Request) void {} + pub fn options(_: *HtmlEndpoint, _: zap.Request) void {} + pub fn get(_: *Self, r: zap.Request) void { var buf: [1024]u8 = undefined; var userFound: bool = false; var sessionFound: bool = false; @@ -205,8 +202,8 @@ pub fn main() !void { var htmlEndpoint = HtmlEndpoint.init(); // we wrap the endpoint with a middleware handler - var htmlHandler = zap.Middleware.EndpointHandler(Handler, Context).init( - htmlEndpoint.endpoint(), // the endpoint + var htmlHandler = zap.Middleware.EndpointHandler(Handler, HtmlEndpoint, Context).init( + &htmlEndpoint, // the endpoint null, // no other handler (we are the last in the chain) .{}, // We can set custom EndpointHandlerOptions here ); diff --git a/src/middleware.zig b/src/middleware.zig index c094fee..0373280 100644 --- a/src/middleware.zig +++ b/src/middleware.zig @@ -63,10 +63,10 @@ pub const EndpointHandlerOptions = struct { }; /// A convenience handler for artibrary zap.Endpoint -pub fn EndpointHandler(comptime HandlerType: anytype, comptime ContextType: anytype) type { +pub fn EndpointHandler(comptime HandlerType: anytype, comptime EndpointType: anytype, comptime ContextType: anytype) type { return struct { handler: HandlerType, - endpoint: *zap.Endpoint, + endpoint: *EndpointType, options: EndpointHandlerOptions, const Self = @This(); @@ -78,7 +78,7 @@ pub fn EndpointHandler(comptime HandlerType: anytype, comptime ContextType: anyt /// /// If the `breakOnFinish` option is `true`, the handler will stop handing requests down the chain /// if the endpoint processed the request. - pub fn init(endpoint: *zap.Endpoint, other: ?*HandlerType, options: EndpointHandlerOptions) Self { + pub fn init(endpoint: *EndpointType, other: ?*HandlerType, options: EndpointHandlerOptions) Self { return .{ .handler = HandlerType.init(onRequest, other), .endpoint = endpoint, @@ -100,9 +100,17 @@ pub fn EndpointHandler(comptime HandlerType: anytype, comptime ContextType: anyt const self: *Self = @fieldParentPtr("handler", handler); r.setUserContext(context); if (!self.options.checkPath or - std.mem.startsWith(u8, r.path orelse "", self.endpoint.settings.path)) + std.mem.startsWith(u8, r.path orelse "", self.endpoint.path)) { - self.endpoint.onRequest(r); + switch (r.methodAsEnum()) { + .GET => self.endpoint.*.get(r), + .POST => self.endpoint.*.post(r), + .PUT => self.endpoint.*.put(r), + .DELETE => self.endpoint.*.delete(r), + .PATCH => self.endpoint.*.patch(r), + .OPTIONS => self.endpoint.*.options(r), + else => {}, + } } // if the request was handled by the endpoint, we may break the chain here From 3856fceb9fce68e0b63b9256863f7fac04b76cdf Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 16 Mar 2025 15:52:26 +0100 Subject: [PATCH 23/57] fix auth tests b/c of new zap.Endpoint --- src/endpoint.zig | 9 ++- src/tests/test_auth.zig | 128 +++++++++++++++++++--------------------- 2 files changed, 67 insertions(+), 70 deletions(-) diff --git a/src/endpoint.zig b/src/endpoint.zig index 047f073..236e33a 100644 --- a/src/endpoint.zig +++ b/src/endpoint.zig @@ -73,9 +73,10 @@ const EndpointWrapper = struct { .DELETE => return self.wrapped.*.delete(r), .PATCH => return self.wrapped.*.patch(r), .OPTIONS => return self.wrapped.*.options(r), - else => {}, + else => { + // TODO: log that method is ignored + }, } - // TODO: log that req fn is not implemented on this EP } }; } @@ -197,6 +198,10 @@ pub const Listener = struct { /// callback in the provided ListenerSettings, this request callback will be /// called every time a request arrives that no endpoint matches. pub fn init(a: std.mem.Allocator, l: ListenerSettings) Self { + // reset the global in case init is called multiple times, as is the + // case in the authentication tests + endpoints = .empty; + // take copy of listener settings before modifying the callback field var ls = l; diff --git a/src/tests/test_auth.zig b/src/tests/test_auth.zig index 2ac4905..b33487d 100644 --- a/src/tests/test_auth.zig +++ b/src/tests/test_auth.zig @@ -1,7 +1,6 @@ const std = @import("std"); const zap = @import("zap"); const Authenticators = zap.Auth; -const Endpoint = zap.Endpoint; const fio = zap; const util = zap; @@ -102,23 +101,6 @@ const HTTP_RESPONSE: []const u8 = ; var received_response: []const u8 = "null"; -fn endpoint_http_get(e: *Endpoint, r: zap.Request) void { - _ = e; - r.sendBody(HTTP_RESPONSE) catch return; - received_response = HTTP_RESPONSE; - std.time.sleep(1 * std.time.ns_per_s); - zap.stop(); -} - -fn endpoint_http_unauthorized(e: *Endpoint, r: zap.Request) void { - _ = e; - r.setStatus(.unauthorized); - r.sendBody("UNAUTHORIZED ACCESS") catch return; - received_response = "UNAUTHORIZED"; - std.time.sleep(1 * std.time.ns_per_s); - zap.stop(); -} - // // http client code for in-process sending of http request // @@ -165,6 +147,31 @@ fn makeRequestThread(a: std.mem.Allocator, url: []const u8, auth: ?ClientAuthReq return try std.Thread.spawn(.{}, makeRequest, .{ a, url, auth }); } +pub const Endpoint = struct { + path: []const u8, + + pub fn get(e: *Endpoint, r: zap.Request) void { + _ = e; + r.sendBody(HTTP_RESPONSE) catch return; + received_response = HTTP_RESPONSE; + std.time.sleep(1 * std.time.ns_per_s); + zap.stop(); + } + + pub fn unauthorized(e: *Endpoint, r: zap.Request) void { + _ = e; + r.setStatus(.unauthorized); + r.sendBody("UNAUTHORIZED ACCESS") catch return; + received_response = "UNAUTHORIZED"; + std.time.sleep(1 * std.time.ns_per_s); + zap.stop(); + } + pub fn post(_: *Endpoint, _: zap.Request) void {} + pub fn put(_: *Endpoint, _: zap.Request) void {} + pub fn delete(_: *Endpoint, _: zap.Request) void {} + pub fn patch(_: *Endpoint, _: zap.Request) void {} + pub fn options(_: *Endpoint, _: zap.Request) void {} +}; // // end of http client code // @@ -187,11 +194,9 @@ test "BearerAuthSingle authenticateRequest OK" { defer listener.deinit(); // create mini endpoint - var ep = Endpoint.init(.{ + var ep: Endpoint = .{ .path = "/test", - .get = endpoint_http_get, - .unauthorized = endpoint_http_unauthorized, - }); + }; // create authenticator const Authenticator = Authenticators.BearerSingle; @@ -199,10 +204,10 @@ test "BearerAuthSingle authenticateRequest OK" { defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator); + const BearerAuthEndpoint = zap.Endpoint.Authenticating(Endpoint, Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); - try listener.register(auth_ep.endpoint()); + try listener.register(&auth_ep); listener.listen() catch {}; // std.debug.print("\n\n*******************************************\n", .{}); @@ -240,11 +245,9 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" { defer listener.deinit(); // create mini endpoint - var ep = Endpoint.init(.{ + var ep: Endpoint = .{ .path = "/test", - .get = endpoint_http_get, - .unauthorized = endpoint_http_unauthorized, - }); + }; const Set = std.StringHashMap(void); var set = Set.init(a); // set @@ -258,12 +261,12 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" { defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator); + const BearerAuthEndpoint = zap.Endpoint.Authenticating(Endpoint, Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); - try listener.register(auth_ep.endpoint()); + try listener.register(&auth_ep); - listener.listen() catch {}; + try listener.listen(); // std.debug.print("Waiting for the following:\n", .{}); // std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Bearer invalid\r", .{}); @@ -277,6 +280,7 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" { }); try std.testing.expectEqualStrings("UNAUTHORIZED", received_response); + std.debug.print("\nI made it\n", .{}); } test "BearerAuthMulti authenticateRequest OK" { @@ -297,11 +301,9 @@ test "BearerAuthMulti authenticateRequest OK" { defer listener.deinit(); // create mini endpoint - var ep = Endpoint.init(.{ + var ep: Endpoint = .{ .path = "/test", - .get = endpoint_http_get, - .unauthorized = endpoint_http_unauthorized, - }); + }; // create authenticator const Authenticator = Authenticators.BearerSingle; @@ -309,12 +311,12 @@ test "BearerAuthMulti authenticateRequest OK" { defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator); + const BearerAuthEndpoint = zap.Endpoint.Authenticating(Endpoint, Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); - try listener.register(auth_ep.endpoint()); + try listener.register(&auth_ep); - listener.listen() catch {}; + try listener.listen(); // std.debug.print("Waiting for the following:\n", .{}); // std.debug.print("./zig-out/bin/http_client_runner http://127.0.0.1:3000/test Bearer " ++ token ++ "\r", .{}); @@ -348,11 +350,9 @@ test "BearerAuthMulti authenticateRequest test-unauthorized" { defer listener.deinit(); // create mini endpoint - var ep = Endpoint.init(.{ + var ep: Endpoint = .{ .path = "/test", - .get = endpoint_http_get, - .unauthorized = endpoint_http_unauthorized, - }); + }; // create authenticator const Authenticator = Authenticators.BearerSingle; @@ -360,10 +360,10 @@ test "BearerAuthMulti authenticateRequest test-unauthorized" { defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator); + const BearerAuthEndpoint = zap.Endpoint.Authenticating(Endpoint, Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); - try listener.register(auth_ep.endpoint()); + try listener.register(&auth_ep); listener.listen() catch {}; // std.debug.print("Waiting for the following:\n", .{}); @@ -399,11 +399,9 @@ test "BasicAuth Token68 authenticateRequest" { defer listener.deinit(); // create mini endpoint - var ep = Endpoint.init(.{ + var ep: Endpoint = .{ .path = "/test", - .get = endpoint_http_get, - .unauthorized = endpoint_http_unauthorized, - }); + }; // create a set of Token68 entries const Set = std.StringHashMap(void); var set = Set.init(a); // set @@ -416,10 +414,10 @@ test "BasicAuth Token68 authenticateRequest" { defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator); + const BearerAuthEndpoint = zap.Endpoint.Authenticating(Endpoint, Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); - try listener.register(auth_ep.endpoint()); + try listener.register(&auth_ep); listener.listen() catch {}; // std.debug.print("Waiting for the following:\n", .{}); @@ -455,11 +453,9 @@ test "BasicAuth Token68 authenticateRequest test-unauthorized" { defer listener.deinit(); // create mini endpoint - var ep = Endpoint.init(.{ + var ep: Endpoint = .{ .path = "/test", - .get = endpoint_http_get, - .unauthorized = endpoint_http_unauthorized, - }); + }; // create a set of Token68 entries const Set = std.StringHashMap(void); var set = Set.init(a); // set @@ -472,10 +468,10 @@ test "BasicAuth Token68 authenticateRequest test-unauthorized" { defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator); + const BearerAuthEndpoint = zap.Endpoint.Authenticating(Endpoint, Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); - try listener.register(auth_ep.endpoint()); + try listener.register(&auth_ep); listener.listen() catch {}; // std.debug.print("Waiting for the following:\n", .{}); @@ -510,11 +506,9 @@ test "BasicAuth UserPass authenticateRequest" { defer listener.deinit(); // create mini endpoint - var ep = Endpoint.init(.{ + var ep: Endpoint = .{ .path = "/test", - .get = endpoint_http_get, - .unauthorized = endpoint_http_unauthorized, - }); + }; // create a set of User -> Pass entries const Map = std.StringHashMap([]const u8); @@ -538,10 +532,10 @@ test "BasicAuth UserPass authenticateRequest" { defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator); + const BearerAuthEndpoint = zap.Endpoint.Authenticating(Endpoint, Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); - try listener.register(auth_ep.endpoint()); + try listener.register(&auth_ep); listener.listen() catch {}; // std.debug.print("Waiting for the following:\n", .{}); @@ -576,11 +570,9 @@ test "BasicAuth UserPass authenticateRequest test-unauthorized" { defer listener.deinit(); // create mini endpoint - var ep = Endpoint.init(.{ + var ep: Endpoint = .{ .path = "/test", - .get = endpoint_http_get, - .unauthorized = endpoint_http_unauthorized, - }); + }; // create a set of User -> Pass entries const Map = std.StringHashMap([]const u8); @@ -605,10 +597,10 @@ test "BasicAuth UserPass authenticateRequest test-unauthorized" { defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator); + const BearerAuthEndpoint = zap.Endpoint.Authenticating(Endpoint, Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); - try listener.register(auth_ep.endpoint()); + try listener.register(&auth_ep); listener.listen() catch {}; // std.debug.print("Waiting for the following:\n", .{}); From ab42f971a0ef93d0352e589b873ad418c3fe8ee7 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 16 Mar 2025 16:17:42 +0100 Subject: [PATCH 24/57] remove usingnamespace --- examples/accept/accept.zig | 2 +- examples/endpoint/userweb.zig | 14 ++++----- examples/hello_json/hello_json.zig | 2 +- src/http_auth.zig | 7 +++-- src/request.zig | 2 +- src/tests/test_http_params.zig | 2 +- src/zap.zig | 47 ++---------------------------- 7 files changed, 17 insertions(+), 59 deletions(-) diff --git a/examples/accept/accept.zig b/examples/accept/accept.zig index 477e973..55c9b53 100644 --- a/examples/accept/accept.zig +++ b/examples/accept/accept.zig @@ -41,7 +41,7 @@ fn on_request_verbose(r: zap.Request) void { }, .JSON => { var buffer: [128]u8 = undefined; - const json = zap.stringifyBuf(&buffer, .{ .message = "Hello from ZAP!!!" }, .{}) orelse return; + const json = zap.util.stringifyBuf(&buffer, .{ .message = "Hello from ZAP!!!" }, .{}) orelse return; r.sendJson(json) catch return; }, .XHTML => { diff --git a/examples/endpoint/userweb.zig b/examples/endpoint/userweb.zig index f95504a..31cc587 100644 --- a/examples/endpoint/userweb.zig +++ b/examples/endpoint/userweb.zig @@ -52,7 +52,7 @@ pub fn get(self: *Self, r: zap.Request) void { var jsonbuf: [256]u8 = undefined; if (self.userIdFromPath(path)) |id| { if (self._users.get(id)) |user| { - if (zap.stringifyBuf(&jsonbuf, user, .{})) |json| { + if (zap.util.stringifyBuf(&jsonbuf, user, .{})) |json| { r.sendJson(json) catch return; } } @@ -76,7 +76,7 @@ pub fn post(self: *Self, r: zap.Request) void { defer u.deinit(); if (self._users.addByName(u.value.first_name, u.value.last_name)) |id| { var jsonbuf: [128]u8 = undefined; - if (zap.stringifyBuf(&jsonbuf, .{ .status = "OK", .id = id }, .{})) |json| { + if (zap.util.stringifyBuf(&jsonbuf, .{ .status = "OK", .id = id }, .{})) |json| { r.sendJson(json) catch return; } } else |err| { @@ -97,11 +97,11 @@ pub fn patch(self: *Self, r: zap.Request) void { defer u.deinit(); var jsonbuf: [128]u8 = undefined; if (self._users.update(id, u.value.first_name, u.value.last_name)) { - if (zap.stringifyBuf(&jsonbuf, .{ .status = "OK", .id = id }, .{})) |json| { + if (zap.util.stringifyBuf(&jsonbuf, .{ .status = "OK", .id = id }, .{})) |json| { r.sendJson(json) catch return; } } else { - if (zap.stringifyBuf(&jsonbuf, .{ .status = "ERROR", .id = id }, .{})) |json| { + if (zap.util.stringifyBuf(&jsonbuf, .{ .status = "ERROR", .id = id }, .{})) |json| { r.sendJson(json) catch return; } } @@ -117,11 +117,11 @@ pub fn delete(self: *Self, r: zap.Request) void { if (self.userIdFromPath(path)) |id| { var jsonbuf: [128]u8 = undefined; if (self._users.delete(id)) { - if (zap.stringifyBuf(&jsonbuf, .{ .status = "OK", .id = id }, .{})) |json| { + if (zap.util.stringifyBuf(&jsonbuf, .{ .status = "OK", .id = id }, .{})) |json| { r.sendJson(json) catch return; } } else { - if (zap.stringifyBuf(&jsonbuf, .{ .status = "ERROR", .id = id }, .{})) |json| { + if (zap.util.stringifyBuf(&jsonbuf, .{ .status = "ERROR", .id = id }, .{})) |json| { r.sendJson(json) catch return; } } @@ -132,6 +132,6 @@ pub fn delete(self: *Self, r: zap.Request) void { pub fn options(_: *Self, r: zap.Request) void { r.setHeader("Access-Control-Allow-Origin", "*") catch return; r.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") catch return; - r.setStatus(zap.StatusCode.no_content); + r.setStatus(zap.http.StatusCode.no_content); r.markAsFinished(true); } diff --git a/examples/hello_json/hello_json.zig b/examples/hello_json/hello_json.zig index 9f26381..56c5cd8 100644 --- a/examples/hello_json/hello_json.zig +++ b/examples/hello_json/hello_json.zig @@ -19,7 +19,7 @@ fn on_request(r: zap.Request) void { var buf: [100]u8 = undefined; var json_to_send: []const u8 = undefined; - if (zap.stringifyBuf(&buf, user, .{})) |json| { + if (zap.util.stringifyBuf(&buf, user, .{})) |json| { json_to_send = json; } else { json_to_send = "null"; diff --git a/src/http_auth.zig b/src/http_auth.zig index ca67d88..894b74e 100644 --- a/src/http_auth.zig +++ b/src/http_auth.zig @@ -61,8 +61,9 @@ pub const AuthResult = enum { /// The authenticator handled the request that didn't pass authentication / /// authorization. /// This is used to implement authenticators that redirect to a login - /// page. An Authenticating endpoint will not do the default, which is trying - /// to call the `unauthorized` callback if one exists orelse ignore the request. + /// page. An Authenticating endpoint will not do the default, which is + /// trying to call the `unauthorized` callback. `unauthorized()` must be + /// implemented in the endpoint. Handled, }; @@ -338,7 +339,7 @@ pub const UserPassSessionArgs = struct { /// cookie max age in seconds; 0 -> session cookie cookieMaxAge: u8 = 0, /// redirect status code, defaults to 302 found - redirectCode: zap.StatusCode = .found, + redirectCode: zap.http.StatusCode = .found, }; /// UserPassSession supports the following use case: diff --git a/src/request.zig b/src/request.zig index 07050ef..a0c2a7e 100644 --- a/src/request.zig +++ b/src/request.zig @@ -495,7 +495,7 @@ pub fn getHeaderCommon(self: *const Self, which: HttpHeaderCommon) ?[]const u8 { .upgrade => fio.HTTP_HEADER_UPGRADE, }; const fiobj = zap.fio.fiobj_hash_get(self.h.*.headers, field); - return zap.fio2str(fiobj); + return zap.util.fio2str(fiobj); } /// Set header. diff --git a/src/tests/test_http_params.zig b/src/tests/test_http_params.zig index 5e8ee8e..31f8b0d 100644 --- a/src/tests/test_http_params.zig +++ b/src/tests/test_http_params.zig @@ -26,7 +26,7 @@ test "http parameters" { var strParams: ?zap.Request.HttpParamStrKVList = null; var params: ?zap.Request.HttpParamKVList = null; - var paramOneStr: ?zap.FreeOrNot = null; + var paramOneStr: ?zap.util.FreeOrNot = null; var paramOneSlice: ?[]const u8 = null; var paramSlices: zap.Request.ParamSliceIterator = undefined; diff --git a/src/zap.zig b/src/zap.zig index 8a777ef..bce9853 100644 --- a/src/zap.zig +++ b/src/zap.zig @@ -9,52 +9,10 @@ pub const fio = @import("fio.zig"); /// Server-Side TLS function wrapper pub const Tls = @import("tls.zig"); -/// Endpoint and supporting types. -/// Create one and define all callbacks. Then, pass it to a HttpListener's -/// `register()` function to register with the listener. -/// -/// **NOTE**: Endpoints must define the following: -/// -/// ```zig -/// path: []const u8, -/// pub fn get(_: *Self, _: zap.Request) void {} -/// pub fn post(_: *Self, _: zap.Request) void {} -/// pub fn put(_: *Self, _: zap.Request) void {} -/// pub fn delete(_: *Self, _: zap.Request) void {} -/// pub fn patch(_: *Self, _: zap.Request) void {} -/// pub fn options(_: *Self, _: zap.Request) void {} -/// // optional, if auth stuff is used: -/// pub fn unauthorized(_: *Self, _: zap.Request) void {} -/// ``` -/// -/// Example: -/// A simple endpoint listening on the /stop route that shuts down zap. The -/// main thread usually continues at the instructions after the call to -/// zap.start(). -/// -/// ```zig -/// const StopEndpoint = struct { -/// -/// pub fn init( path: []const u8,) StopEndpoint { -/// return .{ -/// .path = path, -/// }; -/// } -/// -/// pub fn get(self: *StopEndpoint, r: zap.Request) void { -/// _ = self; -/// _ = r; -/// zap.stop(); -/// } -/// }; -/// ``` pub const Endpoint = @import("endpoint.zig"); pub const Router = @import("router.zig"); -pub usingnamespace @import("util.zig"); -pub usingnamespace @import("http.zig"); - /// A struct to handle Mustache templating. /// /// This is a wrapper around fiobj's mustache template handling. @@ -78,9 +36,8 @@ pub const Middleware = @import("middleware.zig"); pub const WebSockets = @import("websockets.zig"); pub const Log = @import("log.zig"); -const http = @import("http.zig"); - -const util = @import("util.zig"); +pub const http = @import("http.zig"); +pub const util = @import("util.zig"); // TODO: replace with comptime debug logger like in log.zig var _debug: bool = false; From a63e47ea3d99e5ae7149e60372130e6ca30407c5 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 16 Mar 2025 16:18:03 +0100 Subject: [PATCH 25/57] print username, pass in userpass_session example --- examples/userpass_session_auth/userpass_session_auth.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/userpass_session_auth/userpass_session_auth.zig b/examples/userpass_session_auth/userpass_session_auth.zig index 40f07cb..550bfbb 100644 --- a/examples/userpass_session_auth/userpass_session_auth.zig +++ b/examples/userpass_session_auth/userpass_session_auth.zig @@ -157,6 +157,8 @@ pub fn main() !void { defer authenticator.deinit(); std.debug.print("Visit me on http://127.0.0.1:3000\n", .{}); + std.debug.print(" Username: zap", .{}); + std.debug.print(" Password: awesome", .{}); // start worker threads zap.start(.{ From f77f4053205306b62f4ef2230e597d6b19f269c5 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 16 Mar 2025 16:28:23 +0100 Subject: [PATCH 26/57] doc update --- doc/authentication.md | 49 ++++++++++++++++++++------------------ src/endpoint.zig | 55 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 27 deletions(-) diff --git a/doc/authentication.md b/doc/authentication.md index 0df5483..019c3b2 100644 --- a/doc/authentication.md +++ b/doc/authentication.md @@ -9,11 +9,13 @@ authentication, see the [UserPassSession](../src/http_auth.zig#L319) and its For convenience, Authenticator types exist that can authenticate requests. -Zap also provides an `Endpoint.Authenticating` endpoint-wrapper. Have a look at the [example](../examples/endpoint_auth) and the [tests](../src/tests/test_auth.zig). +Zap also provides an `Endpoint.Authenticating` endpoint-wrapper. Have a look at +the [example](../examples/endpoint_auth) and the +[tests](../src/tests/test_auth.zig). The following describes the Authenticator types. All of them provide the -`authenticateRequest()` function, which takes a `zap.Request` and returns -a bool value whether it could be authenticated or not. +`authenticateRequest()` function, which takes a `zap.Request` and returns a bool +value whether it could be authenticated or not. Further down, we show how to use the Authenticators, and also the `Endpoint.Authenticating`. @@ -24,7 +26,7 @@ The `zap.Auth.Basic` Authenticator accepts 2 comptime values: - `Lookup`: either a map to look up passwords for users or a set to lookup base64 encoded tokens (user:pass -> base64-encode = token) -- `kind` : +- `kind` : - `UserPass` : decode the authentication header, split into user and password, then lookup the password in the provided map and compare it. - `Token68` : don't bother decoding, the 'lookup' set is filled with @@ -187,7 +189,8 @@ fn on_request(r: zap.Request) void { Here, we only show using one of the Authenticator types. See the tests for more examples. -The `Endpoint.Authenticating` honors `.unauthorized` in the endpoint settings, where you can pass in a callback to deal with unauthorized requests. If you leave it to `null`, the endpoint will automatically reply with a `401 - Unauthorized` response. +The `Endpoint.Authenticating` uses the `.unauthorized()` method of the endpoint +to deal with unauthorized requests. `unauthorized` must be implemented. The example below should make clear how to wrap an endpoint into an `Endpoint.Authenticating`: @@ -205,18 +208,20 @@ const HTTP_RESPONSE: []const u8 = \\ ; -// authenticated requests go here -fn endpoint_http_get(e: *zap.Endpoint, r: zap.Request) void { - _ = e; - r.sendBody(HTTP_RESPONSE) catch return; -} +pub const Endpoint = struct { + path: []const u8, -// just for fun, we also catch the unauthorized callback -fn endpoint_http_unauthorized(e: *zap.Endpoint, r: zap.Request) void { - _ = e; - r.setStatus(.unauthorized); - r.sendBody("UNAUTHORIZED ACCESS") catch return; -} + // authenticated requests go here + fn get(_: *Endpoint, r: zap.Request) void { + r.sendBody(HTTP_RESPONSE) catch return; + } + + // just for fun, we also catch the unauthorized callback + fn unauthorized(_: *Endpoint, r: zap.Request) void { + r.setStatus(.unauthorized); + r.sendBody("UNAUTHORIZED ACCESS") catch return; + } +}; pub fn main() !void { // setup listener @@ -233,11 +238,9 @@ pub fn main() !void { defer listener.deinit(); // create mini endpoint - var ep = zap.Endpoint.init(.{ + var ep : Endpoint = .{ .path = "/test", - .get = endpoint_http_get, - .unauthorized = endpoint_http_unauthorized, - }); + }; // create authenticator const Authenticator = zap.Auth.BearerSingle; @@ -245,15 +248,15 @@ pub fn main() !void { defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = zap.Endpoint.Authenticating(Authenticator); + const BearerAuthEndpoint = zap.Endpoint.Authenticating(Endpoint, Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); - try listener.register(auth_ep.endpoint()); + try listener.register(&auth_ep); listener.listen() catch {}; std.debug.print( \\ Run the following: - \\ + \\ \\ curl http://localhost:3000/test -i -H "Authorization: Bearer ABCDEFG" -v \\ curl http://localhost:3000/test -i -H "Authorization: Bearer invalid" -v \\ diff --git a/src/endpoint.zig b/src/endpoint.zig index 236e33a..11515b5 100644 --- a/src/endpoint.zig +++ b/src/endpoint.zig @@ -1,9 +1,57 @@ +//! Endpoint and supporting types. +//! Create one and define all callbacks. Then, pass it to a HttpListener's +//! `register()` function to register with the listener. +//! +//! **NOTE**: Endpoints must implement the following "interface": +//! +//! ```zig +//! /// The http request path / slug of the endpoint +//! path: []const u8, +//! +//! /// Handlers by request method: +//! pub fn get(_: *Self, _: zap.Request) void {} +//! pub fn post(_: *Self, _: zap.Request) void {} +//! pub fn put(_: *Self, _: zap.Request) void {} +//! pub fn delete(_: *Self, _: zap.Request) void {} +//! pub fn patch(_: *Self, _: zap.Request) void {} +//! pub fn options(_: *Self, _: zap.Request) void {} +//! +//! // optional, if auth stuff is used: +//! pub fn unauthorized(_: *Self, _: zap.Request) void {} +//! ``` +//! +//! Example: +//! A simple endpoint listening on the /stop route that shuts down zap. The +//! main thread usually continues at the instructions after the call to +//! zap.start(). +//! +//! ```zig +//! const StopEndpoint = struct { +//! +//! pub fn init( path: []const u8,) StopEndpoint { +//! return .{ +//! .path = path, +//! }; +//! } +//! +//! pub fn post(_: *Self, _: zap.Request) void {} +//! pub fn put(_: *Self, _: zap.Request) void {} +//! pub fn delete(_: *Self, _: zap.Request) void {} +//! pub fn patch(_: *Self, _: zap.Request) void {} +//! pub fn options(_: *Self, _: zap.Request) void {} +//! +//! pub fn get(self: *StopEndpoint, r: zap.Request) void { +//! _ = self; +//! _ = r; +//! zap.stop(); +//! } +//! }; +//! ``` + const std = @import("std"); const zap = @import("zap.zig"); const auth = @import("http_auth.zig"); -const Endpoint = @This(); - // zap types const Request = zap.Request; const ListenerSettings = zap.HttpListenerSettings; @@ -93,8 +141,7 @@ const EndpointWrapper = struct { } }; -/// Wrap an endpoint with an Authenticator -> new Endpoint of type Endpoint -/// is available via the `endpoint()` function. +/// Wrap an endpoint with an Authenticator pub fn Authenticating(EndpointType: type, Authenticator: type) type { return struct { authenticator: *Authenticator, From 7503f090eebb15ff8bcc6f142e826481e178a464 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 16 Mar 2025 16:42:08 +0100 Subject: [PATCH 27/57] doc update --- src/endpoint.zig | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/endpoint.zig b/src/endpoint.zig index 11515b5..d9b1b6e 100644 --- a/src/endpoint.zig +++ b/src/endpoint.zig @@ -1,6 +1,9 @@ //! Endpoint and supporting types. -//! Create one and define all callbacks. Then, pass it to a HttpListener's -//! `register()` function to register with the listener. +//! +//! An Endpoint can be any zig struct that defines all the callbacks lilsted +//! below. +//! Pass an instance of an Endpoint struct to zap.Endpoint.Listener.register() +//! function to register with the listener. //! //! **NOTE**: Endpoints must implement the following "interface": //! From fcce4517def8245d809d495cc9150a0abcb57da1 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 16 Mar 2025 20:16:14 +0100 Subject: [PATCH 28/57] introduced error union to request fn return type --- examples/accept/accept.zig | 20 +-- examples/bindataformpost/bindataformpost.zig | 6 +- examples/cookies/cookies.zig | 2 +- examples/endpoint/main.zig | 4 +- examples/endpoint/stopendpoint.zig | 13 +- examples/endpoint/userweb.zig | 53 ++++---- examples/endpoint_auth/endpoint_auth.zig | 15 +- examples/hello/hello.zig | 8 +- examples/hello2/hello2.zig | 8 +- examples/hello_json/hello_json.zig | 16 +-- examples/http_params/http_params.zig | 6 +- examples/https/https.zig | 8 +- examples/middleware/middleware.zig | 6 +- .../middleware_with_endpoint.zig | 20 +-- examples/mustache/mustache.zig | 2 +- examples/routes/routes.zig | 20 +-- examples/senderror/senderror.zig | 2 +- examples/sendfile/sendfile.zig | 2 +- examples/serve/serve.zig | 2 +- examples/simple_router/simple_router.zig | 12 +- .../userpass_session_auth.zig | 22 +-- examples/websockets/websockets.zig | 6 +- src/endpoint.zig | 128 +++++++++++------- src/middleware.zig | 24 ++-- src/router.zig | 14 +- src/tests/test_auth.zig | 15 +- src/tests/test_http_params.zig | 2 +- src/tests/test_sendfile.zig | 2 +- src/util.zig | 6 +- src/zap.zig | 13 +- tools/docserver.zig | 2 +- wrk/zig/main.zig | 4 +- 32 files changed, 246 insertions(+), 217 deletions(-) diff --git a/examples/accept/accept.zig b/examples/accept/accept.zig index 55c9b53..a9e268c 100644 --- a/examples/accept/accept.zig +++ b/examples/accept/accept.zig @@ -5,7 +5,7 @@ var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true, }){}; -fn on_request_verbose(r: zap.Request) void { +fn on_request_verbose(r: zap.Request) !void { // use a local buffer for the parsed accept headers var accept_buffer: [1024]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&accept_buffer); @@ -21,38 +21,38 @@ fn on_request_verbose(r: zap.Request) void { break :content_type .HTML; }; - r.setContentType(content_type) catch return; + try r.setContentType(content_type); switch (content_type) { .TEXT => { - r.sendBody("Hello from ZAP!!!") catch return; + try r.sendBody("Hello from ZAP!!!"); }, .HTML => { - r.sendBody("

Hello from ZAP!!!

") catch return; + try r.sendBody("

Hello from ZAP!!!

"); }, .XML => { - r.sendBody( + try r.sendBody( \\ \\ \\ \\ Hello from ZAP!!! \\ \\ - ) catch return; + ); }, .JSON => { var buffer: [128]u8 = undefined; - const json = zap.util.stringifyBuf(&buffer, .{ .message = "Hello from ZAP!!!" }, .{}) orelse return; - r.sendJson(json) catch return; + const json = try zap.util.stringifyBuf(&buffer, .{ .message = "Hello from ZAP!!!" }, .{}); + try r.sendJson(json); }, .XHTML => { - r.sendBody( + try r.sendBody( \\ \\ \\ \\

Hello from ZAP!!!

\\ \\ - ) catch return; + ); }, } } diff --git a/examples/bindataformpost/bindataformpost.zig b/examples/bindataformpost/bindataformpost.zig index 4c2c7ad..9cc0eda 100644 --- a/examples/bindataformpost/bindataformpost.zig +++ b/examples/bindataformpost/bindataformpost.zig @@ -4,7 +4,7 @@ const zap = @import("zap"); const Handler = struct { var alloc: std.mem.Allocator = undefined; - pub fn on_request(r: zap.Request) void { + pub fn on_request(r: zap.Request) !void { // parse for FORM (body) parameters first r.parseBody() catch |err| { std.log.err("Parse Body error: {any}. Expected if body is empty", .{err}); @@ -24,7 +24,7 @@ const Handler = struct { // // HERE WE HANDLE THE BINARY FILE // - const params = r.parametersToOwnedList(Handler.alloc, false) catch unreachable; + const params = try r.parametersToOwnedList(Handler.alloc, false); defer params.deinit(); for (params.items) |kv| { if (kv.value) |v| { @@ -82,7 +82,7 @@ const Handler = struct { } else |err| { std.log.err("cannot check for terminate param: {any}\n", .{err}); } - r.sendJson("{ \"ok\": true }") catch unreachable; + try r.sendJson("{ \"ok\": true }"); } }; diff --git a/examples/cookies/cookies.zig b/examples/cookies/cookies.zig index 087731f..16fc9a8 100644 --- a/examples/cookies/cookies.zig +++ b/examples/cookies/cookies.zig @@ -35,7 +35,7 @@ pub fn main() !void { const Handler = struct { var alloc: std.mem.Allocator = undefined; - pub fn on_request(r: zap.Request) void { + pub fn on_request(r: zap.Request) !void { std.debug.print("\n=====================================================\n", .{}); defer std.debug.print("=====================================================\n\n", .{}); diff --git a/examples/endpoint/main.zig b/examples/endpoint/main.zig index d6b519b..a38803a 100644 --- a/examples/endpoint/main.zig +++ b/examples/endpoint/main.zig @@ -4,12 +4,12 @@ const UserWeb = @import("userweb.zig"); const StopEndpoint = @import("stopendpoint.zig"); // this is just to demo that we can catch arbitrary slugs as fallback -fn on_request(r: zap.Request) void { +fn on_request(r: zap.Request) !void { if (r.path) |the_path| { std.debug.print("REQUESTED PATH: {s}\n", .{the_path}); } - r.sendBody("

Hello from ZAP!!!

") catch return; + try r.sendBody("

Hello from ZAP!!!

"); } pub fn main() !void { diff --git a/examples/endpoint/stopendpoint.zig b/examples/endpoint/stopendpoint.zig index b9a7c46..d0e0a12 100644 --- a/examples/endpoint/stopendpoint.zig +++ b/examples/endpoint/stopendpoint.zig @@ -6,6 +6,7 @@ const zap = @import("zap"); pub const Self = @This(); path: []const u8, +error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response, pub fn init(path: []const u8) Self { return .{ @@ -13,14 +14,14 @@ pub fn init(path: []const u8) Self { }; } -pub fn get(e: *Self, r: zap.Request) void { +pub fn get(e: *Self, r: zap.Request) anyerror!void { _ = e; _ = r; zap.stop(); } -pub fn post(_: *Self, _: zap.Request) void {} -pub fn put(_: *Self, _: zap.Request) void {} -pub fn delete(_: *Self, _: zap.Request) void {} -pub fn patch(_: *Self, _: zap.Request) void {} -pub fn options(_: *Self, _: zap.Request) void {} +pub fn post(_: *Self, _: zap.Request) anyerror!void {} +pub fn put(_: *Self, _: zap.Request) anyerror!void {} +pub fn delete(_: *Self, _: zap.Request) anyerror!void {} +pub fn patch(_: *Self, _: zap.Request) anyerror!void {} +pub fn options(_: *Self, _: zap.Request) anyerror!void {} diff --git a/examples/endpoint/userweb.zig b/examples/endpoint/userweb.zig index 31cc587..e492408 100644 --- a/examples/endpoint/userweb.zig +++ b/examples/endpoint/userweb.zig @@ -11,6 +11,7 @@ alloc: std.mem.Allocator = undefined, _users: Users = undefined, path: []const u8, +error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response, pub fn init( a: std.mem.Allocator, @@ -42,8 +43,8 @@ fn userIdFromPath(self: *Self, path: []const u8) ?usize { return null; } -pub fn put(_: *Self, _: zap.Request) void {} -pub fn get(self: *Self, r: zap.Request) void { +pub fn put(_: *Self, _: zap.Request) anyerror!void {} +pub fn get(self: *Self, r: zap.Request) anyerror!void { if (r.path) |path| { // /users if (path.len == self.path.len) { @@ -52,33 +53,31 @@ pub fn get(self: *Self, r: zap.Request) void { var jsonbuf: [256]u8 = undefined; if (self.userIdFromPath(path)) |id| { if (self._users.get(id)) |user| { - if (zap.util.stringifyBuf(&jsonbuf, user, .{})) |json| { - r.sendJson(json) catch return; - } + const json = try zap.util.stringifyBuf(&jsonbuf, user, .{}); + try r.sendJson(json); } } } } -fn listUsers(self: *Self, r: zap.Request) void { +fn listUsers(self: *Self, r: zap.Request) !void { if (self._users.toJSON()) |json| { defer self.alloc.free(json); - r.sendJson(json) catch return; + try r.sendJson(json); } else |err| { - std.debug.print("LIST error: {}\n", .{err}); + return err; } } -pub fn post(self: *Self, r: zap.Request) void { +pub fn post(self: *Self, r: zap.Request) anyerror!void { if (r.body) |body| { const maybe_user: ?std.json.Parsed(User) = std.json.parseFromSlice(User, self.alloc, body, .{}) catch null; if (maybe_user) |u| { defer u.deinit(); if (self._users.addByName(u.value.first_name, u.value.last_name)) |id| { var jsonbuf: [128]u8 = undefined; - if (zap.util.stringifyBuf(&jsonbuf, .{ .status = "OK", .id = id }, .{})) |json| { - r.sendJson(json) catch return; - } + const json = try zap.util.stringifyBuf(&jsonbuf, .{ .status = "OK", .id = id }, .{}); + try r.sendJson(json); } else |err| { std.debug.print("ADDING error: {}\n", .{err}); return; @@ -87,7 +86,7 @@ pub fn post(self: *Self, r: zap.Request) void { } } -pub fn patch(self: *Self, r: zap.Request) void { +pub fn patch(self: *Self, r: zap.Request) anyerror!void { if (r.path) |path| { if (self.userIdFromPath(path)) |id| { if (self._users.get(id)) |_| { @@ -97,13 +96,11 @@ pub fn patch(self: *Self, r: zap.Request) void { defer u.deinit(); var jsonbuf: [128]u8 = undefined; if (self._users.update(id, u.value.first_name, u.value.last_name)) { - if (zap.util.stringifyBuf(&jsonbuf, .{ .status = "OK", .id = id }, .{})) |json| { - r.sendJson(json) catch return; - } + const json = try zap.util.stringifyBuf(&jsonbuf, .{ .status = "OK", .id = id }, .{}); + try r.sendJson(json); } else { - if (zap.util.stringifyBuf(&jsonbuf, .{ .status = "ERROR", .id = id }, .{})) |json| { - r.sendJson(json) catch return; - } + const json = try zap.util.stringifyBuf(&jsonbuf, .{ .status = "ERROR", .id = id }, .{}); + try r.sendJson(json); } } } @@ -112,26 +109,24 @@ pub fn patch(self: *Self, r: zap.Request) void { } } -pub fn delete(self: *Self, r: zap.Request) void { +pub fn delete(self: *Self, r: zap.Request) anyerror!void { if (r.path) |path| { if (self.userIdFromPath(path)) |id| { var jsonbuf: [128]u8 = undefined; if (self._users.delete(id)) { - if (zap.util.stringifyBuf(&jsonbuf, .{ .status = "OK", .id = id }, .{})) |json| { - r.sendJson(json) catch return; - } + const json = try zap.util.stringifyBuf(&jsonbuf, .{ .status = "OK", .id = id }, .{}); + try r.sendJson(json); } else { - if (zap.util.stringifyBuf(&jsonbuf, .{ .status = "ERROR", .id = id }, .{})) |json| { - r.sendJson(json) catch return; - } + const json = try zap.util.stringifyBuf(&jsonbuf, .{ .status = "ERROR", .id = id }, .{}); + try r.sendJson(json); } } } } -pub fn options(_: *Self, r: zap.Request) void { - r.setHeader("Access-Control-Allow-Origin", "*") catch return; - r.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") catch return; +pub fn options(_: *Self, r: zap.Request) anyerror!void { + try r.setHeader("Access-Control-Allow-Origin", "*"); + try r.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); r.setStatus(zap.http.StatusCode.no_content); r.markAsFinished(true); } diff --git a/examples/endpoint_auth/endpoint_auth.zig b/examples/endpoint_auth/endpoint_auth.zig index f6f1e8a..57b2a37 100644 --- a/examples/endpoint_auth/endpoint_auth.zig +++ b/examples/endpoint_auth/endpoint_auth.zig @@ -13,24 +13,25 @@ const HTTP_RESPONSE: []const u8 = const Endpoint = struct { // the slug path: []const u8, + error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response, // authenticated requests go here - pub fn get(_: *Endpoint, r: zap.Request) void { + pub fn get(_: *Endpoint, r: zap.Request) !void { r.sendBody(HTTP_RESPONSE) catch return; } // just for fun, we also catch the unauthorized callback - pub fn unauthorized(_: *Endpoint, r: zap.Request) void { + pub fn unauthorized(_: *Endpoint, r: zap.Request) !void { r.setStatus(.unauthorized); r.sendBody("UNAUTHORIZED ACCESS") catch return; } // not implemented, don't care - pub fn post(_: *Endpoint, _: zap.Request) void {} - pub fn put(_: *Endpoint, _: zap.Request) void {} - pub fn delete(_: *Endpoint, _: zap.Request) void {} - pub fn patch(_: *Endpoint, _: zap.Request) void {} - pub fn options(_: *Endpoint, _: zap.Request) void {} + pub fn post(_: *Endpoint, _: zap.Request) !void {} + pub fn put(_: *Endpoint, _: zap.Request) !void {} + pub fn delete(_: *Endpoint, _: zap.Request) !void {} + pub fn patch(_: *Endpoint, _: zap.Request) !void {} + pub fn options(_: *Endpoint, _: zap.Request) !void {} }; pub fn main() !void { diff --git a/examples/hello/hello.zig b/examples/hello/hello.zig index 8b0f760..c7db536 100644 --- a/examples/hello/hello.zig +++ b/examples/hello/hello.zig @@ -1,7 +1,7 @@ const std = @import("std"); const zap = @import("zap"); -fn on_request_verbose(r: zap.Request) void { +fn on_request_verbose(r: zap.Request) !void { if (r.path) |the_path| { std.debug.print("PATH: {s}\n", .{the_path}); } @@ -9,11 +9,11 @@ fn on_request_verbose(r: zap.Request) void { if (r.query) |the_query| { std.debug.print("QUERY: {s}\n", .{the_query}); } - r.sendBody("

Hello from ZAP!!!

") catch return; + try r.sendBody("

Hello from ZAP!!!

"); } -fn on_request_minimal(r: zap.Request) void { - r.sendBody("

Hello from ZAP!!!

") catch return; +fn on_request_minimal(r: zap.Request) !void { + try r.sendBody("

Hello from ZAP!!!

"); } pub fn main() !void { diff --git a/examples/hello2/hello2.zig b/examples/hello2/hello2.zig index 4f43a5e..51e8faa 100644 --- a/examples/hello2/hello2.zig +++ b/examples/hello2/hello2.zig @@ -1,7 +1,7 @@ const std = @import("std"); const zap = @import("zap"); -fn on_request(r: zap.Request) void { +fn on_request(r: zap.Request) !void { const m = r.methodAsEnum(); const m_str = r.method orelse ""; const p = r.path orelse "/"; @@ -20,8 +20,8 @@ fn on_request(r: zap.Request) void { std.debug.print(">> BODY: {s}\n", .{the_body}); } - r.setContentTypeFromPath() catch return; - r.sendBody( + try r.setContentTypeFromPath(); + try r.sendBody( \\ \\

Hello from ZAP!!!

\\
@@ -32,7 +32,7 @@ fn on_request(r: zap.Request) void { \\ \\
\\ - ) catch return; + ); } pub fn main() !void { diff --git a/examples/hello_json/hello_json.zig b/examples/hello_json/hello_json.zig index 56c5cd8..21f9f46 100644 --- a/examples/hello_json/hello_json.zig +++ b/examples/hello_json/hello_json.zig @@ -6,7 +6,7 @@ const User = struct { last_name: ?[]const u8 = null, }; -fn on_request(r: zap.Request) void { +fn on_request(r: zap.Request) !void { if (r.methodAsEnum() != .GET) return; // /user/n @@ -14,16 +14,16 @@ fn on_request(r: zap.Request) void { if (the_path.len < 7 or !std.mem.startsWith(u8, the_path, "/user/")) return; - const user_id: usize = @as(usize, @intCast(the_path[6] - 0x30)); + const user_id: usize = @intCast(the_path[6] - 0x30); + std.debug.print("user_id: {d}\n", .{user_id}); + std.debug.print("users: {d}\n", .{users.count()}); const user = users.get(user_id); + std.debug.print("user: {?}\n", .{user}); - var buf: [100]u8 = undefined; + var buf: [256]u8 = undefined; var json_to_send: []const u8 = undefined; - if (zap.util.stringifyBuf(&buf, user, .{})) |json| { - json_to_send = json; - } else { - json_to_send = "null"; - } + json_to_send = try zap.util.stringifyBuf(&buf, user, .{}); + std.debug.print("<< json: {s}\n", .{json_to_send}); r.setContentType(.JSON) catch return; r.setContentTypeFromFilename("test.json") catch return; diff --git a/examples/http_params/http_params.zig b/examples/http_params/http_params.zig index e387e9b..66976f6 100644 --- a/examples/http_params/http_params.zig +++ b/examples/http_params/http_params.zig @@ -32,7 +32,7 @@ pub fn main() !void { const Handler = struct { var alloc: std.mem.Allocator = undefined; - pub fn on_request(r: zap.Request) void { + pub fn on_request(r: zap.Request) !void { std.debug.print("\n=====================================================\n", .{}); defer std.debug.print("=====================================================\n\n", .{}); @@ -69,7 +69,7 @@ pub fn main() !void { // ================================================================ // iterate over all params as strings - var strparams = r.parametersToOwnedStrList(alloc, false) catch unreachable; + var strparams = try r.parametersToOwnedStrList(alloc, false); defer strparams.deinit(); std.debug.print("\n", .{}); for (strparams.items) |kv| { @@ -79,7 +79,7 @@ pub fn main() !void { std.debug.print("\n", .{}); // iterate over all params - const params = r.parametersToOwnedList(alloc, false) catch unreachable; + const params = try r.parametersToOwnedList(alloc, false); defer params.deinit(); for (params.items) |kv| { std.log.info("Param `{s}` is {any}", .{ kv.key.str, kv.value }); diff --git a/examples/https/https.zig b/examples/https/https.zig index 2806179..bf9331b 100644 --- a/examples/https/https.zig +++ b/examples/https/https.zig @@ -1,7 +1,7 @@ const std = @import("std"); const zap = @import("zap"); -fn on_request_verbose(r: zap.Request) void { +fn on_request_verbose(r: zap.Request) !void { if (r.path) |the_path| { std.debug.print("PATH: {s}\n", .{the_path}); } @@ -9,11 +9,11 @@ fn on_request_verbose(r: zap.Request) void { if (r.query) |the_query| { std.debug.print("QUERY: {s}\n", .{the_query}); } - r.sendBody("

Hello from ZAP!!!

") catch return; + try r.sendBody("

Hello from ZAP!!!

"); } -fn on_request_minimal(r: zap.Request) void { - r.sendBody("

Hello from ZAP!!!

") catch return; +fn on_request_minimal(r: zap.Request) !void { + try r.sendBody("

Hello from ZAP!!!

"); } fn help_and_exit(filename: []const u8, err: anyerror) void { diff --git a/examples/middleware/middleware.zig b/examples/middleware/middleware.zig index 776a930..a02895f 100644 --- a/examples/middleware/middleware.zig +++ b/examples/middleware/middleware.zig @@ -69,7 +69,7 @@ const UserMiddleWare = struct { } // note that the first parameter is of type *Handler, not *Self !!! - pub fn onRequest(handler: *Handler, r: zap.Request, context: *Context) bool { + pub fn onRequest(handler: *Handler, r: zap.Request, context: *Context) !bool { // this is how we would get our self pointer const self: *Self = @fieldParentPtr("handler", handler); @@ -113,7 +113,7 @@ const SessionMiddleWare = struct { } // note that the first parameter is of type *Handler, not *Self !!! - pub fn onRequest(handler: *Handler, r: zap.Request, context: *Context) bool { + pub fn onRequest(handler: *Handler, r: zap.Request, context: *Context) !bool { // this is how we would get our self pointer const self: *Self = @fieldParentPtr("handler", handler); _ = self; @@ -148,7 +148,7 @@ const HtmlMiddleWare = struct { } // note that the first parameter is of type *Handler, not *Self !!! - pub fn onRequest(handler: *Handler, r: zap.Request, context: *Context) bool { + pub fn onRequest(handler: *Handler, r: zap.Request, context: *Context) !bool { // this is how we would get our self pointer const self: *Self = @fieldParentPtr("handler", handler); diff --git a/examples/middleware_with_endpoint/middleware_with_endpoint.zig b/examples/middleware_with_endpoint/middleware_with_endpoint.zig index 3bf9551..4d5f8b4 100644 --- a/examples/middleware_with_endpoint/middleware_with_endpoint.zig +++ b/examples/middleware_with_endpoint/middleware_with_endpoint.zig @@ -57,7 +57,7 @@ const UserMiddleWare = struct { } // note that the first parameter is of type *Handler, not *Self !!! - pub fn onRequest(handler: *Handler, r: zap.Request, context: *Context) bool { + pub fn onRequest(handler: *Handler, r: zap.Request, context: *Context) !bool { // this is how we would get our self pointer const self: *Self = @fieldParentPtr("handler", handler); @@ -103,7 +103,7 @@ const SessionMiddleWare = struct { } // note that the first parameter is of type *Handler, not *Self !!! - pub fn onRequest(handler: *Handler, r: zap.Request, context: *Context) bool { + pub fn onRequest(handler: *Handler, r: zap.Request, context: *Context) !bool { // this is how we would get our self pointer const self: *Self = @fieldParentPtr("handler", handler); _ = self; @@ -146,13 +146,13 @@ const HtmlEndpoint = struct { }; } - pub fn post(_: *HtmlEndpoint, _: zap.Request) void {} - pub fn put(_: *HtmlEndpoint, _: zap.Request) void {} - pub fn delete(_: *HtmlEndpoint, _: zap.Request) void {} - pub fn patch(_: *HtmlEndpoint, _: zap.Request) void {} - pub fn options(_: *HtmlEndpoint, _: zap.Request) void {} + pub fn post(_: *HtmlEndpoint, _: zap.Request) !void {} + pub fn put(_: *HtmlEndpoint, _: zap.Request) !void {} + pub fn delete(_: *HtmlEndpoint, _: zap.Request) !void {} + pub fn patch(_: *HtmlEndpoint, _: zap.Request) !void {} + pub fn options(_: *HtmlEndpoint, _: zap.Request) !void {} - pub fn get(_: *Self, r: zap.Request) void { + pub fn get(_: *Self, r: zap.Request) !void { var buf: [1024]u8 = undefined; var userFound: bool = false; var sessionFound: bool = false; @@ -169,12 +169,12 @@ const HtmlEndpoint = struct { sessionFound = true; std.debug.assert(r.isFinished() == false); - const message = std.fmt.bufPrint(&buf, "User: {s} / {s}, Session: {s} / {s}", .{ + const message = try std.fmt.bufPrint(&buf, "User: {s} / {s}, Session: {s} / {s}", .{ user.name, user.email, session.info, session.token, - }) catch unreachable; + }); r.setContentType(.TEXT) catch unreachable; r.sendBody(message) catch unreachable; std.debug.assert(r.isFinished() == true); diff --git a/examples/mustache/mustache.zig b/examples/mustache/mustache.zig index 26dd6d9..37ea7db 100644 --- a/examples/mustache/mustache.zig +++ b/examples/mustache/mustache.zig @@ -2,7 +2,7 @@ const std = @import("std"); const zap = @import("zap"); const Mustache = @import("zap").Mustache; -fn on_request(r: zap.Request) void { +fn on_request(r: zap.Request) !void { const template = \\ {{=<< >>=}} \\ * Users: diff --git a/examples/routes/routes.zig b/examples/routes/routes.zig index 43222d1..d605f3e 100644 --- a/examples/routes/routes.zig +++ b/examples/routes/routes.zig @@ -3,39 +3,39 @@ const zap = @import("zap"); // NOTE: this is a super simplified example, just using a hashmap to map // from HTTP path to request function. -fn dispatch_routes(r: zap.Request) void { +fn dispatch_routes(r: zap.Request) !void { // dispatch if (r.path) |the_path| { if (routes.get(the_path)) |foo| { - foo(r); + try foo(r); return; } } // or default: present menu - r.sendBody( + try r.sendBody( \\ \\ \\

static

\\

dynamic

\\ \\ - ) catch return; + ); } -fn static_site(r: zap.Request) void { - r.sendBody("

Hello from STATIC ZAP!

") catch return; +fn static_site(r: zap.Request) !void { + try r.sendBody("

Hello from STATIC ZAP!

"); } var dynamic_counter: i32 = 0; -fn dynamic_site(r: zap.Request) void { +fn dynamic_site(r: zap.Request) !void { dynamic_counter += 1; var buf: [128]u8 = undefined; - const filled_buf = std.fmt.bufPrintZ( + const filled_buf = try std.fmt.bufPrintZ( &buf, "

Hello # {d} from DYNAMIC ZAP!!!

", .{dynamic_counter}, - ) catch "ERROR"; - r.sendBody(filled_buf) catch return; + ); + try r.sendBody(filled_buf); } fn setup_routes(a: std.mem.Allocator) !void { diff --git a/examples/senderror/senderror.zig b/examples/senderror/senderror.zig index a9756ce..3fb0c81 100644 --- a/examples/senderror/senderror.zig +++ b/examples/senderror/senderror.zig @@ -5,7 +5,7 @@ fn MAKE_MEGA_ERROR() !void { return error.MEGA_ERROR; } -fn MY_REQUEST_HANDLER(r: zap.Request) void { +fn MY_REQUEST_HANDLER(r: zap.Request) !void { MAKE_MEGA_ERROR() catch |err| { r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505); }; diff --git a/examples/sendfile/sendfile.zig b/examples/sendfile/sendfile.zig index 4c97309..a4ac59e 100644 --- a/examples/sendfile/sendfile.zig +++ b/examples/sendfile/sendfile.zig @@ -6,7 +6,7 @@ var read_len: ?usize = null; const testfile = @embedFile("testfile.txt"); -pub fn on_request(r: zap.Request) void { +pub fn on_request(r: zap.Request) !void { // Sends a file if present in the filesystem orelse returns an error. // // - efficiently sends a file using gzip compression diff --git a/examples/serve/serve.zig b/examples/serve/serve.zig index d9f9d65..789257e 100644 --- a/examples/serve/serve.zig +++ b/examples/serve/serve.zig @@ -1,7 +1,7 @@ const std = @import("std"); const zap = @import("zap"); -fn on_request(r: zap.Request) void { +fn on_request(r: zap.Request) !void { r.setStatus(.not_found); r.sendBody("

404 - File not found

") catch return; } diff --git a/examples/simple_router/simple_router.zig b/examples/simple_router/simple_router.zig index eb58839..cacbcc3 100644 --- a/examples/simple_router/simple_router.zig +++ b/examples/simple_router/simple_router.zig @@ -2,7 +2,7 @@ const std = @import("std"); const zap = @import("zap"); const Allocator = std.mem.Allocator; -fn on_request_verbose(r: zap.Request) void { +fn on_request_verbose(r: zap.Request) !void { if (r.path) |the_path| { std.debug.print("PATH: {s}\n", .{the_path}); } @@ -28,7 +28,7 @@ pub const SomePackage = struct { }; } - pub fn getA(self: *Self, req: zap.Request) void { + pub fn getA(self: *Self, req: zap.Request) !void { std.log.warn("get_a_requested", .{}); const string = std.fmt.allocPrint( @@ -41,7 +41,7 @@ pub const SomePackage = struct { req.sendBody(string) catch return; } - pub fn getB(self: *Self, req: zap.Request) void { + pub fn getB(self: *Self, req: zap.Request) !void { std.log.warn("get_b_requested", .{}); const string = std.fmt.allocPrint( @@ -54,7 +54,7 @@ pub const SomePackage = struct { req.sendBody(string) catch return; } - pub fn incrementA(self: *Self, req: zap.Request) void { + pub fn incrementA(self: *Self, req: zap.Request) !void { std.log.warn("increment_a_requested", .{}); self.a += 1; @@ -63,10 +63,10 @@ pub const SomePackage = struct { } }; -fn not_found(req: zap.Request) void { +fn not_found(req: zap.Request) !void { std.debug.print("not found handler", .{}); - req.sendBody("Not found") catch return; + try req.sendBody("Not found"); } pub fn main() !void { diff --git a/examples/userpass_session_auth/userpass_session_auth.zig b/examples/userpass_session_auth/userpass_session_auth.zig index 550bfbb..c421ddc 100644 --- a/examples/userpass_session_auth/userpass_session_auth.zig +++ b/examples/userpass_session_auth/userpass_session_auth.zig @@ -25,38 +25,38 @@ const img = @embedFile("./html/Ziggy_the_Ziguana.svg.png"); var authenticator: Authenticator = undefined; // the login page (embedded) -fn on_login(r: zap.Request) void { - r.sendBody(loginpage) catch return; +fn on_login(r: zap.Request) !void { + try r.sendBody(loginpage); } // the "normal page" -fn on_normal_page(r: zap.Request) void { +fn on_normal_page(r: zap.Request) !void { zap.debug("on_normal_page()\n", .{}); - r.sendBody( + try r.sendBody( \\ \\

Hello from ZAP!!!

\\

You are logged in!!! \\

logout
\\ - ) catch return; + ); } // the logged-out page -fn on_logout(r: zap.Request) void { +fn on_logout(r: zap.Request) !void { zap.debug("on_logout()\n", .{}); authenticator.logout(&r); // note, the link below doesn't matter as the authenticator will send us // straight to the /login page - r.sendBody( + try r.sendBody( \\ \\

You are logged out!!!

\\
\\

Log back in

\\ - ) catch return; + ); } -fn on_request(r: zap.Request) void { +fn on_request(r: zap.Request) !void { switch (authenticator.authenticateRequest(&r)) { .Handled => { // the authenticator handled the entire request for us. @@ -80,8 +80,8 @@ fn on_request(r: zap.Request) void { // the authenticator. Hence, we name the img for the /login // page: /login/Ziggy....png if (std.mem.startsWith(u8, p, "/login/Ziggy_the_Ziguana.svg.png")) { - r.setContentTypeFromPath() catch unreachable; - r.sendBody(img) catch unreachable; + try r.setContentTypeFromPath(); + try r.sendBody(img); return; } diff --git a/examples/websockets/websockets.zig b/examples/websockets/websockets.zig index e347242..d0e3cee 100644 --- a/examples/websockets/websockets.zig +++ b/examples/websockets/websockets.zig @@ -163,13 +163,13 @@ fn handle_websocket_message( // // HTTP stuff // -fn on_request(r: zap.Request) void { +fn on_request(r: zap.Request) !void { r.setHeader("Server", "zap.example") catch unreachable; - r.sendBody( + try r.sendBody( \\ \\

This is a simple Websocket chatroom example

\\ - ) catch return; + ); } fn on_upgrade(r: zap.Request, target_protocol: []const u8) void { diff --git a/src/endpoint.zig b/src/endpoint.zig index d9b1b6e..6b7dd95 100644 --- a/src/endpoint.zig +++ b/src/endpoint.zig @@ -55,6 +55,16 @@ const std = @import("std"); const zap = @import("zap.zig"); const auth = @import("http_auth.zig"); +/// Endpoint request error handling strategy +pub const ErrorStrategy = enum { + /// log errors to console + log_to_console, + /// log errors to console AND generate a HTML response + log_to_response, + /// raise errors -> TODO: clarify: where can they be caught? in App.run() + raise, +}; + // zap types const Request = zap.Request; const ListenerSettings = zap.HttpListenerSettings; @@ -69,6 +79,14 @@ pub fn checkEndpointType(T: type) void { @compileError(@typeName(T) ++ " has no path field"); } + if (@hasField(T, "error_strategy")) { + if (@FieldType(T, "error_strategy") != ErrorStrategy) { + @compileError(@typeName(@FieldType(T, "error_strategy")) ++ " has wrong type, expected: zap.Endpoint.ErrorStrategy"); + } + } else { + @compileError(@typeName(T) ++ " has no error_strategy field"); + } + const methods_to_check = [_][]const u8{ "get", "post", @@ -79,8 +97,8 @@ pub fn checkEndpointType(T: type) void { }; inline for (methods_to_check) |method| { if (@hasDecl(T, method)) { - if (@TypeOf(@field(T, method)) != fn (_: *T, _: Request) void) { - @compileError(method ++ " method of " ++ @typeName(T) ++ " has wrong type:\n" ++ @typeName(@TypeOf(T.get)) ++ "\nexpected:\n" ++ @typeName(fn (_: *T, _: Request) void)); + if (@TypeOf(@field(T, method)) != fn (_: *T, _: Request) anyerror!void) { + @compileError(method ++ " method of " ++ @typeName(T) ++ " has wrong type:\n" ++ @typeName(@TypeOf(T.get)) ++ "\nexpected:\n" ++ @typeName(fn (_: *T, _: Request) anyerror!void)); } } else { @compileError(@typeName(T) ++ " has no method named `" ++ method ++ "`"); @@ -88,58 +106,65 @@ pub fn checkEndpointType(T: type) void { } } -const EndpointWrapper = struct { - pub const Wrapper = struct { - call: *const fn (*Wrapper, zap.Request) void = undefined, +pub const Wrapper = struct { + pub const Internal = struct { + call: *const fn (*Internal, zap.Request) anyerror!void = undefined, path: []const u8, - destroy: *const fn (allocator: std.mem.Allocator, *Wrapper) void = undefined, + destroy: *const fn (allocator: std.mem.Allocator, *Internal) void = undefined, }; pub fn Wrap(T: type) type { return struct { wrapped: *T, - wrapper: Wrapper, + wrapper: Internal, const Self = @This(); - pub fn unwrap(wrapper: *Wrapper) *Self { + pub fn unwrap(wrapper: *Internal) *Self { const self: *Self = @alignCast(@fieldParentPtr("wrapper", wrapper)); return self; } - pub fn destroy(allocator: std.mem.Allocator, wrapper: *Wrapper) void { + pub fn destroy(allocator: std.mem.Allocator, wrapper: *Internal) void { const self: *Self = @alignCast(@fieldParentPtr("wrapper", wrapper)); allocator.destroy(self); } - pub fn onRequestWrapped(wrapper: *Wrapper, r: zap.Request) void { + pub fn onRequestWrapped(wrapper: *Internal, r: zap.Request) !void { var self: *Self = Self.unwrap(wrapper); - self.onRequest(r); + try self.onRequest(r); } - pub fn onRequest(self: *Self, r: zap.Request) void { - switch (r.methodAsEnum()) { - .GET => return self.wrapped.*.get(r), - .POST => return self.wrapped.*.post(r), - .PUT => return self.wrapped.*.put(r), - .DELETE => return self.wrapped.*.delete(r), - .PATCH => return self.wrapped.*.patch(r), - .OPTIONS => return self.wrapped.*.options(r), - else => { - // TODO: log that method is ignored - }, + pub fn onRequest(self: *Self, r: zap.Request) !void { + const ret = switch (r.methodAsEnum()) { + .GET => self.wrapped.*.get(r), + .POST => self.wrapped.*.post(r), + .PUT => self.wrapped.*.put(r), + .DELETE => self.wrapped.*.delete(r), + .PATCH => self.wrapped.*.patch(r), + .OPTIONS => self.wrapped.*.options(r), + else => error.UnsupportedHtmlRequestMethod, + }; + if (ret) { + // handled without error + } else |err| { + switch (self.wrapped.*.error_strategy) { + .raise => return err, + .log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505), + .log_to_console => zap.debug("Error in {} {s} : {}", .{ Self, r.method orelse "(no method)", err }), + } } } }; } - pub fn init(T: type, value: *T) EndpointWrapper.Wrap(T) { + pub fn init(T: type, value: *T) Wrapper.Wrap(T) { checkEndpointType(T); - var ret: EndpointWrapper.Wrap(T) = .{ + var ret: Wrapper.Wrap(T) = .{ .wrapped = value, .wrapper = .{ .path = value.path }, }; - ret.wrapper.call = EndpointWrapper.Wrap(T).onRequestWrapped; - ret.wrapper.destroy = EndpointWrapper.Wrap(T).destroy; + ret.wrapper.call = Wrapper.Wrap(T).onRequestWrapped; + ret.wrapper.destroy = Wrapper.Wrap(T).destroy; return ret; } }; @@ -150,6 +175,7 @@ pub fn Authenticating(EndpointType: type, Authenticator: type) type { authenticator: *Authenticator, ep: *EndpointType, path: []const u8, + error_strategy: ErrorStrategy, const Self = @This(); /// Init the authenticating endpoint. Pass in a pointer to the endpoint @@ -160,61 +186,62 @@ pub fn Authenticating(EndpointType: type, Authenticator: type) type { .authenticator = authenticator, .ep = e, .path = e.path, + .error_strategy = e.error_strategy, }; } /// Authenticates GET requests using the Authenticator. - pub fn get(self: *Self, r: zap.Request) void { - switch (self.authenticator.authenticateRequest(&r)) { + pub fn get(self: *Self, r: zap.Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&r)) { .AuthFailed => return self.ep.*.unauthorized(r), .AuthOK => self.ep.*.get(r), .Handled => {}, - } + }; } /// Authenticates POST requests using the Authenticator. - pub fn post(self: *Self, r: zap.Request) void { - switch (self.authenticator.authenticateRequest(&r)) { + pub fn post(self: *Self, r: zap.Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&r)) { .AuthFailed => return self.ep.*.unauthorized(r), .AuthOK => self.ep.*.post(r), .Handled => {}, - } + }; } /// Authenticates PUT requests using the Authenticator. - pub fn put(self: *Self, r: zap.Request) void { - switch (self.authenticator.authenticateRequest(&r)) { + pub fn put(self: *Self, r: zap.Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&r)) { .AuthFailed => return self.ep.*.unauthorized(r), .AuthOK => self.ep.*.put(r), .Handled => {}, - } + }; } /// Authenticates DELETE requests using the Authenticator. - pub fn delete(self: *Self, r: zap.Request) void { - switch (self.authenticator.authenticateRequest(&r)) { + pub fn delete(self: *Self, r: zap.Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&r)) { .AuthFailed => return self.ep.*.unauthorized(r), .AuthOK => self.ep.*.delete(r), .Handled => {}, - } + }; } /// Authenticates PATCH requests using the Authenticator. - pub fn patch(self: *Self, r: zap.Request) void { - switch (self.authenticator.authenticateRequest(&r)) { + pub fn patch(self: *Self, r: zap.Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&r)) { .AuthFailed => return self.ep.*.unauthorized(r), .AuthOK => self.ep.*.patch(r), .Handled => {}, - } + }; } /// Authenticates OPTIONS requests using the Authenticator. - pub fn options(self: *Self, r: zap.Request) void { - switch (self.authenticator.authenticateRequest(&r)) { + pub fn options(self: *Self, r: zap.Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&r)) { .AuthFailed => return self.ep.*.unauthorized(r), .AuthOK => self.ep.*.put(r), .Handled => {}, - } + }; } }; } @@ -237,7 +264,7 @@ pub const Listener = struct { const Self = @This(); /// Internal static struct of member endpoints - var endpoints: std.ArrayListUnmanaged(*EndpointWrapper.Wrapper) = .empty; + var endpoints: std.ArrayListUnmanaged(*Wrapper.Internal) = .empty; /// Internal, static request handler callback. Will be set to the optional, /// user-defined request callback that only gets called if no endpoints match @@ -303,23 +330,22 @@ pub const Listener = struct { } const EndpointType = @typeInfo(@TypeOf(e)).pointer.child; checkEndpointType(EndpointType); - const wrapper = try self.allocator.create(EndpointWrapper.Wrap(EndpointType)); - wrapper.* = EndpointWrapper.init(EndpointType, e); + const wrapper = try self.allocator.create(Wrapper.Wrap(EndpointType)); + wrapper.* = Wrapper.init(EndpointType, e); try endpoints.append(self.allocator, &wrapper.wrapper); } - fn onRequest(r: Request) void { + fn onRequest(r: Request) !void { if (r.path) |p| { for (endpoints.items) |wrapper| { if (std.mem.startsWith(u8, p, wrapper.path)) { - wrapper.call(wrapper, r); - return; + return try wrapper.call(wrapper, r); } } } // if set, call the user-provided default callback if (on_request) |foo| { - foo(r); + try foo(r); } } }; diff --git a/src/middleware.zig b/src/middleware.zig index 0373280..b5b92e2 100644 --- a/src/middleware.zig +++ b/src/middleware.zig @@ -17,7 +17,7 @@ pub fn Handler(comptime ContextType: anytype) type { // will be set allocator: ?std.mem.Allocator = null, - pub const RequestFn = *const fn (*Self, zap.Request, *ContextType) bool; + pub const RequestFn = *const fn (*Self, zap.Request, *ContextType) anyerror!bool; const Self = @This(); pub fn init(on_request: RequestFn, other: ?*Self) Self { @@ -30,7 +30,7 @@ pub fn Handler(comptime ContextType: anytype) type { // example for handling a request request // which you can use in your components, e.g.: // return self.handler.handleOther(r, context); - pub fn handleOther(self: *Self, r: zap.Request, context: *ContextType) bool { + pub fn handleOther(self: *Self, r: zap.Request, context: *ContextType) !bool { // in structs embedding a handler, we'd @fieldParentPtr the first // param to get to the real self @@ -41,7 +41,7 @@ pub fn Handler(comptime ContextType: anytype) type { var other_handler_finished = false; if (self.other_handler) |other_handler| { if (other_handler.on_request) |on_request| { - other_handler_finished = on_request(other_handler, r, context); + other_handler_finished = try on_request(other_handler, r, context); } } @@ -96,19 +96,19 @@ pub fn EndpointHandler(comptime HandlerType: anytype, comptime EndpointType: any /// /// If `breakOnFinish` is `true`, the handler will stop handing requests down the chain if /// the endpoint processed the request. - pub fn onRequest(handler: *HandlerType, r: zap.Request, context: *ContextType) bool { + pub fn onRequest(handler: *HandlerType, r: zap.Request, context: *ContextType) !bool { const self: *Self = @fieldParentPtr("handler", handler); r.setUserContext(context); if (!self.options.checkPath or std.mem.startsWith(u8, r.path orelse "", self.endpoint.path)) { switch (r.methodAsEnum()) { - .GET => self.endpoint.*.get(r), - .POST => self.endpoint.*.post(r), - .PUT => self.endpoint.*.put(r), - .DELETE => self.endpoint.*.delete(r), - .PATCH => self.endpoint.*.patch(r), - .OPTIONS => self.endpoint.*.options(r), + .GET => try self.endpoint.*.get(r), + .POST => try self.endpoint.*.post(r), + .PUT => try self.endpoint.*.put(r), + .DELETE => try self.endpoint.*.delete(r), + .PATCH => try self.endpoint.*.patch(r), + .OPTIONS => try self.endpoint.*.options(r), else => {}, } } @@ -176,7 +176,7 @@ pub fn Listener(comptime ContextType: anytype) type { /// Create your own listener if you want different behavior. /// (Didn't want to make this a callback. Submit an issue if you really /// think that's an issue). - pub fn onRequest(r: zap.Request) void { + pub fn onRequest(r: zap.Request) !void { // we are the 1st handler in the chain, so we create a context var context: ContextType = .{}; @@ -191,7 +191,7 @@ pub fn Listener(comptime ContextType: anytype) type { initial_handler.allocator = allocator; if (initial_handler.on_request) |on_request| { // we don't care about the return value at the top level - _ = on_request(initial_handler, r, &context); + _ = try on_request(initial_handler, r, &context); } } } diff --git a/src/router.zig b/src/router.zig index c9ca7d5..53500a6 100644 --- a/src/router.zig +++ b/src/router.zig @@ -21,7 +21,7 @@ pub const Options = struct { }; const CallbackTag = enum { bound, unbound }; -const BoundHandler = *fn (*const anyopaque, zap.Request) void; +const BoundHandler = *fn (*const anyopaque, zap.Request) anyerror!void; const Callback = union(CallbackTag) { bound: struct { instance: usize, handler: usize }, unbound: zap.HttpRequestFn, @@ -94,24 +94,24 @@ pub fn on_request_handler(self: *Self) zap.HttpRequestFn { return zap_on_request; } -fn zap_on_request(r: zap.Request) void { +fn zap_on_request(r: zap.Request) !void { return serve(_instance, r); } -fn serve(self: *Self, r: zap.Request) void { +fn serve(self: *Self, r: zap.Request) !void { const path = r.path orelse "/"; if (self.routes.get(path)) |routeInfo| { switch (routeInfo) { - .bound => |b| @call(.auto, @as(BoundHandler, @ptrFromInt(b.handler)), .{ @as(*anyopaque, @ptrFromInt(b.instance)), r }), - .unbound => |h| h(r), + .bound => |b| try @call(.auto, @as(BoundHandler, @ptrFromInt(b.handler)), .{ @as(*anyopaque, @ptrFromInt(b.instance)), r }), + .unbound => |h| try h(r), } } else if (self.not_found) |handler| { // not found handler - handler(r); + try handler(r); } else { // default 404 output r.setStatus(.not_found); - r.sendBody("404 Not Found") catch return; + try r.sendBody("404 Not Found"); } } diff --git a/src/tests/test_auth.zig b/src/tests/test_auth.zig index b33487d..1dba9a1 100644 --- a/src/tests/test_auth.zig +++ b/src/tests/test_auth.zig @@ -149,8 +149,9 @@ fn makeRequestThread(a: std.mem.Allocator, url: []const u8, auth: ?ClientAuthReq pub const Endpoint = struct { path: []const u8, + error_strategy: zap.Endpoint.ErrorStrategy = .raise, - pub fn get(e: *Endpoint, r: zap.Request) void { + pub fn get(e: *Endpoint, r: zap.Request) !void { _ = e; r.sendBody(HTTP_RESPONSE) catch return; received_response = HTTP_RESPONSE; @@ -158,7 +159,7 @@ pub const Endpoint = struct { zap.stop(); } - pub fn unauthorized(e: *Endpoint, r: zap.Request) void { + pub fn unauthorized(e: *Endpoint, r: zap.Request) !void { _ = e; r.setStatus(.unauthorized); r.sendBody("UNAUTHORIZED ACCESS") catch return; @@ -166,11 +167,11 @@ pub const Endpoint = struct { std.time.sleep(1 * std.time.ns_per_s); zap.stop(); } - pub fn post(_: *Endpoint, _: zap.Request) void {} - pub fn put(_: *Endpoint, _: zap.Request) void {} - pub fn delete(_: *Endpoint, _: zap.Request) void {} - pub fn patch(_: *Endpoint, _: zap.Request) void {} - pub fn options(_: *Endpoint, _: zap.Request) void {} + pub fn post(_: *Endpoint, _: zap.Request) !void {} + pub fn put(_: *Endpoint, _: zap.Request) !void {} + pub fn delete(_: *Endpoint, _: zap.Request) !void {} + pub fn patch(_: *Endpoint, _: zap.Request) !void {} + pub fn options(_: *Endpoint, _: zap.Request) !void {} }; // // end of http client code diff --git a/src/tests/test_http_params.zig b/src/tests/test_http_params.zig index 31f8b0d..167cbc0 100644 --- a/src/tests/test_http_params.zig +++ b/src/tests/test_http_params.zig @@ -30,7 +30,7 @@ test "http parameters" { var paramOneSlice: ?[]const u8 = null; var paramSlices: zap.Request.ParamSliceIterator = undefined; - pub fn on_request(r: zap.Request) void { + pub fn on_request(r: zap.Request) !void { ran = true; r.parseQuery(); param_count = r.getParamCount(); diff --git a/src/tests/test_sendfile.zig b/src/tests/test_sendfile.zig index 8b16a8a..964ee99 100644 --- a/src/tests/test_sendfile.zig +++ b/src/tests/test_sendfile.zig @@ -24,7 +24,7 @@ fn makeRequest(a: std.mem.Allocator, url: []const u8) !void { fn makeRequestThread(a: std.mem.Allocator, url: []const u8) !std.Thread { return try std.Thread.spawn(.{}, makeRequest, .{ a, url }); } -pub fn on_request(r: zap.Request) void { +pub fn on_request(r: zap.Request) !void { r.sendFile("src/tests/testfile.txt") catch unreachable; } diff --git a/src/util.zig b/src/util.zig index 4b6a4c9..663155c 100644 --- a/src/util.zig +++ b/src/util.zig @@ -74,12 +74,12 @@ pub fn stringifyBuf( buffer: []u8, value: anytype, options: std.json.StringifyOptions, -) ?[]const u8 { +) ![]const u8 { var fba = std.heap.FixedBufferAllocator.init(buffer); var string = std.ArrayList(u8).init(fba.allocator()); if (std.json.stringify(value, options, string.writer())) { return string.items; - } else |_| { // error - return null; + } else |err| { // error + return err; } } diff --git a/src/zap.zig b/src/zap.zig index bce9853..93d3ecb 100644 --- a/src/zap.zig +++ b/src/zap.zig @@ -132,7 +132,7 @@ pub const ContentType = enum { pub const FioHttpRequestFn = *const fn (r: [*c]fio.http_s) callconv(.C) void; /// Zap Http request callback function type. -pub const HttpRequestFn = *const fn (Request) void; +pub const HttpRequestFn = *const fn (Request) anyerror!void; /// websocket connection upgrade callback type /// fn(request, targetstring) @@ -202,8 +202,10 @@ pub const HttpListener = struct { req.markAsFinished(false); std.debug.assert(l.settings.on_request != null); if (l.settings.on_request) |on_request| { - // l.settings.on_request.?(req); - on_request(req); + on_request(req) catch |err| { + // TODO: log / handle the error in a better way + std.debug.print("zap on_request error: {}", .{err}); + }; } } } @@ -225,7 +227,10 @@ pub const HttpListener = struct { var user_context: Request.UserContext = .{}; req._user_context = &user_context; - l.settings.on_response.?(req); + l.settings.on_response.?(req) catch |err| { + // TODO: log / handle the error in a better way + std.debug.print("zap on_response error: {}", .{err}); + }; } } diff --git a/tools/docserver.zig b/tools/docserver.zig index e6fc5ba..5368f20 100644 --- a/tools/docserver.zig +++ b/tools/docserver.zig @@ -1,7 +1,7 @@ const std = @import("std"); const zap = @import("zap"); -fn on_request(r: zap.Request) void { +fn on_request(r: zap.Request) !void { r.setStatus(.not_found); r.sendBody("

404 - File not found

") catch return; } diff --git a/wrk/zig/main.zig b/wrk/zig/main.zig index 20fa3f3..ef68612 100644 --- a/wrk/zig/main.zig +++ b/wrk/zig/main.zig @@ -1,8 +1,8 @@ const std = @import("std"); const zap = @import("zap"); -fn on_request_minimal(r: zap.Request) void { - r.sendBody("Hello from ZAP!!!") catch return; +fn on_request_minimal(r: zap.Request) !void { + try r.sendBody("Hello from ZAP!!!"); } pub fn main() !void { From 24aacac58dd4206f2b8c225e3a161ee4604861c1 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 16 Mar 2025 20:34:33 +0100 Subject: [PATCH 29/57] fix measure.sh for macos --- wrk/measure.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/wrk/measure.sh b/wrk/measure.sh index 5520f0a..4b8e078 100755 --- a/wrk/measure.sh +++ b/wrk/measure.sh @@ -5,9 +5,13 @@ DURATION_SECONDS=10 SUBJECT=$1 - +if echo $(uname) -eq "Darwin" ; then +TSK_SRV="" +TSK_LOAD="" +else TSK_SRV="taskset -c 0,1,2,3" TSK_LOAD="taskset -c 4,5,6,7" +fi if [ "$SUBJECT" = "" ] ; then echo "usage: $0 subject # subject: zig or go" @@ -29,7 +33,7 @@ if [ "$SUBJECT" = "zigstd" ] ; then fi if [ "$SUBJECT" = "go" ] ; then - cd wrk/go && go build main.go + cd wrk/go && go build main.go $TSK_SRV ./main & PID=$! URL=http://127.0.0.1:8090/hello @@ -94,7 +98,7 @@ sleep 1 echo "========================================================================" echo " $SUBJECT" echo "========================================================================" -$TSK_LOAD wrk -c $CONNECTIONS -t $THREADS -d $DURATION_SECONDS --latency $URL +$TSK_LOAD wrk -c $CONNECTIONS -t $THREADS -d $DURATION_SECONDS --latency $URL kill $PID From 7f61b821d38c3352491c1803bc03a7b317c83a13 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Fri, 21 Mar 2025 11:32:27 +0100 Subject: [PATCH 30/57] updated .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 158e964..67b5d59 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ scratch .vs/ **/*.perflog wrk/*.png +mycert.pem +release.md +mykey.pem From ad2b2f2eb034e78e414c5fcdbdc3aa5f38eb2482 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Fri, 21 Mar 2025 11:32:27 +0100 Subject: [PATCH 31/57] added skeleton for App.zig --- src/App.zig | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/App.zig diff --git a/src/App.zig b/src/App.zig new file mode 100644 index 0000000..317e2ea --- /dev/null +++ b/src/App.zig @@ -0,0 +1,34 @@ +//! WIP: zap.App. +//! +//! - Per Request Arena(s) thread-local? +//! - Custom "State" Context, type-safe +//! - route handlers +//! - automatic error catching & logging, optional report to HTML + +const std = @import("std"); +const zap = @import("zap.zig"); + +pub const Opts = struct { + request_error_strategy: enum { + /// log errors to console + log_to_console, + /// log errors to console AND generate a HTML response + log_to_response, + /// raise errors -> TODO: clarify: where can they be caught? in App.run() + raise, + }, +}; + +threadlocal var arena: ?std.heap.ArenaAllocator = null; + +pub fn create(comptime Context: type, context: *Context, opts: Opts) type { + return struct { + context: *Context = context, + error_strategy: @TypeOf(opts.request_error_strategy) = opts.request_error_strategy, + endpoints: std.StringArrayHashMapUnmanaged(*zap.Endpoint.Wrapper.Internal) = .empty, + + // pub fn addEndpoint(slug: []const u8, endpoint: anytype) !zap.Endpoint { + // // TODO: inspect endpoint: does it have + // } + }; +} From 9eb254d5f8b207c204d9e925d7c7cb3582144752 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Fri, 21 Mar 2025 11:45:13 +0100 Subject: [PATCH 32/57] get rid of zap.util.FreeOrNot --- examples/accept/accept.zig | 13 +- examples/bindataformpost/bindataformpost.zig | 31 +-- examples/cookies/cookies.zig | 24 +- examples/hello2/hello2.zig | 13 +- examples/hello_json/hello_json.zig | 1 + examples/http_params/http_params.zig | 28 +-- examples/simple_router/simple_router.zig | 13 +- .../userpass_session_auth.zig | 2 + src/fio.zig | 4 +- src/http_auth.zig | 43 ++-- src/request.zig | 227 +++++++++--------- src/tests/test_http_params.zig | 97 ++++---- src/util.zig | 45 ++-- 13 files changed, 278 insertions(+), 263 deletions(-) diff --git a/examples/accept/accept.zig b/examples/accept/accept.zig index a9e268c..fc9f766 100644 --- a/examples/accept/accept.zig +++ b/examples/accept/accept.zig @@ -66,7 +66,18 @@ pub fn main() !void { }); try listener.listen(); - std.debug.print("Listening on 0.0.0.0:3000\n", .{}); + std.debug.print( + \\ Listening on 0.0.0.0:3000 + \\ + \\ Test me with: + \\ curl --header "Accept: text/plain" localhost:3000 + \\ curl --header "Accept: text/html" localhost:3000 + \\ curl --header "Accept: application/xml" localhost:3000 + \\ curl --header "Accept: application/json" localhost:3000 + \\ curl --header "Accept: application/xhtml+xml" localhost:3000 + \\ + \\ + , .{}); // start worker threads zap.start(.{ diff --git a/examples/bindataformpost/bindataformpost.zig b/examples/bindataformpost/bindataformpost.zig index 9cc0eda..c1d5fe8 100644 --- a/examples/bindataformpost/bindataformpost.zig +++ b/examples/bindataformpost/bindataformpost.zig @@ -24,12 +24,12 @@ const Handler = struct { // // HERE WE HANDLE THE BINARY FILE // - const params = try r.parametersToOwnedList(Handler.alloc, false); + const params = try r.parametersToOwnedList(Handler.alloc); defer params.deinit(); for (params.items) |kv| { if (kv.value) |v| { std.debug.print("\n", .{}); - std.log.info("Param `{s}` in owned list is {any}\n", .{ kv.key.str, v }); + std.log.info("Param `{s}` in owned list is {any}\n", .{ kv.key, v }); switch (v) { // single-file upload zap.Request.HttpParam.Hash_Binfile => |*file| { @@ -55,32 +55,20 @@ const Handler = struct { files.*.deinit(); }, else => { - // might be a string param, we don't care - // let's just get it as string - // always_alloc param = false -> the string will be a slice from the request buffer - // --> no deinit necessary - if (r.getParamStr(Handler.alloc, kv.key.str, false)) |maybe_str| { - const value: []const u8 = if (maybe_str) |s| s.str else "(no value)"; - // above, we didn't defer s.deinit because the string is just a slice from the request buffer - std.log.debug(" {s} = {s}", .{ kv.key.str, value }); - } else |err| { - std.log.err("Error: {any}\n", .{err}); - } + // let's just get it as its raw slice + const value: []const u8 = r.getParamSlice(kv.key) orelse "(no value)"; + std.log.debug(" {s} = {s}", .{ kv.key, value }); }, } } } // check if we received a terminate=true parameter - if (r.getParamStr(Handler.alloc, "terminate", false)) |maybe_str| { - if (maybe_str) |*s| { - std.log.info("?terminate={s}\n", .{s.str}); - if (std.mem.eql(u8, s.str, "true")) { - zap.stop(); - } + if (r.getParamSlice("terminate")) |str| { + std.log.info("?terminate={s}\n", .{str}); + if (std.mem.eql(u8, str, "true")) { + zap.stop(); } - } else |err| { - std.log.err("cannot check for terminate param: {any}\n", .{err}); } try r.sendJson("{ \"ok\": true }"); } @@ -90,6 +78,7 @@ pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true, }){}; + defer _ = gpa.detectLeaks(); const allocator = gpa.allocator(); Handler.alloc = allocator; diff --git a/examples/cookies/cookies.zig b/examples/cookies/cookies.zig index 16fc9a8..22d8e18 100644 --- a/examples/cookies/cookies.zig +++ b/examples/cookies/cookies.zig @@ -44,12 +44,12 @@ pub fn main() !void { const cookie_count = r.getCookiesCount(); std.log.info("cookie_count: {}", .{cookie_count}); - // iterate over all cookies as strings (always_alloc=false) - var strCookies = r.cookiesToOwnedStrList(alloc, false) catch unreachable; + // iterate over all cookies as strings + var strCookies = r.cookiesToOwnedStrList(alloc) catch unreachable; defer strCookies.deinit(); std.debug.print("\n", .{}); for (strCookies.items) |kv| { - std.log.info("CookieStr `{s}` is `{s}`", .{ kv.key.str, kv.value.str }); + std.log.info("CookieStr `{s}` is `{s}`", .{ kv.key, kv.value }); // we don't need to deinit kv.key and kv.value because we requested always_alloc=false // so they are just slices into the request buffer } @@ -57,26 +57,22 @@ pub fn main() !void { std.debug.print("\n", .{}); // // iterate over all cookies - const cookies = r.cookiesToOwnedList(alloc, false) catch unreachable; + const cookies = r.cookiesToOwnedList(alloc) catch unreachable; defer cookies.deinit(); for (cookies.items) |kv| { - std.log.info("cookie `{s}` is {any}", .{ kv.key.str, kv.value }); + std.log.info("cookie `{s}` is {any}", .{ kv.key, kv.value }); } // let's get cookie "ZIG_ZAP" by name std.debug.print("\n", .{}); - if (r.getCookieStr(alloc, "ZIG_ZAP", false)) |maybe_str| { - if (maybe_str) |*s| { - defer s.deinit(); // unnecessary because always_alloc=false - - std.log.info("Cookie ZIG_ZAP = {s}", .{s.str}); + if (r.getCookieStr(alloc, "ZIG_ZAP")) |maybe_str| { + if (maybe_str) |s| { + defer alloc.free(s); + std.log.info("Cookie ZIG_ZAP = {s}", .{s}); } else { std.log.info("Cookie ZIG_ZAP not found!", .{}); } - } - // since we provided "false" for duplicating strings in the call - // to getCookieStr(), there won't be an allocation error - else |err| { + } else |err| { std.log.err("ERROR!\n", .{}); std.log.err("cannot check for `ZIG_ZAP` cookie: {any}\n", .{err}); } diff --git a/examples/hello2/hello2.zig b/examples/hello2/hello2.zig index 51e8faa..5302c4c 100644 --- a/examples/hello2/hello2.zig +++ b/examples/hello2/hello2.zig @@ -43,7 +43,18 @@ pub fn main() !void { }); try listener.listen(); - std.debug.print("Listening on 0.0.0.0:3000\n", .{}); + std.debug.print( + \\ Listening on 0.0.0.0:3000 + \\ + \\ Test me with: + \\ curl http://localhost:3000 + \\ curl --header "special-header: test" localhost:3000 + \\ + \\ ... or open http://localhost:3000 in the browser + \\ and watch the log output here + \\ + \\ + , .{}); // start worker threads zap.start(.{ diff --git a/examples/hello_json/hello_json.zig b/examples/hello_json/hello_json.zig index 21f9f46..af693ef 100644 --- a/examples/hello_json/hello_json.zig +++ b/examples/hello_json/hello_json.zig @@ -43,6 +43,7 @@ fn setupUserData(a: std.mem.Allocator) !void { pub fn main() !void { const a = std.heap.page_allocator; try setupUserData(a); + defer users.deinit(); var listener = zap.HttpListener.init(.{ .port = 3000, .on_request = on_request, diff --git a/examples/http_params/http_params.zig b/examples/http_params/http_params.zig index 66976f6..4034eaf 100644 --- a/examples/http_params/http_params.zig +++ b/examples/http_params/http_params.zig @@ -27,6 +27,8 @@ pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true, }){}; + defer _ = gpa.detectLeaks(); + const allocator = gpa.allocator(); const Handler = struct { @@ -69,42 +71,38 @@ pub fn main() !void { // ================================================================ // iterate over all params as strings - var strparams = try r.parametersToOwnedStrList(alloc, false); + var strparams = try r.parametersToOwnedStrList(alloc); 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.log.info("ParamStr `{s}` is `{s}`", .{ kv.key, kv.value }); } std.debug.print("\n", .{}); // iterate over all params - const params = try r.parametersToOwnedList(alloc, false); + const params = try r.parametersToOwnedList(alloc); defer params.deinit(); for (params.items) |kv| { - std.log.info("Param `{s}` is {any}", .{ kv.key.str, kv.value }); + std.log.info("Param `{s}` is {any}", .{ kv.key, kv.value }); } // let's get param "one" by name std.debug.print("\n", .{}); - if (r.getParamStr(alloc, "one", false)) |maybe_str| { - if (maybe_str) |*s| { - defer s.deinit(); - - std.log.info("Param one = {s}", .{s.str}); + if (r.getParamStr(alloc, "one")) |maybe_str| { + if (maybe_str) |s| { + defer alloc.free(s); + std.log.info("Param one = {s}", .{s}); } 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| { + } else |err| { std.log.err("cannot check for `one` param: {any}\n", .{err}); } // check if we received a terminate=true parameter - if (r.getParamSlice("terminate")) |maybe_str| { - if (std.mem.eql(u8, maybe_str, "true")) { + if (r.getParamSlice("terminate")) |s| { + if (std.mem.eql(u8, s, "true")) { zap.stop(); } } diff --git a/examples/simple_router/simple_router.zig b/examples/simple_router/simple_router.zig index cacbcc3..f91ed2c 100644 --- a/examples/simple_router/simple_router.zig +++ b/examples/simple_router/simple_router.zig @@ -98,8 +98,17 @@ pub fn main() !void { }); try listener.listen(); - std.debug.print("Listening on 0.0.0.0:3000\n", .{}); - + std.debug.print( + \\ Listening on 0.0.0.0:3000 + \\ + \\ Test me with: + \\ curl http://localhost:3000/ + \\ curl http://localhost:3000/geta + \\ curl http://localhost:3000/getb + \\ curl http://localhost:3000/inca + \\ + \\ + , .{}); // start worker threads zap.start(.{ .threads = 2, diff --git a/examples/userpass_session_auth/userpass_session_auth.zig b/examples/userpass_session_auth/userpass_session_auth.zig index c421ddc..fcf7744 100644 --- a/examples/userpass_session_auth/userpass_session_auth.zig +++ b/examples/userpass_session_auth/userpass_session_auth.zig @@ -72,6 +72,7 @@ fn on_request(r: zap.Request) !void { .AuthOK => { // the authenticator says it is ok to proceed as usual std.log.info("Auth OK", .{}); + // dispatch to target path if (r.path) |p| { // used in the login page @@ -124,6 +125,7 @@ pub fn main() !void { // to detect leaks { const allocator = gpa.allocator(); + var listener = zap.HttpListener.init(.{ .port = 3000, .on_request = on_request, diff --git a/src/fio.zig b/src/fio.zig index 7696989..4b6b3cd 100644 --- a/src/fio.zig +++ b/src/fio.zig @@ -13,7 +13,7 @@ pub const fio_url_s = extern struct { pub extern fn fio_url_parse(url: [*c]const u8, length: usize) fio_url_s; /// Negative thread / worker values indicate a fraction of the number of CPU cores. i.e., -2 will normally indicate "half" (1/2) the number of cores. -/// +/// /// If one value is set to zero, it will be the absolute value of the other value. i.e.: if .threads == -2 and .workers == 0, than facil.io will run 2 worker processes with (cores/2) threads per process. pub const struct_fio_start_args = extern struct { /// The number of threads to run in the thread pool. @@ -420,7 +420,9 @@ pub fn fiobj_obj2cstr(o: FIOBJ) callconv(.C) fio_str_info_s { // pub const http_cookie_args_s = opaque {}; pub extern fn http_set_header(h: [*c]http_s, name: FIOBJ, value: FIOBJ) c_int; +/// set header, copying the data pub extern fn http_set_header2(h: [*c]http_s, name: fio_str_info_s, value: fio_str_info_s) c_int; +/// set cookie, taking ownership of data pub extern fn http_set_cookie(h: [*c]http_s, http_cookie_args_s) c_int; pub extern fn http_sendfile(h: [*c]http_s, fd: c_int, length: usize, offset: usize) c_int; pub extern fn http_sendfile2(h: [*c]http_s, prefix: [*c]const u8, prefix_len: usize, encoded: [*c]const u8, encoded_len: usize) c_int; diff --git a/src/http_auth.zig b/src/http_auth.zig index 894b74e..e4b9fa3 100644 --- a/src/http_auth.zig +++ b/src/http_auth.zig @@ -450,15 +450,15 @@ pub fn UserPassSession(comptime Lookup: type, comptime lockedPwLookups: bool) ty r.parseCookies(false); // check for session cookie - if (r.getCookieStr(self.allocator, self.settings.cookieName, false)) |maybe_cookie| { + if (r.getCookieStr(self.allocator, self.settings.cookieName)) |maybe_cookie| { if (maybe_cookie) |cookie| { - defer cookie.deinit(); + defer self.allocator.free(cookie); self.tokenLookupLock.lock(); defer self.tokenLookupLock.unlock(); - if (self.sessionTokens.getKeyPtr(cookie.str)) |keyPtr| { + if (self.sessionTokens.getKeyPtr(cookie)) |keyPtr| { const keySlice = keyPtr.*; // if cookie is a valid session, remove it! - _ = self.sessionTokens.remove(cookie.str); + _ = self.sessionTokens.remove(cookie); // only now can we let go of the cookie str slice that // was used as the key self.allocator.free(keySlice); @@ -486,18 +486,18 @@ pub fn UserPassSession(comptime Lookup: type, comptime lockedPwLookups: bool) ty r.parseCookies(false); // check for session cookie - if (r.getCookieStr(self.allocator, self.settings.cookieName, false)) |maybe_cookie| { + if (r.getCookieStr(self.allocator, self.settings.cookieName)) |maybe_cookie| { if (maybe_cookie) |cookie| { - defer cookie.deinit(); + defer self.allocator.free(cookie); // locked or unlocked token lookup self.tokenLookupLock.lock(); defer self.tokenLookupLock.unlock(); - if (self.sessionTokens.contains(cookie.str)) { + if (self.sessionTokens.contains(cookie)) { // cookie is a valid session! - zap.debug("Auth: COOKIE IS OK!!!!: {s}\n", .{cookie.str}); + zap.debug("Auth: COOKIE IS OK!!!!: {s}\n", .{cookie}); return .AuthOK; } else { - zap.debug("Auth: COOKIE IS BAD!!!!: {s}\n", .{cookie.str}); + zap.debug("Auth: COOKIE IS BAD!!!!: {s}\n", .{cookie}); // this is not necessarily a bad thing. it could be a // stale cookie from a previous session. So let's check // if username and password are being sent and correct. @@ -508,27 +508,26 @@ pub fn UserPassSession(comptime Lookup: type, comptime lockedPwLookups: bool) ty } // get params of username and password - if (r.getParamStr(self.allocator, self.settings.usernameParam, false)) |maybe_username| { - if (maybe_username) |*username| { - defer username.deinit(); - if (r.getParamStr(self.allocator, self.settings.passwordParam, false)) |maybe_pw| { - if (maybe_pw) |*pw| { - defer pw.deinit(); - + if (r.getParamStr(self.allocator, self.settings.usernameParam)) |maybe_username| { + if (maybe_username) |username| { + defer self.allocator.free(username); + if (r.getParamStr(self.allocator, self.settings.passwordParam)) |maybe_pw| { + if (maybe_pw) |pw| { + defer self.allocator.free(pw); // now check const correct_pw_optional = brk: { if (lockedPwLookups) { self.passwordLookupLock.lock(); defer self.passwordLookupLock.unlock(); - break :brk self.lookup.*.get(username.str); + break :brk self.lookup.*.get(username); } else { - break :brk self.lookup.*.get(username.str); + break :brk self.lookup.*.get(username); } }; if (correct_pw_optional) |correct_pw| { - if (std.mem.eql(u8, pw.str, correct_pw)) { + if (std.mem.eql(u8, pw, correct_pw)) { // create session token - if (self.createAndStoreSessionToken(username.str, pw.str)) |token| { + if (self.createAndStoreSessionToken(username, pw)) |token| { defer self.allocator.free(token); // now set the cookie header if (r.setCookie(.{ @@ -549,12 +548,12 @@ pub fn UserPassSession(comptime Lookup: type, comptime lockedPwLookups: bool) ty } } } else |err| { - zap.debug("getParamSt() for password failed in UserPassSession: {any}", .{err}); + zap.debug("getParamStr() for password failed in UserPassSession: {any}", .{err}); return .AuthFailed; } } } else |err| { - zap.debug("getParamSt() for user failed in UserPassSession: {any}", .{err}); + zap.debug("getParamStr() for user failed in UserPassSession: {any}", .{err}); return .AuthFailed; } return .AuthFailed; diff --git a/src/request.zig b/src/request.zig index a0c2a7e..3fcc0ee 100644 --- a/src/request.zig +++ b/src/request.zig @@ -1,4 +1,6 @@ const std = @import("std"); +const Allocator = std.mem.Allocator; + const Log = @import("log.zig"); const http = @import("http.zig"); const fio = @import("fio.zig"); @@ -20,21 +22,18 @@ pub const HttpError = error{ /// 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(); - } + key: []const u8, + value: []const u8, }; /// List of key value pairs of Http param strings. pub const HttpParamStrKVList = struct { items: []HttpParamStrKV, - allocator: std.mem.Allocator, + allocator: Allocator, pub fn deinit(self: *@This()) void { - for (self.items) |*item| { - item.deinit(); + for (self.items) |item| { + self.allocator.free(item.key); + self.allocator.free(item.value); } self.allocator.free(self.items); } @@ -43,10 +42,13 @@ pub const HttpParamStrKVList = struct { /// List of key value pairs of Http params (might be of different types). pub const HttpParamKVList = struct { items: []HttpParamKV, - allocator: std.mem.Allocator, + allocator: Allocator, pub fn deinit(self: *const @This()) void { - for (self.items) |*item| { - item.deinit(); + for (self.items) |item| { + self.allocator.free(item.key); + if (item.value) |v| { + v.free(self.allocator); + } } self.allocator.free(self.items); } @@ -70,28 +72,31 @@ pub const HttpParam = union(HttpParamValueType) { Int: isize, Float: f64, /// we don't do writable strings here - String: util.FreeOrNot, + String: []const u8, /// value will always be null Unsupported: ?void, /// we assume hashes are because of file transmissions Hash_Binfile: HttpParamBinaryFile, /// value will always be null Array_Binfile: std.ArrayList(HttpParamBinaryFile), + + pub fn free(self: HttpParam, alloc: Allocator) void { + switch (self) { + .String => |s| alloc.free(s), + .Array_Binfile => |a| { + a.deinit(); + }, + else => { + // nothing to free + }, + } + } }; /// Key value pair of one typed Http param pub const HttpParamKV = struct { - key: util.FreeOrNot, + key: []const u8, value: ?HttpParam, - pub fn deinit(self: *@This()) void { - self.key.deinit(); - if (self.value) |p| { - switch (p) { - .String => |*s| s.deinit(), - else => {}, - } - } - } }; /// Struct representing an uploaded file. @@ -112,7 +117,7 @@ pub const HttpParamBinaryFile = struct { } }; -fn parseBinfilesFrom(a: std.mem.Allocator, o: fio.FIOBJ) !HttpParam { +fn parseBinfilesFrom(a: Allocator, o: fio.FIOBJ) !HttpParam { const key_name = fio.fiobj_str_new("name", 4); const key_data = fio.fiobj_str_new("data", 4); const key_type = fio.fiobj_str_new("type", 4); @@ -225,14 +230,15 @@ fn parseBinfilesFrom(a: std.mem.Allocator, o: fio.FIOBJ) !HttpParam { } /// Parse FIO object into a typed Http param. Supports file uploads. -pub fn Fiobj2HttpParam(a: std.mem.Allocator, o: fio.FIOBJ, dupe_string: bool) !?HttpParam { +/// Allocator is only used for file uploads. +pub fn fiobj2HttpParam(a: Allocator, o: fio.FIOBJ) !?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(a, o, dupe_string) }, + fio.FIOBJ_T_STRING => .{ .String = try util.fio2strAlloc(a, o) }, fio.FIOBJ_T_ARRAY => { return .{ .Unsupported = null }; }, @@ -423,6 +429,7 @@ pub fn setContentTypeFromFilename(self: *const Self, filename: []const u8) !void const e = ext[1..]; const obj = fio.http_mimetype_find(@constCast(e.ptr), e.len); + // fio2str is OK since setHeader takes a copy if (util.fio2str(obj)) |mime_str| { try self.setHeader("content-type", mime_str); } @@ -438,6 +445,8 @@ pub fn setContentTypeFromFilename(self: *const Self, filename: []const u8) !void pub fn getHeader(self: *const Self, name: []const u8) ?[]const u8 { const hname = fio.fiobj_str_new(util.toCharPtr(name), name.len); defer fio.fiobj_free_wrapped(hname); + // fio2str is OK since headers are always strings -> slice is returned + // (and not a slice into some threadlocal "global") return util.fio2str(fio.fiobj_hash_get(self.h.*.headers, hname)); } @@ -495,6 +504,8 @@ pub fn getHeaderCommon(self: *const Self, which: HttpHeaderCommon) ?[]const u8 { .upgrade => fio.HTTP_HEADER_UPGRADE, }; const fiobj = zap.fio.fiobj_hash_get(self.h.*.headers, field); + // fio2str is OK since headers are always strings -> slice is returned + // (and not a slice into some threadlocal "global") return zap.util.fio2str(fiobj); } @@ -606,7 +617,7 @@ pub const AcceptItem = struct { const AcceptHeaderList = std.ArrayList(AcceptItem); /// Parses `Accept:` http header into `list`, ordered from highest q factor to lowest -pub fn parseAcceptHeaders(self: *const Self, allocator: std.mem.Allocator) !AcceptHeaderList { +pub fn parseAcceptHeaders(self: *const Self, allocator: Allocator) !AcceptHeaderList { const accept_str = self.getHeaderCommon(.accept) orelse return error.NoAcceptHeader; const comma_count = std.mem.count(u8, accept_str, ","); @@ -681,7 +692,7 @@ pub fn setCookie(self: *const Self, args: CookieArgs) HttpError!void { } /// Returns named cookie. Works like getParamStr(). -pub fn getCookieStr(self: *const Self, a: std.mem.Allocator, name: []const u8, always_alloc: bool) !?util.FreeOrNot { +pub fn getCookieStr(self: *const Self, a: Allocator, name: []const u8) !?[]const u8 { if (self.h.*.cookies == 0) return null; const key = fio.fiobj_str_new(name.ptr, name.len); defer fio.fiobj_free_wrapped(key); @@ -689,7 +700,9 @@ pub fn getCookieStr(self: *const Self, a: std.mem.Allocator, name: []const u8, a if (value == fio.FIOBJ_INVALID) { return null; } - return try util.fio2strAllocOrNot(a, value, always_alloc); + // we are not entirely sure if cookies fiobjs are always strings + // hence. fio2strAlloc + return try util.fio2strAlloc(a, value); } /// Returns the number of cookies after parsing. @@ -708,15 +721,72 @@ pub fn getParamCount(self: *const Self) isize { return fio.fiobj_obj2num(self.h.*.params); } +const CallbackContext_KV = struct { + allocator: Allocator, + params: *std.ArrayList(HttpParamKV), + last_error: ?anyerror = null, + + pub fn callback(fiobj_value: fio.FIOBJ, context_: ?*anyopaque) callconv(.C) c_int { + const ctx: *@This() = @as(*@This(), @ptrCast(@alignCast(context_))); + // this is thread-safe, guaranteed by fio + const fiobj_key: fio.FIOBJ = fio.fiobj_hash_key_in_loop(); + ctx.params.append(.{ + .key = util.fio2strAlloc(ctx.allocator, fiobj_key) catch |err| { + ctx.last_error = err; + return -1; + }, + .value = fiobj2HttpParam(ctx.allocator, fiobj_value) 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; + } +}; + +const CallbackContext_StrKV = struct { + allocator: Allocator, + params: *std.ArrayList(HttpParamStrKV), + last_error: ?anyerror = null, + + pub fn callback(fiobj_value: fio.FIOBJ, context_: ?*anyopaque) callconv(.C) c_int { + const ctx: *@This() = @as(*@This(), @ptrCast(@alignCast(context_))); + // this is thread-safe, guaranteed by fio + const fiobj_key: fio.FIOBJ = fio.fiobj_hash_key_in_loop(); + ctx.params.append(.{ + .key = util.fio2strAlloc(ctx.allocator, fiobj_key) catch |err| { + ctx.last_error = err; + return -1; + }, + .value = util.fio2strAlloc(ctx.allocator, fiobj_value) 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; + } +}; + /// Same as parametersToOwnedStrList() but for cookies -pub fn cookiesToOwnedStrList(self: *const Self, a: std.mem.Allocator, always_alloc: bool) anyerror!HttpParamStrKVList { +pub fn cookiesToOwnedStrList(self: *const Self, a: Allocator) anyerror!HttpParamStrKVList { var params = try std.ArrayList(HttpParamStrKV).initCapacity(a, @as(usize, @intCast(self.getCookiesCount()))); - var context: _parametersToOwnedStrSliceContext = .{ + var context: CallbackContext_StrKV = .{ .params = ¶ms, .allocator = a, - .always_alloc = always_alloc, }; - const howmany = fio.fiobj_each1(self.h.*.cookies, 0, _each_nextParamStr, &context); + const howmany = fio.fiobj_each1(self.h.*.cookies, 0, CallbackContext_StrKV.callback, &context); if (howmany != self.getCookiesCount()) { return error.HttpIterParams; } @@ -724,10 +794,10 @@ pub fn cookiesToOwnedStrList(self: *const Self, a: std.mem.Allocator, always_all } /// Same as parametersToOwnedList() but for cookies -pub fn cookiesToOwnedList(self: *const Self, a: std.mem.Allocator, dupe_strings: bool) !HttpParamKVList { +pub fn cookiesToOwnedList(self: *const Self, a: Allocator) !HttpParamKVList { var params = try std.ArrayList(HttpParamKV).initCapacity(a, @as(usize, @intCast(self.getCookiesCount()))); - var context: _parametersToOwnedSliceContext = .{ .params = ¶ms, .allocator = a, .dupe_strings = dupe_strings }; - const howmany = fio.fiobj_each1(self.h.*.cookies, 0, _each_nextParam, &context); + var context: CallbackContext_KV = .{ .params = ¶ms, .allocator = a }; + const howmany = fio.fiobj_each1(self.h.*.cookies, 0, CallbackContext_KV.callback, &context); if (howmany != self.getCookiesCount()) { return error.HttpIterParams; } @@ -747,50 +817,21 @@ pub fn cookiesToOwnedList(self: *const Self, a: std.mem.Allocator, dupe_strings: /// /// 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 { +pub fn parametersToOwnedStrList(self: *const Self, a: Allocator) anyerror!HttpParamStrKVList { var params = try std.ArrayList(HttpParamStrKV).initCapacity(a, @as(usize, @intCast(self.getParamCount()))); - var context: _parametersToOwnedStrSliceContext = .{ + + var context: CallbackContext_StrKV = .{ .params = ¶ms, .allocator = a, - .always_alloc = always_alloc, }; - const howmany = fio.fiobj_each1(self.h.*.params, 0, _each_nextParamStr, &context); + + const howmany = fio.fiobj_each1(self.h.*.params, 0, CallbackContext_StrKV.callback, &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 = @as(*_parametersToOwnedStrSliceContext, @ptrCast(@alignCast(context))); - // this is thread-safe, guaranteed by fio - const fiobj_key: fio.FIOBJ = fio.fiobj_hash_key_in_loop(); - ctx.params.append(.{ - .key = util.fio2strAllocOrNot(ctx.allocator, fiobj_key, ctx.always_alloc) catch |err| { - ctx.last_error = err; - return -1; - }, - .value = util.fio2strAllocOrNot(ctx.allocator, fiobj_value, 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: /// @@ -804,47 +845,19 @@ fn _each_nextParamStr(fiobj_value: fio.FIOBJ, context: ?*anyopaque) callconv(.C) /// /// 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 { +pub fn parametersToOwnedList(self: *const Self, a: Allocator) !HttpParamKVList { var params = try std.ArrayList(HttpParamKV).initCapacity(a, @as(usize, @intCast(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); + + var context: CallbackContext_KV = .{ .params = ¶ms, .allocator = a }; + + const howmany = fio.fiobj_each1(self.h.*.params, 0, CallbackContext_KV.callback, &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 = @as(*_parametersToOwnedSliceContext, @ptrCast(@alignCast(context))); - // this is thread-safe, guaranteed by fio - const fiobj_key: fio.FIOBJ = fio.fiobj_hash_key_in_loop(); - ctx.params.append(.{ - .key = util.fio2strAllocOrNot(ctx.allocator, fiobj_key, ctx.dupe_strings) catch |err| { - ctx.last_error = err; - return -1; - }, - .value = Fiobj2HttpParam(ctx.allocator, fiobj_value, 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 +/// get named parameter (parsed) as string /// Supported param types that will be converted: /// /// - Bool @@ -856,8 +869,8 @@ fn _each_nextParam(fiobj_value: fio.FIOBJ, context: ?*anyopaque) callconv(.C) c_ /// 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, a: std.mem.Allocator, name: []const u8, always_alloc: bool) !?util.FreeOrNot { +/// The returned string needs to be deallocated. +pub fn getParamStr(self: *const Self, a: Allocator, name: []const u8) !?[]const u8 { if (self.h.*.params == 0) return null; const key = fio.fiobj_str_new(name.ptr, name.len); defer fio.fiobj_free_wrapped(key); @@ -865,7 +878,7 @@ pub fn getParamStr(self: *const Self, a: std.mem.Allocator, name: []const u8, al if (value == fio.FIOBJ_INVALID) { return null; } - return try util.fio2strAllocOrNot(a, value, always_alloc); + return try util.fio2strAlloc(a, value); } /// similar to getParamStr, except it will return the part of the querystring diff --git a/src/tests/test_http_params.zig b/src/tests/test_http_params.zig index 167cbc0..df4ef18 100644 --- a/src/tests/test_http_params.zig +++ b/src/tests/test_http_params.zig @@ -26,7 +26,7 @@ test "http parameters" { var strParams: ?zap.Request.HttpParamStrKVList = null; var params: ?zap.Request.HttpParamKVList = null; - var paramOneStr: ?zap.util.FreeOrNot = null; + var paramOneStr: ?[]const u8 = null; var paramOneSlice: ?[]const u8 = null; var paramSlices: zap.Request.ParamSliceIterator = undefined; @@ -36,20 +36,18 @@ test "http parameters" { param_count = r.getParamCount(); // true -> make copies of temp strings - strParams = r.parametersToOwnedStrList(alloc, true) catch unreachable; + strParams = r.parametersToOwnedStrList(alloc) catch unreachable; // true -> make copies of temp strings - params = r.parametersToOwnedList(alloc, true) catch unreachable; + params = r.parametersToOwnedList(alloc) catch unreachable; - var maybe_str = r.getParamStr(alloc, "one", true) catch unreachable; - if (maybe_str) |*s| { - paramOneStr = s.*; - } + paramOneStr = r.getParamStr(alloc, "one") catch unreachable; + std.debug.print("\n\nparamOneStr = {s} = {*} \n\n", .{ paramOneStr.?, paramOneStr.?.ptr }); - paramOneSlice = blk: { - if (r.getParamSlice("one")) |val| break :blk alloc.dupe(u8, val) catch unreachable; - break :blk null; - }; + // we need to dupe it here because the request object r will get + // invalidated at the end of the function but we need to check + // its correctness later in the test + paramOneSlice = if (r.getParamSlice("one")) |slice| alloc.dupe(u8, slice) catch unreachable else null; paramSlices = r.getParamSlices(); } @@ -84,44 +82,45 @@ test "http parameters" { if (Handler.params) |*p| { p.deinit(); } - if (Handler.paramOneStr) |*p| { - // allocator.free(p); - p.deinit(); + + if (Handler.paramOneStr) |p| { + allocator.free(p); } if (Handler.paramOneSlice) |p| { - Handler.alloc.free(p); + allocator.free(p); } } - try std.testing.expectEqual(Handler.ran, true); - try std.testing.expectEqual(Handler.param_count, 5); + try std.testing.expectEqual(true, Handler.ran); + try std.testing.expectEqual(5, Handler.param_count); try std.testing.expect(Handler.paramOneStr != null); - try std.testing.expectEqualStrings(Handler.paramOneStr.?.str, "1"); + std.debug.print("\n\n=== Handler.paramOneStr = {s} = {*}\n\n", .{ Handler.paramOneStr.?, Handler.paramOneStr.?.ptr }); + try std.testing.expectEqualStrings("1", Handler.paramOneStr.?); try std.testing.expect(Handler.paramOneSlice != null); - try std.testing.expectEqualStrings(Handler.paramOneSlice.?, "1"); + try std.testing.expectEqualStrings("1", Handler.paramOneSlice.?); 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"); + try std.testing.expectEqualStrings("one", kv.key); + try std.testing.expectEqualStrings("1", kv.value); }, 1 => { - try std.testing.expectEqualStrings(kv.key.str, "two"); - try std.testing.expectEqualStrings(kv.value.str, "2"); + try std.testing.expectEqualStrings("two", kv.key); + try std.testing.expectEqualStrings("2", kv.value); }, 2 => { - try std.testing.expectEqualStrings(kv.key.str, "string"); - try std.testing.expectEqualStrings(kv.value.str, "hello world"); + try std.testing.expectEqualStrings("string", kv.key); + try std.testing.expectEqualStrings("hello world", kv.value); }, 3 => { - try std.testing.expectEqualStrings(kv.key.str, "float"); - try std.testing.expectEqualStrings(kv.value.str, "6.28"); + try std.testing.expectEqualStrings("float", kv.key); + try std.testing.expectEqualStrings("6.28", kv.value); }, 4 => { - try std.testing.expectEqualStrings(kv.key.str, "bool"); - try std.testing.expectEqualStrings(kv.value.str, "true"); + try std.testing.expectEqualStrings("bool", kv.key); + try std.testing.expectEqualStrings("true", kv.value); }, else => return error.TooManyArgs, } @@ -131,24 +130,24 @@ test "http parameters" { while (Handler.paramSlices.next()) |param| { switch (pindex) { 0 => { - try std.testing.expectEqualStrings(param.name, "one"); - try std.testing.expectEqualStrings(param.value, "1"); + try std.testing.expectEqualStrings("one", param.name); + try std.testing.expectEqualStrings("1", param.value); }, 1 => { - try std.testing.expectEqualStrings(param.name, "two"); - try std.testing.expectEqualStrings(param.value, "2"); + try std.testing.expectEqualStrings("two", param.name); + try std.testing.expectEqualStrings("2", param.value); }, 2 => { - try std.testing.expectEqualStrings(param.name, "string"); - try std.testing.expectEqualStrings(param.value, "hello+world"); + try std.testing.expectEqualStrings("string", param.name); + try std.testing.expectEqualStrings("hello+world", param.value); }, 3 => { - try std.testing.expectEqualStrings(param.name, "float"); - try std.testing.expectEqualStrings(param.value, "6.28"); + try std.testing.expectEqualStrings("float", param.name); + try std.testing.expectEqualStrings("6.28", param.value); }, 4 => { - try std.testing.expectEqualStrings(param.name, "bool"); - try std.testing.expectEqualStrings(param.value, "true"); + try std.testing.expectEqualStrings("bool", param.name); + try std.testing.expectEqualStrings("true", param.value); }, else => return error.TooManyArgs, } @@ -158,42 +157,42 @@ test "http parameters" { for (Handler.params.?.items, 0..) |kv, i| { switch (i) { 0 => { - try std.testing.expectEqualStrings(kv.key.str, "one"); + try std.testing.expectEqualStrings("one", kv.key); try std.testing.expect(kv.value != null); switch (kv.value.?) { - .Int => |n| try std.testing.expectEqual(n, 1), + .Int => |n| try std.testing.expectEqual(1, n), else => return error.InvalidHttpParamType, } }, 1 => { - try std.testing.expectEqualStrings(kv.key.str, "two"); + try std.testing.expectEqualStrings("two", kv.key); try std.testing.expect(kv.value != null); switch (kv.value.?) { - .Int => |n| try std.testing.expectEqual(n, 2), + .Int => |n| try std.testing.expectEqual(2, n), else => return error.InvalidHttpParamType, } }, 2 => { - try std.testing.expectEqualStrings(kv.key.str, "string"); + try std.testing.expectEqualStrings("string", kv.key); try std.testing.expect(kv.value != null); switch (kv.value.?) { - .String => |s| try std.testing.expectEqualStrings(s.str, "hello world"), + .String => |s| try std.testing.expectEqualStrings("hello world", s), else => return error.InvalidHttpParamType, } }, 3 => { - try std.testing.expectEqualStrings(kv.key.str, "float"); + try std.testing.expectEqualStrings("float", kv.key); try std.testing.expect(kv.value != null); switch (kv.value.?) { - .Float => |f| try std.testing.expectEqual(f, 6.28), + .Float => |f| try std.testing.expectEqual(6.28, f), else => return error.InvalidHttpParamType, } }, 4 => { - try std.testing.expectEqualStrings(kv.key.str, "bool"); + try std.testing.expectEqualStrings("bool", kv.key); try std.testing.expect(kv.value != null); switch (kv.value.?) { - .Bool => |b| try std.testing.expectEqual(b, true), + .Bool => |b| try std.testing.expectEqual(true, b), else => return error.InvalidHttpParamType, } }, diff --git a/src/util.zig b/src/util.zig index 663155c..1719fe9 100644 --- a/src/util.zig +++ b/src/util.zig @@ -6,6 +6,9 @@ const zap = @import("zap.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. FIO temp memory strings do not need to be freed. +/// +/// IMPORTANT!!! The "temp" memory can refer to a shared buffer that subsequent +/// calls to this function will **overwrite**!!! pub fn fio2str(o: fio.FIOBJ) ?[]const u8 { if (o == 0) return null; const x: fio.fio_str_info_s = fio.fiobj_obj2cstr(o); @@ -14,39 +17,21 @@ pub fn fio2str(o: fio.FIOBJ) ?[]const u8 { return x.data[0..x.len]; } -/// A "string" type used internally that carries a flag whether its buffer needs -/// to be freed or not - and honors it in `deinit()`. That way, it's always -/// safe to call deinit(). -/// For instance, slices taken directly from the zap.Request need not be freed. -/// But the ad-hoc created string representation of a float parameter must be -/// freed after use. -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); - } - } -}; - /// Used internally: convert a FIO object into its string representation. -/// Depending on the type of the object, a buffer will be created. Hence a -/// FreeOrNot type is used as the return type. -pub fn fio2strAllocOrNot(a: std.mem.Allocator, o: fio.FIOBJ, always_alloc: bool) !FreeOrNot { - if (o == 0) return .{ .str = "null", .freeme = false }; - if (o == fio.FIOBJ_INVALID) return .{ .str = "invalid", .freeme = false }; +/// This always allocates, so choose your allocator wisely. +/// Let's never use that +pub fn fio2strAlloc(a: std.mem.Allocator, o: fio.FIOBJ) ![]const u8 { + if (o == 0) return try a.dupe(u8, "null"); + if (o == fio.FIOBJ_INVALID) return try a.dupe(u8, "invalid"); return switch (fio.fiobj_type(o)) { - fio.FIOBJ_T_TRUE => .{ .str = "true", .freeme = false }, - fio.FIOBJ_T_FALSE => .{ .str = "false", .freeme = false }, + fio.FIOBJ_T_TRUE => try a.dupe(u8, "true"), + fio.FIOBJ_T_FALSE => try a.dupe(u8, "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 = "unknown_type", .freeme = false }, + fio.FIOBJ_T_NUMBER => try a.dupe(u8, fio2str(o) orelse "null"), + fio.FIOBJ_T_FLOAT => try a.dupe(u8, fio2str(o) orelse "null"), + // if the string comes out of the request, it is safe to not make a copy + fio.FIOBJ_T_STRING => try a.dupe(u8, fio2str(o) orelse ""), + else => try a.dupe(u8, "unknown_type"), }; } From 3aaa7fcc24771dd9b1e0773b7437c08e1132e24e Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Fri, 21 Mar 2025 18:25:44 +0100 Subject: [PATCH 33/57] removed Self and @This() as much as possible --- examples/endpoint/stopendpoint.zig | 16 ++-- examples/endpoint/users.zig | 26 +++--- examples/endpoint/userweb.zig | 24 +++--- examples/middleware/middleware.zig | 32 +++---- .../middleware_with_endpoint.zig | 28 +++--- examples/simple_router/simple_router.zig | 10 +-- examples/websockets/websockets.zig | 8 +- src/endpoint.zig | 50 ++++++----- src/http_auth.zig | 54 ++++++------ src/log.zig | 6 +- src/mustache.zig | 18 ++-- src/request.zig | 86 +++++++++---------- src/router.zig | 16 ++-- src/tests/test_http_params.zig | 1 - src/zap.zig | 19 ++-- 15 files changed, 181 insertions(+), 213 deletions(-) diff --git a/examples/endpoint/stopendpoint.zig b/examples/endpoint/stopendpoint.zig index d0e0a12..f96ec37 100644 --- a/examples/endpoint/stopendpoint.zig +++ b/examples/endpoint/stopendpoint.zig @@ -3,25 +3,25 @@ const zap = @import("zap"); /// A simple endpoint listening on the /stop route that shuts down zap /// the main thread usually continues at the instructions after the call to zap.start(). -pub const Self = @This(); +pub const StopEndpoint = @This(); path: []const u8, error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response, -pub fn init(path: []const u8) Self { +pub fn init(path: []const u8) StopEndpoint { return .{ .path = path, }; } -pub fn get(e: *Self, r: zap.Request) anyerror!void { +pub fn get(e: *StopEndpoint, r: zap.Request) anyerror!void { _ = e; _ = r; zap.stop(); } -pub fn post(_: *Self, _: zap.Request) anyerror!void {} -pub fn put(_: *Self, _: zap.Request) anyerror!void {} -pub fn delete(_: *Self, _: zap.Request) anyerror!void {} -pub fn patch(_: *Self, _: zap.Request) anyerror!void {} -pub fn options(_: *Self, _: zap.Request) anyerror!void {} +pub fn post(_: *StopEndpoint, _: zap.Request) anyerror!void {} +pub fn put(_: *StopEndpoint, _: zap.Request) anyerror!void {} +pub fn delete(_: *StopEndpoint, _: zap.Request) anyerror!void {} +pub fn patch(_: *StopEndpoint, _: zap.Request) anyerror!void {} +pub fn options(_: *StopEndpoint, _: zap.Request) anyerror!void {} diff --git a/examples/endpoint/users.zig b/examples/endpoint/users.zig index 2ab7a17..3238fd2 100644 --- a/examples/endpoint/users.zig +++ b/examples/endpoint/users.zig @@ -5,7 +5,7 @@ users: std.AutoHashMap(usize, InternalUser) = undefined, lock: std.Thread.Mutex = undefined, count: usize = 0, -pub const Self = @This(); +pub const Users = @This(); const InternalUser = struct { id: usize = 0, @@ -21,7 +21,7 @@ pub const User = struct { last_name: []const u8, }; -pub fn init(a: std.mem.Allocator) Self { +pub fn init(a: std.mem.Allocator) Users { return .{ .alloc = a, .users = std.AutoHashMap(usize, InternalUser).init(a), @@ -29,13 +29,13 @@ pub fn init(a: std.mem.Allocator) Self { }; } -pub fn deinit(self: *Self) void { +pub fn deinit(self: *Users) void { self.users.deinit(); } // the request will be freed (and its mem reused by facilio) when it's // completed, so we take copies of the names -pub fn addByName(self: *Self, first: ?[]const u8, last: ?[]const u8) !usize { +pub fn addByName(self: *Users, first: ?[]const u8, last: ?[]const u8) !usize { var user: InternalUser = undefined; user.firstnamelen = 0; user.lastnamelen = 0; @@ -64,7 +64,7 @@ pub fn addByName(self: *Self, first: ?[]const u8, last: ?[]const u8) !usize { } } -pub fn delete(self: *Self, id: usize) bool { +pub fn delete(self: *Users, id: usize) bool { // We lock only on insertion, deletion, and listing self.lock.lock(); defer self.lock.unlock(); @@ -76,7 +76,7 @@ pub fn delete(self: *Self, id: usize) bool { return ret; } -pub fn get(self: *Self, id: usize) ?User { +pub fn get(self: *Users, id: usize) ?User { // we don't care about locking here, as our usage-pattern is unlikely to // get a user by id that is not known yet if (self.users.getPtr(id)) |pUser| { @@ -90,7 +90,7 @@ pub fn get(self: *Self, id: usize) ?User { } pub fn update( - self: *Self, + self: *Users, id: usize, first: ?[]const u8, last: ?[]const u8, @@ -112,7 +112,7 @@ pub fn update( return false; } -pub fn toJSON(self: *Self) ![]const u8 { +pub fn toJSON(self: *Users) ![]const u8 { self.lock.lock(); defer self.lock.unlock(); @@ -137,7 +137,7 @@ pub fn toJSON(self: *Self) ![]const u8 { // // Note: the following code is kept in here because it taught us a lesson // -pub fn listWithRaceCondition(self: *Self, out: *std.ArrayList(User)) !void { +pub fn listWithRaceCondition(self: *Users, out: *std.ArrayList(User)) !void { // We lock only on insertion, deletion, and listing // // NOTE: race condition: @@ -169,18 +169,14 @@ pub fn listWithRaceCondition(self: *Self, out: *std.ArrayList(User)) !void { const JsonUserIteratorWithRaceCondition = struct { it: std.AutoHashMap(usize, InternalUser).ValueIterator = undefined, - const This = @This(); - // careful: - // - Self refers to the file's struct - // - This refers to the JsonUserIterator struct - pub fn init(internal_users: *std.AutoHashMap(usize, InternalUser)) This { + pub fn init(internal_users: *std.AutoHashMap(usize, InternalUser)) JsonUserIteratorWithRaceCondition { return .{ .it = internal_users.valueIterator(), }; } - pub fn next(this: *This) ?User { + pub fn next(this: *JsonUserIteratorWithRaceCondition) ?User { if (this.it.next()) |pUser| { // we get a pointer to the internal user. so it should be safe to // create slices from its first and last name buffers diff --git a/examples/endpoint/userweb.zig b/examples/endpoint/userweb.zig index e492408..d45370c 100644 --- a/examples/endpoint/userweb.zig +++ b/examples/endpoint/userweb.zig @@ -5,7 +5,7 @@ const User = Users.User; // an Endpoint -pub const Self = @This(); +pub const UserWeb = @This(); alloc: std.mem.Allocator = undefined, _users: Users = undefined, @@ -16,7 +16,7 @@ error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response, pub fn init( a: std.mem.Allocator, user_path: []const u8, -) Self { +) UserWeb { return .{ .alloc = a, ._users = Users.init(a), @@ -24,15 +24,15 @@ pub fn init( }; } -pub fn deinit(self: *Self) void { +pub fn deinit(self: *UserWeb) void { self._users.deinit(); } -pub fn users(self: *Self) *Users { +pub fn users(self: *UserWeb) *Users { return &self._users; } -fn userIdFromPath(self: *Self, path: []const u8) ?usize { +fn userIdFromPath(self: *UserWeb, path: []const u8) ?usize { if (path.len >= self.path.len + 2) { if (path[self.path.len] != '/') { return null; @@ -43,8 +43,8 @@ fn userIdFromPath(self: *Self, path: []const u8) ?usize { return null; } -pub fn put(_: *Self, _: zap.Request) anyerror!void {} -pub fn get(self: *Self, r: zap.Request) anyerror!void { +pub fn put(_: *UserWeb, _: zap.Request) anyerror!void {} +pub fn get(self: *UserWeb, r: zap.Request) anyerror!void { if (r.path) |path| { // /users if (path.len == self.path.len) { @@ -60,7 +60,7 @@ pub fn get(self: *Self, r: zap.Request) anyerror!void { } } -fn listUsers(self: *Self, r: zap.Request) !void { +fn listUsers(self: *UserWeb, r: zap.Request) !void { if (self._users.toJSON()) |json| { defer self.alloc.free(json); try r.sendJson(json); @@ -69,7 +69,7 @@ fn listUsers(self: *Self, r: zap.Request) !void { } } -pub fn post(self: *Self, r: zap.Request) anyerror!void { +pub fn post(self: *UserWeb, r: zap.Request) anyerror!void { if (r.body) |body| { const maybe_user: ?std.json.Parsed(User) = std.json.parseFromSlice(User, self.alloc, body, .{}) catch null; if (maybe_user) |u| { @@ -86,7 +86,7 @@ pub fn post(self: *Self, r: zap.Request) anyerror!void { } } -pub fn patch(self: *Self, r: zap.Request) anyerror!void { +pub fn patch(self: *UserWeb, r: zap.Request) anyerror!void { if (r.path) |path| { if (self.userIdFromPath(path)) |id| { if (self._users.get(id)) |_| { @@ -109,7 +109,7 @@ pub fn patch(self: *Self, r: zap.Request) anyerror!void { } } -pub fn delete(self: *Self, r: zap.Request) anyerror!void { +pub fn delete(self: *UserWeb, r: zap.Request) anyerror!void { if (r.path) |path| { if (self.userIdFromPath(path)) |id| { var jsonbuf: [128]u8 = undefined; @@ -124,7 +124,7 @@ pub fn delete(self: *Self, r: zap.Request) anyerror!void { } } -pub fn options(_: *Self, r: zap.Request) anyerror!void { +pub fn options(_: *UserWeb, r: zap.Request) anyerror!void { try r.setHeader("Access-Control-Allow-Origin", "*"); try r.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); r.setStatus(zap.http.StatusCode.no_content); diff --git a/examples/middleware/middleware.zig b/examples/middleware/middleware.zig index a02895f..844272e 100644 --- a/examples/middleware/middleware.zig +++ b/examples/middleware/middleware.zig @@ -6,8 +6,6 @@ const SharedAllocator = struct { // static var allocator: std.mem.Allocator = undefined; - const Self = @This(); - // just a convenience function pub fn init(a: std.mem.Allocator) void { allocator = a; @@ -43,8 +41,6 @@ const Handler = zap.Middleware.Handler(Context); const UserMiddleWare = struct { handler: Handler, - const Self = @This(); - // Just some arbitrary struct we want in the per-request context // note: it MUST have all default values!!! // note: it MUST have all default values!!! @@ -57,22 +53,22 @@ const UserMiddleWare = struct { email: []const u8 = undefined, }; - pub fn init(other: ?*Handler) Self { + pub fn init(other: ?*Handler) UserMiddleWare { return .{ .handler = Handler.init(onRequest, other), }; } // we need the handler as a common interface to chain stuff - pub fn getHandler(self: *Self) *Handler { + pub fn getHandler(self: *UserMiddleWare) *Handler { return &self.handler; } - // note that the first parameter is of type *Handler, not *Self !!! + // note that the first parameter is of type *Handler, not *UserMiddleWare !!! pub fn onRequest(handler: *Handler, r: zap.Request, context: *Context) !bool { // this is how we would get our self pointer - const self: *Self = @fieldParentPtr("handler", handler); + const self: *UserMiddleWare = @fieldParentPtr("handler", handler); _ = self; // do our work: fill in the user field of the context @@ -92,8 +88,6 @@ const UserMiddleWare = struct { const SessionMiddleWare = struct { handler: Handler, - const Self = @This(); - // Just some arbitrary struct we want in the per-request context // note: it MUST have all default values!!! const Session = struct { @@ -101,21 +95,21 @@ const SessionMiddleWare = struct { token: []const u8 = undefined, }; - pub fn init(other: ?*Handler) Self { + pub fn init(other: ?*Handler) SessionMiddleWare { return .{ .handler = Handler.init(onRequest, other), }; } // we need the handler as a common interface to chain stuff - pub fn getHandler(self: *Self) *Handler { + pub fn getHandler(self: *SessionMiddleWare) *Handler { return &self.handler; } - // note that the first parameter is of type *Handler, not *Self !!! + // note that the first parameter is of type *Handler, not *SessionMiddleWare !!! pub fn onRequest(handler: *Handler, r: zap.Request, context: *Context) !bool { // this is how we would get our self pointer - const self: *Self = @fieldParentPtr("handler", handler); + const self: *SessionMiddleWare = @fieldParentPtr("handler", handler); _ = self; context.session = Session{ @@ -134,24 +128,22 @@ const SessionMiddleWare = struct { const HtmlMiddleWare = struct { handler: Handler, - const Self = @This(); - - pub fn init(other: ?*Handler) Self { + pub fn init(other: ?*Handler) HtmlMiddleWare { return .{ .handler = Handler.init(onRequest, other), }; } // we need the handler as a common interface to chain stuff - pub fn getHandler(self: *Self) *Handler { + pub fn getHandler(self: *HtmlMiddleWare) *Handler { return &self.handler; } - // note that the first parameter is of type *Handler, not *Self !!! + // note that the first parameter is of type *Handler, not *HtmlMiddleWare !!! pub fn onRequest(handler: *Handler, r: zap.Request, context: *Context) !bool { // this is how we would get our self pointer - const self: *Self = @fieldParentPtr("handler", handler); + const self: *HtmlMiddleWare = @fieldParentPtr("handler", handler); _ = self; std.debug.print("\n\nHtmlMiddleware: handling request with context: {any}\n\n", .{context}); diff --git a/examples/middleware_with_endpoint/middleware_with_endpoint.zig b/examples/middleware_with_endpoint/middleware_with_endpoint.zig index 4d5f8b4..108cf28 100644 --- a/examples/middleware_with_endpoint/middleware_with_endpoint.zig +++ b/examples/middleware_with_endpoint/middleware_with_endpoint.zig @@ -6,8 +6,6 @@ const SharedAllocator = struct { // static var allocator: std.mem.Allocator = undefined; - const Self = @This(); - // just a convenience function pub fn init(a: std.mem.Allocator) void { allocator = a; @@ -35,8 +33,6 @@ const Handler = zap.Middleware.Handler(Context); const UserMiddleWare = struct { handler: Handler, - const Self = @This(); - // Just some arbitrary struct we want in the per-request context // This is so that it can be constructed via .{} // as we can't expect the listener to know how to initialize our context structs @@ -45,22 +41,22 @@ const UserMiddleWare = struct { email: []const u8 = undefined, }; - pub fn init(other: ?*Handler) Self { + pub fn init(other: ?*Handler) UserMiddleWare { return .{ .handler = Handler.init(onRequest, other), }; } // we need the handler as a common interface to chain stuff - pub fn getHandler(self: *Self) *Handler { + pub fn getHandler(self: *UserMiddleWare) *Handler { return &self.handler; } - // note that the first parameter is of type *Handler, not *Self !!! + // note that the first parameter is of type *Handler, not *UserMiddleWare !!! pub fn onRequest(handler: *Handler, r: zap.Request, context: *Context) !bool { // this is how we would get our self pointer - const self: *Self = @fieldParentPtr("handler", handler); + const self: *UserMiddleWare = @fieldParentPtr("handler", handler); _ = self; // do our work: fill in the user field of the context @@ -82,8 +78,6 @@ const UserMiddleWare = struct { const SessionMiddleWare = struct { handler: Handler, - const Self = @This(); - // Just some arbitrary struct we want in the per-request context // note: it MUST have all default values!!! const Session = struct { @@ -91,21 +85,21 @@ const SessionMiddleWare = struct { token: []const u8 = undefined, }; - pub fn init(other: ?*Handler) Self { + pub fn init(other: ?*Handler) SessionMiddleWare { return .{ .handler = Handler.init(onRequest, other), }; } // we need the handler as a common interface to chain stuff - pub fn getHandler(self: *Self) *Handler { + pub fn getHandler(self: *SessionMiddleWare) *Handler { return &self.handler; } - // note that the first parameter is of type *Handler, not *Self !!! + // note that the first parameter is of type *Handler, not *SessionMiddleWare !!! pub fn onRequest(handler: *Handler, r: zap.Request, context: *Context) !bool { // this is how we would get our self pointer - const self: *Self = @fieldParentPtr("handler", handler); + const self: *SessionMiddleWare = @fieldParentPtr("handler", handler); _ = self; context.session = Session{ @@ -136,11 +130,9 @@ const SessionMiddleWare = struct { // `breakOnFinish` parameter. // const HtmlEndpoint = struct { - const Self = @This(); - path: []const u8 = "(undefined)", - pub fn init() Self { + pub fn init() HtmlEndpoint { return .{ .path = "/doesn't+matter", }; @@ -152,7 +144,7 @@ const HtmlEndpoint = struct { pub fn patch(_: *HtmlEndpoint, _: zap.Request) !void {} pub fn options(_: *HtmlEndpoint, _: zap.Request) !void {} - pub fn get(_: *Self, r: zap.Request) !void { + pub fn get(_: *HtmlEndpoint, r: zap.Request) !void { var buf: [1024]u8 = undefined; var userFound: bool = false; var sessionFound: bool = false; diff --git a/examples/simple_router/simple_router.zig b/examples/simple_router/simple_router.zig index f91ed2c..72658ba 100644 --- a/examples/simple_router/simple_router.zig +++ b/examples/simple_router/simple_router.zig @@ -14,13 +14,11 @@ fn on_request_verbose(r: zap.Request) !void { } pub const SomePackage = struct { - const Self = @This(); - allocator: Allocator, a: i8, b: i8, - pub fn init(allocator: Allocator, a: i8, b: i8) Self { + pub fn init(allocator: Allocator, a: i8, b: i8) SomePackage { return .{ .allocator = allocator, .a = a, @@ -28,7 +26,7 @@ pub const SomePackage = struct { }; } - pub fn getA(self: *Self, req: zap.Request) !void { + pub fn getA(self: *SomePackage, req: zap.Request) !void { std.log.warn("get_a_requested", .{}); const string = std.fmt.allocPrint( @@ -41,7 +39,7 @@ pub const SomePackage = struct { req.sendBody(string) catch return; } - pub fn getB(self: *Self, req: zap.Request) !void { + pub fn getB(self: *SomePackage, req: zap.Request) !void { std.log.warn("get_b_requested", .{}); const string = std.fmt.allocPrint( @@ -54,7 +52,7 @@ pub const SomePackage = struct { req.sendBody(string) catch return; } - pub fn incrementA(self: *Self, req: zap.Request) !void { + pub fn incrementA(self: *SomePackage, req: zap.Request) !void { std.log.warn("increment_a_requested", .{}); self.a += 1; diff --git a/examples/websockets/websockets.zig b/examples/websockets/websockets.zig index d0e3cee..7e44452 100644 --- a/examples/websockets/websockets.zig +++ b/examples/websockets/websockets.zig @@ -20,13 +20,11 @@ const ContextManager = struct { lock: std.Thread.Mutex = .{}, contexts: ContextList = undefined, - const Self = @This(); - pub fn init( allocator: std.mem.Allocator, channelName: []const u8, usernamePrefix: []const u8, - ) Self { + ) ContextManager { return .{ .allocator = allocator, .channel = channelName, @@ -35,14 +33,14 @@ const ContextManager = struct { }; } - pub fn deinit(self: *Self) void { + pub fn deinit(self: *ContextManager) void { for (self.contexts.items) |ctx| { self.allocator.free(ctx.userName); } self.contexts.deinit(); } - pub fn newContext(self: *Self) !*Context { + pub fn newContext(self: *ContextManager) !*Context { self.lock.lock(); defer self.lock.unlock(); diff --git a/src/endpoint.zig b/src/endpoint.zig index 6b7dd95..d4e9159 100644 --- a/src/endpoint.zig +++ b/src/endpoint.zig @@ -37,11 +37,11 @@ //! }; //! } //! -//! pub fn post(_: *Self, _: zap.Request) void {} -//! pub fn put(_: *Self, _: zap.Request) void {} -//! pub fn delete(_: *Self, _: zap.Request) void {} -//! pub fn patch(_: *Self, _: zap.Request) void {} -//! pub fn options(_: *Self, _: zap.Request) void {} +//! pub fn post(_: *StopEndpoint, _: zap.Request) void {} +//! pub fn put(_: *StopEndpoint, _: zap.Request) void {} +//! pub fn delete(_: *StopEndpoint, _: zap.Request) void {} +//! pub fn patch(_: *StopEndpoint, _: zap.Request) void {} +//! pub fn options(_: *StopEndpoint, _: zap.Request) void {} //! //! pub fn get(self: *StopEndpoint, r: zap.Request) void { //! _ = self; @@ -117,24 +117,24 @@ pub const Wrapper = struct { wrapped: *T, wrapper: Internal, - const Self = @This(); + const Envelope = @This(); - pub fn unwrap(wrapper: *Internal) *Self { - const self: *Self = @alignCast(@fieldParentPtr("wrapper", wrapper)); + pub fn unwrap(wrapper: *Internal) *Envelope { + const self: *Envelope = @alignCast(@fieldParentPtr("wrapper", wrapper)); return self; } pub fn destroy(allocator: std.mem.Allocator, wrapper: *Internal) void { - const self: *Self = @alignCast(@fieldParentPtr("wrapper", wrapper)); + const self: *Envelope = @alignCast(@fieldParentPtr("wrapper", wrapper)); allocator.destroy(self); } pub fn onRequestWrapped(wrapper: *Internal, r: zap.Request) !void { - var self: *Self = Self.unwrap(wrapper); + var self: *Envelope = Envelope.unwrap(wrapper); try self.onRequest(r); } - pub fn onRequest(self: *Self, r: zap.Request) !void { + pub fn onRequest(self: *Envelope, r: zap.Request) !void { const ret = switch (r.methodAsEnum()) { .GET => self.wrapped.*.get(r), .POST => self.wrapped.*.post(r), @@ -150,7 +150,7 @@ pub const Wrapper = struct { switch (self.wrapped.*.error_strategy) { .raise => return err, .log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505), - .log_to_console => zap.debug("Error in {} {s} : {}", .{ Self, r.method orelse "(no method)", err }), + .log_to_console => zap.debug("Error in {} {s} : {}", .{ Envelope, r.method orelse "(no method)", err }), } } } @@ -176,12 +176,12 @@ pub fn Authenticating(EndpointType: type, Authenticator: type) type { ep: *EndpointType, path: []const u8, error_strategy: ErrorStrategy, - const Self = @This(); + const AuthenticatingEndpoint = @This(); /// Init the authenticating endpoint. Pass in a pointer to the endpoint /// you want to wrap, and the Authenticator that takes care of authenticating /// requests. - pub fn init(e: *EndpointType, authenticator: *Authenticator) Self { + pub fn init(e: *EndpointType, authenticator: *Authenticator) AuthenticatingEndpoint { return .{ .authenticator = authenticator, .ep = e, @@ -191,7 +191,7 @@ pub fn Authenticating(EndpointType: type, Authenticator: type) type { } /// Authenticates GET requests using the Authenticator. - pub fn get(self: *Self, r: zap.Request) anyerror!void { + pub fn get(self: *AuthenticatingEndpoint, r: zap.Request) anyerror!void { try switch (self.authenticator.authenticateRequest(&r)) { .AuthFailed => return self.ep.*.unauthorized(r), .AuthOK => self.ep.*.get(r), @@ -200,7 +200,7 @@ pub fn Authenticating(EndpointType: type, Authenticator: type) type { } /// Authenticates POST requests using the Authenticator. - pub fn post(self: *Self, r: zap.Request) anyerror!void { + pub fn post(self: *AuthenticatingEndpoint, r: zap.Request) anyerror!void { try switch (self.authenticator.authenticateRequest(&r)) { .AuthFailed => return self.ep.*.unauthorized(r), .AuthOK => self.ep.*.post(r), @@ -209,7 +209,7 @@ pub fn Authenticating(EndpointType: type, Authenticator: type) type { } /// Authenticates PUT requests using the Authenticator. - pub fn put(self: *Self, r: zap.Request) anyerror!void { + pub fn put(self: *AuthenticatingEndpoint, r: zap.Request) anyerror!void { try switch (self.authenticator.authenticateRequest(&r)) { .AuthFailed => return self.ep.*.unauthorized(r), .AuthOK => self.ep.*.put(r), @@ -218,7 +218,7 @@ pub fn Authenticating(EndpointType: type, Authenticator: type) type { } /// Authenticates DELETE requests using the Authenticator. - pub fn delete(self: *Self, r: zap.Request) anyerror!void { + pub fn delete(self: *AuthenticatingEndpoint, r: zap.Request) anyerror!void { try switch (self.authenticator.authenticateRequest(&r)) { .AuthFailed => return self.ep.*.unauthorized(r), .AuthOK => self.ep.*.delete(r), @@ -227,7 +227,7 @@ pub fn Authenticating(EndpointType: type, Authenticator: type) type { } /// Authenticates PATCH requests using the Authenticator. - pub fn patch(self: *Self, r: zap.Request) anyerror!void { + pub fn patch(self: *AuthenticatingEndpoint, r: zap.Request) anyerror!void { try switch (self.authenticator.authenticateRequest(&r)) { .AuthFailed => return self.ep.*.unauthorized(r), .AuthOK => self.ep.*.patch(r), @@ -236,7 +236,7 @@ pub fn Authenticating(EndpointType: type, Authenticator: type) type { } /// Authenticates OPTIONS requests using the Authenticator. - pub fn options(self: *Self, r: zap.Request) anyerror!void { + pub fn options(self: *AuthenticatingEndpoint, r: zap.Request) anyerror!void { try switch (self.authenticator.authenticateRequest(&r)) { .AuthFailed => return self.ep.*.unauthorized(r), .AuthOK => self.ep.*.put(r), @@ -261,8 +261,6 @@ pub const Listener = struct { listener: HttpListener, allocator: std.mem.Allocator, - const Self = @This(); - /// Internal static struct of member endpoints var endpoints: std.ArrayListUnmanaged(*Wrapper.Internal) = .empty; @@ -274,7 +272,7 @@ pub const Listener = struct { /// Initialize a new endpoint listener. Note, if you pass an `on_request` /// callback in the provided ListenerSettings, this request callback will be /// called every time a request arrives that no endpoint matches. - pub fn init(a: std.mem.Allocator, l: ListenerSettings) Self { + pub fn init(a: std.mem.Allocator, l: ListenerSettings) Listener { // reset the global in case init is called multiple times, as is the // case in the authentication tests endpoints = .empty; @@ -297,7 +295,7 @@ pub const Listener = struct { /// De-init the listener and free its resources. /// Registered endpoints will not be de-initialized automatically; just removed /// from the internal map. - pub fn deinit(self: *Self) void { + pub fn deinit(self: *Listener) void { for (endpoints.items) |endpoint_wrapper| { endpoint_wrapper.destroy(self.allocator, endpoint_wrapper); } @@ -306,7 +304,7 @@ pub const Listener = struct { /// Call this to start listening. After this, no more endpoints can be /// registered. - pub fn listen(self: *Self) !void { + pub fn listen(self: *Listener) !void { try self.listener.listen(); } @@ -314,7 +312,7 @@ pub const Listener = struct { /// NOTE: endpoint paths are matched with startsWith -> so use endpoints with distinctly starting names!! /// If you try to register an endpoint whose path would shadow an already registered one, you will /// receive an EndpointPathShadowError. - pub fn register(self: *Self, e: anytype) !void { + pub fn register(self: *Listener, e: anytype) !void { for (endpoints.items) |other| { if (std.mem.startsWith( u8, diff --git a/src/http_auth.zig b/src/http_auth.zig index e4b9fa3..d7cd127 100644 --- a/src/http_auth.zig +++ b/src/http_auth.zig @@ -86,14 +86,14 @@ pub fn Basic(comptime Lookup: type, comptime kind: BasicAuthStrategy) type { realm: ?[]const u8, lookup: *Lookup, - const Self = @This(); + const BasicAuth = @This(); /// Creates a BasicAuth. `lookup` must implement `.get([]const u8) -> []const u8` /// different implementations can /// - either decode, lookup and compare passwords /// - or just check for existence of the base64-encoded user:pass combination /// if realm is provided (not null), a copy of it is taken -> call deinit() to clean up - pub fn init(allocator: std.mem.Allocator, lookup: *Lookup, realm: ?[]const u8) !Self { + pub fn init(allocator: std.mem.Allocator, lookup: *Lookup, realm: ?[]const u8) !BasicAuth { return .{ .allocator = allocator, .lookup = lookup, @@ -102,7 +102,7 @@ pub fn Basic(comptime Lookup: type, comptime kind: BasicAuthStrategy) type { } /// Deinit the authenticator. - pub fn deinit(self: *Self) void { + pub fn deinit(self: *BasicAuth) void { if (self.realm) |the_realm| { self.allocator.free(the_realm); } @@ -110,7 +110,7 @@ pub fn Basic(comptime Lookup: type, comptime kind: BasicAuthStrategy) type { /// Use this to decode the auth_header into user:pass, lookup pass in lookup. /// Note: usually, you don't want to use this; you'd go for `authenticateRequest()`. - pub fn authenticateUserPass(self: *Self, auth_header: []const u8) AuthResult { + pub fn authenticateUserPass(self: *BasicAuth, auth_header: []const u8) AuthResult { zap.debug("AuthenticateUserPass\n", .{}); const encoded = auth_header[AuthScheme.Basic.str().len..]; const decoder = std.base64.standard.Decoder; @@ -173,14 +173,14 @@ pub fn Basic(comptime Lookup: type, comptime kind: BasicAuthStrategy) type { /// Use this to just look up if the base64-encoded auth_header exists in lookup. /// Note: usually, you don't want to use this; you'd go for `authenticateRequest()`. - pub fn authenticateToken68(self: *Self, auth_header: []const u8) AuthResult { + pub fn authenticateToken68(self: *BasicAuth, auth_header: []const u8) AuthResult { const token = auth_header[AuthScheme.Basic.str().len..]; return if (self.lookup.*.contains(token)) .AuthOK else .AuthFailed; } /// dispatch based on kind (.UserPass / .Token689) and try to authenticate based on the header. /// Note: usually, you don't want to use this; you'd go for `authenticateRequest()`. - pub fn authenticate(self: *Self, auth_header: []const u8) AuthResult { + pub fn authenticate(self: *BasicAuth, auth_header: []const u8) AuthResult { zap.debug("AUTHENTICATE\n", .{}); switch (kind) { .UserPass => return self.authenticateUserPass(auth_header), @@ -192,7 +192,7 @@ pub fn Basic(comptime Lookup: type, comptime kind: BasicAuthStrategy) type { /// /// Tries to extract the authentication header and perform the authentication. /// If no authentication header is found, an authorization header is tried. - pub fn authenticateRequest(self: *Self, r: *const zap.Request) AuthResult { + pub fn authenticateRequest(self: *BasicAuth, r: *const zap.Request) AuthResult { zap.debug("AUTHENTICATE REQUEST\n", .{}); if (extractAuthHeader(.Basic, r)) |auth_header| { zap.debug("Authentication Header found!\n", .{}); @@ -225,12 +225,10 @@ pub const BearerSingle = struct { token: []const u8, realm: ?[]const u8, - const Self = @This(); - /// Creates a Single-Token Bearer Authenticator. /// Takes a copy of the token. /// If realm is provided (not null), a copy is taken call deinit() to clean up. - pub fn init(allocator: std.mem.Allocator, token: []const u8, realm: ?[]const u8) !Self { + pub fn init(allocator: std.mem.Allocator, token: []const u8, realm: ?[]const u8) !BearerSingle { return .{ .allocator = allocator, .token = try allocator.dupe(u8, token), @@ -240,7 +238,7 @@ pub const BearerSingle = struct { /// Try to authenticate based on the header. /// Note: usually, you don't want to use this; you'd go for `authenticateRequest()`. - pub fn authenticate(self: *Self, auth_header: []const u8) AuthResult { + pub fn authenticate(self: *BearerSingle, auth_header: []const u8) AuthResult { if (checkAuthHeader(.Bearer, auth_header) == false) { return .AuthFailed; } @@ -251,7 +249,7 @@ pub const BearerSingle = struct { /// The zap authentication request handler. /// /// Tries to extract the authentication header and perform the authentication. - pub fn authenticateRequest(self: *Self, r: *const zap.Request) AuthResult { + pub fn authenticateRequest(self: *BearerSingle, r: *const zap.Request) AuthResult { if (extractAuthHeader(.Bearer, r)) |auth_header| { return self.authenticate(auth_header); } @@ -259,7 +257,7 @@ pub const BearerSingle = struct { } /// Deinits the authenticator. - pub fn deinit(self: *Self) void { + pub fn deinit(self: *BearerSingle) void { if (self.realm) |the_realm| { self.allocator.free(the_realm); } @@ -283,12 +281,12 @@ pub fn BearerMulti(comptime Lookup: type) type { lookup: *Lookup, realm: ?[]const u8, - const Self = @This(); + const BearerMultiAuth = @This(); /// Creates a Multi Token Bearer Authenticator. `lookup` must implement /// `.get([]const u8) -> []const u8` to look up tokens. /// If realm is provided (not null), a copy of it is taken -> call deinit() to clean up. - pub fn init(allocator: std.mem.Allocator, lookup: *Lookup, realm: ?[]const u8) !Self { + pub fn init(allocator: std.mem.Allocator, lookup: *Lookup, realm: ?[]const u8) !BearerMultiAuth { return .{ .allocator = allocator, .lookup = lookup, @@ -298,7 +296,7 @@ pub fn BearerMulti(comptime Lookup: type) type { /// Deinit the authenticator. Only required if a realm was provided at /// init() time. - pub fn deinit(self: *Self) void { + pub fn deinit(self: *BearerMultiAuth) void { if (self.realm) |the_realm| { self.allocator.free(the_realm); } @@ -306,7 +304,7 @@ pub fn BearerMulti(comptime Lookup: type) type { /// Try to authenticate based on the header. /// Note: usually, you don't want to use this; you'd go for `authenticateRequest()`. - pub fn authenticate(self: *Self, auth_header: []const u8) AuthResult { + pub fn authenticate(self: *BearerMultiAuth, auth_header: []const u8) AuthResult { if (checkAuthHeader(.Bearer, auth_header) == false) { return .AuthFailed; } @@ -317,7 +315,7 @@ pub fn BearerMulti(comptime Lookup: type) type { /// The zap authentication request handler. /// /// Tries to extract the authentication header and perform the authentication. - pub fn authenticateRequest(self: *Self, r: *const zap.Request) AuthResult { + pub fn authenticateRequest(self: *BearerMultiAuth, r: *const zap.Request) AuthResult { if (extractAuthHeader(.Bearer, r)) |auth_header| { return self.authenticate(auth_header); } @@ -388,7 +386,7 @@ pub fn UserPassSession(comptime Lookup: type, comptime lockedPwLookups: bool) ty passwordLookupLock: std.Thread.Mutex = .{}, tokenLookupLock: std.Thread.Mutex = .{}, - const Self = @This(); + const UserPassSessionAuth = @This(); const SessionTokenMap = std.StringHashMap(void); const Hash = std.crypto.hash.sha2.Sha256; @@ -400,8 +398,8 @@ pub fn UserPassSession(comptime Lookup: type, comptime lockedPwLookups: bool) ty allocator: std.mem.Allocator, lookup: *Lookup, args: UserPassSessionArgs, - ) !Self { - const ret: Self = .{ + ) !UserPassSessionAuth { + const ret: UserPassSessionAuth = .{ .allocator = allocator, .settings = .{ .usernameParam = try allocator.dupe(u8, args.usernameParam), @@ -419,7 +417,7 @@ pub fn UserPassSession(comptime Lookup: type, comptime lockedPwLookups: bool) ty } /// De-init this authenticator. - pub fn deinit(self: *Self) void { + pub fn deinit(self: *UserPassSessionAuth) void { self.allocator.free(self.settings.usernameParam); self.allocator.free(self.settings.passwordParam); self.allocator.free(self.settings.loginPage); @@ -434,7 +432,7 @@ pub fn UserPassSession(comptime Lookup: type, comptime lockedPwLookups: bool) ty } /// Check for session token cookie, remove the token from the valid tokens - pub fn logout(self: *Self, r: *const zap.Request) void { + pub fn logout(self: *UserPassSessionAuth, r: *const zap.Request) void { // we erase the list of valid tokens server-side (later) and set the // cookie to "invalid" on the client side. if (r.setCookie(.{ @@ -469,7 +467,7 @@ pub fn UserPassSession(comptime Lookup: type, comptime lockedPwLookups: bool) ty } } - fn _internal_authenticateRequest(self: *Self, r: *const zap.Request) AuthResult { + fn _internal_authenticateRequest(self: *UserPassSessionAuth, r: *const zap.Request) AuthResult { // if we're requesting the login page, let the request through if (r.path) |p| { if (std.mem.startsWith(u8, p, self.settings.loginPage)) { @@ -562,7 +560,7 @@ pub fn UserPassSession(comptime Lookup: type, comptime lockedPwLookups: bool) ty /// The zap authentication request handler. /// /// See above for how it works. - pub fn authenticateRequest(self: *Self, r: *const zap.Request) AuthResult { + pub fn authenticateRequest(self: *UserPassSessionAuth, r: *const zap.Request) AuthResult { switch (self._internal_authenticateRequest(r)) { .AuthOK => { // username and pass are ok -> created token, set header, caller can continue @@ -582,11 +580,11 @@ pub fn UserPassSession(comptime Lookup: type, comptime lockedPwLookups: bool) ty } } - fn redirect(self: *Self, r: *const zap.Request) !void { + fn redirect(self: *UserPassSessionAuth, r: *const zap.Request) !void { try r.redirectTo(self.settings.loginPage, self.settings.redirectCode); } - fn createSessionToken(self: *Self, username: []const u8, password: []const u8) ![]const u8 { + fn createSessionToken(self: *UserPassSessionAuth, username: []const u8, password: []const u8) ![]const u8 { var hasher = Hash.init(.{}); hasher.update(username); hasher.update(password); @@ -602,7 +600,7 @@ pub fn UserPassSession(comptime Lookup: type, comptime lockedPwLookups: bool) ty return token_str; } - fn createAndStoreSessionToken(self: *Self, username: []const u8, password: []const u8) ![]const u8 { + fn createAndStoreSessionToken(self: *UserPassSessionAuth, username: []const u8, password: []const u8) ![]const u8 { const token = try self.createSessionToken(username, password); self.tokenLookupLock.lock(); defer self.tokenLookupLock.unlock(); diff --git a/src/log.zig b/src/log.zig index 210eefc..8736511 100644 --- a/src/log.zig +++ b/src/log.zig @@ -5,15 +5,15 @@ const std = @import("std"); debugOn: bool, /// Access to facil.io's logging facilities -const Self = @This(); +const Log = @This(); -pub fn init(comptime debug: bool) Self { +pub fn init(comptime debug: bool) Log { return .{ .debugOn = debug, }; } -pub fn log(self: *const Self, comptime fmt: []const u8, args: anytype) void { +pub fn log(self: *const Log, comptime fmt: []const u8, args: anytype) void { if (self.debugOn) { std.debug.print("[zap] - " ++ fmt, args); } diff --git a/src/mustache.zig b/src/mustache.zig index 761b25f..82eeffe 100644 --- a/src/mustache.zig +++ b/src/mustache.zig @@ -2,7 +2,7 @@ const std = @import("std"); const fio = @import("fio.zig"); const util = @import("util.zig"); -const Self = @This(); +const Mustache = @This(); const struct_mustache_s = opaque {}; const mustache_s = struct_mustache_s; @@ -51,7 +51,7 @@ pub const Error = error{ /// Create a new `Mustache` instance; `deinit()` should be called to free /// the object after usage. -pub fn init(load_args: LoadArgs) Error!Self { +pub fn init(load_args: LoadArgs) Error!Mustache { var err: mustache_error_en = undefined; const args: MustacheLoadArgsFio = .{ @@ -72,7 +72,7 @@ pub fn init(load_args: LoadArgs) Error!Self { const ret = fiobj_mustache_new(args); switch (err) { - 0 => return Self{ + 0 => return Mustache{ .handle = ret.?, }, 1 => return Error.MUSTACHE_ERR_TOO_DEEP, @@ -93,18 +93,18 @@ pub fn init(load_args: LoadArgs) Error!Self { /// Convenience function to create a new `Mustache` instance with in-memory data loaded; /// `deinit()` should be called to free the object after usage.. -pub fn fromData(data: []const u8) Error!Self { - return Self.init(.{ .data = data }); +pub fn fromData(data: []const u8) Error!Mustache { + return Mustache.init(.{ .data = data }); } /// Convenience function to create a new `Mustache` instance with file-based data loaded; /// `deinit()` should be called to free the object after usage.. -pub fn fromFile(filename: []const u8) Error!Self { - return Self.init(.{ .filename = filename }); +pub fn fromFile(filename: []const u8) Error!Mustache { + return Mustache.init(.{ .filename = filename }); } /// Free the data backing a `Mustache` instance. -pub fn deinit(self: *Self) void { +pub fn deinit(self: *Mustache) void { fiobj_mustache_free(self.handle); } @@ -137,7 +137,7 @@ pub const BuildResult = struct { // TODO: The build may be slow because it needs to convert zig types to facil.io // types. However, this needs to be investigated into. // See `fiobjectify` for more information. -pub fn build(self: *Self, data: anytype) BuildResult { +pub fn build(self: *Mustache, data: anytype) BuildResult { const T = @TypeOf(data); if (@typeInfo(T) != .@"struct") { @compileError("No struct: '" ++ @typeName(T) ++ "'"); diff --git a/src/request.zig b/src/request.zig index 3fcc0ee..09c25f4 100644 --- a/src/request.zig +++ b/src/request.zig @@ -30,7 +30,7 @@ pub const HttpParamStrKV = struct { pub const HttpParamStrKVList = struct { items: []HttpParamStrKV, allocator: Allocator, - pub fn deinit(self: *@This()) void { + pub fn deinit(self: *HttpParamStrKVList) void { for (self.items) |item| { self.allocator.free(item.key); self.allocator.free(item.value); @@ -43,7 +43,7 @@ pub const HttpParamStrKVList = struct { pub const HttpParamKVList = struct { items: []HttpParamKV, allocator: Allocator, - pub fn deinit(self: *const @This()) void { + pub fn deinit(self: *const HttpParamKVList) void { for (self.items) |item| { self.allocator.free(item.key); if (item.value) |v| { @@ -109,7 +109,7 @@ pub const HttpParamBinaryFile = struct { filename: ?[]const u8 = null, /// format function for printing file upload data - pub fn format(value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + pub fn format(value: HttpParamBinaryFile, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { const d = value.data orelse "\\0"; const m = value.mimetype orelse "null"; const f = value.filename orelse "null"; @@ -286,30 +286,30 @@ pub const UserContext = struct { user_context: ?*anyopaque = null, }; -const Self = @This(); +const Request = @This(); /// mark the current request as finished. Important for middleware-style /// request handler chaining. Called when sending a body, redirecting, etc. -pub fn markAsFinished(self: *const Self, finished: bool) void { +pub fn markAsFinished(self: *const Request, finished: bool) void { // we might be a copy self._is_finished.* = finished; } /// tell whether request processing has finished. (e.g. response sent, /// redirected, ...) -pub fn isFinished(self: *const Self) bool { +pub fn isFinished(self: *const Request) bool { // we might be a copy return self._is_finished.*; } /// if you absolutely must, you can set any context on the request here // (note, this line is linked to from the readme) -- TODO: sync -pub fn setUserContext(self: *const Self, context: *anyopaque) void { +pub fn setUserContext(self: *const Request, context: *anyopaque) void { self._user_context.*.user_context = context; } /// get the associated user context of the request. -pub fn getUserContext(self: *const Self, comptime Context: type) ?*Context { +pub fn getUserContext(self: *const Request, comptime Context: type) ?*Context { if (self._user_context.*.user_context) |ptr| { return @as(*Context, @ptrCast(@alignCast(ptr))); } else { @@ -322,7 +322,7 @@ pub fn getUserContext(self: *const Self, comptime Context: type) ?*Context { /// const err = zap.HttpError; // this is to show that `err` is an Error /// r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505); /// ``` -pub fn sendError(self: *const Self, err: anyerror, err_trace: ?std.builtin.StackTrace, errorcode_num: usize) void { +pub fn sendError(self: *const Request, err: anyerror, err_trace: ?std.builtin.StackTrace, errorcode_num: usize) void { // TODO: query accept headers if (self._internal_sendError(err, err_trace, errorcode_num)) { return; @@ -332,7 +332,7 @@ pub fn sendError(self: *const Self, err: anyerror, err_trace: ?std.builtin.Stack } /// Used internally. Probably does not need to be public. -pub fn _internal_sendError(self: *const Self, err: anyerror, err_trace: ?std.builtin.StackTrace, errorcode_num: usize) !void { +pub fn _internal_sendError(self: *const Request, err: anyerror, err_trace: ?std.builtin.StackTrace, errorcode_num: usize) !void { // TODO: query accept headers // TODO: let's hope 20k is enough. Maybe just really allocate here self.h.*.status = errorcode_num; @@ -352,7 +352,7 @@ pub fn _internal_sendError(self: *const Self, err: anyerror, err_trace: ?std.bui } /// Send body. -pub fn sendBody(self: *const Self, body: []const u8) HttpError!void { +pub fn sendBody(self: *const Request, body: []const u8) HttpError!void { const ret = fio.http_send_body(self.h, @as( *anyopaque, @ptrFromInt(@intFromPtr(body.ptr)), @@ -363,7 +363,7 @@ pub fn sendBody(self: *const Self, body: []const u8) HttpError!void { } /// Set content type and send json buffer. -pub fn sendJson(self: *const Self, json: []const u8) HttpError!void { +pub fn sendJson(self: *const Request, json: []const u8) HttpError!void { if (self.setContentType(.JSON)) { if (fio.http_send_body(self.h, @as( *anyopaque, @@ -374,7 +374,7 @@ pub fn sendJson(self: *const Self, json: []const u8) HttpError!void { } /// Set content type. -pub fn setContentType(self: *const Self, c: ContentType) HttpError!void { +pub fn setContentType(self: *const Request, c: ContentType) HttpError!void { const s = switch (c) { .TEXT => "text/plain", .JSON => "application/json", @@ -385,7 +385,7 @@ pub fn setContentType(self: *const Self, c: ContentType) HttpError!void { } /// redirect to path with status code 302 by default -pub fn redirectTo(self: *const Self, path: []const u8, code: ?http.StatusCode) HttpError!void { +pub fn redirectTo(self: *const Request, path: []const u8, code: ?http.StatusCode) HttpError!void { self.setStatus(if (code) |status| status else .found); try self.setHeader("Location", path); try self.sendBody("moved"); @@ -394,7 +394,7 @@ pub fn redirectTo(self: *const Self, path: []const u8, code: ?http.StatusCode) H /// shows how to use the logger pub fn setContentTypeWithLogger( - self: *const Self, + self: *const Request, c: ContentType, logger: *const Log, ) HttpError!void { @@ -408,7 +408,7 @@ pub fn setContentTypeWithLogger( } /// Tries to determine the content type by file extension of request path, and sets it. -pub fn setContentTypeFromPath(self: *const Self) !void { +pub fn setContentTypeFromPath(self: *const Request) !void { const t = fio.http_mimetype_find2(self.h.*.path); if (fio.is_invalid(t) == 1) return error.HttpSetContentType; const ret = fio.fiobj_hash_set( @@ -422,7 +422,7 @@ pub fn setContentTypeFromPath(self: *const Self) !void { /// Tries to determine the content type by filename extension, and sets it. /// If the extension cannot be determined, NoExtensionInFilename error is /// returned. -pub fn setContentTypeFromFilename(self: *const Self, filename: []const u8) !void { +pub fn setContentTypeFromFilename(self: *const Request, filename: []const u8) !void { const ext = std.fs.path.extension(filename); if (ext.len > 1) { @@ -442,7 +442,7 @@ pub fn setContentTypeFromFilename(self: *const Self, filename: []const u8) !void /// NOTE that header-names are lowerased automatically while parsing the request. /// so please only use lowercase keys! /// Returned mem is temp. Do not free it. -pub fn getHeader(self: *const Self, name: []const u8) ?[]const u8 { +pub fn getHeader(self: *const Request, name: []const u8) ?[]const u8 { const hname = fio.fiobj_str_new(util.toCharPtr(name), name.len); defer fio.fiobj_free_wrapped(hname); // fio2str is OK since headers are always strings -> slice is returned @@ -485,7 +485,7 @@ pub const HttpHeaderCommon = enum(usize) { /// Returns the header value of a given common header key. Returned memory /// should not be freed. -pub fn getHeaderCommon(self: *const Self, which: HttpHeaderCommon) ?[]const u8 { +pub fn getHeaderCommon(self: *const Request, which: HttpHeaderCommon) ?[]const u8 { const field = switch (which) { .accept => fio.HTTP_HEADER_ACCEPT, .cache_control => fio.HTTP_HEADER_CACHE_CONTROL, @@ -510,7 +510,7 @@ pub fn getHeaderCommon(self: *const Self, which: HttpHeaderCommon) ?[]const u8 { } /// Set header. -pub fn setHeader(self: *const Self, name: []const u8, value: []const u8) HttpError!void { +pub fn setHeader(self: *const Request, name: []const u8, value: []const u8) HttpError!void { const hname: fio.fio_str_info_s = .{ .data = util.toCharPtr(name), .len = name.len, @@ -538,12 +538,12 @@ pub fn setHeader(self: *const Self, name: []const u8, value: []const u8) HttpErr } /// Set status by numeric value. -pub fn setStatusNumeric(self: *const Self, status: usize) void { +pub fn setStatusNumeric(self: *const Request, status: usize) void { self.h.*.status = status; } /// Set status by enum. -pub fn setStatus(self: *const Self, status: http.StatusCode) void { +pub fn setStatus(self: *const Request, status: http.StatusCode) void { self.h.*.status = @as(usize, @intCast(@intFromEnum(status))); } @@ -558,7 +558,7 @@ pub fn setStatus(self: *const Self, status: http.StatusCode) void { /// /// Important: sets last-modified and cache-control headers with a max-age value of 1 hour! /// You can override that by setting those headers yourself, e.g.: setHeader("Cache-Control", "no-cache") -pub fn sendFile(self: *const Self, file_path: []const u8) !void { +pub fn sendFile(self: *const Request, file_path: []const u8) !void { if (fio.http_sendfile2(self.h, util.toCharPtr(file_path), file_path.len, null, 0) != 0) return error.SendFile; self.markAsFinished(true); @@ -572,7 +572,7 @@ pub fn sendFile(self: *const Self, file_path: []const u8) !void { /// - application/x-www-form-urlencoded /// - application/json /// - multipart/form-data -pub fn parseBody(self: *const Self) HttpError!void { +pub fn parseBody(self: *const Request) HttpError!void { if (fio.http_parse_body(self.h) == -1) return error.HttpParseBody; } @@ -581,12 +581,12 @@ pub fn parseBody(self: *const Self) HttpError!void { /// object that doesn't have a hash map at its root. /// /// Result is accessible via parametersToOwnedSlice(), parametersToOwnedStrSlice() -pub fn parseQuery(self: *const Self) void { +pub fn parseQuery(self: *const Request) void { fio.http_parse_query(self.h); } /// Parse received cookie headers -pub fn parseCookies(self: *const Self, url_encoded: bool) void { +pub fn parseCookies(self: *const Request, url_encoded: bool) void { fio.http_parse_cookies(self.h, if (url_encoded) 1 else 0); } @@ -617,7 +617,7 @@ pub const AcceptItem = struct { const AcceptHeaderList = std.ArrayList(AcceptItem); /// Parses `Accept:` http header into `list`, ordered from highest q factor to lowest -pub fn parseAcceptHeaders(self: *const Self, allocator: Allocator) !AcceptHeaderList { +pub fn parseAcceptHeaders(self: *const Request, allocator: Allocator) !AcceptHeaderList { const accept_str = self.getHeaderCommon(.accept) orelse return error.NoAcceptHeader; const comma_count = std.mem.count(u8, accept_str, ","); @@ -663,7 +663,7 @@ pub fn parseAcceptHeaders(self: *const Self, allocator: Allocator) !AcceptHeader } /// Set a response cookie -pub fn setCookie(self: *const Self, args: CookieArgs) HttpError!void { +pub fn setCookie(self: *const Request, args: CookieArgs) HttpError!void { const c: fio.http_cookie_args_s = .{ .name = util.toCharPtr(args.name), .name_len = @as(isize, @intCast(args.name.len)), @@ -692,7 +692,7 @@ pub fn setCookie(self: *const Self, args: CookieArgs) HttpError!void { } /// Returns named cookie. Works like getParamStr(). -pub fn getCookieStr(self: *const Self, a: Allocator, name: []const u8) !?[]const u8 { +pub fn getCookieStr(self: *const Request, a: Allocator, name: []const u8) !?[]const u8 { if (self.h.*.cookies == 0) return null; const key = fio.fiobj_str_new(name.ptr, name.len); defer fio.fiobj_free_wrapped(key); @@ -708,7 +708,7 @@ pub fn getCookieStr(self: *const Self, a: Allocator, name: []const u8) !?[]const /// Returns the number of cookies after parsing. /// /// Parse with parseCookies() -pub fn getCookiesCount(self: *const Self) isize { +pub fn getCookiesCount(self: *const Request) isize { if (self.h.*.cookies == 0) return 0; return fio.fiobj_obj2num(self.h.*.cookies); } @@ -716,7 +716,7 @@ pub fn getCookiesCount(self: *const Self) isize { /// Returns the number of parameters after parsing. /// /// Parse with parseBody() and / or parseQuery() -pub fn getParamCount(self: *const Self) isize { +pub fn getParamCount(self: *const Request) isize { if (self.h.*.params == 0) return 0; return fio.fiobj_obj2num(self.h.*.params); } @@ -727,7 +727,7 @@ const CallbackContext_KV = struct { last_error: ?anyerror = null, pub fn callback(fiobj_value: fio.FIOBJ, context_: ?*anyopaque) callconv(.C) c_int { - const ctx: *@This() = @as(*@This(), @ptrCast(@alignCast(context_))); + const ctx: *CallbackContext_KV = @as(*CallbackContext_KV, @ptrCast(@alignCast(context_))); // this is thread-safe, guaranteed by fio const fiobj_key: fio.FIOBJ = fio.fiobj_hash_key_in_loop(); ctx.params.append(.{ @@ -756,7 +756,7 @@ const CallbackContext_StrKV = struct { last_error: ?anyerror = null, pub fn callback(fiobj_value: fio.FIOBJ, context_: ?*anyopaque) callconv(.C) c_int { - const ctx: *@This() = @as(*@This(), @ptrCast(@alignCast(context_))); + const ctx: *CallbackContext_StrKV = @as(*CallbackContext_StrKV, @ptrCast(@alignCast(context_))); // this is thread-safe, guaranteed by fio const fiobj_key: fio.FIOBJ = fio.fiobj_hash_key_in_loop(); ctx.params.append(.{ @@ -780,7 +780,7 @@ const CallbackContext_StrKV = struct { }; /// Same as parametersToOwnedStrList() but for cookies -pub fn cookiesToOwnedStrList(self: *const Self, a: Allocator) anyerror!HttpParamStrKVList { +pub fn cookiesToOwnedStrList(self: *const Request, a: Allocator) anyerror!HttpParamStrKVList { var params = try std.ArrayList(HttpParamStrKV).initCapacity(a, @as(usize, @intCast(self.getCookiesCount()))); var context: CallbackContext_StrKV = .{ .params = ¶ms, @@ -794,7 +794,7 @@ pub fn cookiesToOwnedStrList(self: *const Self, a: Allocator) anyerror!HttpParam } /// Same as parametersToOwnedList() but for cookies -pub fn cookiesToOwnedList(self: *const Self, a: Allocator) !HttpParamKVList { +pub fn cookiesToOwnedList(self: *const Request, a: Allocator) !HttpParamKVList { var params = try std.ArrayList(HttpParamKV).initCapacity(a, @as(usize, @intCast(self.getCookiesCount()))); var context: CallbackContext_KV = .{ .params = ¶ms, .allocator = a }; const howmany = fio.fiobj_each1(self.h.*.cookies, 0, CallbackContext_KV.callback, &context); @@ -817,7 +817,7 @@ pub fn cookiesToOwnedList(self: *const Self, a: Allocator) !HttpParamKVList { /// /// Requires parseBody() and/or parseQuery() have been called. /// Returned list needs to be deinited. -pub fn parametersToOwnedStrList(self: *const Self, a: Allocator) anyerror!HttpParamStrKVList { +pub fn parametersToOwnedStrList(self: *const Request, a: Allocator) anyerror!HttpParamStrKVList { var params = try std.ArrayList(HttpParamStrKV).initCapacity(a, @as(usize, @intCast(self.getParamCount()))); var context: CallbackContext_StrKV = .{ @@ -845,7 +845,7 @@ pub fn parametersToOwnedStrList(self: *const Self, a: Allocator) anyerror!HttpPa /// /// Requires parseBody() and/or parseQuery() have been called. /// Returned slice needs to be freed. -pub fn parametersToOwnedList(self: *const Self, a: Allocator) !HttpParamKVList { +pub fn parametersToOwnedList(self: *const Request, a: Allocator) !HttpParamKVList { var params = try std.ArrayList(HttpParamKV).initCapacity(a, @as(usize, @intCast(self.getParamCount()))); var context: CallbackContext_KV = .{ .params = ¶ms, .allocator = a }; @@ -870,7 +870,7 @@ pub fn parametersToOwnedList(self: *const Self, a: Allocator) !HttpParamKVList { /// /// Requires parseBody() and/or parseQuery() have been called. /// The returned string needs to be deallocated. -pub fn getParamStr(self: *const Self, a: Allocator, name: []const u8) !?[]const u8 { +pub fn getParamStr(self: *const Request, a: Allocator, name: []const u8) !?[]const u8 { if (self.h.*.params == 0) return null; const key = fio.fiobj_str_new(name.ptr, name.len); defer fio.fiobj_free_wrapped(key); @@ -885,7 +885,7 @@ pub fn getParamStr(self: *const Self, a: Allocator, name: []const u8) !?[]const /// after the equals sign, non-decoded, and always as character slice. /// - no allocation! /// - does not requre parseQuery() or anything to be called in advance -pub fn getParamSlice(self: *const Self, name: []const u8) ?[]const u8 { +pub fn getParamSlice(self: *const Request, name: []const u8) ?[]const u8 { if (self.query) |query| { var amp_it = std.mem.tokenizeScalar(u8, query, '&'); while (amp_it.next()) |maybe_pair| { @@ -908,13 +908,13 @@ pub const ParameterSlices = struct { name: []const u8, value: []const u8 }; pub const ParamSliceIterator = struct { amp_it: std.mem.TokenIterator(u8, .scalar), - pub fn init(query: []const u8) @This() { + pub fn init(query: []const u8) ParamSliceIterator { return .{ .amp_it = std.mem.tokenizeScalar(u8, query, '&'), }; } - pub fn next(self: *@This()) ?ParameterSlices { + pub fn next(self: *ParamSliceIterator) ?ParameterSlices { while (self.amp_it.next()) |maybe_pair| { if (std.mem.indexOfScalar(u8, maybe_pair, '=')) |pos_of_eq| { const pname = maybe_pair[0..pos_of_eq]; @@ -931,11 +931,11 @@ pub const ParamSliceIterator = struct { /// Returns an iterator that yields all query parameters on next() in the /// form of a ParameterSlices struct { .name, .value } /// As with getParamSlice(), the value is not decoded -pub fn getParamSlices(self: *const Self) ParamSliceIterator { +pub fn getParamSlices(self: *const Request) ParamSliceIterator { const query = self.query orelse ""; return ParamSliceIterator.init(query); } -pub fn methodAsEnum(self: *const Self) http.Method { +pub fn methodAsEnum(self: *const Request) http.Method { return http.methodToEnum(self.method); } diff --git a/src/router.zig b/src/router.zig index 53500a6..f13d0a9 100644 --- a/src/router.zig +++ b/src/router.zig @@ -9,10 +9,10 @@ const RouterError = error{ EmptyPath, }; -const Self = @This(); +const Router = @This(); /// This is a singleton -var _instance: *Self = undefined; +var _instance: *Router = undefined; /// Options to pass to init() pub const Options = struct { @@ -31,7 +31,7 @@ routes: std.StringHashMap(Callback), not_found: ?zap.HttpRequestFn, /// Create a new Router -pub fn init(allocator: Allocator, options: Options) Self { +pub fn init(allocator: Allocator, options: Options) Router { return .{ .routes = std.StringHashMap(Callback).init(allocator), @@ -40,12 +40,12 @@ pub fn init(allocator: Allocator, options: Options) Self { } /// Deinit the router -pub fn deinit(self: *Self) void { +pub fn deinit(self: *Router) void { self.routes.deinit(); } /// Call this to add a route with an unbound handler: a handler that is not member of a struct. -pub fn handle_func_unbound(self: *Self, path: []const u8, h: zap.HttpRequestFn) !void { +pub fn handle_func_unbound(self: *Router, path: []const u8, h: zap.HttpRequestFn) !void { if (path.len == 0) { return RouterError.EmptyPath; } @@ -71,7 +71,7 @@ pub fn handle_func_unbound(self: *Self, path: []const u8, h: zap.HttpRequestFn) /// /// my_router.handle_func("/getA", &handler_instance, HandlerType.getA); /// ``` -pub fn handle_func(self: *Self, path: []const u8, instance: *anyopaque, handler: anytype) !void { +pub fn handle_func(self: *Router, path: []const u8, instance: *anyopaque, handler: anytype) !void { // TODO: assert type of instance has handler if (path.len == 0) { @@ -89,7 +89,7 @@ pub fn handle_func(self: *Self, path: []const u8, instance: *anyopaque, handler: } /// Get the zap request handler function needed for a listener -pub fn on_request_handler(self: *Self) zap.HttpRequestFn { +pub fn on_request_handler(self: *Router) zap.HttpRequestFn { _instance = self; return zap_on_request; } @@ -98,7 +98,7 @@ fn zap_on_request(r: zap.Request) !void { return serve(_instance, r); } -fn serve(self: *Self, r: zap.Request) !void { +fn serve(self: *Router, r: zap.Request) !void { const path = r.path orelse "/"; if (self.routes.get(path)) |routeInfo| { diff --git a/src/tests/test_http_params.zig b/src/tests/test_http_params.zig index df4ef18..c80512d 100644 --- a/src/tests/test_http_params.zig +++ b/src/tests/test_http_params.zig @@ -42,7 +42,6 @@ test "http parameters" { params = r.parametersToOwnedList(alloc) catch unreachable; paramOneStr = r.getParamStr(alloc, "one") catch unreachable; - std.debug.print("\n\nparamOneStr = {s} = {*} \n\n", .{ paramOneStr.?, paramOneStr.?.ptr }); // we need to dupe it here because the request object r will get // invalidated at the end of the function but we need to check diff --git a/src/zap.zig b/src/zap.zig index 93d3ecb..9389d84 100644 --- a/src/zap.zig +++ b/src/zap.zig @@ -170,11 +170,10 @@ pub const HttpListenerSettings = struct { pub const HttpListener = struct { settings: HttpListenerSettings, - const Self = @This(); var the_one_and_only_listener: ?*HttpListener = null; /// Create a listener - pub fn init(settings: HttpListenerSettings) Self { + pub fn init(settings: HttpListenerSettings) HttpListener { std.debug.assert(settings.on_request != null); return .{ .settings = settings, @@ -264,7 +263,7 @@ pub const HttpListener = struct { } /// Start listening - pub fn listen(self: *Self) !void { + pub fn listen(self: *HttpListener) !void { var pfolder: [*c]const u8 = null; var pfolder_len: usize = 0; @@ -275,10 +274,10 @@ pub const HttpListener = struct { } const x: fio.http_settings_s = .{ - .on_request = if (self.settings.on_request) |_| Self.theOneAndOnlyRequestCallBack else null, - .on_upgrade = if (self.settings.on_upgrade) |_| Self.theOneAndOnlyUpgradeCallBack else null, - .on_response = if (self.settings.on_response) |_| Self.theOneAndOnlyResponseCallBack else null, - .on_finish = if (self.settings.on_finish) |_| Self.theOneAndOnlyFinishCallBack else null, + .on_request = if (self.settings.on_request) |_| HttpListener.theOneAndOnlyRequestCallBack else null, + .on_upgrade = if (self.settings.on_upgrade) |_| HttpListener.theOneAndOnlyUpgradeCallBack else null, + .on_response = if (self.settings.on_response) |_| HttpListener.theOneAndOnlyResponseCallBack else null, + .on_finish = if (self.settings.on_finish) |_| HttpListener.theOneAndOnlyFinishCallBack else null, .udata = null, .public_folder = pfolder, .public_folder_length = pfolder_len, @@ -316,7 +315,7 @@ pub const HttpListener = struct { // the request if it isn't set. hence, if started under full load, the // first request(s) might not be serviced, as long as it takes from // fio.http_listen() to here - Self.the_one_and_only_listener = self; + HttpListener.the_one_and_only_listener = self; } }; @@ -336,10 +335,8 @@ pub const LowLevel = struct { keepalive_timeout_s: u8 = 5, log: bool = false, - const Self = @This(); - /// Create settings with defaults - pub fn init() Self { + pub fn init() ListenSettings { return .{}; } }; From dcd07b7025b394a16b9adc2cad5935a045106751 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Fri, 21 Mar 2025 19:28:50 +0100 Subject: [PATCH 34/57] remove debug print from test --- src/tests/test_http_params.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tests/test_http_params.zig b/src/tests/test_http_params.zig index c80512d..02d7519 100644 --- a/src/tests/test_http_params.zig +++ b/src/tests/test_http_params.zig @@ -94,7 +94,6 @@ test "http parameters" { try std.testing.expectEqual(true, Handler.ran); try std.testing.expectEqual(5, Handler.param_count); try std.testing.expect(Handler.paramOneStr != null); - std.debug.print("\n\n=== Handler.paramOneStr = {s} = {*}\n\n", .{ Handler.paramOneStr.?, Handler.paramOneStr.?.ptr }); try std.testing.expectEqualStrings("1", Handler.paramOneStr.?); try std.testing.expect(Handler.paramOneSlice != null); try std.testing.expectEqualStrings("1", Handler.paramOneSlice.?); From f4e42f25578a482b5aad4dca1617f3558e5cb88a Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Fri, 21 Mar 2025 19:28:50 +0100 Subject: [PATCH 35/57] add zap.Request.headersToOwnedList() --- examples/accept/accept.zig | 15 +++++++++++++++ src/request.zig | 14 ++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/examples/accept/accept.zig b/examples/accept/accept.zig index fc9f766..8e29350 100644 --- a/examples/accept/accept.zig +++ b/examples/accept/accept.zig @@ -21,6 +21,21 @@ fn on_request_verbose(r: zap.Request) !void { break :content_type .HTML; }; + // just for fun: print ALL headers + var maybe_headers: ?zap.Request.HttpParamStrKVList = blk: { + const h = r.headersToOwnedList(gpa.allocator()) catch |err| { + std.debug.print("Error getting headers: {}\n", .{err}); + break :blk null; + }; + break :blk h; + }; + if (maybe_headers) |*headers| { + defer headers.deinit(); + for (headers.items) |header| { + std.debug.print("Header {s} = {s}\n", .{ header.key, header.value }); + } + } + try r.setContentType(content_type); switch (content_type) { .TEXT => { diff --git a/src/request.zig b/src/request.zig index 09c25f4..74a84ef 100644 --- a/src/request.zig +++ b/src/request.zig @@ -30,6 +30,7 @@ pub const HttpParamStrKV = struct { pub const HttpParamStrKVList = struct { items: []HttpParamStrKV, allocator: Allocator, + pub fn deinit(self: *HttpParamStrKVList) void { for (self.items) |item| { self.allocator.free(item.key); @@ -537,6 +538,19 @@ pub fn setHeader(self: *const Request, name: []const u8, value: []const u8) Http return error.HttpSetHeader; } +pub fn headersToOwnedList(self: *const Request, a: Allocator) !HttpParamStrKVList { + var headers = std.ArrayList(HttpParamStrKV).init(a); + var context: CallbackContext_StrKV = .{ + .params = &headers, + .allocator = a, + }; + const howmany = fio.fiobj_each1(self.h.*.headers, 0, CallbackContext_StrKV.callback, &context); + if (howmany != headers.items.len) { + return error.HttpIterHeaders; + } + return .{ .items = try headers.toOwnedSlice(), .allocator = a }; +} + /// Set status by numeric value. pub fn setStatusNumeric(self: *const Request, status: usize) void { self.h.*.status = status; From 7da0a6fe4e6f3ed102ed399c6ddabbbbddfde06d Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Fri, 21 Mar 2025 20:08:43 +0100 Subject: [PATCH 36/57] continue working on zap.App --- src/App.zig | 245 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 232 insertions(+), 13 deletions(-) diff --git a/src/App.zig b/src/App.zig index 8b3e98d..1fd67da 100644 --- a/src/App.zig +++ b/src/App.zig @@ -6,25 +6,244 @@ //! - automatic error catching & logging, optional report to HTML const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const RwLock = std.Thread.RwLock; + const zap = @import("zap.zig"); +const Request = zap.Request; pub const Opts = struct { - request_error_strategy: enum { - /// log errors to console - log_to_console, - /// log errors to console AND generate a HTML response - log_to_response, - /// raise errors -> TODO: clarify: where can they be caught? in App.run() - raise, - }, + /// ErrorStrategy for (optional) request handler if no endpoint matches + default_error_strategy: zap.Endpoint.ErrorStrategy = .log_to_console, + arena_retain_capacity: usize = 16 * 1024 * 1024, }; -threadlocal var arena: ?std.heap.ArenaAllocator = null; +threadlocal var _arena: ?ArenaAllocator = null; -pub fn create(comptime Context: type, context: *Context, opts: Opts) type { +/// creates an App with custom app context +pub fn Create(comptime Context: type) type { return struct { - context: *Context = context, - error_strategy: @TypeOf(opts.request_error_strategy) = opts.request_error_strategy, - endpoints: std.StringArrayHashMapUnmanaged(*zap.Endpoint.Wrapper.Internal) = .empty, + const App = @This(); + + // we make the following fields static so we can access them from a + // context-free, pure zap request handler + const InstanceData = struct { + context: *Context = undefined, + gpa: Allocator = undefined, + opts: Opts = undefined, + endpoints: std.StringArrayHashMapUnmanaged(*Endpoint.Wrapper.Interface) = .empty, + + there_can_be_only_one: bool = false, + track_arenas: std.ArrayListUnmanaged(*ArenaAllocator) = .empty, + track_arena_lock: RwLock = .{}, + }; + var _static: InstanceData = .{}; + + /// Internal, static request handler callback. Will be set to the optional, + /// user-defined request callback that only gets called if no endpoints match + /// a request. + var on_request: ?*const fn (Allocator, *Context, Request) anyerror!void = null; + + pub const Endpoint = struct { + pub const Wrapper = struct { + pub const Interface = struct { + call: *const fn (*Interface, zap.Request) anyerror!void = undefined, + path: []const u8, + destroy: *const fn (allocator: Allocator, *Interface) void = undefined, + }; + pub fn Wrap(T: type) type { + return struct { + wrapped: *T, + wrapper: Interface, + opts: Opts, + app_context: *Context, + + const Wrapped = @This(); + + pub fn unwrap(wrapper: *Interface) *Wrapped { + const self: *Wrapped = @alignCast(@fieldParentPtr("wrapper", wrapper)); + return self; + } + + pub fn destroy(allocator: Allocator, wrapper: *Interface) void { + const self: *Wrapped = @alignCast(@fieldParentPtr("wrapper", wrapper)); + allocator.destroy(self); + } + + pub fn onRequestWrapped(wrapper: *Interface, r: zap.Request) !void { + var self: *Wrapped = Wrapped.unwrap(wrapper); + const arena = try get_arena(); + try self.onRequest(arena.allocator(), self.app_context, r); + arena.reset(.{ .retain_capacity = self.opts.arena_retain_capacity }); + } + + pub fn onRequest(self: *Wrapped, arena: Allocator, app_context: *Context, r: zap.Request) !void { + const ret = switch (r.methodAsEnum()) { + .GET => self.wrapped.*.get(arena, app_context, r), + .POST => self.wrapped.*.post(arena, app_context, r), + .PUT => self.wrapped.*.put(arena, app_context, r), + .DELETE => self.wrapped.*.delete(arena, app_context, r), + .PATCH => self.wrapped.*.patch(arena, app_context, r), + .OPTIONS => self.wrapped.*.options(arena, app_context, r), + else => error.UnsupportedHtmlRequestMethod, + }; + if (ret) { + // handled without error + } else |err| { + switch (self.wrapped.*.error_strategy) { + .raise => return err, + .log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505), + .log_to_console => zap.debug("Error in {} {s} : {}", .{ Wrapped, r.method orelse "(no method)", err }), + } + } + } + }; + } + + pub fn init(T: type, value: *T, app_opts: Opts, app_context: *Context) Wrapper.Wrap(T) { + checkEndpointType(T); + var ret: Wrapper.Wrap(T) = .{ + .wrapped = value, + .wrapper = .{ .path = value.path }, + .opts = app_opts, + .app_context = app_context, + }; + ret.wrapper.call = Wrapper.Wrap(T).onRequestWrapped; + ret.wrapper.destroy = Wrapper.Wrap(T).destroy; + return ret; + } + + pub fn checkEndpointType(T: type) void { + if (@hasField(T, "path")) { + if (@FieldType(T, "path") != []const u8) { + @compileError(@typeName(@FieldType(T, "path")) ++ " has wrong type, expected: []const u8"); + } + } else { + @compileError(@typeName(T) ++ " has no path field"); + } + + if (@hasField(T, "error_strategy")) { + if (@FieldType(T, "error_strategy") != zap.Endpoint.ErrorStrategy) { + @compileError(@typeName(@FieldType(T, "error_strategy")) ++ " has wrong type, expected: zap.Endpoint.ErrorStrategy"); + } + } else { + @compileError(@typeName(T) ++ " has no error_strategy field"); + } + + const methods_to_check = [_][]const u8{ + "get", + "post", + "put", + "delete", + "patch", + "options", + }; + inline for (methods_to_check) |method| { + if (@hasDecl(T, method)) { + if (@TypeOf(@field(T, method)) != fn (_: *T, _: Allocator, _: *Context, _: zap.Request) anyerror!void) { + @compileError(method ++ " method of " ++ @typeName(T) ++ " has wrong type:\n" ++ @typeName(@TypeOf(T.get)) ++ "\nexpected:\n" ++ @typeName(fn (_: *T, _: Allocator, _: *Context, _: zap.Request) anyerror!void)); + } + } else { + @compileError(@typeName(T) ++ " has no method named `" ++ method ++ "`"); + } + } + } + }; + }; + + pub const Listener = struct { + pub const Settings = struct { + // + }; + }; + + pub fn init(gpa_: Allocator, context_: *Context, opts_: Opts) !App { + if (App._static._there_can_be_only_one) { + return error.OnlyOneAppAllowed; + } + App._static.context = context_; + App._static.gpa = gpa_; + App._static.opts = opts_; + App._static.there_can_be_only_one = true; + return .{}; + } + + pub fn deinit() void { + App._static.endpoints.deinit(_static.gpa); + + App._static.track_arena_lock.lock(); + defer App._static.track_arena_lock.unlock(); + for (App._static.track_arenas.items) |arena| { + arena.deinit(); + } + } + + fn get_arena() !*ArenaAllocator { + App._static.track_arena_lock.lockShared(); + if (_arena == null) { + App._static.track_arena_lock.unlockShared(); + App._static.track_arena_lock.lock(); + defer App._static.track_arena_lock.unlock(); + _arena = ArenaAllocator.init(App._static.gpa); + try App._static.track_arenas.append(App._static.gpa, &_arena.?); + } else { + App._static.track_arena_lock.unlockShared(); + return &_arena.?; + } + } + + /// Register an endpoint with this listener. + /// NOTE: endpoint paths are matched with startsWith -> so use endpoints with distinctly starting names!! + /// If you try to register an endpoint whose path would shadow an already registered one, you will + /// receive an EndpointPathShadowError. + pub fn register(self: *App, endpoint: anytype) !void { + for (App._static.endpoints.items) |other| { + if (std.mem.startsWith( + u8, + other.path, + endpoint.path, + ) or std.mem.startsWith( + u8, + endpoint.path, + other.path, + )) { + return zap.Endpoint.EndpointListenerError.EndpointPathShadowError; + } + } + const EndpointType = @typeInfo(@TypeOf(endpoint)).pointer.child; + Endpoint.Wrapper.checkEndpointType(EndpointType); + const wrapper = try self.gpa.create(Endpoint.Wrapper.Wrap(EndpointType)); + wrapper.* = Endpoint.Wrapper.init(EndpointType, endpoint); + try App._static.endpoints.append(self.gpa, &wrapper.wrapper); + } + + pub fn listen(self: *App, l: Listener.Settings) !void { + _ = self; + _ = l; + // TODO: do it + } + + fn onRequest(r: Request) !void { + if (r.path) |p| { + for (App._static.endpoints.items) |wrapper| { + if (std.mem.startsWith(u8, p, wrapper.path)) { + return try wrapper.call(wrapper, r); + } + } + } + if (on_request) |foo| { + if (_arena == null) { + _arena = ArenaAllocator.init(App._static.gpa); + } + foo(_arena.allocator(), App._static.context, r) catch |err| { + switch (App._static.opts.default_error_strategy) { + .raise => return err, + .log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505), + .log_to_console => zap.debug("Error in {} {s} : {}", .{ App, r.method orelse "(no method)", err }), + } + }; + } + } }; } From 9d3434c21c2d6f34af50d219e6401db911150d47 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Fri, 21 Mar 2025 20:08:43 +0100 Subject: [PATCH 37/57] WIP side-experiment WIP BoundFunction --- src/BoundFunction.zig | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/BoundFunction.zig diff --git a/src/BoundFunction.zig b/src/BoundFunction.zig new file mode 100644 index 0000000..94cc9f6 --- /dev/null +++ b/src/BoundFunction.zig @@ -0,0 +1,36 @@ +const std = @import("std"); + +// attempt 1: explicitly typed +pub fn Create(Fn: type, Instance: type) type { + return struct { + instance: *Instance, + function: *const Fn, + + const BoundFunction = @This(); + + pub fn init(function: *const Fn, instance: *Instance) BoundFunction { + return .{ + .instance = instance, + .function = function, + }; + } + + pub fn call(self: *const BoundFunction, arg: anytype) void { + @call(.auto, self.function, .{ self.instance, arg }); + } + }; +} + +test "BoundFunction" { + const X = struct { + field: usize = 0, + pub fn foo(self: *@This(), other: usize) void { + std.debug.print("field={d}, other={d}\n", .{ self.field, other }); + } + }; + + var x: X = .{ .field = 27 }; + + var bound = Create(@TypeOf(X.foo), X).init(X.foo, &x); + bound.call(3); +} From 3353b6a5251f0e3c856aa1d30b1901519232e439 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Sat, 22 Mar 2025 20:31:10 +0100 Subject: [PATCH 38/57] WIP: progress on Bound function --- src/BoundFunction.zig | 148 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 130 insertions(+), 18 deletions(-) diff --git a/src/BoundFunction.zig b/src/BoundFunction.zig index 94cc9f6..71c5cfe 100644 --- a/src/BoundFunction.zig +++ b/src/BoundFunction.zig @@ -1,36 +1,148 @@ const std = @import("std"); -// attempt 1: explicitly typed -pub fn Create(Fn: type, Instance: type) type { +/// Bind a function with specific signature to a method of any instance of a given type +pub fn OldBind(func: anytype, instance: anytype) OldBound(@typeInfo(@TypeOf(instance)).pointer.child, func) { + const Instance = @typeInfo(@TypeOf(instance)).pointer.child; + return OldBound(Instance, func).init(@constCast(instance)); +} + +pub fn OldBound(Instance: type, func: anytype) type { + // Verify Func is a function type + const func_info = @typeInfo(@TypeOf(func)); + if (func_info != .@"fn") { + @compileError("OldBound expexts a function type as second parameter"); + } + + // Verify first parameter is pointer to Instance type + const params = func_info.@"fn".params; + if (params.len == 0 or (params[0].type != *Instance and params[0].type != *const Instance)) { + @compileError("Function's first parameter must be " ++ @typeName(Instance) ++ " but got: " ++ @typeName(params[0].type.?)); + } + return struct { instance: *Instance, - function: *const Fn, + + const OldBoundFunction = @This(); + + pub fn call(self: OldBoundFunction, args: anytype) func_info.@"fn".return_type.? { + return @call(.auto, func, .{self.instance} ++ args); + } + + // convenience init + pub fn init(instance_: *Instance) OldBoundFunction { + return .{ .instance = instance_ }; + } + }; +} + +test "OldBound" { + const Person = struct { + name: []const u8, + _buf: [1024]u8 = undefined, + + // takes const instance + pub fn greet(self: *const @This(), gpa: std.mem.Allocator, greeting: []const u8) ![]const u8 { + return std.fmt.allocPrint(gpa, "{s}, {s}!\n", .{ greeting, self.name }); + } + + // takes non-const instance + pub fn farewell(self: *@This(), message: []const u8) ![]const u8 { + return std.fmt.bufPrint(self._buf[0..], "{s}, {s}!\n", .{ message, self.name }); + } + }; + + var alice: Person = .{ .name = "Alice" }; + + // creation variant a: manually instantiate + const bound_greet: OldBound(Person, Person.greet) = .{ .instance = &alice }; + + // creation variant b: call init function + const bound_farewell = OldBound(Person, Person.farewell).init(&alice); + + const ta = std.testing.allocator; + const greeting = try bound_greet.call(.{ ta, "Hello" }); + defer ta.free(greeting); + + try std.testing.expectEqualStrings("Hello, Alice!\n", greeting); + try std.testing.expectEqualStrings("Goodbye, Alice!\n", try bound_farewell.call(.{"Goodbye"})); +} + +test OldBind { + const Person = struct { + name: []const u8, + _buf: [1024]u8 = undefined, + + // takes const instance + pub fn greet(self: *const @This(), gpa: std.mem.Allocator, greeting: []const u8) ![]const u8 { + return std.fmt.allocPrint(gpa, "{s}, {s}!\n", .{ greeting, self.name }); + } + + // takes non-const instance + pub fn farewell(self: *@This(), message: []const u8) ![]const u8 { + return std.fmt.bufPrint(self._buf[0..], "{s}, {s}!\n", .{ message, self.name }); + } + }; + + var alice: Person = .{ .name = "Alice" }; + + const bound_greet = OldBind(Person.greet, &alice); + const bound_farewell = OldBind(Person.farewell, &alice); + + const ta = std.testing.allocator; + const greeting = try bound_greet.call(.{ ta, "Hello" }); + defer ta.free(greeting); + + try std.testing.expectEqualStrings("Hello, Alice!\n", greeting); + try std.testing.expectEqualStrings("Goodbye, Alice!\n", try bound_farewell.call(.{"Goodbye"})); +} + +/// Bind functions like `fn(a: X, b: Y)` to an instance of a struct. When called, the instance's `pub fn(self: *This(), a: X, b: Y)` is called. +/// +/// make callbacks stateful when they're not meant to be? +// pub fn Bound(Instance: type, Func: type, func: anytype) type { +pub fn Bound(Instance: type, Func: type, DFunc: type) type { + + // TODO: construct DFunc on-the-fly + + // Verify Func is a function type + const func_info = @typeInfo(Func); + // if (func_info != .@"fn") { + // @compileError("Bound expexts a function type as second parameter"); + // } + return struct { + instance: *Instance, + foo: *const DFunc, const BoundFunction = @This(); - pub fn init(function: *const Fn, instance: *Instance) BoundFunction { - return .{ - .instance = instance, - .function = function, - }; + pub fn call(self: BoundFunction, args: anytype) func_info.@"fn".return_type.? { + return @call(.auto, self.foo, .{self.instance} ++ args); } - pub fn call(self: *const BoundFunction, arg: anytype) void { - @call(.auto, self.function, .{ self.instance, arg }); + // convenience init + pub fn init(instance_: *Instance, foo_: *const DFunc) BoundFunction { + return .{ .instance = instance_, .foo = foo_ }; } }; } -test "BoundFunction" { - const X = struct { - field: usize = 0, - pub fn foo(self: *@This(), other: usize) void { - std.debug.print("field={d}, other={d}\n", .{ self.field, other }); +test Bound { + const Person = struct { + name: []const u8, + _buf: [1024]u8 = undefined, + + pub fn speak(self: *@This(), message: []const u8) ![]const u8 { + return std.fmt.bufPrint(self._buf[0..], "{s} says: >>{s}!<<\n", .{ self.name, message }); } }; - var x: X = .{ .field = 27 }; + const CallBack = fn ([]const u8) anyerror![]const u8; - var bound = Create(@TypeOf(X.foo), X).init(X.foo, &x); - bound.call(3); + var alice: Person = .{ .name = "Alice" }; + + const bound_greet = Bound(Person, CallBack, @TypeOf(Person.speak)).init(&alice, &Person.speak); + + const greeting = try bound_greet.call(.{"Hello"}); + + try std.testing.expectEqualStrings("Alice says: >>Hello!<<\n", greeting); } From 6dd3c93a0f443dfd615af0d08e34bdcf7f7ea34c Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 23 Mar 2025 23:28:09 +0100 Subject: [PATCH 39/57] BoundFunction YEAH --- src/BoundFunction.zig | 52 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/src/BoundFunction.zig b/src/BoundFunction.zig index 71c5cfe..7eaeac0 100644 --- a/src/BoundFunction.zig +++ b/src/BoundFunction.zig @@ -96,22 +96,58 @@ test OldBind { try std.testing.expectEqualStrings("Goodbye, Alice!\n", try bound_farewell.call(.{"Goodbye"})); } +/// Creates a function type with instance pointer prepended to args +fn PrependFnArg(Func: type, Instance: type) type { + const InstancePtr = *Instance; + + // Get the function type + const fn_info = @typeInfo(Func); + if (fn_info != .@"fn") { + @compileError("Second argument must be a function"); + } + + // Create new parameter list with instance pointer prepended + comptime var new_params: [fn_info.@"fn".params.len + 1]std.builtin.Type.Fn.Param = undefined; + new_params[0] = .{ + .is_generic = false, + .is_noalias = false, + .type = InstancePtr, + }; + + // Copy original parameters + for (fn_info.@"fn".params, 0..) |param, i| { + new_params[i + 1] = param; + } + + // Return the new function type + return @Type(.{ + .@"fn" = .{ + .calling_convention = fn_info.@"fn".calling_convention, + .is_generic = fn_info.@"fn".is_generic, + .is_var_args = fn_info.@"fn".is_var_args, + .return_type = fn_info.@"fn".return_type, + .params = &new_params, + }, + }); +} /// Bind functions like `fn(a: X, b: Y)` to an instance of a struct. When called, the instance's `pub fn(self: *This(), a: X, b: Y)` is called. /// /// make callbacks stateful when they're not meant to be? // pub fn Bound(Instance: type, Func: type, func: anytype) type { -pub fn Bound(Instance: type, Func: type, DFunc: type) type { +pub fn Bind(Instance: type, Func: type) type { // TODO: construct DFunc on-the-fly // Verify Func is a function type const func_info = @typeInfo(Func); - // if (func_info != .@"fn") { - // @compileError("Bound expexts a function type as second parameter"); - // } + if (func_info != .@"fn") { + @compileError("Bound expexts a function type as second parameter"); + } + + const InstanceMethod = PrependFnArg(Func, Instance); return struct { instance: *Instance, - foo: *const DFunc, + foo: *const InstanceMethod, const BoundFunction = @This(); @@ -120,13 +156,13 @@ pub fn Bound(Instance: type, Func: type, DFunc: type) type { } // convenience init - pub fn init(instance_: *Instance, foo_: *const DFunc) BoundFunction { + pub fn init(instance_: *Instance, foo_: *const InstanceMethod) BoundFunction { return .{ .instance = instance_, .foo = foo_ }; } }; } -test Bound { +test Bind { const Person = struct { name: []const u8, _buf: [1024]u8 = undefined, @@ -140,7 +176,7 @@ test Bound { var alice: Person = .{ .name = "Alice" }; - const bound_greet = Bound(Person, CallBack, @TypeOf(Person.speak)).init(&alice, &Person.speak); + const bound_greet = Bind(Person, CallBack).init(&alice, &Person.speak); const greeting = try bound_greet.call(.{"Hello"}); From d4a20795420382b0dc39da41c5c1425b7079acb6 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Thu, 27 Mar 2025 01:12:40 +0100 Subject: [PATCH 40/57] BoundFunction POLYMORPHIC!!! --- src/BoundFunction.zig | 415 +++++++++++++++++++++++++++--------------- 1 file changed, 272 insertions(+), 143 deletions(-) diff --git a/src/BoundFunction.zig b/src/BoundFunction.zig index 7eaeac0..97c2b6f 100644 --- a/src/BoundFunction.zig +++ b/src/BoundFunction.zig @@ -1,125 +1,22 @@ const std = @import("std"); -/// Bind a function with specific signature to a method of any instance of a given type -pub fn OldBind(func: anytype, instance: anytype) OldBound(@typeInfo(@TypeOf(instance)).pointer.child, func) { - const Instance = @typeInfo(@TypeOf(instance)).pointer.child; - return OldBound(Instance, func).init(@constCast(instance)); -} - -pub fn OldBound(Instance: type, func: anytype) type { - // Verify Func is a function type - const func_info = @typeInfo(@TypeOf(func)); - if (func_info != .@"fn") { - @compileError("OldBound expexts a function type as second parameter"); - } - - // Verify first parameter is pointer to Instance type - const params = func_info.@"fn".params; - if (params.len == 0 or (params[0].type != *Instance and params[0].type != *const Instance)) { - @compileError("Function's first parameter must be " ++ @typeName(Instance) ++ " but got: " ++ @typeName(params[0].type.?)); - } - - return struct { - instance: *Instance, - - const OldBoundFunction = @This(); - - pub fn call(self: OldBoundFunction, args: anytype) func_info.@"fn".return_type.? { - return @call(.auto, func, .{self.instance} ++ args); - } - - // convenience init - pub fn init(instance_: *Instance) OldBoundFunction { - return .{ .instance = instance_ }; - } - }; -} - -test "OldBound" { - const Person = struct { - name: []const u8, - _buf: [1024]u8 = undefined, - - // takes const instance - pub fn greet(self: *const @This(), gpa: std.mem.Allocator, greeting: []const u8) ![]const u8 { - return std.fmt.allocPrint(gpa, "{s}, {s}!\n", .{ greeting, self.name }); - } - - // takes non-const instance - pub fn farewell(self: *@This(), message: []const u8) ![]const u8 { - return std.fmt.bufPrint(self._buf[0..], "{s}, {s}!\n", .{ message, self.name }); - } - }; - - var alice: Person = .{ .name = "Alice" }; - - // creation variant a: manually instantiate - const bound_greet: OldBound(Person, Person.greet) = .{ .instance = &alice }; - - // creation variant b: call init function - const bound_farewell = OldBound(Person, Person.farewell).init(&alice); - - const ta = std.testing.allocator; - const greeting = try bound_greet.call(.{ ta, "Hello" }); - defer ta.free(greeting); - - try std.testing.expectEqualStrings("Hello, Alice!\n", greeting); - try std.testing.expectEqualStrings("Goodbye, Alice!\n", try bound_farewell.call(.{"Goodbye"})); -} - -test OldBind { - const Person = struct { - name: []const u8, - _buf: [1024]u8 = undefined, - - // takes const instance - pub fn greet(self: *const @This(), gpa: std.mem.Allocator, greeting: []const u8) ![]const u8 { - return std.fmt.allocPrint(gpa, "{s}, {s}!\n", .{ greeting, self.name }); - } - - // takes non-const instance - pub fn farewell(self: *@This(), message: []const u8) ![]const u8 { - return std.fmt.bufPrint(self._buf[0..], "{s}, {s}!\n", .{ message, self.name }); - } - }; - - var alice: Person = .{ .name = "Alice" }; - - const bound_greet = OldBind(Person.greet, &alice); - const bound_farewell = OldBind(Person.farewell, &alice); - - const ta = std.testing.allocator; - const greeting = try bound_greet.call(.{ ta, "Hello" }); - defer ta.free(greeting); - - try std.testing.expectEqualStrings("Hello, Alice!\n", greeting); - try std.testing.expectEqualStrings("Goodbye, Alice!\n", try bound_farewell.call(.{"Goodbye"})); -} - -/// Creates a function type with instance pointer prepended to args -fn PrependFnArg(Func: type, Instance: type) type { - const InstancePtr = *Instance; - - // Get the function type +/// Helper function that returns a function type with ArgType prepended to the +/// function's args. +/// Example: +/// Func = fn(usize) void +/// ArgType = *Instance +/// -------------------------- +/// Result = fn(*Instance, usize) void +fn PrependFnArg(Func: type, ArgType: type) type { const fn_info = @typeInfo(Func); - if (fn_info != .@"fn") { - @compileError("Second argument must be a function"); - } + if (fn_info != .@"fn") @compileError("First argument must be a function type"); - // Create new parameter list with instance pointer prepended comptime var new_params: [fn_info.@"fn".params.len + 1]std.builtin.Type.Fn.Param = undefined; - new_params[0] = .{ - .is_generic = false, - .is_noalias = false, - .type = InstancePtr, - }; - - // Copy original parameters + new_params[0] = .{ .is_generic = false, .is_noalias = false, .type = ArgType }; for (fn_info.@"fn".params, 0..) |param, i| { new_params[i + 1] = param; } - // Return the new function type return @Type(.{ .@"fn" = .{ .calling_convention = fn_info.@"fn".calling_convention, @@ -130,55 +27,287 @@ fn PrependFnArg(Func: type, Instance: type) type { }, }); } -/// Bind functions like `fn(a: X, b: Y)` to an instance of a struct. When called, the instance's `pub fn(self: *This(), a: X, b: Y)` is called. -/// -/// make callbacks stateful when they're not meant to be? -// pub fn Bound(Instance: type, Func: type, func: anytype) type { -pub fn Bind(Instance: type, Func: type) type { - // TODO: construct DFunc on-the-fly - - // Verify Func is a function type +// External Generic Interface (CallbackInterface) +pub fn CallbackInterface(comptime Func: type) type { const func_info = @typeInfo(Func); - if (func_info != .@"fn") { - @compileError("Bound expexts a function type as second parameter"); - } + if (func_info != .@"fn") @compileError("CallbackInterface expects a function type"); + if (func_info.@"fn".is_generic) @compileError("CallbackInterface does not support generic functions"); + if (func_info.@"fn".is_var_args) @compileError("CallbackInterface does not support var_args functions"); + + const ArgsTupleType = std.meta.ArgsTuple(Func); + const ReturnType = func_info.@"fn".return_type.?; + const FnPtrType = *const fn (ctx: ?*const anyopaque, args: ArgsTupleType) ReturnType; - const InstanceMethod = PrependFnArg(Func, Instance); return struct { - instance: *Instance, - foo: *const InstanceMethod, + ctx: ?*const anyopaque, + callFn: FnPtrType, + pub const Interface = @This(); - const BoundFunction = @This(); - - pub fn call(self: BoundFunction, args: anytype) func_info.@"fn".return_type.? { - return @call(.auto, self.foo, .{self.instance} ++ args); - } - - // convenience init - pub fn init(instance_: *Instance, foo_: *const InstanceMethod) BoundFunction { - return .{ .instance = instance_, .foo = foo_ }; + pub fn call(self: Interface, args: ArgsTupleType) ReturnType { + if (self.ctx == null) @panic("Called uninitialized CallbackInterface"); + if (ReturnType == void) { + self.callFn(self.ctx, args); + } else { + return self.callFn(self.ctx, args); + } } }; } -test Bind { +pub fn Bind(Instance: type, Func: type) type { + const func_info = @typeInfo(Func); + if (func_info != .@"fn") @compileError("Bind expects a function type as second parameter"); + if (func_info.@"fn".is_generic) @compileError("Binding generic functions is not supported"); + if (func_info.@"fn".is_var_args) @compileError("Binding var_args functions is not currently supported"); + + const ReturnType = func_info.@"fn".return_type.?; + const OriginalParams = func_info.@"fn".params; // Needed for comptime loops + const ArgsTupleType = std.meta.ArgsTuple(Func); + const InstanceMethod = PrependFnArg(Func, *Instance); + const InterfaceType = CallbackInterface(Func); + + return struct { + instance: *Instance, + method: *const InstanceMethod, + pub const BoundFunction = @This(); + + // Trampoline function using runtime tuple construction + fn callDetached(ctx: ?*const anyopaque, args: ArgsTupleType) ReturnType { + if (ctx == null) @panic("callDetached called with null context"); + const self: *const BoundFunction = @ptrCast(@alignCast(ctx.?)); + + // 1. Define the tuple type needed for the call: .{*Instance, OriginalArgs...} + const CallArgsTupleType = comptime T: { + var tuple_fields: [OriginalParams.len + 1]std.builtin.Type.StructField = undefined; + // Field 0: *Instance type + tuple_fields[0] = .{ + .name = "0", + .type = @TypeOf(self.instance), + .default_value_ptr = null, + .is_comptime = false, + .alignment = 0, + }; + // Fields 1..N: Original argument types (use ArgsTupleType fields) + for (std.meta.fields(ArgsTupleType), 0..) |field, i| { + tuple_fields[i + 1] = .{ + .name = std.fmt.comptimePrint("{d}", .{i + 1}), + .type = field.type, + .default_value_ptr = null, + .is_comptime = false, + .alignment = 0, + }; + } + break :T @Type(.{ .@"struct" = .{ + .layout = .auto, + .fields = &tuple_fields, + .decls = &.{}, + .is_tuple = true, + } }); + }; + + // 2. Create and populate the tuple at runtime + var call_args_tuple: CallArgsTupleType = undefined; + @field(call_args_tuple, "0") = self.instance; // Set the instance pointer + + // Copy original args from 'args' tuple to 'call_args_tuple' + comptime var i = 0; + inline while (i < OriginalParams.len) : (i += 1) { + const src_field_name = comptime std.fmt.comptimePrint("{}", .{i}); + const dest_field_name = comptime std.fmt.comptimePrint("{}", .{i + 1}); + @field(call_args_tuple, dest_field_name) = @field(args, src_field_name); + } + + // 3. Perform the call using the populated tuple + if (ReturnType == void) { + @call(.auto, self.method, call_args_tuple); + } else { + return @call(.auto, self.method, call_args_tuple); + } + } + + pub fn interface(self: *const BoundFunction) InterfaceType { + return .{ .ctx = @ptrCast(self), .callFn = &callDetached }; + } + + // Direct call convenience method using runtime tuple construction + pub fn call(self: *const BoundFunction, args: anytype) ReturnType { + // 1. Verify 'args' is the correct ArgsTupleType or compatible tuple literal + // (This check could be more robust if needed) + if (@TypeOf(args) != ArgsTupleType) { + // Attempt reasonable check for tuple literal compatibility + if (@typeInfo(@TypeOf(args)) != .@"struct" or !@typeInfo(@TypeOf(args)).@"struct".is_tuple) { + @compileError(std.fmt.comptimePrint( + "Direct .call expects arguments as a tuple literal compatible with {}, found type {}", + .{ ArgsTupleType, @TypeOf(args) }, + )); + } + // Further check field count/types if necessary + if (std.meta.fields(@TypeOf(args)).len != OriginalParams.len) { + @compileError(std.fmt.comptimePrint( + "Direct .call tuple literal has wrong number of arguments (expected {}, got {}) for {}", + .{ OriginalParams.len, std.meta.fields(@TypeOf(args)).len, ArgsTupleType }, + )); + } + // Could add type checks per field here too + } + + // 2. Define the tuple type needed for the call: .{*Instance, OriginalArgs...} + const CallArgsTupleType = comptime T: { + var tuple_fields: [OriginalParams.len + 1]std.builtin.Type.StructField = undefined; + tuple_fields[0] = .{ + .name = "0", + .type = @TypeOf(self.instance), + .default_value_ptr = null, + .is_comptime = false, + .alignment = 0, + }; + for (std.meta.fields(ArgsTupleType), 0..) |field, i| { + tuple_fields[i + 1] = .{ + .name = std.fmt.comptimePrint("{d}", .{i + 1}), + .type = field.type, + .default_value_ptr = null, + .is_comptime = false, + .alignment = 0, + }; + } + break :T @Type(.{ .@"struct" = .{ + .layout = .auto, + .fields = &tuple_fields, + .decls = &.{}, + .is_tuple = true, + } }); + }; + + // 3. Create and populate the tuple at runtime + var call_args_tuple: CallArgsTupleType = undefined; + @field(call_args_tuple, "0") = self.instance; + + comptime var i = 0; + inline while (i < OriginalParams.len) : (i += 1) { + const field_name = comptime std.fmt.comptimePrint("{}", .{i}); + // Check if field exists in args (useful for struct literals, less for tuples) + // For tuple literals, direct access should work if type check passed. + // if (@hasField(@TypeOf(args), field_name)) { ... } + const dest_field_name = comptime std.fmt.comptimePrint("{}", .{i + 1}); + @field(call_args_tuple, dest_field_name) = @field(args, field_name); + } + + // 4. Perform the call using the populated tuple + if (ReturnType == void) { + @call(.auto, self.method, call_args_tuple); + } else { + return @call(.auto, self.method, call_args_tuple); + } + } + + pub fn init(instance_: *Instance, method_: *const InstanceMethod) BoundFunction { + return .{ .instance = instance_, .method = method_ }; + } + }; +} + +const testing = std.testing; + +test "Bind Direct Call" { const Person = struct { name: []const u8, _buf: [1024]u8 = undefined, - - pub fn speak(self: *@This(), message: []const u8) ![]const u8 { - return std.fmt.bufPrint(self._buf[0..], "{s} says: >>{s}!<<\n", .{ self.name, message }); + pub fn speak(self: *@This(), msg: []const u8) ![]const u8 { + return std.fmt.bufPrint(&self._buf, "{s}: {s}", .{ self.name, msg }); } }; + const FuncSig = fn ([]const u8) anyerror![]const u8; + var p = Person{ .name = "Alice" }; + const bound = Bind(Person, FuncSig).init(&p, &Person.speak); + const res = try bound.call(.{"Hi"}); // Pass tuple literal + try testing.expectEqualStrings("Alice: Hi", res); +} +test "BindInterface Call (External)" { + const Person = struct { + name: []const u8, + _buf: [1024]u8 = undefined, + pub fn speak(self: *@This(), message: []const u8) ![]const u8 { + return std.fmt.bufPrint(&self._buf, "{s} says: >>{s}!<<\n", .{ self.name, message }); + } + }; const CallBack = fn ([]const u8) anyerror![]const u8; + var alice: Person = .{ .name = "Alice" }; + const BoundSpeak = Bind(Person, CallBack); + const bound_speak = BoundSpeak.init(&alice, &Person.speak); + var alice_interface = bound_speak.interface(); + const greeting = try alice_interface.call(.{"Hello"}); // Pass tuple literal + try testing.expectEqualStrings("Alice says: >>Hello!<<\n", greeting); +} + +test "BindInterface Polymorphism (External)" { + const Person = struct { + name: []const u8, + _buf: [1024]u8 = undefined, + pub fn speak(self: *@This(), message: []const u8) ![]const u8 { + return std.fmt.bufPrint(&self._buf, "{s} says: >>{s}!<<\n", .{ self.name, message }); + } + }; + const Dog = struct { + name: []const u8, + _buf: [1024]u8 = undefined, + pub fn bark(self: *@This(), message: []const u8) ![]const u8 { + return std.fmt.bufPrint(&self._buf, "{s} barks: >>{s}!<<\n", .{ self.name, message }); + } + }; + const CallBack = fn ([]const u8) anyerror![]const u8; + const CbInterface = CallbackInterface(CallBack); var alice: Person = .{ .name = "Alice" }; + const bound_alice = Bind(Person, CallBack).init(&alice, &Person.speak); + const alice_interface = bound_alice.interface(); - const bound_greet = Bind(Person, CallBack).init(&alice, &Person.speak); + var bob: Dog = .{ .name = "Bob" }; + const bound_bob = Bind(Dog, CallBack).init(&bob, &Dog.bark); + const bob_interface = bound_bob.interface(); - const greeting = try bound_greet.call(.{"Hello"}); + const interfaces = [_]CbInterface{ alice_interface, bob_interface }; + var results: [2][]const u8 = undefined; + for (interfaces, 0..) |iface, i| { + results[i] = try iface.call(.{"Test"}); + } // Pass tuple literal - try std.testing.expectEqualStrings("Alice says: >>Hello!<<\n", greeting); + try testing.expectEqualStrings("Alice says: >>Test!<<\n", results[0]); + try testing.expectEqualStrings("Bob barks: >>Test!<<\n", results[1]); +} + +test "Void Return Type (External Interface)" { + var counter: u32 = 0; + const Counter = struct { + count: *u32, + pub fn increment(self: *@This(), amount: u32) void { + self.count.* += amount; + } + }; + const Decrementer = struct { + count: *u32, + pub fn decrement(self: *@This(), amount: u32) void { + self.count.* -= amount; + } + }; + const IncrementFn = fn (u32) void; + const IncInterface = CallbackInterface(IncrementFn); + + var my_counter = Counter{ .count = &counter }; + const bound_inc = Bind(Counter, IncrementFn).init(&my_counter, &Counter.increment); + bound_inc.call(.{5}); + try testing.expectEqual(@as(u32, 5), counter); + + var my_dec = Decrementer{ .count = &counter }; + const bound_dec = Bind(Decrementer, IncrementFn).init(&my_dec, &Decrementer.decrement); + + const iface1 = bound_inc.interface(); + const iface2 = bound_dec.interface(); + const void_ifaces = [_]IncInterface{ iface1, iface2 }; + + void_ifaces[0].call(.{3}); // counter = 5 + 3 = 8 + try testing.expectEqual(@as(u32, 8), counter); + void_ifaces[1].call(.{2}); // counter = 8 - 2 = 6 + try testing.expectEqual(@as(u32, 6), counter); } From efd26fd3bb57959cf281053208a9584fd452dcac Mon Sep 17 00:00:00 2001 From: renerocksai Date: Thu, 27 Mar 2025 01:12:40 +0100 Subject: [PATCH 41/57] app cosmetics --- src/App.zig | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/App.zig b/src/App.zig index 1fd67da..ac55f30 100644 --- a/src/App.zig +++ b/src/App.zig @@ -55,24 +55,24 @@ pub fn Create(comptime Context: type) type { pub fn Wrap(T: type) type { return struct { wrapped: *T, - wrapper: Interface, + interface: Interface, opts: Opts, app_context: *Context, const Wrapped = @This(); - pub fn unwrap(wrapper: *Interface) *Wrapped { - const self: *Wrapped = @alignCast(@fieldParentPtr("wrapper", wrapper)); + pub fn unwrap(interface: *Interface) *Wrapped { + const self: *Wrapped = @alignCast(@fieldParentPtr("interface", interface)); return self; } pub fn destroy(allocator: Allocator, wrapper: *Interface) void { - const self: *Wrapped = @alignCast(@fieldParentPtr("wrapper", wrapper)); + const self: *Wrapped = @alignCast(@fieldParentPtr("interface", wrapper)); allocator.destroy(self); } - pub fn onRequestWrapped(wrapper: *Interface, r: zap.Request) !void { - var self: *Wrapped = Wrapped.unwrap(wrapper); + pub fn onRequestWrapped(interface: *Interface, r: zap.Request) !void { + var self: *Wrapped = Wrapped.unwrap(interface); const arena = try get_arena(); try self.onRequest(arena.allocator(), self.app_context, r); arena.reset(.{ .retain_capacity = self.opts.arena_retain_capacity }); From 8ecceba81ec59249cc819d8265e9e3729714d1fc Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sat, 29 Mar 2025 09:25:46 +0100 Subject: [PATCH 42/57] Improve code comment in router.zig From 8654ad8310b7eb8466feb52f564ca7be02626e3a Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sat, 29 Mar 2025 09:25:46 +0100 Subject: [PATCH 43/57] Improve code comment in router.zig --- src/router.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/router.zig b/src/router.zig index f13d0a9..9b99453 100644 --- a/src/router.zig +++ b/src/router.zig @@ -45,6 +45,7 @@ pub fn deinit(self: *Router) void { } /// Call this to add a route with an unbound handler: a handler that is not member of a struct. +/// To be precise: a handler that doesn't take an instance pointer as first argument. pub fn handle_func_unbound(self: *Router, path: []const u8, h: zap.HttpRequestFn) !void { if (path.len == 0) { return RouterError.EmptyPath; From 51d17a1868daeb95617f85443afbc67f17d13235 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sat, 29 Mar 2025 09:25:46 +0100 Subject: [PATCH 44/57] restore lost changes --- src/App.zig | 228 ++++++++++++++++++++++++----------------------- src/endpoint.zig | 4 +- 2 files changed, 117 insertions(+), 115 deletions(-) diff --git a/src/App.zig b/src/App.zig index ac55f30..d85ec13 100644 --- a/src/App.zig +++ b/src/App.zig @@ -32,7 +32,7 @@ pub fn Create(comptime Context: type) type { context: *Context = undefined, gpa: Allocator = undefined, opts: Opts = undefined, - endpoints: std.StringArrayHashMapUnmanaged(*Endpoint.Wrapper.Interface) = .empty, + endpoints: std.StringArrayHashMapUnmanaged(*Endpoint.Interface) = .empty, there_can_be_only_one: bool = false, track_arenas: std.ArrayListUnmanaged(*ArenaAllocator) = .empty, @@ -46,116 +46,118 @@ pub fn Create(comptime Context: type) type { var on_request: ?*const fn (Allocator, *Context, Request) anyerror!void = null; pub const Endpoint = struct { - pub const Wrapper = struct { - pub const Interface = struct { - call: *const fn (*Interface, zap.Request) anyerror!void = undefined, - path: []const u8, - destroy: *const fn (allocator: Allocator, *Interface) void = undefined, - }; - pub fn Wrap(T: type) type { - return struct { - wrapped: *T, - interface: Interface, - opts: Opts, - app_context: *Context, - - const Wrapped = @This(); - - pub fn unwrap(interface: *Interface) *Wrapped { - const self: *Wrapped = @alignCast(@fieldParentPtr("interface", interface)); - return self; - } - - pub fn destroy(allocator: Allocator, wrapper: *Interface) void { - const self: *Wrapped = @alignCast(@fieldParentPtr("interface", wrapper)); - allocator.destroy(self); - } - - pub fn onRequestWrapped(interface: *Interface, r: zap.Request) !void { - var self: *Wrapped = Wrapped.unwrap(interface); - const arena = try get_arena(); - try self.onRequest(arena.allocator(), self.app_context, r); - arena.reset(.{ .retain_capacity = self.opts.arena_retain_capacity }); - } - - pub fn onRequest(self: *Wrapped, arena: Allocator, app_context: *Context, r: zap.Request) !void { - const ret = switch (r.methodAsEnum()) { - .GET => self.wrapped.*.get(arena, app_context, r), - .POST => self.wrapped.*.post(arena, app_context, r), - .PUT => self.wrapped.*.put(arena, app_context, r), - .DELETE => self.wrapped.*.delete(arena, app_context, r), - .PATCH => self.wrapped.*.patch(arena, app_context, r), - .OPTIONS => self.wrapped.*.options(arena, app_context, r), - else => error.UnsupportedHtmlRequestMethod, - }; - if (ret) { - // handled without error - } else |err| { - switch (self.wrapped.*.error_strategy) { - .raise => return err, - .log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505), - .log_to_console => zap.debug("Error in {} {s} : {}", .{ Wrapped, r.method orelse "(no method)", err }), - } - } - } - }; - } - - pub fn init(T: type, value: *T, app_opts: Opts, app_context: *Context) Wrapper.Wrap(T) { - checkEndpointType(T); - var ret: Wrapper.Wrap(T) = .{ - .wrapped = value, - .wrapper = .{ .path = value.path }, - .opts = app_opts, - .app_context = app_context, - }; - ret.wrapper.call = Wrapper.Wrap(T).onRequestWrapped; - ret.wrapper.destroy = Wrapper.Wrap(T).destroy; - return ret; - } - - pub fn checkEndpointType(T: type) void { - if (@hasField(T, "path")) { - if (@FieldType(T, "path") != []const u8) { - @compileError(@typeName(@FieldType(T, "path")) ++ " has wrong type, expected: []const u8"); - } - } else { - @compileError(@typeName(T) ++ " has no path field"); - } - - if (@hasField(T, "error_strategy")) { - if (@FieldType(T, "error_strategy") != zap.Endpoint.ErrorStrategy) { - @compileError(@typeName(@FieldType(T, "error_strategy")) ++ " has wrong type, expected: zap.Endpoint.ErrorStrategy"); - } - } else { - @compileError(@typeName(T) ++ " has no error_strategy field"); - } - - const methods_to_check = [_][]const u8{ - "get", - "post", - "put", - "delete", - "patch", - "options", - }; - inline for (methods_to_check) |method| { - if (@hasDecl(T, method)) { - if (@TypeOf(@field(T, method)) != fn (_: *T, _: Allocator, _: *Context, _: zap.Request) anyerror!void) { - @compileError(method ++ " method of " ++ @typeName(T) ++ " has wrong type:\n" ++ @typeName(@TypeOf(T.get)) ++ "\nexpected:\n" ++ @typeName(fn (_: *T, _: Allocator, _: *Context, _: zap.Request) anyerror!void)); - } - } else { - @compileError(@typeName(T) ++ " has no method named `" ++ method ++ "`"); - } - } - } + pub const Interface = struct { + call: *const fn (*Interface, zap.Request) anyerror!void = undefined, + path: []const u8, + destroy: *const fn (allocator: Allocator, *Interface) void = undefined, }; + pub fn Wrap(T: type) type { + return struct { + wrapped: *T, + interface: Interface, + opts: Opts, + app_context: *Context, + + const Wrapped = @This(); + + pub fn unwrap(interface: *Interface) *Wrapped { + const self: *Wrapped = @alignCast(@fieldParentPtr("interface", interface)); + return self; + } + + pub fn destroy(allocator: Allocator, wrapper: *Interface) void { + const self: *Wrapped = @alignCast(@fieldParentPtr("interface", wrapper)); + allocator.destroy(self); + } + + pub fn onRequestWrapped(interface: *Interface, r: zap.Request) !void { + var self: *Wrapped = Wrapped.unwrap(interface); + const arena = try get_arena(); + try self.onRequest(arena.allocator(), self.app_context, r); + arena.reset(.{ .retain_capacity = self.opts.arena_retain_capacity }); + } + + pub fn onRequest(self: *Wrapped, arena: Allocator, app_context: *Context, r: zap.Request) !void { + const ret = switch (r.methodAsEnum()) { + .GET => self.wrapped.*.get(arena, app_context, r), + .POST => self.wrapped.*.post(arena, app_context, r), + .PUT => self.wrapped.*.put(arena, app_context, r), + .DELETE => self.wrapped.*.delete(arena, app_context, r), + .PATCH => self.wrapped.*.patch(arena, app_context, r), + .OPTIONS => self.wrapped.*.options(arena, app_context, r), + else => error.UnsupportedHtmlRequestMethod, + }; + if (ret) { + // handled without error + } else |err| { + switch (self.wrapped.*.error_strategy) { + .raise => return err, + .log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505), + .log_to_console => zap.debug("Error in {} {s} : {}", .{ Wrapped, r.method orelse "(no method)", err }), + } + } + } + }; + } + + pub fn init(T: type, value: *T, app_opts: Opts, app_context: *Context) Endpoint.Wrap(T) { + checkEndpointType(T); + var ret: Endpoint.Wrap(T) = .{ + .wrapped = value, + .wrapper = .{ .path = value.path }, + .opts = app_opts, + .app_context = app_context, + }; + ret.wrapper.call = Endpoint.Wrap(T).onRequestWrapped; + ret.wrapper.destroy = Endpoint.Wrap(T).destroy; + return ret; + } + + pub fn checkEndpointType(T: type) void { + if (@hasField(T, "path")) { + if (@FieldType(T, "path") != []const u8) { + @compileError(@typeName(@FieldType(T, "path")) ++ " has wrong type, expected: []const u8"); + } + } else { + @compileError(@typeName(T) ++ " has no path field"); + } + + if (@hasField(T, "error_strategy")) { + if (@FieldType(T, "error_strategy") != zap.Endpoint.ErrorStrategy) { + @compileError(@typeName(@FieldType(T, "error_strategy")) ++ " has wrong type, expected: zap.Endpoint.ErrorStrategy"); + } + } else { + @compileError(@typeName(T) ++ " has no error_strategy field"); + } + + const methods_to_check = [_][]const u8{ + "get", + "post", + "put", + "delete", + "patch", + "options", + }; + inline for (methods_to_check) |method| { + if (@hasDecl(T, method)) { + if (@TypeOf(@field(T, method)) != fn (_: *T, _: Allocator, _: *Context, _: zap.Request) anyerror!void) { + @compileError(method ++ " method of " ++ @typeName(T) ++ " has wrong type:\n" ++ @typeName(@TypeOf(T.get)) ++ "\nexpected:\n" ++ @typeName(fn (_: *T, _: Allocator, _: *Context, _: zap.Request) anyerror!void)); + } + } else { + @compileError(@typeName(T) ++ " has no method named `" ++ method ++ "`"); + } + } + } }; - pub const Listener = struct { - pub const Settings = struct { - // - }; + pub const ListenerSettings = struct { + port: usize, + interface: [*c]const u8 = null, + public_folder: ?[]const u8 = null, + max_clients: ?isize = null, + max_body_size: ?usize = null, + timeout: ?u8 = null, + tls: ?zap.Tls = null, }; pub fn init(gpa_: Allocator, context_: *Context, opts_: Opts) !App { @@ -208,17 +210,17 @@ pub fn Create(comptime Context: type) type { endpoint.path, other.path, )) { - return zap.Endpoint.EndpointListenerError.EndpointPathShadowError; + return zap.Endpoint.ListenerError.EndpointPathShadowError; } } const EndpointType = @typeInfo(@TypeOf(endpoint)).pointer.child; - Endpoint.Wrapper.checkEndpointType(EndpointType); - const wrapper = try self.gpa.create(Endpoint.Wrapper.Wrap(EndpointType)); - wrapper.* = Endpoint.Wrapper.init(EndpointType, endpoint); + Endpoint.checkEndpointType(EndpointType); + const wrapper = try self.gpa.create(Endpoint.Wrap(EndpointType)); + wrapper.* = Endpoint.init(EndpointType, endpoint); try App._static.endpoints.append(self.gpa, &wrapper.wrapper); } - pub fn listen(self: *App, l: Listener.Settings) !void { + pub fn listen(self: *App, l: ListenerSettings) !void { _ = self; _ = l; // TODO: do it diff --git a/src/endpoint.zig b/src/endpoint.zig index b004788..6ca970b 100644 --- a/src/endpoint.zig +++ b/src/endpoint.zig @@ -246,7 +246,7 @@ pub fn Authenticating(EndpointType: type, Authenticator: type) type { }; } -pub const EndpointListenerError = error{ +pub const ListenerError = error{ /// Since we use .startsWith to check for matching paths, you cannot use /// endpoint paths that overlap at the beginning. --> When trying to register /// an endpoint whose path would shadow an already registered one, you will @@ -323,7 +323,7 @@ pub const Listener = struct { e.path, other.path, )) { - return EndpointListenerError.EndpointPathShadowError; + return ListenerError.EndpointPathShadowError; } } const EndpointType = @typeInfo(@TypeOf(e)).pointer.child; From 458d6ee071975af01ea4a2faa0da50d1265c09fb Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sat, 29 Mar 2025 14:02:58 +0100 Subject: [PATCH 45/57] First zap.App test SUCCESS!!! --- build.zig | 1 + examples/app/main.zig | 89 ++++++++++++++++++++ src/App.zig | 184 +++++++++++++++++++++++++++--------------- src/zap.zig | 2 + 4 files changed, 211 insertions(+), 65 deletions(-) create mode 100644 examples/app/main.zig diff --git a/build.zig b/build.zig index 8364752..1629048 100644 --- a/build.zig +++ b/build.zig @@ -50,6 +50,7 @@ pub fn build(b: *std.Build) !void { name: []const u8, src: []const u8, }{ + .{ .name = "app", .src = "examples/app/main.zig" }, .{ .name = "hello", .src = "examples/hello/hello.zig" }, .{ .name = "https", .src = "examples/https/https.zig" }, .{ .name = "hello2", .src = "examples/hello2/hello2.zig" }, diff --git a/examples/app/main.zig b/examples/app/main.zig new file mode 100644 index 0000000..6186e31 --- /dev/null +++ b/examples/app/main.zig @@ -0,0 +1,89 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const zap = @import("zap"); + +const MyContext = struct { + db_connection: []const u8, + + pub fn init(connection: []const u8) MyContext { + return .{ + .db_connection = connection, + }; + } +}; + +const SimpleEndpoint = struct { + + // Endpoint Interface part + path: []const u8, + error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response, + + some_data: []const u8, + + pub fn init(path: []const u8, data: []const u8) SimpleEndpoint { + return .{ + .path = path, + .some_data = data, + }; + } + + pub fn get(e: *SimpleEndpoint, arena: Allocator, context: *MyContext, r: zap.Request) anyerror!void { + r.setStatus(.ok); + + const thread_id = std.Thread.getCurrentId(); + // look, we use the arena allocator here + // and we also just try it, not worrying about errors + const response_text = try std.fmt.allocPrint( + arena, + \\Hello! + \\context.db_connection: {s} + \\endpoint.data: {s} + \\arena: {} + \\thread_id: {} + \\ + , + .{ context.db_connection, e.some_data, arena.ptr, thread_id }, + ); + try r.sendBody(response_text); + std.time.sleep(std.time.ns_per_ms * 300); + } + + // empty stubs for all other request methods + pub fn post(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) anyerror!void {} + pub fn put(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) anyerror!void {} + pub fn delete(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) anyerror!void {} + pub fn patch(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) anyerror!void {} + pub fn options(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) anyerror!void {} +}; + +pub fn main() !void { + var my_context = MyContext.init("db connection established!"); + + var gpa: std.heap.GeneralPurposeAllocator(.{ + // just to be explicit + .thread_safe = true, + }) = .{}; + defer std.debug.print("\n\nLeaks detected: {}\n\n", .{gpa.deinit() != .ok}); + + const allocator = gpa.allocator(); + const App = zap.App.Create(MyContext); + var app = try App.init(allocator, &my_context, .{}); + defer app.deinit(); + + var my_endpoint = SimpleEndpoint.init("/", "some endpoint specific data"); + + try app.register(&my_endpoint); + + try app.listen(.{ + .interface = "0.0.0.0", + .port = 3000, + }); + std.debug.print("Listening on 0.0.0.0:3000\n", .{}); + + // start worker threads -- only 1 process!!! + zap.start(.{ + .threads = 2, + .workers = 1, + }); +} diff --git a/src/App.zig b/src/App.zig index d85ec13..9314ae2 100644 --- a/src/App.zig +++ b/src/App.zig @@ -8,19 +8,19 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; -const RwLock = std.Thread.RwLock; +const Thread = std.Thread; +const RwLock = Thread.RwLock; const zap = @import("zap.zig"); const Request = zap.Request; +const HttpListener = zap.HttpListener; -pub const Opts = struct { +pub const AppOpts = struct { /// ErrorStrategy for (optional) request handler if no endpoint matches default_error_strategy: zap.Endpoint.ErrorStrategy = .log_to_console, arena_retain_capacity: usize = 16 * 1024 * 1024, }; -threadlocal var _arena: ?ArenaAllocator = null; - /// creates an App with custom app context pub fn Create(comptime Context: type) type { return struct { @@ -31,12 +31,22 @@ pub fn Create(comptime Context: type) type { const InstanceData = struct { context: *Context = undefined, gpa: Allocator = undefined, - opts: Opts = undefined, - endpoints: std.StringArrayHashMapUnmanaged(*Endpoint.Interface) = .empty, + opts: AppOpts = undefined, + // endpoints: std.StringArrayHashMapUnmanaged(*Endpoint.Interface) = .empty, + endpoints: std.ArrayListUnmanaged(*Endpoint.Interface) = .empty, there_can_be_only_one: bool = false, - track_arenas: std.ArrayListUnmanaged(*ArenaAllocator) = .empty, + track_arenas: std.AutoHashMapUnmanaged(Thread.Id, ArenaAllocator) = .empty, track_arena_lock: RwLock = .{}, + + /// the internal http listener + listener: HttpListener = undefined, + + /// function pointer to handler for otherwise unhandled requests + /// Will automatically be set if your Context provides an unhandled + /// function of type `fn(*Context, Allocator, Request)` + /// + unhandled: ?*const fn (*Context, Allocator, Request) anyerror!void = null, }; var _static: InstanceData = .{}; @@ -47,15 +57,16 @@ pub fn Create(comptime Context: type) type { pub const Endpoint = struct { pub const Interface = struct { - call: *const fn (*Interface, zap.Request) anyerror!void = undefined, + call: *const fn (*Interface, Request) anyerror!void = undefined, path: []const u8, - destroy: *const fn (allocator: Allocator, *Interface) void = undefined, + destroy: *const fn (*Interface, Allocator) void = undefined, }; pub fn Wrap(T: type) type { return struct { wrapped: *T, interface: Interface, - opts: Opts, + + // tbh: unnecessary, since we have it in _static app_context: *Context, const Wrapped = @This(); @@ -65,19 +76,19 @@ pub fn Create(comptime Context: type) type { return self; } - pub fn destroy(allocator: Allocator, wrapper: *Interface) void { - const self: *Wrapped = @alignCast(@fieldParentPtr("interface", wrapper)); + pub fn destroy(interface: *Interface, allocator: Allocator) void { + const self: *Wrapped = @alignCast(@fieldParentPtr("interface", interface)); allocator.destroy(self); } - pub fn onRequestWrapped(interface: *Interface, r: zap.Request) !void { + pub fn onRequestWrapped(interface: *Interface, r: Request) !void { var self: *Wrapped = Wrapped.unwrap(interface); - const arena = try get_arena(); + var arena = try get_arena(); try self.onRequest(arena.allocator(), self.app_context, r); - arena.reset(.{ .retain_capacity = self.opts.arena_retain_capacity }); + _ = arena.reset(.{ .retain_with_limit = _static.opts.arena_retain_capacity }); } - pub fn onRequest(self: *Wrapped, arena: Allocator, app_context: *Context, r: zap.Request) !void { + pub fn onRequest(self: *Wrapped, arena: Allocator, app_context: *Context, r: Request) !void { const ret = switch (r.methodAsEnum()) { .GET => self.wrapped.*.get(arena, app_context, r), .POST => self.wrapped.*.post(arena, app_context, r), @@ -100,17 +111,17 @@ pub fn Create(comptime Context: type) type { }; } - pub fn init(T: type, value: *T, app_opts: Opts, app_context: *Context) Endpoint.Wrap(T) { + pub fn init(T: type, value: *T) Endpoint.Wrap(T) { checkEndpointType(T); - var ret: Endpoint.Wrap(T) = .{ + return .{ .wrapped = value, - .wrapper = .{ .path = value.path }, - .opts = app_opts, - .app_context = app_context, + .interface = .{ + .path = value.path, + .call = Endpoint.Wrap(T).onRequestWrapped, + .destroy = Endpoint.Wrap(T).destroy, + }, + .app_context = _static.context, }; - ret.wrapper.call = Endpoint.Wrap(T).onRequestWrapped; - ret.wrapper.destroy = Endpoint.Wrap(T).destroy; - return ret; } pub fn checkEndpointType(T: type) void { @@ -140,8 +151,10 @@ pub fn Create(comptime Context: type) type { }; inline for (methods_to_check) |method| { if (@hasDecl(T, method)) { - if (@TypeOf(@field(T, method)) != fn (_: *T, _: Allocator, _: *Context, _: zap.Request) anyerror!void) { - @compileError(method ++ " method of " ++ @typeName(T) ++ " has wrong type:\n" ++ @typeName(@TypeOf(T.get)) ++ "\nexpected:\n" ++ @typeName(fn (_: *T, _: Allocator, _: *Context, _: zap.Request) anyerror!void)); + const Method = @TypeOf(@field(T, method)); + const Expected = fn (_: *T, _: Allocator, _: *Context, _: Request) anyerror!void; + if (Method != Expected) { + @compileError(method ++ " method of " ++ @typeName(T) ++ " has wrong type:\n" ++ @typeName(Method) ++ "\nexpected:\n" ++ @typeName(Expected)); } } else { @compileError(@typeName(T) ++ " has no method named `" ++ method ++ "`"); @@ -151,8 +164,10 @@ pub fn Create(comptime Context: type) type { }; pub const ListenerSettings = struct { - port: usize, + /// IP interface, e.g. 0.0.0.0 interface: [*c]const u8 = null, + /// IP port to listen on + port: usize, public_folder: ?[]const u8 = null, max_clients: ?isize = null, max_body_size: ?usize = null, @@ -160,47 +175,79 @@ pub fn Create(comptime Context: type) type { tls: ?zap.Tls = null, }; - pub fn init(gpa_: Allocator, context_: *Context, opts_: Opts) !App { - if (App._static._there_can_be_only_one) { + pub fn init(gpa_: Allocator, context_: *Context, opts_: AppOpts) !App { + if (_static.there_can_be_only_one) { return error.OnlyOneAppAllowed; } - App._static.context = context_; - App._static.gpa = gpa_; - App._static.opts = opts_; - App._static.there_can_be_only_one = true; + _static.context = context_; + _static.gpa = gpa_; + _static.opts = opts_; + _static.there_can_be_only_one = true; + + // set unhandled callback if provided by Context + if (@hasDecl(Context, "unhandled")) { + // try if we can use it + const Unhandled = @TypeOf(@field(Context, "unhandled")); + const Expected = fn (_: *Context, _: Allocator, _: Request) anyerror!void; + if (Unhandled != Expected) { + @compileError("`unhandled` method of " ++ @typeName(Context) ++ " has wrong type:\n" ++ @typeName(Unhandled) ++ "\nexpected:\n" ++ @typeName(Expected)); + } + _static.unhandled = Context.unhandled; + } return .{}; } - pub fn deinit() void { - App._static.endpoints.deinit(_static.gpa); + pub fn deinit(_: *App) void { + // we created endpoint wrappers but only tracked their interfaces + // hence, we need to destroy the wrappers through their interfaces + if (false) { + var it = _static.endpoints.iterator(); + while (it.next()) |kv| { + const interface = kv.value_ptr; + interface.*.destroy(_static.gpa); + } + } else { + for (_static.endpoints.items) |interface| { + interface.destroy(interface, _static.gpa); + } + } + _static.endpoints.deinit(_static.gpa); - App._static.track_arena_lock.lock(); - defer App._static.track_arena_lock.unlock(); - for (App._static.track_arenas.items) |arena| { + _static.track_arena_lock.lock(); + defer _static.track_arena_lock.unlock(); + + var it = _static.track_arenas.valueIterator(); + while (it.next()) |arena| { + // std.debug.print("deiniting arena: {*}\n", .{arena}); arena.deinit(); } + _static.track_arenas.deinit(_static.gpa); } - fn get_arena() !*ArenaAllocator { - App._static.track_arena_lock.lockShared(); - if (_arena == null) { - App._static.track_arena_lock.unlockShared(); - App._static.track_arena_lock.lock(); - defer App._static.track_arena_lock.unlock(); - _arena = ArenaAllocator.init(App._static.gpa); - try App._static.track_arenas.append(App._static.gpa, &_arena.?); + pub fn get_arena() !*ArenaAllocator { + const thread_id = std.Thread.getCurrentId(); + _static.track_arena_lock.lockShared(); + if (_static.track_arenas.getPtr(thread_id)) |arena| { + _static.track_arena_lock.unlockShared(); + return arena; } else { - App._static.track_arena_lock.unlockShared(); - return &_arena.?; + _static.track_arena_lock.unlockShared(); + _static.track_arena_lock.lock(); + defer _static.track_arena_lock.unlock(); + const arena = ArenaAllocator.init(_static.gpa); + try _static.track_arenas.put(_static.gpa, thread_id, arena); + return _static.track_arenas.getPtr(thread_id).?; } } /// Register an endpoint with this listener. - /// NOTE: endpoint paths are matched with startsWith -> so use endpoints with distinctly starting names!! - /// If you try to register an endpoint whose path would shadow an already registered one, you will - /// receive an EndpointPathShadowError. - pub fn register(self: *App, endpoint: anytype) !void { - for (App._static.endpoints.items) |other| { + /// NOTE: endpoint paths are matched with startsWith + /// -> so use endpoints with distinctly starting names!! + /// If you try to register an endpoint whose path would shadow an + /// already registered one, you will receive an + /// EndpointPathShadowError. + pub fn register(_: *App, endpoint: anytype) !void { + for (_static.endpoints.items) |other| { if (std.mem.startsWith( u8, other.path, @@ -215,31 +262,38 @@ pub fn Create(comptime Context: type) type { } const EndpointType = @typeInfo(@TypeOf(endpoint)).pointer.child; Endpoint.checkEndpointType(EndpointType); - const wrapper = try self.gpa.create(Endpoint.Wrap(EndpointType)); + const wrapper = try _static.gpa.create(Endpoint.Wrap(EndpointType)); wrapper.* = Endpoint.init(EndpointType, endpoint); - try App._static.endpoints.append(self.gpa, &wrapper.wrapper); + try _static.endpoints.append(_static.gpa, &wrapper.interface); } - pub fn listen(self: *App, l: ListenerSettings) !void { - _ = self; - _ = l; - // TODO: do it + pub fn listen(_: *App, l: ListenerSettings) !void { + _static.listener = HttpListener.init(.{ + .interface = l.interface, + .port = l.port, + .public_folder = l.public_folder, + .max_clients = l.max_clients, + .max_body_size = l.max_body_size, + .timeout = l.timeout, + .tls = l.tls, + + .on_request = onRequest, + }); + try _static.listener.listen(); } fn onRequest(r: Request) !void { if (r.path) |p| { - for (App._static.endpoints.items) |wrapper| { + for (_static.endpoints.items) |wrapper| { if (std.mem.startsWith(u8, p, wrapper.path)) { return try wrapper.call(wrapper, r); } } } if (on_request) |foo| { - if (_arena == null) { - _arena = ArenaAllocator.init(App._static.gpa); - } - foo(_arena.allocator(), App._static.context, r) catch |err| { - switch (App._static.opts.default_error_strategy) { + var arena = try get_arena(); + foo(arena.allocator(), _static.context, r) catch |err| { + switch (_static.opts.default_error_strategy) { .raise => return err, .log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505), .log_to_console => zap.debug("Error in {} {s} : {}", .{ App, r.method orelse "(no method)", err }), diff --git a/src/zap.zig b/src/zap.zig index 9389d84..09844a8 100644 --- a/src/zap.zig +++ b/src/zap.zig @@ -13,6 +13,8 @@ pub const Endpoint = @import("endpoint.zig"); pub const Router = @import("router.zig"); +pub const App = @import("App.zig"); + /// A struct to handle Mustache templating. /// /// This is a wrapper around fiobj's mustache template handling. From b05004291e418dd206e255a0b0eb8b6d4c9e329c Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 30 Mar 2025 12:20:43 +0200 Subject: [PATCH 46/57] refactored endpoint Binder/Bind/Bound/Interface ... namings --- src/App.zig | 63 +++++++++++++++++++------------------- src/endpoint.zig | 78 +++++++++++++++++++++++++----------------------- 2 files changed, 73 insertions(+), 68 deletions(-) diff --git a/src/App.zig b/src/App.zig index 9314ae2..ad0740f 100644 --- a/src/App.zig +++ b/src/App.zig @@ -32,7 +32,6 @@ pub fn Create(comptime Context: type) type { context: *Context = undefined, gpa: Allocator = undefined, opts: AppOpts = undefined, - // endpoints: std.StringArrayHashMapUnmanaged(*Endpoint.Interface) = .empty, endpoints: std.ArrayListUnmanaged(*Endpoint.Interface) = .empty, there_can_be_only_one: bool = false, @@ -61,64 +60,68 @@ pub fn Create(comptime Context: type) type { path: []const u8, destroy: *const fn (*Interface, Allocator) void = undefined, }; - pub fn Wrap(T: type) type { + pub fn Bind(ArbitraryEndpoint: type) type { return struct { - wrapped: *T, + endpoint: *ArbitraryEndpoint, interface: Interface, // tbh: unnecessary, since we have it in _static app_context: *Context, - const Wrapped = @This(); + const Bound = @This(); - pub fn unwrap(interface: *Interface) *Wrapped { - const self: *Wrapped = @alignCast(@fieldParentPtr("interface", interface)); + pub fn unwrap(interface: *Interface) *Bound { + const self: *Bound = @alignCast(@fieldParentPtr("interface", interface)); return self; } pub fn destroy(interface: *Interface, allocator: Allocator) void { - const self: *Wrapped = @alignCast(@fieldParentPtr("interface", interface)); + const self: *Bound = @alignCast(@fieldParentPtr("interface", interface)); allocator.destroy(self); } - pub fn onRequestWrapped(interface: *Interface, r: Request) !void { - var self: *Wrapped = Wrapped.unwrap(interface); + pub fn onRequestInterface(interface: *Interface, r: Request) !void { + var self: *Bound = Bound.unwrap(interface); var arena = try get_arena(); try self.onRequest(arena.allocator(), self.app_context, r); _ = arena.reset(.{ .retain_with_limit = _static.opts.arena_retain_capacity }); } - pub fn onRequest(self: *Wrapped, arena: Allocator, app_context: *Context, r: Request) !void { + pub fn onRequest(self: *Bound, arena: Allocator, app_context: *Context, r: Request) !void { const ret = switch (r.methodAsEnum()) { - .GET => self.wrapped.*.get(arena, app_context, r), - .POST => self.wrapped.*.post(arena, app_context, r), - .PUT => self.wrapped.*.put(arena, app_context, r), - .DELETE => self.wrapped.*.delete(arena, app_context, r), - .PATCH => self.wrapped.*.patch(arena, app_context, r), - .OPTIONS => self.wrapped.*.options(arena, app_context, r), + .GET => self.endpoint.*.get(arena, app_context, r), + .POST => self.endpoint.*.post(arena, app_context, r), + .PUT => self.endpoint.*.put(arena, app_context, r), + .DELETE => self.endpoint.*.delete(arena, app_context, r), + .PATCH => self.endpoint.*.patch(arena, app_context, r), + .OPTIONS => self.endpoint.*.options(arena, app_context, r), else => error.UnsupportedHtmlRequestMethod, }; if (ret) { // handled without error } else |err| { - switch (self.wrapped.*.error_strategy) { + switch (self.endpoint.*.error_strategy) { .raise => return err, .log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505), - .log_to_console => zap.debug("Error in {} {s} : {}", .{ Wrapped, r.method orelse "(no method)", err }), + .log_to_console => zap.debug( + "Error in {} {s} : {}", + .{ Bound, r.method orelse "(no method)", err }, + ), } } } }; } - pub fn init(T: type, value: *T) Endpoint.Wrap(T) { - checkEndpointType(T); + pub fn init(ArbitraryEndpoint: type, endpoint: *ArbitraryEndpoint) Endpoint.Bind(ArbitraryEndpoint) { + checkEndpointType(ArbitraryEndpoint); + const BoundEp = Endpoint.Bind(ArbitraryEndpoint); return .{ - .wrapped = value, + .endpoint = endpoint, .interface = .{ - .path = value.path, - .call = Endpoint.Wrap(T).onRequestWrapped, - .destroy = Endpoint.Wrap(T).destroy, + .path = endpoint.path, + .call = BoundEp.onRequestInterface, + .destroy = BoundEp.destroy, }, .app_context = _static.context, }; @@ -262,9 +265,9 @@ pub fn Create(comptime Context: type) type { } const EndpointType = @typeInfo(@TypeOf(endpoint)).pointer.child; Endpoint.checkEndpointType(EndpointType); - const wrapper = try _static.gpa.create(Endpoint.Wrap(EndpointType)); - wrapper.* = Endpoint.init(EndpointType, endpoint); - try _static.endpoints.append(_static.gpa, &wrapper.interface); + const bound = try _static.gpa.create(Endpoint.Bind(EndpointType)); + bound.* = Endpoint.init(EndpointType, endpoint); + try _static.endpoints.append(_static.gpa, &bound.interface); } pub fn listen(_: *App, l: ListenerSettings) !void { @@ -284,9 +287,9 @@ pub fn Create(comptime Context: type) type { fn onRequest(r: Request) !void { if (r.path) |p| { - for (_static.endpoints.items) |wrapper| { - if (std.mem.startsWith(u8, p, wrapper.path)) { - return try wrapper.call(wrapper, r); + for (_static.endpoints.items) |interface| { + if (std.mem.startsWith(u8, p, interface.path)) { + return try interface.call(interface, r); } } } diff --git a/src/endpoint.zig b/src/endpoint.zig index 6ca970b..c32a489 100644 --- a/src/endpoint.zig +++ b/src/endpoint.zig @@ -106,66 +106,68 @@ pub fn checkEndpointType(T: type) void { } } -pub const Wrapper = struct { +pub const Binder = struct { pub const Interface = struct { call: *const fn (*Interface, zap.Request) anyerror!void = undefined, path: []const u8, - destroy: *const fn (allocator: std.mem.Allocator, *Interface) void = undefined, + destroy: *const fn (*Interface, std.mem.Allocator) void = undefined, }; - pub fn Wrap(T: type) type { + pub fn Bind(ArbitraryEndpoint: type) type { return struct { - wrapped: *T, - wrapper: Interface, + endpoint: *ArbitraryEndpoint, + interface: Interface, - const Wrapped = @This(); + const Bound = @This(); - pub fn unwrap(wrapper: *Interface) *Wrapped { - const self: *Wrapped = @alignCast(@fieldParentPtr("wrapper", wrapper)); + pub fn unwrap(interface: *Interface) *Bound { + const self: *Bound = @alignCast(@fieldParentPtr("interface", interface)); return self; } - pub fn destroy(allocator: std.mem.Allocator, wrapper: *Interface) void { - const self: *Wrapped = @alignCast(@fieldParentPtr("wrapper", wrapper)); + pub fn destroy(interface: *Interface, allocator: std.mem.Allocator) void { + const self: *Bound = @alignCast(@fieldParentPtr("interface", interface)); allocator.destroy(self); } - pub fn onRequestWrapped(wrapper: *Interface, r: zap.Request) !void { - var self: *Wrapped = Wrapped.unwrap(wrapper); + pub fn onRequestInterface(interface: *Interface, r: zap.Request) !void { + var self: *Bound = Bound.unwrap(interface); try self.onRequest(r); } - pub fn onRequest(self: *Wrapped, r: zap.Request) !void { + pub fn onRequest(self: *Bound, r: zap.Request) !void { const ret = switch (r.methodAsEnum()) { - .GET => self.wrapped.*.get(r), - .POST => self.wrapped.*.post(r), - .PUT => self.wrapped.*.put(r), - .DELETE => self.wrapped.*.delete(r), - .PATCH => self.wrapped.*.patch(r), - .OPTIONS => self.wrapped.*.options(r), + .GET => self.endpoint.*.get(r), + .POST => self.endpoint.*.post(r), + .PUT => self.endpoint.*.put(r), + .DELETE => self.endpoint.*.delete(r), + .PATCH => self.endpoint.*.patch(r), + .OPTIONS => self.endpoint.*.options(r), else => error.UnsupportedHtmlRequestMethod, }; if (ret) { // handled without error } else |err| { - switch (self.wrapped.*.error_strategy) { + switch (self.endpoint.*.error_strategy) { .raise => return err, .log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505), - .log_to_console => zap.debug("Error in {} {s} : {}", .{ Wrapped, r.method orelse "(no method)", err }), + .log_to_console => zap.debug("Error in {} {s} : {}", .{ Bound, r.method orelse "(no method)", err }), } } } }; } - pub fn init(T: type, value: *T) Wrapper.Wrap(T) { - checkEndpointType(T); - var ret: Wrapper.Wrap(T) = .{ - .wrapped = value, - .wrapper = .{ .path = value.path }, + pub fn init(ArbitraryEndpoint: type, value: *ArbitraryEndpoint) Binder.Bind(ArbitraryEndpoint) { + checkEndpointType(ArbitraryEndpoint); + const BoundEp = Binder.Bind(ArbitraryEndpoint); + return .{ + .endpoint = value, + .interface = .{ + .path = value.path, + .call = BoundEp.onRequestInterface, + .destroy = BoundEp.destroy, + }, }; - ret.wrapper.call = Wrapper.Wrap(T).onRequestWrapped; - ret.wrapper.destroy = Wrapper.Wrap(T).destroy; - return ret; } }; @@ -262,7 +264,7 @@ pub const Listener = struct { allocator: std.mem.Allocator, /// Internal static interface struct of member endpoints - var endpoints: std.ArrayListUnmanaged(*Wrapper.Interface) = .empty; + var endpoints: std.ArrayListUnmanaged(*Binder.Interface) = .empty; /// Internal, static request handler callback. Will be set to the optional, /// user-defined request callback that only gets called if no endpoints match @@ -296,8 +298,8 @@ pub const Listener = struct { /// Registered endpoints will not be de-initialized automatically; just removed /// from the internal map. pub fn deinit(self: *Listener) void { - for (endpoints.items) |endpoint_wrapper| { - endpoint_wrapper.destroy(self.allocator, endpoint_wrapper); + for (endpoints.items) |interface| { + interface.destroy(interface, self.allocator); } endpoints.deinit(self.allocator); } @@ -328,16 +330,16 @@ pub const Listener = struct { } const EndpointType = @typeInfo(@TypeOf(e)).pointer.child; checkEndpointType(EndpointType); - const wrapper = try self.allocator.create(Wrapper.Wrap(EndpointType)); - wrapper.* = Wrapper.init(EndpointType, e); - try endpoints.append(self.allocator, &wrapper.wrapper); + const bound = try self.allocator.create(Binder.Bind(EndpointType)); + bound.* = Binder.init(EndpointType, e); + try endpoints.append(self.allocator, &bound.interface); } fn onRequest(r: Request) !void { if (r.path) |p| { - for (endpoints.items) |wrapper| { - if (std.mem.startsWith(u8, p, wrapper.path)) { - return try wrapper.call(wrapper, r); + for (endpoints.items) |interface| { + if (std.mem.startsWith(u8, p, interface.path)) { + return try interface.call(interface, r); } } } From b8ca82f0fd4b448d773f859593b1dd146c638878 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 30 Mar 2025 13:09:36 +0200 Subject: [PATCH 47/57] zap.App.Create(Context).Endpoint.Authenticating -> zap.App is READY. --- build.zig | 3 +- examples/app/auth.zig | 119 +++++++++++++++++++++++++++ examples/app/{main.zig => basic.zig} | 0 src/App.zig | 82 +++++++++++++++++- 4 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 examples/app/auth.zig rename examples/app/{main.zig => basic.zig} (100%) diff --git a/build.zig b/build.zig index 1629048..b13002a 100644 --- a/build.zig +++ b/build.zig @@ -50,7 +50,8 @@ pub fn build(b: *std.Build) !void { name: []const u8, src: []const u8, }{ - .{ .name = "app", .src = "examples/app/main.zig" }, + .{ .name = "app_basic", .src = "examples/app/basic.zig" }, + .{ .name = "app_auth", .src = "examples/app/auth.zig" }, .{ .name = "hello", .src = "examples/hello/hello.zig" }, .{ .name = "https", .src = "examples/https/https.zig" }, .{ .name = "hello2", .src = "examples/hello2/hello2.zig" }, diff --git a/examples/app/auth.zig b/examples/app/auth.zig new file mode 100644 index 0000000..ef73219 --- /dev/null +++ b/examples/app/auth.zig @@ -0,0 +1,119 @@ +const std = @import("std"); +const zap = @import("zap"); + +const Allocator = std.mem.Allocator; + +// The "Application Context" +const MyContext = struct { + bearer_token: []const u8, +}; + +// We reply with this +const HTTP_RESPONSE_TEMPLATE: []const u8 = + \\ + \\ {s} from ZAP on {s} (token {s} == {s} : {s})!!! + \\ + \\ +; + +// Our simple endpoint that will be wrapped by the authenticator +const MyEndpoint = struct { + // the slug + path: []const u8, + error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response, + + fn get_bearer_token(r: zap.Request) []const u8 { + const auth_header = zap.Auth.extractAuthHeader(.Bearer, &r) orelse "Bearer (no token)"; + return auth_header[zap.Auth.AuthScheme.Bearer.str().len..]; + } + + // authenticated GET requests go here + // we use the endpoint, the context, the arena, and try + pub fn get(ep: *MyEndpoint, arena: Allocator, context: *MyContext, r: zap.Request) !void { + const used_token = get_bearer_token(r); + const response = try std.fmt.allocPrint( + arena, + HTTP_RESPONSE_TEMPLATE, + .{ "Hello", ep.path, used_token, context.bearer_token, "OK" }, + ); + r.setStatus(.ok); + try r.sendBody(response); + } + + // we also catch the unauthorized callback + // we use the endpoint, the context, the arena, and try + pub fn unauthorized(ep: *MyEndpoint, arena: Allocator, context: *MyContext, r: zap.Request) !void { + r.setStatus(.unauthorized); + const used_token = get_bearer_token(r); + const response = try std.fmt.allocPrint( + arena, + HTTP_RESPONSE_TEMPLATE, + .{ "UNAUTHORIZED", ep.path, used_token, context.bearer_token, "NOT OK" }, + ); + try r.sendBody(response); + } + + // not implemented, don't care + pub fn post(_: *MyEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} + pub fn put(_: *MyEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} + pub fn delete(_: *MyEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} + pub fn patch(_: *MyEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} + pub fn options(_: *MyEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} +}; + +pub fn main() !void { + var gpa: std.heap.GeneralPurposeAllocator(.{ + // just to be explicit + .thread_safe = true, + }) = .{}; + defer std.debug.print("\n\nLeaks detected: {}\n\n", .{gpa.deinit() != .ok}); + const allocator = gpa.allocator(); + + // our global app context + var my_context: MyContext = .{ .bearer_token = "ABCDEFG" }; // ABCDEFG is our Bearer token + + // our global app that holds the context + // App is the type + // app is the instance + const App = zap.App.Create(MyContext); + var app = try App.init(allocator, &my_context, .{}); + defer app.deinit(); + + // create mini endpoint + var ep: MyEndpoint = .{ + .path = "/test", + }; + + // create authenticator, use token from context + const Authenticator = zap.Auth.BearerSingle; // Simple Authenticator that uses a single bearer token + var authenticator = try Authenticator.init(allocator, my_context.bearer_token, null); + defer authenticator.deinit(); + + // create authenticating endpoint by combining endpoint and authenticator + const BearerAuthEndpoint = App.Endpoint.Authenticating(MyEndpoint, Authenticator); + var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); + + // make the authenticating endpoint known to the app + try app.register(&auth_ep); + + // listen + try app.listen(.{ + .interface = "0.0.0.0", + .port = 3000, + }); + std.debug.print( + \\ Run the following: + \\ + \\ curl http://localhost:3000/test -i -H "Authorization: Bearer ABCDEFG" -v + \\ curl http://localhost:3000/test -i -H "Authorization: Bearer invalid" -v + \\ + \\ and see what happens + \\ + , .{}); + + // start worker threads + zap.start(.{ + .threads = 2, + .workers = 1, + }); +} diff --git a/examples/app/main.zig b/examples/app/basic.zig similarity index 100% rename from examples/app/main.zig rename to examples/app/basic.zig diff --git a/src/App.zig b/src/App.zig index ad0740f..4699b77 100644 --- a/src/App.zig +++ b/src/App.zig @@ -14,10 +14,11 @@ const RwLock = Thread.RwLock; const zap = @import("zap.zig"); const Request = zap.Request; const HttpListener = zap.HttpListener; +const ErrorStrategy = zap.Endpoint.ErrorStrategy; pub const AppOpts = struct { /// ErrorStrategy for (optional) request handler if no endpoint matches - default_error_strategy: zap.Endpoint.ErrorStrategy = .log_to_console, + default_error_strategy: ErrorStrategy = .log_to_console, arena_retain_capacity: usize = 16 * 1024 * 1024, }; @@ -137,7 +138,7 @@ pub fn Create(comptime Context: type) type { } if (@hasField(T, "error_strategy")) { - if (@FieldType(T, "error_strategy") != zap.Endpoint.ErrorStrategy) { + if (@FieldType(T, "error_strategy") != ErrorStrategy) { @compileError(@typeName(@FieldType(T, "error_strategy")) ++ " has wrong type, expected: zap.Endpoint.ErrorStrategy"); } } else { @@ -164,6 +165,83 @@ pub fn Create(comptime Context: type) type { } } } + + /// Wrap an endpoint with an Authenticator + pub fn Authenticating(EndpointType: type, Authenticator: type) type { + return struct { + authenticator: *Authenticator, + ep: *EndpointType, + path: []const u8, + error_strategy: ErrorStrategy, + const AuthenticatingEndpoint = @This(); + + /// Init the authenticating endpoint. Pass in a pointer to the endpoint + /// you want to wrap, and the Authenticator that takes care of authenticating + /// requests. + pub fn init(e: *EndpointType, authenticator: *Authenticator) AuthenticatingEndpoint { + return .{ + .authenticator = authenticator, + .ep = e, + .path = e.path, + .error_strategy = e.error_strategy, + }; + } + + /// Authenticates GET requests using the Authenticator. + pub fn get(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&request)) { + .AuthFailed => return self.ep.*.unauthorized(arena, context, request), + .AuthOK => self.ep.*.get(arena, context, request), + .Handled => {}, + }; + } + + /// Authenticates POST requests using the Authenticator. + pub fn post(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&request)) { + .AuthFailed => return self.ep.*.unauthorized(arena, context, request), + .AuthOK => self.ep.*.post(arena, context, request), + .Handled => {}, + }; + } + + /// Authenticates PUT requests using the Authenticator. + pub fn put(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&request)) { + .AuthFailed => return self.ep.*.unauthorized(arena, context, request), + .AuthOK => self.ep.*.put(arena, context, request), + .Handled => {}, + }; + } + + /// Authenticates DELETE requests using the Authenticator. + pub fn delete(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&request)) { + .AuthFailed => return self.ep.*.unauthorized(arena, context, request), + .AuthOK => self.ep.*.delete(arena, context, request), + .Handled => {}, + }; + } + + /// Authenticates PATCH requests using the Authenticator. + pub fn patch(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&request)) { + .AuthFailed => return self.ep.*.unauthorized(arena, context, request), + .AuthOK => self.ep.*.patch(arena, context, request), + .Handled => {}, + }; + } + + /// Authenticates OPTIONS requests using the Authenticator. + pub fn options(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&request)) { + .AuthFailed => return self.ep.*.unauthorized(arena, context, request), + .AuthOK => self.ep.*.put(arena, context, request), + .Handled => {}, + }; + } + }; + } }; pub const ListenerSettings = struct { From 307cc2f13eafb7ec35d9010dd126e4a5852476f0 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 30 Mar 2025 14:46:08 +0200 Subject: [PATCH 48/57] remove legacy, update README --- README.md | 229 +++--- blazingly-fast.md | 332 -------- create-archive.sh | 35 - doc/build-localhost.md | 101 --- doc/other-versions.md | 40 - doc/zig-ception.md | 81 -- examples/app/basic.zig | 25 +- flake.lock | 12 +- flake.nix | 24 +- introducing.md | 100 --- shell.nix | 35 - wrk/axum-sanic.md | 70 -- wrk/axum/hello-axum/Cargo.lock | 752 ------------------ wrk/axum/hello-axum/Cargo.md | 129 --- wrk/axum/hello-axum/Cargo.toml | 10 - wrk/axum/hello-axum/src/main.rs | 29 - wrk/cpp/build.zig | 39 - wrk/cpp/build.zig.zon | 10 - wrk/cpp/hello.html | 1 - wrk/cpp/main.cpp | 80 -- wrk/csharp/Program.cs | 8 - wrk/csharp/Properties/launchSettings.json | 15 - wrk/csharp/csharp.csproj | 8 - wrk/go/main.go | 16 - wrk/graph.py | 90 --- wrk/measure.sh | 104 --- wrk/measure_all.sh | 32 - wrk/other_measurements.md | 52 -- wrk/python/main.py | 32 - wrk/rust/bythebook-improved/.gitignore | 14 - wrk/rust/bythebook-improved/Cargo.toml | 9 - wrk/rust/bythebook-improved/src/lib.rs | 101 --- wrk/rust/bythebook-improved/src/main.rs | 34 - wrk/rust/bythebook/.gitignore | 14 - wrk/rust/bythebook/Cargo.toml | 8 - wrk/rust/bythebook/hello.html | 1 - wrk/rust/bythebook/src/lib.rs | 92 --- wrk/rust/bythebook/src/main.rs | 43 - wrk/rust/clean/.gitignore | 14 - wrk/rust/clean/Cargo.toml | 8 - wrk/rust/clean/hello.html | 1 - wrk/rust/clean/src/lib.rs | 101 --- wrk/rust/clean/src/main.rs | 32 - wrk/samples/README_req_per_sec.png | Bin 43673 -> 0 bytes wrk/samples/README_xfer_per_sec.png | Bin 33535 -> 0 bytes wrk/samples/laptop_req_per_sec_graph.png | Bin 55470 -> 0 bytes wrk/samples/laptop_xfer_per_sec_graph.png | Bin 45684 -> 0 bytes wrk/samples/req_per_sec_graph.png | Bin 60154 -> 0 bytes wrk/samples/workstation_req_per_sec_graph.png | Bin 53636 -> 0 bytes .../workstation_xfer_per_sec_graph.png | Bin 45958 -> 0 bytes wrk/samples/xfer_per_sec_graph.png | Bin 47465 -> 0 bytes wrk/sanic/sanic-app.py | 12 - wrk/zig/main.zig | 24 - wrk/zigstd/main.zig | 33 - 54 files changed, 143 insertions(+), 2889 deletions(-) delete mode 100644 blazingly-fast.md delete mode 100755 create-archive.sh delete mode 100644 doc/build-localhost.md delete mode 100644 doc/other-versions.md delete mode 100644 doc/zig-ception.md delete mode 100644 introducing.md delete mode 100644 shell.nix delete mode 100644 wrk/axum-sanic.md delete mode 100644 wrk/axum/hello-axum/Cargo.lock delete mode 100644 wrk/axum/hello-axum/Cargo.md delete mode 100644 wrk/axum/hello-axum/Cargo.toml delete mode 100644 wrk/axum/hello-axum/src/main.rs delete mode 100644 wrk/cpp/build.zig delete mode 100644 wrk/cpp/build.zig.zon delete mode 100644 wrk/cpp/hello.html delete mode 100644 wrk/cpp/main.cpp delete mode 100644 wrk/csharp/Program.cs delete mode 100644 wrk/csharp/Properties/launchSettings.json delete mode 100644 wrk/csharp/csharp.csproj delete mode 100644 wrk/go/main.go delete mode 100644 wrk/graph.py delete mode 100755 wrk/measure.sh delete mode 100755 wrk/measure_all.sh delete mode 100644 wrk/other_measurements.md delete mode 100644 wrk/python/main.py delete mode 100644 wrk/rust/bythebook-improved/.gitignore delete mode 100644 wrk/rust/bythebook-improved/Cargo.toml delete mode 100644 wrk/rust/bythebook-improved/src/lib.rs delete mode 100644 wrk/rust/bythebook-improved/src/main.rs delete mode 100644 wrk/rust/bythebook/.gitignore delete mode 100644 wrk/rust/bythebook/Cargo.toml delete mode 100644 wrk/rust/bythebook/hello.html delete mode 100644 wrk/rust/bythebook/src/lib.rs delete mode 100644 wrk/rust/bythebook/src/main.rs delete mode 100644 wrk/rust/clean/.gitignore delete mode 100644 wrk/rust/clean/Cargo.toml delete mode 100644 wrk/rust/clean/hello.html delete mode 100644 wrk/rust/clean/src/lib.rs delete mode 100644 wrk/rust/clean/src/main.rs delete mode 100644 wrk/samples/README_req_per_sec.png delete mode 100644 wrk/samples/README_xfer_per_sec.png delete mode 100644 wrk/samples/laptop_req_per_sec_graph.png delete mode 100644 wrk/samples/laptop_xfer_per_sec_graph.png delete mode 100644 wrk/samples/req_per_sec_graph.png delete mode 100644 wrk/samples/workstation_req_per_sec_graph.png delete mode 100644 wrk/samples/workstation_xfer_per_sec_graph.png delete mode 100644 wrk/samples/xfer_per_sec_graph.png delete mode 100644 wrk/sanic/sanic-app.py delete mode 100644 wrk/zig/main.zig delete mode 100644 wrk/zigstd/main.zig diff --git a/README.md b/README.md index a9c12b1..2706bc6 100644 --- a/README.md +++ b/README.md @@ -16,25 +16,21 @@ web application framework](https://facil.io). ## **⚡ZAP⚡ IS FAST, ROBUST, AND STABLE** -After having used ZAP in production for a year, I can confidently assert that it +After having used ZAP in production for years, I can confidently assert that it proved to be: - ⚡ **blazingly fast** ⚡ - 💪 **extremely robust** 💪 -Exactly the goals I set out to achieve! - ## FAQ: - Q: **What version of Zig does Zap support?** - - Zap uses the latest stable zig release (0.13.0), so you don't have to keep + - Zap uses the latest stable zig release (0.14.0), so you don't have to keep up with frequent breaking changes. It's an "LTS feature". - Q: **Can Zap build with Zig's master branch?** - - See the `zig-master` branch. An example of how to use it is - [here](https://github.com/zigzap/hello-master). Please note that the - zig-master branch is not the official master branch of ZAP. Be aware that - I don't provide `build.zig.zon` snippets or tagged releases for it for - the time being. If you know what you are doing, that shouldn't stop you + - See the `zig-master` branch. Please note that the zig-master branch is not + the official master branch of ZAP. Be aware that I don't provide tagged + releases for it. If you know what you are doing, that shouldn't stop you from using it with zig master though. - Q: **Where is the API documentation?** - Docs are a work in progress. You can check them out @@ -43,7 +39,7 @@ Exactly the goals I set out to achieve! - Q: **Does ZAP work on Windows?** - No. This is due to the underlying facil.io C library. Future versions of facil.io might support Windows but there is no timeline yet. Your best - options on Windows are WSL2 or a docker container. + options on Windows are **WSL2 or a docker container**. - Q: **Does ZAP support TLS / HTTPS?** - Yes, ZAP supports using the system's openssl. See the [https](./examples/https/https.zig) example and make sure to build with @@ -55,29 +51,43 @@ Exactly the goals I set out to achieve! ## Here's what works -I recommend checking out **Endpoint-based examples for more realistic -use cases**. Most of the examples are super stripped down to only include -what's necessary to show a feature. +**NOTE:** I recommend checking out **the new App-based** or the Endpoint-based +examples, as they reflect how I intended Zap to be used. -**NOTE: To see API docs, run `zig build run-docserver`.** To specify a custom +Most of the examples are super stripped down to only include what's necessary to +show a feature. + +**To see API docs, run `zig build run-docserver`.** To specify a custom port and docs dir: `zig build docserver && zig-out/bin/docserver --port=8989 --docs=path/to/docs`. -- **Super easy build process**: Zap's `build.zig` now uses the new Zig package - manager for its C-dependencies, no git submodules anymore. - - _tested on Linux and macOS (arm, M1)_ -- **[hello](examples/hello/hello.zig)**: welcomes you with some static HTML -- **[routes](examples/routes/routes.zig)**: a super easy example dispatching on - the HTTP path. **NOTE**: The dispatch in the example is a super-basic - DIY-style dispatch. See endpoint-based examples for more realistic use cases. -- **[serve](examples/serve/serve.zig)**: the traditional static web server with - optional dynamic request handling -- **[sendfile](examples/sendfile/sendfile.zig)**: simple example of how to send - a file, honoring compression headers, etc. -- **[bindataformpost](examples/bindataformpost/bindataformpost.zig)**: example - to receive binary files via form post. -- **[hello_json](examples/hello_json/hello_json.zig)**: serves you json - dependent on HTTP path +### New App-Based Examples + +- **[app_basic](examples/app/basic.zig)**: Shows how to use zap.App with a +simple Endpoint. +- **[app_basic](examples/app/auth.zig)**: Shows how to use zap.App with an +Endpoint using an Authenticator. + +See the other examples for specific uses of Zap. + +Benefits of using `zap.App`: + +- Provides a global, user-defined "Application Context" to all endpoints. +- Made to work with "Endpoints": an endpoint is a struct that covers a `/slug` + of the requested URL and provides a callback for each supported request method + (get, put, delete, options, post, head, patch). +- Each request callback receives: + - a per-thread arena allocator you can use for throwaway allocations without + worrying about freeing them. + - the global "Application Context" of your app's choice +- Endpoint request callbacks are allowed to return errors: + - you can use `try`. + - the endpoint's ErrorStrategy defines if runtime errors should be reported to + the console, to the response (=browser for debugging), or if the error + should be returned. + +### Legacy Endpoint-based examples + - **[endpoint](examples/endpoint/)**: a simple JSON REST API example featuring a `/users` endpoint for performing PUT/DELETE/GET/POST operations and listing users, together with a simple frontend to play with. **It also introduces a @@ -87,26 +97,16 @@ port and docs dir: `zig build docserver && zig-out/bin/docserver --port=8989 `GeneralPurposeAllocator` to report memory leaks when ZAP is shut down. The [StopEndpoint](examples/endpoint/stopendpoint.zig) just stops ZAP when receiving a request on the `/stop` route. -- **[mustache](examples/mustache/mustache.zig)**: a simple example using - [mustache](https://mustache.github.io/) templating. - **[endpoint authentication](examples/endpoint_auth/endpoint_auth.zig)**: a simple authenticated endpoint. Read more about authentication [here](./doc/authentication.md). -- **[http parameters](examples/http_params/http_params.zig)**: a simple example - sending itself query parameters of all supported types. -- **[cookies](examples/cookies/cookies.zig)**: a simple example sending itself a - cookie and responding with a session cookie. -- **[websockets](examples/websockets/)**: a simple websockets chat for the - browser. -- **[Username/Password Session - Authentication](./examples/userpass_session_auth/)**: A convenience - authenticator that redirects un-authenticated requests to a login page and - sends cookies containing session tokens based on username/password pairs - received via POST request. + + +### Legacy Middleware-Style examples + - **[MIDDLEWARE support](examples/middleware/middleware.zig)**: chain together request handlers in middleware style. Provide custom context structs, totally - type-safe, using **[ZIG-CEPTION](doc/zig-ception.md)**. If you come from GO - this might appeal to you. + type-safe. If you come from GO this might appeal to you. - **[MIDDLEWARE with endpoint support](examples/middleware_with_endpoint/middleware_with_endpoint.zig)**: Same as the example above, but this time we use an endpoint at the end of the @@ -126,6 +126,36 @@ port and docs dir: `zig build docserver && zig-out/bin/docserver --port=8989 struct in the callbacks via the `@fieldParentPtr()` trick that is used extensively in Zap's examples, like the [endpoint example](examples/endpoint/endpoint.zig). + +### Specific and Very Basic Examples + +- **[hello](examples/hello/hello.zig)**: welcomes you with some static HTML +- **[routes](examples/routes/routes.zig)**: a super easy example dispatching on + the HTTP path. **NOTE**: The dispatch in the example is a super-basic + DIY-style dispatch. See endpoint-based examples for more realistic use cases. +- [**simple_router**](examples/simple_router/simple_router.zig): See how you + can use `zap.Router` to dispatch to handlers by HTTP path. +- **[serve](examples/serve/serve.zig)**: the traditional static web server with + optional dynamic request handling +- **[sendfile](examples/sendfile/sendfile.zig)**: simple example of how to send + a file, honoring compression headers, etc. +- **[bindataformpost](examples/bindataformpost/bindataformpost.zig)**: example + to receive binary files via form post. +- **[hello_json](examples/hello_json/hello_json.zig)**: serves you json + dependent on HTTP path +- **[mustache](examples/mustache/mustache.zig)**: a simple example using + [mustache](https://mustache.github.io/) templating. +- **[http parameters](examples/http_params/http_params.zig)**: a simple example + sending itself query parameters of all supported types. +- **[cookies](examples/cookies/cookies.zig)**: a simple example sending itself a + cookie and responding with a session cookie. +- **[websockets](examples/websockets/)**: a simple websockets chat for the + browser. +- **[Username/Password Session + Authentication](./examples/userpass_session_auth/)**: A convenience + authenticator that redirects un-authenticated requests to a login page and + sends cookies containing session tokens based on username/password pairs + received via POST request. - [**Error Trace Responses**](./examples/senderror/senderror.zig): You can now call `r.sendError(err, status_code)` when you catch an error and a stack trace will be returned to the client / browser. @@ -136,12 +166,6 @@ port and docs dir: `zig build docserver && zig-out/bin/docserver --port=8989 - run it like this: `ZAP_USE_OPENSSL=true zig build run-https` OR like this: `zig build -Dopenssl=true run-https` - it will tell you how to generate certificates -- [**simple_router**](examples/simple_router/simple_router.zig): See how you - can use `zap.Router` to dispatch to handlers by HTTP path. - -I'll continue wrapping more of facil.io's functionality and adding stuff to zap -to a point where I can use it as the JSON REST API backend for real research -projects, serving thousands of concurrent clients. ## ⚡blazingly fast⚡ @@ -158,25 +182,7 @@ machine (x86_64-linux): - Zig Zap was nearly 30% faster than GO - Zig Zap had over 50% more throughput than GO - -**Update**: Thanks to @felipetrz, I got to test against more realistic Python -and Rust examples. Both python `sanic` and rust `axum` were easy enough to -integrate. - -**Update**: I have automated the benchmarks. See -[blazingly-fast.md](./blazingly-fast.md) for more information. Also, thanks to -@alexpyattaev, the benchmarks are fairer now, pinning server and client to -specific CPU cores. - -**Update**: I have consolidated the benchmarks to one good representative per -language. See more details in [blazingly-fast.md](./blazingly-fast.md). It -contains rust implementations that come pretty close to Zap's performance in the -simplistic testing scenario. - -![](./wrk/samples/README_req_per_sec.png) - -![](./wrk/samples/README_xfer_per_sec.png) - +- **YMMV !!!** So, being somewhere in the ballpark of basic GO performance, zig zap seems to be ... of reasonable performance 😎. @@ -184,7 +190,38 @@ So, being somewhere in the ballpark of basic GO performance, zig zap seems to be I can rest my case that developing ZAP was a good idea because it's faster than both alternatives: a) staying with Python, and b) creating a GO + Zig hybrid. -See more details in [blazingly-fast.md](blazingly-fast.md). +### On (now missing) Micro-Benchmakrs + +I used to have some micro-benchmarks in this repo, showing that Zap beat all the +other things I tried, and eventually got tired of the meaningless discussions +they provoked, the endless issues and PRs that followed, wanting me to add and +maintain even more contestants, do more justice to beloved other frameworks, +etc. + +Case in point, even for me the micro-benchmarks became meaningless. They were +just some rough indicator to me confirming that I didn't do anything terribly +wrong to facil.io, and that facil.io proved to be a reasonable choice, also from +a performance perspective. + +However, none of the projects I use Zap for, ever even remotely resembled +anything close to a static HTTP response micro-benchmark. + +For my more CPU-heavy than IO-heavy use-cases, a thread-based microframework +that's super robust is still my preferred choice, to this day. + +Having said that, I would **still love** for other, pure-zig HTTP frameworks to +eventually make Zap obsolete. Now, in 2025, the list of candidates is looking +really promising. + +### 📣 Shout-Outs + +- [httpz](https://github.com/karlseguin/http.zig) : Pure Zig! Closer to Zap's + model. Performance = good! +- [jetzig](https://github.com/jetzig-framework/jetzig) : Comfortably develop + modern web applications quickly, using http.zig under the hood +- [zzz](https://github.com/tardy-org/zzz) : Super promising, super-fast, +especially for IO-heavy tasks, io_uring support - need I say more? + ## 💪 Robust @@ -217,9 +254,10 @@ local variables that require tens of megabytes of stack space. ### 🛡️ Memory-safe See the [StopEndpoint](examples/endpoint/stopendpoint.zig) in the -[endpoint](examples/endpoint) example. That example uses ZIG's awesome -`GeneralPurposeAllocator` to report memory leaks when ZAP is shut down. The -`StopEndpoint` just stops ZAP when receiving a request on the `/stop` route. +[endpoint](examples/endpoint) example. The `StopEndpoint` just stops ZAP when +receiving a request on the `/stop` route. That example uses ZIG's awesome +`GeneralPurposeAllocator` in [main.zig](examples/endpoint/main.zig) to report +memory leaks when ZAP is shut down. You can use the same strategy in your debug builds and tests to check if your code leaks memory. @@ -228,7 +266,7 @@ code leaks memory. ## Getting started -Make sure you have **zig 0.13.0** installed. Fetch it from +Make sure you have **zig 0.14.0** installed. Fetch it from [here](https://ziglang.org/download). ```shell @@ -237,12 +275,11 @@ $ cd zap $ zig build run-hello $ # open http://localhost:3000 in your browser ``` - ... and open [http://localhost:3000](http://localhost:3000) in your browser. ## Using ⚡zap⚡ in your own projects -Make sure you have **the latest zig release (0.13.0)** installed. Fetch it from +Make sure you have **the latest zig release (0.14.0)** installed. Fetch it from [here](https://ziglang.org/download). If you don't have an existing zig project, create one like this: @@ -250,17 +287,11 @@ If you don't have an existing zig project, create one like this: ```shell $ mkdir zaptest && cd zaptest $ zig init -$ git init ## (optional) ``` -**Note**: Nix/NixOS users are lucky; you can use the existing `flake.nix` and run -`nix develop` to get a development shell providing zig and all -dependencies to build and run the GO, python, and rust examples for the -`wrk` performance tests. For the mere building of zap projects, -`nix develop .#build` will only fetch zig 0.11.0. TODO: upgrade to latest zig. With an existing Zig project, adding Zap to it is easy: -1. Add zap to your `build.zig.zon` +1. Zig fetch zap 2. Add zap to your `build.zig` In your zig project folder (where `build.zig` is located), run: @@ -284,26 +315,9 @@ Then, in your `build.zig`'s `build` function, add the following before exe.root_module.addImport("zap", zap.module("zap")); ``` -From then on, you can use the Zap package in your project. Check out the -examples to see how to use Zap. +From then on, you can use the Zap package in your project via `const zap = +@import("zap");`. Check out the examples to see how to use Zap. -## Updating your project to the latest version of zap - -You can change the URL to Zap in your `build.zig.zon` - -- easiest: use a tagged release -- or to one of the tagged versions, e.g. `0.0.9` -- or to the latest commit of `zap` - -### Using a tagged release - -Go to the [release page](https://github.com/zigzap/zap/releases). Every release -will state its version number and also provide instructions for changing -`build.zig.zon` and `build.zig`. - -### Using other versions - -See [here](./doc/other-versions.md). ## Contribute to ⚡zap⚡ - blazingly fast @@ -314,16 +328,7 @@ world a blazingly fast place by providing patches or pull requests, add documentation or examples, or interesting issues and bug reports - you'll know what to do when you receive your calling 👼. -Check out [CONTRIBUTING.md](CONTRIBUTING.md) for more details. - -See also [introducing.md](introducing.md) for more on the state and progress of -this project. - -**We now have our own [ZAP discord](https://discord.gg/jQAAN6Ubyj) server!!!** - -You can also reach me on [the zig showtime discord -server](https://discord.gg/CBzE3VMb) under the handle renerocksai -(renerocksai#1894). +**We have our own [ZAP discord](https://discord.gg/jQAAN6Ubyj) server!!!** ## Support ⚡zap⚡ diff --git a/blazingly-fast.md b/blazingly-fast.md deleted file mode 100644 index 41f186a..0000000 --- a/blazingly-fast.md +++ /dev/null @@ -1,332 +0,0 @@ -# ⚡blazingly fast⚡ - -Initially, I conducted a series of quick tests, using wrk with simple HTTP -servers written in GO and in zig zap. I made sure that all servers only output -17 bytes of HTTP body. - -Just to get some sort of indication, I also included measurements for python -since I used to write my REST APIs in python before creating zig zap. - -You can check out the scripts I used for the tests in [./wrk](wrk/). - -## Why - -I aimed to enhance the performance of my Python + Flask backends by replacing -them with a Zig version. To evaluate the success of this transition, I compared -the performance of a static HTTP server implemented in Python and its Zig -counterpart, which showed significant improvements. - -To further assess the Zig server's performance, I compared it with a Go -implementation, to compare against a widely used industry-standard. I expected -similar performance levels but was pleasantly surprised when Zap outperformed Go -by approximately 30% on my test machine. - -Intrigued by Rust's reputed performance capabilities, I also experimented with a -Rust version. The results of this experiment are discussed in the -[Flaws](#flaws) section below. - -## What - -So, what are the benchmarks testing? - -- simple http servers that reply to GET requests with a constant, 17-bytes long response -- 4 cores are assigned to the subject under test (the respective server) -- 4 cores are assigned to `wrk` - - using 4 threads - - aiming at 400 concurrent connections - -## How - -I have fully automated the benchmarks and graph generation. - -To generate the data: - -```console -$ ./wrk/measure_all.sh -``` - -To generate the graphs: - -```console -$ python wrk/graph.py -``` - -For dependencies, please see the [flake.nix](./flake.nix#L46). - -## Flaws - -The benchmarks have limitations, such as the lack of request latencies. The Rust -community has often criticized these benchmarks as biased. However, no such -criticisms have come from the Go or Python communities. - -In response to the Rust community's concerns, we've added three Rust -implementations for comparison: - -- A standard version from [the Rust book](https://doc.rust-lang.org/book/ch20-00-final-project-a-web-server.html). -- An "axum" version to highlight Rust's speed. -- A refined version of the Rust book version. - -Originally, the goal was to compare "batteries included" versions, which created -a disparity by comparing the optimized zap / facil.io code with basic bundled -functionalities. These tests were for personal interest and not meant to be -definitive benchmarks. - -To address this bias, we've added the Rust-axum and Python-sanic benchmarks. For -more information, refer to the relevant discussions and pull requests. - - -## More benchmarks? - -I often receive requests or PRs to include additional benchmarks, which a lot of -times I find to be either ego-driven or a cause for unnecessary disputes. People -tend to favor their preferred language or framework. Zig, Rust, C, and C++ are -all capable of efficiently creating fast web servers, with different frameworks -potentially excelling in certain benchmarks. My main concern was whether Zap, -given its current level of abstraction, could compete with standard web servers. -This question has been answered, and I see no need for further benchmarks. - -So far, we have the following benchmark subjects (implementations) which you'll -find in the graphs below: - -- **zig-zap** : ZAP implementation -- **go** : GO implementation -- **python** : Python implementation -- **python-sanic** : Python implementation with sanic framework -- **rust-bythebook** : Rust example from the Rust book (not representative) -- **rust-bythebook-improved** : Improved version of the by-the-book code (thx @alexpyattaev) -- **rust-clean** : A clean, straight-forward Rust implementation (thx @alexpyattaev) -- **rust-axum** : Rust implementation using the axum framework (realistic) -- **(csharp)** : CSharp implementation (thx @leo-costa) -- **cpp-beast** : A C++ implementation using boost::beast (thx @kassane) - - -## The computer makes the difference - -After automating the performance benchmarks, I gathered data from three -different computers. It's interesting to see the variation in relative numbers. - - -### The test machine (graphs in the README) - -![](./wrk/samples/req_per_sec_graph.png) - -![](./wrk/samples/xfer_per_sec_graph.png) - -``` -➜ neofetch --stdout -rs@ryzen --------- -OS: NixOS 23.05.997.ddf4688dc7a (Stoat) x86_64 -Host: Micro-Star International Co., Ltd. B550-A PRO (MS-7C56) -Kernel: 6.3.7 -Uptime: 15 days, 11 hours, 13 mins -Packages: 2094 (nix-system), 1356 (nix-user), 7 (flatpak) -Shell: bash 5.2.15 -Resolution: 3840x2160 -DE: none+i3 -WM: i3 -Terminal: tmux -CPU: AMD Ryzen 5 5600X (12) @ 3.700GHz -GPU: AMD ATI Radeon RX 6700/6700 XT/6750 XT / 6800M/6850M XT -Memory: 4981MiB / 32028MiB - - -➜ lscpu -Architecture: x86_64 - CPU op-mode(s): 32-bit, 64-bit - Address sizes: 48 bits physical, 48 bits virtual - Byte Order: Little Endian -CPU(s): 12 - On-line CPU(s) list: 0-11 -Vendor ID: AuthenticAMD - Model name: AMD Ryzen 5 5600X 6-Core Processor - CPU family: 25 - Model: 33 - Thread(s) per core: 2 - Core(s) per socket: 6 - Socket(s): 1 - Stepping: 0 - Frequency boost: enabled - CPU(s) scaling MHz: 67% - CPU max MHz: 4650.2920 - CPU min MHz: 2200.0000 - BogoMIPS: 7399.43 - Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt - pdpe1gb rdtscp lm constant_tsc rep_good nopl nonstop_tsc cpuid extd_apicid aperfmperf rapl pni pclmulqdq monitor ssse3 fma cx16 - sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefet - ch osvw ibs skinit wdt tce topoext perfctr_core perfctr_nb bpext perfctr_llc mwaitx cpb cat_l3 cdp_l3 hw_pstate ssbd mba ibrs ib - pb stibp vmmcall fsgsbase bmi1 avx2 smep bmi2 erms invpcid cqm rdt_a rdseed adx smap clflushopt clwb sha_ni xsaveopt xsavec xget - bv1 xsaves cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local clzero irperf xsaveerptr rdpru wbnoinvd arat npt lbrv svm_lock nrip - _save tsc_scale vmcb_clean flushbyasid decodeassists pausefilter pfthreshold avic v_vmsave_vmload vgif v_spec_ctrl umip pku ospk - e vaes vpclmulqdq rdpid overflow_recov succor smca fsrm -Virtualization features: - Virtualization: AMD-V -Caches (sum of all): - L1d: 192 KiB (6 instances) - L1i: 192 KiB (6 instances) - L2: 3 MiB (6 instances) - L3: 32 MiB (1 instance) -NUMA: - NUMA node(s): 1 - NUMA node0 CPU(s): 0-11 -Vulnerabilities: - Itlb multihit: Not affected - L1tf: Not affected - Mds: Not affected - Meltdown: Not affected - Mmio stale data: Not affected - Retbleed: Not affected - Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl - Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization - Spectre v2: Mitigation; Retpolines, IBPB conditional, IBRS_FW, STIBP always-on, RSB filling, PBRSB-eIBRS Not affected - Srbds: Not affected - Tsx async abort: Not affected -``` - -### Workstation at work - -A beast. Many cores (which we don't use). - -![](./wrk/samples/workstation_req_per_sec_graph.png) - -![](./wrk/samples/workstation_xfer_per_sec_graph.png) - -``` -[rene@nixos:~]$ neofetch --stdout -rene@nixos ----------- -OS: NixOS 23.05.2947.475d5ae2c4cb (Stoat) x86_64 -Host: LENOVO 1038 -Kernel: 6.1.46 -Uptime: 26 mins -Packages: 5804 (nix-system), 566 (nix-user) -Shell: bash 5.2.15 -Terminal: /dev/pts/2 -CPU: Intel Xeon Gold 5218 (64) @ 3.900GHz -GPU: NVIDIA Quadro P620 -GPU: NVIDIA Tesla M40 -Memory: 1610MiB / 95247MiB - - -[rene@nixos:~]$ lscpu -Architecture: x86_64 - CPU op-mode(s): 32-bit, 64-bit - Address sizes: 46 bits physical, 48 bits virtual - Byte Order: Little Endian -CPU(s): 64 - On-line CPU(s) list: 0-63 -Vendor ID: GenuineIntel - Model name: Intel(R) Xeon(R) Gold 5218 CPU @ 2.30GHz - CPU family: 6 - Model: 85 - Thread(s) per core: 2 - Core(s) per socket: 16 - Socket(s): 2 - Stepping: 7 - CPU(s) scaling MHz: 57% - CPU max MHz: 3900,0000 - CPU min MHz: 1000,0000 - BogoMIPS: 4600,00 - Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs b - ts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_ - deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb cat_l3 cdp_l3 invpcid_single intel_ppin ssbd mba ibrs ibpb stibp ibrs_enhanced tpr_shadow vnmi flexpri - ority ept vpid ept_ad fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid cqm mpx rdt_a avx512f avx512dq rdseed adx smap clflushopt clwb intel_pt avx512cd avx512bw avx512vl xsaveopt xs - avec xgetbv1 xsaves cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local dtherm ida arat pln pts hwp hwp_act_window hwp_epp hwp_pkg_req pku ospke avx512_vnni md_clear flush_l1d arch_capabi - lities -Virtualization features: - Virtualization: VT-x -Caches (sum of all): - L1d: 1 MiB (32 instances) - L1i: 1 MiB (32 instances) - L2: 32 MiB (32 instances) - L3: 44 MiB (2 instances) -NUMA: - NUMA node(s): 2 - NUMA node0 CPU(s): 0-15,32-47 - NUMA node1 CPU(s): 16-31,48-63 -Vulnerabilities: - Gather data sampling: Mitigation; Microcode - Itlb multihit: KVM: Mitigation: VMX disabled - L1tf: Not affected - Mds: Not affected - Meltdown: Not affected - Mmio stale data: Mitigation; Clear CPU buffers; SMT vulnerable - Retbleed: Mitigation; Enhanced IBRS - Spec rstack overflow: Not affected - Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl - Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization - Spectre v2: Mitigation; Enhanced IBRS, IBPB conditional, RSB filling, PBRSB-eIBRS SW sequence - Srbds: Not affected - Tsx async abort: Mitigation; TSX disabled -``` - - -### Work Laptop - -Very strange. It absolutely **LOVES** zap 🤣! - -![](./wrk/samples/laptop_req_per_sec_graph.png) - -![](./wrk/samples/laptop_xfer_per_sec_graph.png) - -``` -➜ neofetch --stdout -rs@nixos --------- -OS: NixOS 23.05.2918.4cdad15f34e6 (Stoat) x86_64 -Host: LENOVO 20TKS0W700 -Kernel: 6.1.45 -Uptime: 1 day, 4 hours, 50 mins -Packages: 6259 (nix-system), 267 (nix-user), 9 (flatpak) -Shell: bash 5.2.15 -Resolution: 3840x1600, 3840x2160 -DE: none+i3 -WM: i3 -Terminal: tmux -CPU: Intel i9-10885H (16) @ 5.300GHz -GPU: NVIDIA GeForce GTX 1650 Ti Mobile -Memory: 4525MiB / 31805MiB - - -➜ lscpu -Architecture: x86_64 -CPU op-mode(s): 32-bit, 64-bit -Address sizes: 39 bits physical, 48 bits virtual -Byte Order: Little Endian -CPU(s): 16 -On-line CPU(s) list: 0-15 -Vendor ID: GenuineIntel -Model name: Intel(R) Core(TM) i9-10885H CPU @ 2.40GHz -CPU family: 6 -Model: 165 -Thread(s) per core: 2 -Core(s) per socket: 8 -Socket(s): 1 -Stepping: 2 -CPU(s) scaling MHz: 56% -CPU max MHz: 5300.0000 -CPU min MHz: 800.0000 -BogoMIPS: 4800.00 -Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single ssbd ibrs ibpb stibp ibrs_enhanced tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust sgx bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp pku ospke sgx_lc md_clear flush_l1d arch_capabilities -Virtualization: VT-x -L1d cache: 256 KiB (8 instances) -L1i cache: 256 KiB (8 instances) -L2 cache: 2 MiB (8 instances) -L3 cache: 16 MiB (1 instance) -NUMA node(s): 1 -NUMA node0 CPU(s): 0-15 -Vulnerability Gather data sampling: Mitigation; Microcode -Vulnerability Itlb multihit: KVM: Mitigation: VMX disabled -Vulnerability L1tf: Not affected -Vulnerability Mds: Not affected -Vulnerability Meltdown: Not affected -Vulnerability Mmio stale data: Mitigation; Clear CPU buffers; SMT vulnerable -Vulnerability Retbleed: Mitigation; Enhanced IBRS -Vulnerability Spec rstack overflow: Not affected -Vulnerability Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl -Vulnerability Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization -Vulnerability Spectre v2: Mitigation; Enhanced IBRS, IBPB conditional, RSB filling, PBRSB-eIBRS SW sequence -Vulnerability Srbds: Mitigation; Microcode -Vulnerability Tsx async abort: Not affected -``` - diff --git a/create-archive.sh b/create-archive.sh deleted file mode 100755 index 88f393d..0000000 --- a/create-archive.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -tag=$1 -override=$2 - -if [ "$tag" == "--override" ] ; then - override=$tag - tag="" -fi - -if [ "$tag" == "" ] ; then - tag=$(git rev-parse --abbrev-ref HEAD) - echo "Warning: no tag provided, using: >> $tag <<" -fi - - -git archive --format=tar.gz -o ${tag}.tar.gz --prefix=zap-$tag/ HEAD - -git diff --quiet - -if [ $? -ne 0 ] ; then - if [ "$override" == "--override" ] ; then - ./zig-out/bin/pkghash -g --tag=$tag --template=doc/release-template.md - else - echo "WARNING: GIT WORKING TREE IS DIRTY!" - echo "If you want to get zig hash anyway, run:" - echo "./zig-out/bin/pkghash -g" - echo "or, with full-blown release-notes:" - echo "./zig-out/bin/pkghash -g --tag=$tag --template=doc/release-template.md" - echo "" - echo "To skip this message and do the pkghash thing anyway, supply the" - echo "--override parameter" - fi -else - ./zig-out/bin/pkghash -g --tag=$tag --template=doc/release-template.md -fi diff --git a/doc/build-localhost.md b/doc/build-localhost.md deleted file mode 100644 index 8725fde..0000000 --- a/doc/build-localhost.md +++ /dev/null @@ -1,101 +0,0 @@ -# Self-hosting release packages until Zig master is fixed - -Recently, GitHub started hosting release archives on a dedicated host -codeload.github.com. This is when the problems started. Back then, zig's package -manager was not expecting to be re-directed to a different URL. On top of that, -GitHub changed the redirected-to URLs so they wouldn't end in `.tar.gz` anymore. - -Above issues were fixed but after some progress on `zig.http` related standard -library stuff, a similar error started impacting the package manager: parsing -long TLS responses has the [issue -ziglang/zig#15990](https://github.com/ziglang/zig/issues/15590). - -So, here we are. Since this topic has come up often enough now, it deserves its -own doc. - -## The workaround: self-hosting on localhost - -My workaround is: not using https! The easiest way to do this, is: - -- create the tar archive yourself -- start a python http server on the command line -- replace the URL in the build.zig.zon with a http and localhost one. - -For simple packages, this is relatively easy. But zap itself has a -`build.zig.zon` that references its `facilio` dependency. For that reason, ZAP's -build.zig.zon also needs to change: to only reference localhost packages. - -The consequence of changing build.zig.zon is: zap's package hash changes! --> -Any build.zig.zon that wants to use ZAP needs to change, too. - -This is why, for the time being, I am always creating two releases, -a `release-0.0.n` one and `release-0.0.n-localhost` one, for each release. - - -So, while the TLS bug persists, you have to use the `-localhost` releases. The -procedure is: - -- fetch zap's dependency `facilio` from GitHub -- fetch zap's `localhost` release from GitHub -- _(use the localhost URL and package hash in your build.zig)_ -- start a local http server -- run zig build -- stop the http server - -Here is an example for the `release-0.0.20-localhost` release which is the -current release at the time of writing: - -```console -$ # get dependency required by zap -$ wget https://github.com/zigzap/facil.io/archive/refs/tags/zap-0.0.8.tar.gz -$ # get zap itself -$ wget https://github.com/zigzap/zap/archive/refs/tags/release-0.0.20-localhost.tar.gz -$ # start a http server on port 8000 -$ python -m http.server -``` - -... and use the following in your build.zig.zon: - -```zig - // zap release-0.0.20-localhost - .zap = .{ - .url = "http://127.0.0.1/release-0.0.20-localhost.tar.gz", - .hash = "12204c663be7639e98af40ad738780014b18bcf35efbdb4c701aad51c7dec45abf4d", - } -``` - -After the first successful zig build, zig will have cached both dependencies, -the direct zap one and the transient facilio one, and you won't need to start an -HTTP server again until you want to update your dependencies. - - -## Building Release Packages yourself - -- In your branch, replace `build.zig.zon` with `build.zig.zon.localhost` -- **make sure everything is committed and your branch is clean.** This is - essential for calculating the package hash. -- `zig build pkghash` if you haven't already. -- tag your release: `git tag MY_TAG` - - I recommend putting `localhost` in the tagname -- `./create-archive.sh MY_TAG` - -The `create-archive.sh` script will spit out release notes that contain the -hashes, as well as a `MY_TAG.tar.gz`. - -You can then host this via python HTTP server and proceed as if you had -downloaded it from github. - -If all goes well, your dependend code should be able to use your freshly-built -zap release, depending on it via localhost URL in its `build.zig.zon`. - -If not, fix bugs, rinse, and repeat. - -You may want to push to your fork and create a GitHub 'localhost' release. - -When you're happy with the release, you may consider replacing `build.zig.zon` -with the non-localhost version from the master branch. Commit it, make sure your -worktree is clean, and perform above steps again. This time, using a tag that -doesn't contain `localhost`. You can then push to your fork and create a release -for the future when zig's bug is fixed. - - diff --git a/doc/other-versions.md b/doc/other-versions.md deleted file mode 100644 index c8c1005..0000000 --- a/doc/other-versions.md +++ /dev/null @@ -1,40 +0,0 @@ -# Alternatives to released versions - - -## Using a tagged version - -Go to [to the tags page](https://github.com/zigzap/zap/tags) to view all -available tagged versions of zap. From there, right click on the `tar.gz` link -to copy the URL to put into your `build.zig.zon`. - -After changing the `.url` field, you will get an error like this at the next -attempt to `zig build`: - -``` -.../build.zig.zon:8:21: error: hash mismatch: -expected: 12205fd0b60720fb2a40d82118ee75c15cb5589bb9faf901c8a39a93551dd6253049, -found: 1220f4ea8be4a85716ae1362d34c077dca10f10d1baf9196fc890e658c56f78b7424 -.hash = "12205fd0b60720fb2a40d82118ee75c15cb5589bb9faf901c8a39a93551dd6253049", -^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -``` - -**Note:** If you don't get this error, clean your global zig cache: `rm -fr -~/.cache/zig`. This shouldn't happen with current zig master anymore. - -With the new URL, the old hash in the `build.zig.zon` is no longer valid. You -need to take the hash value displayed after `found: ` in the error message as -the `.hash` value in `build.zig.zon`. - - -## Using an arbitrary (last) commit - -Use the same workflow as above for tags, excpept for the URL, use this schema: - -```zig -.url = "https://github.com/zigzap/zap/archive/[COMMIT-HASH].tar.gz", -``` - -Replace `[COMMIT-HASH]` with the full commit hash as provided, e.g. by `git -log`. - - diff --git a/doc/zig-ception.md b/doc/zig-ception.md deleted file mode 100644 index cdea9d8..0000000 --- a/doc/zig-ception.md +++ /dev/null @@ -1,81 +0,0 @@ -# ZIG-CEPTION! - -In ZAP, we have great zig-ception moment in the [middleware -example](../examples/middleware/middleware.zig). But first we need to introduce -one key function of `zap.Middleware`: **combining structs at comptime!** - -## Combining structs at runtime - -Here is how it is used in user-code: - -```zig -// create a combined context struct -const Context = struct { - user: ?UserMiddleWare.User = null, - session: ?SessionMiddleWare.Session = null, -}; -``` - -Why do we create combined structs? Because all our Middleware handler functions -need to receive a per-request context. But each wants their own data: the User -middleware might want to access a User struct, the Session middleware might want -a Session struct, and so on. So, which struct should we use in the prototype of -the "on_request" callback function? We could just use an `anyopaque` pointer. -That would solve the generic function prototype problem. But then everyone -implementing such a handler would need to cast this pointer back into - what? -Into the same type that the caller of the handler used. It gets really messy -when we continue this train of thought. - -So, in ZAP, I opted for one Context type for all request handlers. Since ZAP is -a library, it cannot know what your preferred Context struct is. What it should -consist of. Therefore, it lets you combine all the structs your and maybe your -3rd parties's middleware components require - at comptime! And derive the -callback function prototype from that. If you look at the [middleware -example](../examples/middleware/middleware.zig), you'll notice, it's really -smooth to use. - -**NOTE:** In your contexts, please also use OPTIONALS. They are set null at -context creation time. And will aid you in not shooting yourself in the foot -when accessing context fields that haven't been initialized - which may happen -when the order of your chain of components isn't perfect yet. 😉 - -## The zig-ception moment - -Have a look at an excerpt of the example: - -```zig -// create a combined context struct -const Context = struct { - user: ?UserMiddleWare.User = null, - session: ?SessionMiddleWare.Session = null, -}; - -// we create a Handler type based on our Context -const Handler = zap.Middleware.Handler(Context); - -// -// ZIG-CEPTION!!! -// -// Note how amazing zig is: -// - we create the "mixed" context based on the both middleware structs -// - we create the handler based on this context -// - we create the middleware structs based on the handler -// - which needs the context -// - which needs the middleware structs -// - ZIG-CEPTION! - -// Example user middleware: puts user info into the context -const UserMiddleWare = struct { - handler: Handler, - - // .. the UserMiddleWare depends on the handler - // which depends on the Context - // which depends on this UserMiddleWare struct - // ZIG-CEPTION!!! -``` - -## 🤯 - -The comments in the code say it all. - -**Isn't ZIG AMAZING?** diff --git a/examples/app/basic.zig b/examples/app/basic.zig index 6186e31..752b0d0 100644 --- a/examples/app/basic.zig +++ b/examples/app/basic.zig @@ -3,6 +3,7 @@ const Allocator = std.mem.Allocator; const zap = @import("zap"); +// The global Application Context const MyContext = struct { db_connection: []const u8, @@ -13,12 +14,14 @@ const MyContext = struct { } }; +// A very simple endpoint handling only GET requests const SimpleEndpoint = struct { - // Endpoint Interface part + // zap.App.Endpoint Interface part path: []const u8, error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response, + // data specific for this endpoint some_data: []const u8, pub fn init(path: []const u8, data: []const u8) SimpleEndpoint { @@ -28,12 +31,14 @@ const SimpleEndpoint = struct { }; } + // handle GET requests pub fn get(e: *SimpleEndpoint, arena: Allocator, context: *MyContext, r: zap.Request) anyerror!void { + const thread_id = std.Thread.getCurrentId(); + r.setStatus(.ok); - const thread_id = std.Thread.getCurrentId(); - // look, we use the arena allocator here - // and we also just try it, not worrying about errors + // look, we use the arena allocator here -> no need to free the response_text later! + // and we also just `try` it, not worrying about errors const response_text = try std.fmt.allocPrint( arena, \\Hello! @@ -58,23 +63,29 @@ const SimpleEndpoint = struct { }; pub fn main() !void { - var my_context = MyContext.init("db connection established!"); - + // setup allocations var gpa: std.heap.GeneralPurposeAllocator(.{ // just to be explicit .thread_safe = true, }) = .{}; defer std.debug.print("\n\nLeaks detected: {}\n\n", .{gpa.deinit() != .ok}); - const allocator = gpa.allocator(); + + // create an app context + var my_context = MyContext.init("db connection established!"); + + // create an App instance const App = zap.App.Create(MyContext); var app = try App.init(allocator, &my_context, .{}); defer app.deinit(); + // create the endpoint var my_endpoint = SimpleEndpoint.init("/", "some endpoint specific data"); + // register the endpoint with the app try app.register(&my_endpoint); + // listen on the network try app.listen(.{ .interface = "0.0.0.0", .port = 3000, diff --git a/flake.lock b/flake.lock index ee9c5fd..97a50f0 100644 --- a/flake.lock +++ b/flake.lock @@ -70,11 +70,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1741037377, - "narHash": "sha256-SvtvVKHaUX4Owb+PasySwZsoc5VUeTf1px34BByiOxw=", + "lastModified": 1743259260, + "narHash": "sha256-ArWLUgRm1tKHiqlhnymyVqi5kLNCK5ghvm06mfCl4QY=", "owner": "nixos", "repo": "nixpkgs", - "rev": "02032da4af073d0f6110540c8677f16d4be0117f", + "rev": "eb0e0f21f15c559d2ac7633dc81d079d1caf5f5f", "type": "github" }, "original": { @@ -145,11 +145,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1741263138, - "narHash": "sha256-qlX8tgtZMTSOEeAM8AmC7K6mixgYOguhl/xLj5xQrXc=", + "lastModified": 1743250246, + "narHash": "sha256-gVFyxsxfqnEXSldeeURim7RRZGwPX4f/egLcSC7CXec=", "owner": "mitchellh", "repo": "zig-overlay", - "rev": "627055069ee1409e8c9be7bcc533e8823fb87b18", + "rev": "b0da956a6db25564d0ee461e669fb07a348d2528", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index f6781a1..cf4d61a 100644 --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,6 @@ inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; - # nixpkgs.url = "github:nixos/nixpkgs/release-23.05"; flake-utils.url = "github:numtide/flake-utils"; # required for latest zig @@ -38,28 +37,10 @@ in rec { devShells.default = pkgs.mkShell { nativeBuildInputs = with pkgs; [ - # TODO: re-enable this once it is fixed: zigpkgs."0.14.0" - zigpkgs.master + zigpkgs."0.14.0" bat wrk - python3 - python3Packages.sanic - python3Packages.setuptools - python3Packages.matplotlib - poetry - poetry - pkgs.rustc - pkgs.cargo - pkgs.gcc - pkgs.rustfmt - pkgs.clippy - pkgs.go - pkgs.gotools - pkgs.gopls - pkgs.golint - pkgs.dotnet-sdk_8 - pkgs.dotnet-runtime_8 pkgs.zlib pkgs.icu pkgs.openssl @@ -85,9 +66,8 @@ devShells.build = pkgs.mkShell { nativeBuildInputs = with pkgs; [ - # zigpkgs."0.14.0" + zigpkgs."0.14.0" zigpkgs.master - pkgs.openssl ]; buildInputs = with pkgs; [ diff --git a/introducing.md b/introducing.md deleted file mode 100644 index 2d951cb..0000000 --- a/introducing.md +++ /dev/null @@ -1,100 +0,0 @@ -# Introducing ⚡zap⚡ - blazingly fast backends in zig - -Zap is intended to become my [zig](https://ziglang.org) replacement for the kind of REST APIs I used to write in [python](https://python.org) with [Flask](https://flask.palletsprojects.com) and [mongodb](https://www.mongodb.com), etc. It can be considered to be a microframework for web applications. - -What I need for that is a blazingly fast, robust HTTP server that I can use with zig. While facil.io supports TLS, I don't care about HTTPS support. In production, I use [nginx](https://www.nginx.com) as a reverse proxy anyway. - -Zap wraps and patches [facil.io - the C web application framework](https://facil.io). - -At the time of writing, ZAP is only a few days old and aims to be: - -- **robust** -- **fast** -- **minimal** - -**⚡ZAP⚡ IS SUPER ALPHA** - -_Under the hood, everything is super robust and fast. My zig wrappers are fresh, juicy, and alpha._ - -Here's what works: - -- **Super easy build process**: zap's `build.zig` fetches facilio's git sub-module, applies a patch to its logging for microsecond precision, and then builds and optionally runs everything. - - _tested on Linux and macOS (arm, M1)_ -- **[hello](https://github.com/renerocksai/zap/blob/master/examples/hello/hello.zig)**: welcomes you with some static HTML -- **[routes](https://github.com/renerocksai/zap/blob/master/examples/routes/routes.zig)**: a super easy example dispatching on the HTTP path -- **[serve](https://github.com/renerocksai/zap/blob/master/examples/serve/serve.zig)**: the traditional static web server with optional dynamic request handling -- **[hello_json](https://github.com/renerocksai/zap/blob/master/examples/hello_json/hello_json.zig)**: serves you json dependent on HTTP path -- **[endpoint](https://github.com/renerocksai/zap/blob/master/examples/endpoint/)**: a simple JSON REST API example featuring a `/users` endpoint for PUTting/DELETE-ing/GET-ting/POST-ing and listing users, together with a static HTML and JavaScript frontend to play with. - -If you want to take it for a quick spin: - -```shell -$ git clone https://github.com/renerocksai/zap.git -$ cd zap -$ zig build run-hello -$ # open http://localhost:3000 in your browser -``` - -See [the README](https://github.com/renerocksai/zap) for how easy it is to get started, how to run the examples, and how to use zap in your own projects. - -I'll continue wrapping more of facil.io's functionality and adding stuff to zap to a point where I can use it as the JSON REST API backend for real research projects, serving thousands of concurrent clients. Now that the endpoint example works, ZAP has actually become pretty usable to me. - -**Side-note:** It never ceases to amaze me how productive I can be in zig, eventhough I am still considering myself to be a newbie. Sometimes, it's almost like writing python but with all the nice speed and guarantees that zig gives you. Also, the C integration abilities of zig are just phenomenal! I am super excited about zig's future! - -Now, on to the guiding principles of Zap. - -## robust - -A common recommendation for doing web stuff in zig is to write the actual HTTP server in Go, and use zig for the real work. While there is a selection of notable and cool HTTP server implementations written in zig out there, at the time of writing, most of them seem to a) depend on zig's async facilities which are unsupported until ca. April 2023 when async will return to the self-hosted compiler, and b) have not matured to a point where **I** feel safe using them in production. These are just my opionions and they could be totally wrong though. - -However, when I conduct my next online research experiment with thousands of concurrent clients, I cannot afford to run into potential maturity-problems of the HTTP server. These projects typically feature a you-get-one-shot process with little room for errors or re-tries. - -With zap, if something should go wrong, at least I'd be close enough to the source-code to, hopefully, be able to fix it in production. With that out of the way, I am super confident that facil.io is very mature compared to many of the alternatives. My `wrk` tests also look promising. - -I intend to add app-specific performance tests, e.g. stress-testing the endpoint example, to make sure the zap endpoint framework is able to sustain a high load without running into performance or memory problems. That will be interesting. - - -## ⚡blazingly fast⚡ - -Claiming to be blazingly fast is the new black. At least, zap doesn't slow you down and if your server performs poorly, it's probably not exactly zap's fault. Zap relies on the [facil.io](https://facil.io) framework and so it can't really claim any performance fame for itself. In this initial implementation of zap, I didn't care about optimizations at all. - -But, how fast is it? Being blazingly fast is relative. When compared with a simple GO HTTP server, a simple zig zap HTTP server performed really good on my machine: - -- zig zap was nearly 30% faster than GO -- zig zap had over 50% more throughput than GO - -I intentionally only tested static HTTP output, as that seemed to be the best common ground of all test subjects to me. The measurements were for just getting a ballpark baseline anyway. - -**Update**: I was intrigued comparing to a basic rust HTTP server. Unfortunately, knowing nothing at all about rust, I couldn't find a simple, just-a-few-lines one like in Go and Python right away and hence tried to go for the one in the book [The Rust Programming Language](https://doc.rust-lang.org/book/ch20-00-final-project-a-web-server.html). Wanting it to be of a somewhat fair comparison, I opted for the multi-threaded example. It didn't work out-of-the-book, but I got it to work (essentially, by commenting out all "print" statements) and changed it to not read files but outputting static text just like the other examples. **Maybe someone with rust experience** can have a look at my [wrk/rust/hello](wrk/rust/hello) code and tell me why it is surprisingly 'slow', as I expected it to be faster than or at least on-par with the basic Go example. I'll enable the GitHub discussions for this matter. My suspicion is bad performance of the mutexes. - -![table](https://raw.githubusercontent.com/renerocksai/zap/master/wrk_table_summary.png) - -![charts](https://raw.githubusercontent.com/renerocksai/zap/master/wrk_charts_summary.png) - -So, being somewhere in the ballpark of basic GO performance, zig zap seems to be ... of reasonable performance 😎. - -See more details in [blazingly-fast.md](https://github.com/renerocksai/zap/blob/master/blazingly-fast.md). - -## minimal - -Zap is minimal by necessity. I only (have time to) add what I need - for serving REST APIs and HTML. The primary use-case are frontends that I wrote that communicate with my APIs. Hence, the focus is more on getting stuff done rather than conforming to every standard there is. Even though facilio is able to support TLS, I don't care about that - at least for now. Also, if you present `404 - File not found` as human-readable HTML to the user, nobody forces you to also set the status code to 404, so it can be OK to spare those nanoseconds. Gotta go fast! - -Facilio comes with Mustache parsing, TLS via third-party libs, websockets, redis support, concurrency stuff, Base64 support, logging facilities, pub/sub / cluster messages API, hash algorithm implementations, its own memory allocator, and so forth. It is really an amazing project! - -On the lower level, you can use all of the above by working with `zap.C`. I'll zig-wrap what I need for my projects first, before adding more fancy stuff. - -Also, there are nice and well-typed zig implementations for some of the above extra functionalities, and zap-wrapping them needs careful consideration. E.g. it might not be worth the extra effort to wrap facil.io's mustache support when there is a good zig alternative already. Performance / out-of-the-box integration might be arguments pro wrapping them in zap. - -## wrapping up - zig is WYSIWYG code - -I am super excited about both zig and zap's future. I am still impressed by how easy it is to integrate a C codebase into a zig project, then benefiting from and building on top of battle-tested high-performance C code. Additionally, with zig you get C-like performance with almost Python-like comfort. And you can be sure no exception is trying to get you when you least expect it. No hidden allocations, no hidden control-flows, how cool is that? **WYSIWYG code!** - -Provided that the incorporated C code is well-written and -tested, WYSIWYG even holds mostly true for combined Zig and C projects. - -You can truly build on the soulders of giants here. Mind you, it took me less than a week to arrive at the current state of zap where I am confident that I can already use it to write the one or other REST API with it and, after stress-testing, just move it into production - from merely researching Zig and C web frameworks a few days ago. - -Oh, and have I mentioned Zig's built-in build system and testing framework? Those are both super amazing and super convenient. `zig build` is so much more useful than `make` (which I quite like to be honest). And `zig test` is just amazing, too. Zig's physical code layout: which file is located where and how can it be built, imported, tested - it all makes so much sense. Such a coherent, pleasant experience. - -Looking forward, I am also tempted to try adding some log-and-replay facilities as a kind of backup for when things go wrong. I wouldn't be confident to attemt such things in C because I'd view them as being too much work; too much could go wrong. But with Zig, I am rather excited about the possibilities that open up and eager to try such things. - -For great justice! diff --git a/shell.nix b/shell.nix deleted file mode 100644 index b7dafc3..0000000 --- a/shell.nix +++ /dev/null @@ -1,35 +0,0 @@ -{ - pkgs ? import { - overlays = [ - (import (builtins.fetchTarball { - # url = https://github.com/nix-community/neovim-nightly-overlay/archive/master.tar.gz; - url = https://github.com/nix-community/neovim-nightly-overlay/archive/72ff8b1ca0331a8735c1eeaefb95c12dfe21d30a.tar.gz; - })) - ]; - } -} : -pkgs.mkShell { - nativeBuildInputs = [ - pkgs.neovim-nightly - pkgs.bat - pkgs.wrk - pkgs.python3 - pkgs.rustc - pkgs.cargo - pkgs.gcc - pkgs.rustfmt - pkgs.clippy - ]; - - buildInputs = [ - pkgs.go - pkgs.gotools - pkgs.gopls - # pkgs.go-outline - # pkgs.gocode - # pkgs.gopkgs - # pkgs.gocode-gomod - # pkgs.godef - pkgs.golint - ]; -} diff --git a/wrk/axum-sanic.md b/wrk/axum-sanic.md deleted file mode 100644 index f22a1cc..0000000 --- a/wrk/axum-sanic.md +++ /dev/null @@ -1,70 +0,0 @@ -# axum - -```console -zap on  newwrk [$!?] via ↯ v0.11.0-dev.2837+b55b8e774 via  impure (nix-shell) -➜ wrk/measure.sh axum - Finished release [optimized] target(s) in 0.05s -======================================================================== - axum -======================================================================== -Running 10s test @ http://127.0.0.1:3000 - 4 threads and 400 connections - Thread Stats Avg Stdev Max +/- Stdev - Latency 527.01us 260.08us 8.47ms 74.31% - Req/Sec 151.11k 4.06k 166.63k 71.25% - Latency Distribution - 50% 518.00us - 75% 644.00us - 90% 811.00us - 99% 1.39ms - 6014492 requests in 10.01s, 768.61MB read -Requests/sec: 600582.38 -Transfer/sec: 76.75MB - -zap on  newwrk [$!?] via ↯ v0.11.0-dev.2837+b55b8e774 via  impure (nix-shell) took 11s -➜ wrk/measure.sh axum - Finished release [optimized] target(s) in 0.05s -======================================================================== - axum -======================================================================== -Running 10s test @ http://127.0.0.1:3000 - 4 threads and 400 connections - Thread Stats Avg Stdev Max +/- Stdev - Latency 534.89us 280.25us 7.37ms 76.81% - Req/Sec 150.03k 4.26k 162.67k 72.75% - Latency Distribution - 50% 520.00us - 75% 647.00us - 90% 831.00us - 99% 1.50ms - 5969526 requests in 10.01s, 762.86MB read -Requests/sec: 596134.58 -Transfer/sec: 76.18MB - -zap on  newwrk [$!?] via ↯ v0.11.0-dev.2837+b55b8e774 via  impure (nix-shell) took 11s -➜ wrk/measure.sh axum - Finished release [optimized] target(s) in 0.05s -======================================================================== - axum -======================================================================== -Running 10s test @ http://127.0.0.1:3000 - 4 threads and 400 connections - Thread Stats Avg Stdev Max +/- Stdev - Latency 519.96us 269.86us 11.92ms 76.98% - Req/Sec 151.29k 4.32k 164.52k 69.75% - Latency Distribution - 50% 509.00us - 75% 635.00us - 90% 800.00us - 99% 1.41ms - 6021199 requests in 10.01s, 769.46MB read -Requests/sec: 601482.51 -Transfer/sec: 76.86MB - -zap on  newwrk [$!?] via ↯ v0.11.0-dev.2837+b55b8e774 via  impure (nix-shell) took 11s -➜ -``` - -# sanic - - diff --git a/wrk/axum/hello-axum/Cargo.lock b/wrk/axum/hello-axum/Cargo.lock deleted file mode 100644 index e7d07d9..0000000 --- a/wrk/axum/hello-axum/Cargo.lock +++ /dev/null @@ -1,752 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "async-trait" -version = "0.1.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.15", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "axum" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43" -dependencies = [ - "async-trait", - "axum-core", - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "hyper", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-http", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "mime", - "tower-layer", - "tower-service", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bytes" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" - -[[package]] -name = "futures-task" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" - -[[package]] -name = "futures-util" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "pin-utils", -] - -[[package]] -name = "hello-axum" -version = "0.1.0" -dependencies = [ - "axum", - "tokio", -] - -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "http" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "http-range-header" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" - -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - -[[package]] -name = "httpdate" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" - -[[package]] -name = "hyper" -version = "0.14.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "itoa" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" - -[[package]] -name = "libc" -version = "0.2.142" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" - -[[package]] -name = "lock_api" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "matchit" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mio" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.45.0", -] - -[[package]] -name = "num_cpus" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "once_cell" -version = "1.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-sys 0.45.0", -] - -[[package]] -name = "percent-encoding" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" - -[[package]] -name = "pin-project" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "proc-macro2" -version = "1.0.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags", -] - -[[package]] -name = "ryu" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "serde" -version = "1.0.160" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" - -[[package]] -name = "serde_json" -version = "1.0.96" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - -[[package]] -name = "smallvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" - -[[package]] -name = "socket2" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - -[[package]] -name = "tokio" -version = "1.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f" -dependencies = [ - "autocfg", - "bytes", - "libc", - "mio", - "num_cpus", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.48.0", -] - -[[package]] -name = "tokio-macros" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.15", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-range-header", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" - -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" -dependencies = [ - "cfg-if", - "log", - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "try-lock" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" - -[[package]] -name = "unicode-ident" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" - -[[package]] -name = "want" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" -dependencies = [ - "log", - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.0", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" -dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/wrk/axum/hello-axum/Cargo.md b/wrk/axum/hello-axum/Cargo.md deleted file mode 100644 index 0700a75..0000000 --- a/wrk/axum/hello-axum/Cargo.md +++ /dev/null @@ -1,129 +0,0 @@ -# 60 dependencies!!! - -```console -➜ cargo build - Updating crates.io index - Downloaded percent-encoding v2.2.0 - Downloaded sync_wrapper v0.1.2 - Downloaded async-trait v0.1.68 - Downloaded unicode-ident v1.0.8 - Downloaded tracing v0.1.37 - Downloaded mime v0.3.17 - Downloaded http-body v0.4.5 - Downloaded bytes v1.4.0 - Downloaded httpdate v1.0.2 - Downloaded httparse v1.8.0 - Downloaded http v0.2.9 - Downloaded lock_api v0.4.9 - Downloaded num_cpus v1.15.0 - Downloaded parking_lot v0.12.1 - Downloaded pin-project v1.0.12 - Downloaded parking_lot_core v0.9.7 - Downloaded form_urlencoded v1.1.0 - Downloaded log v0.4.17 - Downloaded memchr v2.5.0 - Downloaded matchit v0.5.0 - Downloaded pin-project-lite v0.2.9 - Downloaded scopeguard v1.1.0 - Downloaded pin-utils v0.1.0 - Downloaded once_cell v1.17.1 - Downloaded serde_urlencoded v0.7.1 - Downloaded pin-project-internal v1.0.12 - Downloaded ryu v1.0.13 - Downloaded quote v1.0.26 - Downloaded proc-macro2 v1.0.56 - Downloaded signal-hook-registry v1.4.1 - Downloaded socket2 v0.4.9 - Downloaded tower-service v0.3.2 - Downloaded tower-layer v0.3.2 - Downloaded tower v0.4.13 - Downloaded autocfg v1.1.0 - Downloaded syn v2.0.15 - Downloaded try-lock v0.2.4 - Downloaded futures-task v0.3.28 - Downloaded futures-core v0.3.28 - Downloaded fnv v1.0.7 - Downloaded bitflags v1.3.2 - Downloaded futures-util v0.3.28 - Downloaded hyper v0.14.26 - Downloaded axum v0.5.17 - Downloaded smallvec v1.10.0 - Downloaded want v0.3.0 - Downloaded axum-core v0.2.9 - Downloaded mio v0.8.6 - Downloaded tokio-macros v2.1.0 - Downloaded serde v1.0.160 - Downloaded tower-http v0.3.5 - Downloaded serde_json v1.0.96 - Downloaded syn v1.0.109 - Downloaded futures-channel v0.3.28 - Downloaded tracing-core v0.1.30 - Downloaded itoa v1.0.6 - Downloaded cfg-if v1.0.0 - Downloaded http-range-header v0.3.0 - Downloaded libc v0.2.142 - Downloaded tokio v1.28.0 - Downloaded 60 crates (4.1 MB) in 2.81s - Compiling proc-macro2 v1.0.56 - Compiling unicode-ident v1.0.8 - Compiling quote v1.0.26 - Compiling libc v0.2.142 - Compiling cfg-if v1.0.0 - Compiling autocfg v1.1.0 - Compiling log v0.4.17 - Compiling futures-core v0.3.28 - Compiling pin-project-lite v0.2.9 - Compiling bytes v1.4.0 - Compiling itoa v1.0.6 - Compiling futures-task v0.3.28 - Compiling parking_lot_core v0.9.7 - Compiling futures-util v0.3.28 - Compiling syn v1.0.109 - Compiling smallvec v1.10.0 - Compiling scopeguard v1.1.0 - Compiling once_cell v1.17.1 - Compiling pin-utils v0.1.0 - Compiling fnv v1.0.7 - Compiling serde v1.0.160 - Compiling tower-service v0.3.2 - Compiling futures-channel v0.3.28 - Compiling httparse v1.8.0 - Compiling tower-layer v0.3.2 - Compiling async-trait v0.1.68 - Compiling try-lock v0.2.4 - Compiling serde_json v1.0.96 - Compiling ryu v1.0.13 - Compiling memchr v2.5.0 - Compiling percent-encoding v2.2.0 - Compiling http-range-header v0.3.0 - Compiling httpdate v1.0.2 - Compiling bitflags v1.3.2 - Compiling mime v0.3.17 - Compiling sync_wrapper v0.1.2 - Compiling matchit v0.5.0 - Compiling http v0.2.9 - Compiling tracing-core v0.1.30 - Compiling form_urlencoded v1.1.0 - Compiling lock_api v0.4.9 - Compiling tokio v1.28.0 - Compiling want v0.3.0 - Compiling tracing v0.1.37 - Compiling syn v2.0.15 - Compiling http-body v0.4.5 - Compiling num_cpus v1.15.0 - Compiling socket2 v0.4.9 - Compiling signal-hook-registry v1.4.1 - Compiling mio v0.8.6 - Compiling parking_lot v0.12.1 - Compiling serde_urlencoded v0.7.1 - Compiling tokio-macros v2.1.0 - Compiling pin-project-internal v1.0.12 - Compiling axum-core v0.2.9 - Compiling pin-project v1.0.12 - Compiling tower v0.4.13 - Compiling hyper v0.14.26 - Compiling tower-http v0.3.5 - Compiling axum v0.5.17 - Compiling hello-axum v0.1.0 (/home/rs/code/github.com/zigzap/zap/wrk/axum/hello-axum) - Finished dev [unoptimized + debuginfo] target(s) in 53.19s -``` diff --git a/wrk/axum/hello-axum/Cargo.toml b/wrk/axum/hello-axum/Cargo.toml deleted file mode 100644 index 26b3b91..0000000 --- a/wrk/axum/hello-axum/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "hello-axum" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -axum = "0.5" -tokio = { version = "1", features = ["full"] } diff --git a/wrk/axum/hello-axum/src/main.rs b/wrk/axum/hello-axum/src/main.rs deleted file mode 100644 index 7ab437a..0000000 --- a/wrk/axum/hello-axum/src/main.rs +++ /dev/null @@ -1,29 +0,0 @@ -use axum::{routing::get, Router}; -use std::net::SocketAddr; - -#[tokio::main] -async fn main() { - // Route all requests on "/" endpoint to anonymous handler. - // - // A handler is an async function which returns something that implements - // `axum::response::IntoResponse`. - - // A closure or a function can be used as handler. - - let app = Router::new().route("/", get(handler)); - // Router::new().route("/", get(|| async { "Hello, world!" })); - - // Address that server will bind to. - let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); - - // Use `hyper::server::Server` which is re-exported through `axum::Server` to serve the app. - axum::Server::bind(&addr) - // Hyper server takes a make service. - .serve(app.into_make_service()) - .await - .unwrap(); -} - -async fn handler() -> &'static str { - "Hello from axum!!" -} diff --git a/wrk/cpp/build.zig b/wrk/cpp/build.zig deleted file mode 100644 index caa0656..0000000 --- a/wrk/cpp/build.zig +++ /dev/null @@ -1,39 +0,0 @@ -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - - const optimize = b.standardOptimizeOption(.{}); - - const exe = b.addExecutable(.{ - .name = "cpp-beast", - .target = target, - .optimize = optimize, - }); - exe.addIncludePath(.{ .path = "." }); - exe.addCSourceFiles(&.{"main.cpp"}, &.{ - "-Wall", - "-Wextra", - "-Wshadow", - }); - const libasio_dep = b.dependency("beast", .{ - .target = target, - .optimize = optimize, - }); - const libasio = libasio_dep.artifact("beast"); - for (libasio.include_dirs.items) |include| { - exe.include_dirs.append(include) catch {}; - } - exe.linkLibrary(libasio); - exe.linkLibCpp(); - - b.installArtifact(exe); - const run_cmd = b.addRunArtifact(exe); - run_cmd.step.dependOn(b.getInstallStep()); - if (b.args) |args| { - run_cmd.addArgs(args); - } - - const run_step = b.step("run", "Run C++ Http Server"); - run_step.dependOn(&run_cmd.step); -} diff --git a/wrk/cpp/build.zig.zon b/wrk/cpp/build.zig.zon deleted file mode 100644 index ff48e0d..0000000 --- a/wrk/cpp/build.zig.zon +++ /dev/null @@ -1,10 +0,0 @@ -.{ - .name = "cpp-beast", - .version = "0.1.0", - .dependencies = .{ - .beast = .{ - .url = "https://github.com/kassane/beast/archive/df69ba4d48fbe874730f6a28e9528d9ef7a9547c.tar.gz", - .hash = "1220548f8727394522081ab48ed2f7111c20fa5f051ff287ec3c3f82340faa5d68c2", - }, - }, -} diff --git a/wrk/cpp/hello.html b/wrk/cpp/hello.html deleted file mode 100644 index 205d9a7..0000000 --- a/wrk/cpp/hello.html +++ /dev/null @@ -1 +0,0 @@ -Hello from C++!! diff --git a/wrk/cpp/main.cpp b/wrk/cpp/main.cpp deleted file mode 100644 index 3241b9b..0000000 --- a/wrk/cpp/main.cpp +++ /dev/null @@ -1,80 +0,0 @@ -#include -#include -#include -#include -#include - -namespace beast = boost::beast; -namespace http = beast::http; -namespace net = boost::asio; -using tcp = net::ip::tcp; - -std::string read_html_file(const std::string& file_path) { - std::ifstream file(file_path); - if (!file) { - return "File not found: " + file_path; - } - std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - file.close(); - return content; -} - -void handle_client(tcp::socket socket, const std::string& msg) { - try { - // Construct an HTTP response with the HTML content - http::response response; - response.version(11); - response.result(http::status::ok); - response.reason("OK"); - response.set(http::field::server, "C++ Server"); - response.set(http::field::content_type, "text/html"); - response.body() = msg; - response.prepare_payload(); - - // Send the response to the client - http::write(socket, response); - } catch (const std::exception& e) { - std::cerr << "Error: " << e.what() << std::endl; - } -} - -int main() { - try { - net::io_context io_context{BOOST_ASIO_CONCURRENCY_HINT_UNSAFE_IO}; - - // Create an endpoint to bind to - tcp::endpoint endpoint(tcp::v4(), 8070); - - // Create and bind the acceptor - tcp::acceptor acceptor(io_context, endpoint); - std::cout << "Server listening on port 8070..." << std::endl; - - // static 17-byte string - std::string msg = "Hello from C++!!!"; - // or - // Read HTML content from a file (e.g., "index.html") - // std::string html_content = read_html_file("hello.html"); - - // std::cout << "str len: " << (html_content.length() == msg.length()) << std::boolalpha << "\n"; - - // Create a thread pool with 4 threads - net::thread_pool pool(4); - - while (true) { - // Wait for a client to connect - tcp::socket socket(io_context); - acceptor.accept(socket); - - // Post a task to the thread pool to handle the client request - net::post(pool, [socket = std::move(socket), msg]() mutable { - handle_client(std::move(socket), msg); - }); - } - - // The thread pool destructor will ensure that all threads are joined properly. - } catch (const std::exception& e) { - std::cerr << "Error: " << e.what() << std::endl; - } - - return 0; -} diff --git a/wrk/csharp/Program.cs b/wrk/csharp/Program.cs deleted file mode 100644 index 1b1f7bf..0000000 --- a/wrk/csharp/Program.cs +++ /dev/null @@ -1,8 +0,0 @@ -var builder = WebApplication.CreateBuilder(args); -builder.Logging.ClearProviders(); - -var app = builder.Build(); - -app.MapGet("/", () => "Hello from C#"); - -app.Run(); diff --git a/wrk/csharp/Properties/launchSettings.json b/wrk/csharp/Properties/launchSettings.json deleted file mode 100644 index d790ec0..0000000 --- a/wrk/csharp/Properties/launchSettings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/launchsettings.json", - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "http://localhost:5026", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} \ No newline at end of file diff --git a/wrk/csharp/csharp.csproj b/wrk/csharp/csharp.csproj deleted file mode 100644 index 274b6dd..0000000 --- a/wrk/csharp/csharp.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - net8.0 - enable - enable - true - - diff --git a/wrk/go/main.go b/wrk/go/main.go deleted file mode 100644 index 508f696..0000000 --- a/wrk/go/main.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "fmt" - "net/http" -) - -func hello(w http.ResponseWriter, req *http.Request) { - fmt.Fprintf(w, "hello from GO!!!\n") -} - -func main() { - print("listening on 0.0.0.0:8090\n") - http.HandleFunc("/hello", hello) - http.ListenAndServe(":8090", nil) -} diff --git a/wrk/graph.py b/wrk/graph.py deleted file mode 100644 index b446d98..0000000 --- a/wrk/graph.py +++ /dev/null @@ -1,90 +0,0 @@ -import re -import os -import matplotlib.pyplot as plt -from matplotlib.ticker import FuncFormatter -from collections import defaultdict -import statistics - -directory = "./wrk" # Replace with the actual directory path - -requests_sec = defaultdict(list) -transfers_sec = defaultdict(list) - -mean_requests = {} -mean_transfers = {} - - -def plot(kind='', title='', ylabel='', means=None): - # Sort the labels and requests_sec lists together based on the requests_sec values - labels = [] - values = [] - - # silly, I know - for k, v in means.items(): - labels.append(k) - values.append(v) - - # sort the labels and value lists - labels, values = zip(*sorted(zip(labels, values), key=lambda x: x[1], reverse=True)) - - # Plot the graph - plt.figure(figsize=(10, 6)) # Adjust the figure size as needed - bars = plt.bar(labels, values) - plt.xlabel("Subject") - plt.ylabel(ylabel) - plt.title(title) - plt.xticks(rotation=45) # Rotate x-axis labels for better readability - - # Display the actual values on top of the bars - for bar in bars: - yval = bar.get_height() - plt.text(bar.get_x() + bar.get_width() / 2, yval, f'{yval:,.2f}', ha='center', va='bottom') - - plt.tight_layout() # Adjust the spacing of the graph elements - png_name = f"{directory}/{kind.lower()}_graph.png" - plt.savefig(png_name) # Save the graph as a PNG file - print(f"Generated: {png_name}") - - -if __name__ == '__main__': - if not os.path.isdir(".git"): - print("Please run from root directory of the repository!") - print("e.g. python wrk/graph.py") - import sys - sys.exit(1) - - # Iterate over the files in the directory - for filename in os.listdir(directory): - if filename.endswith(".perflog"): - label = os.path.splitext(filename)[0] - file_path = os.path.join(directory, filename) - - with open(file_path, "r") as file: - lines = file.readlines() - for line in lines: - # Extract the Requests/sec value using regular expressions - match = re.search(r"Requests/sec:\s+([\d.]+)", line) - if match: - requests_sec[label].append(float(match.group(1))) - match = re.search(r"Transfer/sec:\s+([\d.]+)", line) - if match: - value = float(match.group(1)) - if 'KB' in line: - value *= 1024 - elif 'MB' in line: - value *= 1024 * 1024 - value /= 1024.0 * 1024 - transfers_sec[label].append(value) - - # calculate means - for k, v in requests_sec.items(): - mean_requests[k] = statistics.mean(v) - - for k, v in transfers_sec.items(): - mean_transfers[k] = statistics.mean(v) - - # save the plots - plot(kind='req_per_sec', title='Requests/sec Comparison', - ylabel='requests/sec', means=mean_requests) - plot(kind='xfer_per_sec', title='Transfer/sec Comparison', - ylabel='transfer/sec [MB]', means=mean_transfers) diff --git a/wrk/measure.sh b/wrk/measure.sh deleted file mode 100755 index 4b8e078..0000000 --- a/wrk/measure.sh +++ /dev/null @@ -1,104 +0,0 @@ -#! /usr/bin/env bash -THREADS=4 -CONNECTIONS=400 -DURATION_SECONDS=10 - -SUBJECT=$1 - -if echo $(uname) -eq "Darwin" ; then -TSK_SRV="" -TSK_LOAD="" -else -TSK_SRV="taskset -c 0,1,2,3" -TSK_LOAD="taskset -c 4,5,6,7" -fi - -if [ "$SUBJECT" = "" ] ; then - echo "usage: $0 subject # subject: zig or go" - exit 1 -fi - -if [ "$SUBJECT" = "zig-zap" ] ; then - zig build -Doptimize=ReleaseFast wrk > /dev/null - $TSK_SRV ./zig-out/bin/wrk & - PID=$! - URL=http://127.0.0.1:3000 -fi - -if [ "$SUBJECT" = "zigstd" ] ; then - zig build -Doptimize=ReleaseFast wrk_zigstd > /dev/null - $TSK_SRV ./zig-out/bin/wrk_zigstd & - PID=$! - URL=http://127.0.0.1:3000 -fi - -if [ "$SUBJECT" = "go" ] ; then - cd wrk/go && go build main.go - $TSK_SRV ./main & - PID=$! - URL=http://127.0.0.1:8090/hello -fi - -if [ "$SUBJECT" = "python" ] ; then - $TSK_SRV python wrk/python/main.py & - PID=$! - URL=http://127.0.0.1:8080 -fi - -if [ "$SUBJECT" = "python-sanic" ] ; then - $TSK_SRV python wrk/sanic/sanic-app.py & - PID=$! - URL=http://127.0.0.1:8000 -fi - -if [ "$SUBJECT" = "rust-bythebook" ] ; then - cd wrk/rust/bythebook && cargo build --release - $TSK_SRV ./target/release/hello & - PID=$! - URL=http://127.0.0.1:7878 -fi - -if [ "$SUBJECT" = "rust-bythebook-improved" ] ; then - cd wrk/rust/bythebook-improved && cargo build --release - $TSK_SRV ./target/release/hello & - PID=$! - URL=http://127.0.0.1:7878 -fi - - -if [ "$SUBJECT" = "rust-clean" ] ; then - cd wrk/rust/clean && cargo build --release - $TSK_SRV ./target/release/hello & - PID=$! - URL=http://127.0.0.1:7878 -fi - -if [ "$SUBJECT" = "rust-axum" ] ; then - cd wrk/axum/hello-axum && cargo build --release - $TSK_SRV ./target/release/hello-axum & - PID=$! - URL=http://127.0.0.1:3000 -fi - -if [ "$SUBJECT" = "csharp" ] ; then - cd wrk/csharp && dotnet publish csharp.csproj -o ./out - $TSK_SRV ./out/csharp --urls "http://127.0.0.1:5026" & - PID=$! - URL=http://127.0.0.1:5026 -fi - -if [ "$SUBJECT" = "cpp-beast" ] ; then - cd wrk/cpp && zig build -Doptimize=ReleaseFast - $TSK_SRV ./zig-out/bin/cpp-beast 127.0.0.1 8070 . & - PID=$! - URL=http://127.0.0.1:8070 -fi - -sleep 1 -echo "========================================================================" -echo " $SUBJECT" -echo "========================================================================" -$TSK_LOAD wrk -c $CONNECTIONS -t $THREADS -d $DURATION_SECONDS --latency $URL - -kill $PID - diff --git a/wrk/measure_all.sh b/wrk/measure_all.sh deleted file mode 100755 index 0e80a59..0000000 --- a/wrk/measure_all.sh +++ /dev/null @@ -1,32 +0,0 @@ -#! /usr/bin/env bash - -if [ ! -d ".git" ] ; then - echo "This script must be run from the root directory of the repository!" - echo "./wrk/measure_all.sh" - exit 1 -fi - -SUBJECTS="$1" - -if [ "$SUBJECTS" = "README" ] ; then - rm -f wrk/*.perflog - SUBJECTS="zig-zap go python-sanic rust-axum csharp cpp-beast" - # above targets csharp and cpp-beast are out of date! - SUBJECTS="zig-zap go python-sanic rust-axum" -fi - -if [ -z "$SUBJECTS" ] ; then - SUBJECTS="zig-zap go python python-sanic rust-bythebook rust-bythebook-improved rust-clean rust-axum csharp cpp-beast" - # above targets csharp and cpp-beast are out of date! - SUBJECTS="zig-zap go python python-sanic rust-bythebook rust-bythebook-improved rust-clean rust-axum" -fi - -for S in $SUBJECTS; do - L="$S.perflog" - rm -f wrk/$L - for R in 1 2 3 ; do - ./wrk/measure.sh $S | tee -a wrk/$L - done -done - -echo "Finished" diff --git a/wrk/other_measurements.md b/wrk/other_measurements.md deleted file mode 100644 index 7cd695f..0000000 --- a/wrk/other_measurements.md +++ /dev/null @@ -1,52 +0,0 @@ -# other measurements - -## zap wrk 'example' with and without logging - -**NO** performance regressions observable: - -With `logging=true`: - -``` -[nix-shell:~/code/github.com/renerocksai/zap]$ ./wrk/measure.sh zig > out 2> /dev/null - -[nix-shell:~/code/github.com/renerocksai/zap]$ cat out -======================================================================== - zig -======================================================================== -Running 10s test @ http://127.0.0.1:3000 - 4 threads and 400 connections - Thread Stats Avg Stdev Max +/- Stdev - Latency 343.91us 286.75us 18.37ms 95.58% - Req/Sec 162.61k 3.61k 174.96k 76.75% - Latency Distribution - 50% 302.00us - 75% 342.00us - 90% 572.00us - 99% 697.00us - 6470789 requests in 10.01s, 0.96GB read -Requests/sec: 646459.59 -Transfer/sec: 98.03MB -``` - -With `logging=false`: - -``` -[nix-shell:~/code/github.com/renerocksai/zap]$ ./wrk/measure.sh zig -Listening on 0.0.0.0:3000 -======================================================================== - zig -======================================================================== -Running 10s test @ http://127.0.0.1:3000 - 4 threads and 400 connections - Thread Stats Avg Stdev Max +/- Stdev - Latency 336.10us 122.28us 14.67ms 88.55% - Req/Sec 159.82k 7.71k 176.75k 56.00% - Latency Distribution - 50% 310.00us - 75% 343.00us - 90% 425.00us - 99% 699.00us - 6359415 requests in 10.01s, 0.94GB read -Requests/sec: 635186.96 -Transfer/sec: 96.32MB -``` diff --git a/wrk/python/main.py b/wrk/python/main.py deleted file mode 100644 index f3c77d1..0000000 --- a/wrk/python/main.py +++ /dev/null @@ -1,32 +0,0 @@ -# Python 3 server example -from http.server import BaseHTTPRequestHandler, HTTPServer - -hostName = "127.0.0.1" -serverPort = 8080 - - -class MyServer(BaseHTTPRequestHandler): - def do_GET(self): - try: - self.send_response(200) - self.send_header("Content-type", "text/html") - self.end_headers() - self.wfile.write(bytes("HI FROM PYTHON!!!", "utf-8")) - except: - pass - - def log_message(self, format, *args): - return - - -if __name__ == "__main__": - webServer = HTTPServer((hostName, serverPort), MyServer) - print("Server started http://%s:%s" % (hostName, serverPort)) - - try: - webServer.serve_forever() - except KeyboardInterrupt: - pass - - webServer.server_close() - print("Server stopped.") diff --git a/wrk/rust/bythebook-improved/.gitignore b/wrk/rust/bythebook-improved/.gitignore deleted file mode 100644 index 6985cf1..0000000 --- a/wrk/rust/bythebook-improved/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb diff --git a/wrk/rust/bythebook-improved/Cargo.toml b/wrk/rust/bythebook-improved/Cargo.toml deleted file mode 100644 index c9ba87b..0000000 --- a/wrk/rust/bythebook-improved/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "hello" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -# crossbeam = { version = "0.8.2", features = ["crossbeam-channel"] } diff --git a/wrk/rust/bythebook-improved/src/lib.rs b/wrk/rust/bythebook-improved/src/lib.rs deleted file mode 100644 index 6e5d88d..0000000 --- a/wrk/rust/bythebook-improved/src/lib.rs +++ /dev/null @@ -1,101 +0,0 @@ -//Crossbeam should, but does not make this faster. -//use crossbeam::channel::bounded; -use std::{net::TcpStream, sync::mpsc, thread}; -type Job = (fn(TcpStream), TcpStream); - -type Sender = mpsc::Sender; -//type Sender = crossbeam::channel::Sender; - -type Receiver = mpsc::Receiver; -//type Receiver = crossbeam::channel::Receiver; - -pub struct ThreadPool { - workers: Vec, - senders: Vec, - - next_sender: usize, -} - -impl ThreadPool { - /// Create a new ThreadPool. - /// - /// The size is the number of threads in the pool. - /// - /// # Panics - /// - /// The `new` function will panic if the size is zero. - pub fn new(size: usize) -> ThreadPool { - assert!(size > 0); - - let mut workers = Vec::with_capacity(size); - let mut senders = Vec::with_capacity(size); - - for id in 0..size { - //let (sender, receiver) = bounded(2); - let (sender, receiver) = mpsc::channel(); - senders.push(sender); - workers.push(Worker::new(id, receiver)); - } - - ThreadPool { - workers, - senders, - next_sender: 0, - } - } - /// round robin over available workers to ensure we never have to buffer requests - pub fn execute(&mut self, handler: fn(TcpStream), stream: TcpStream) { - let job = (handler, stream); - self.senders[self.next_sender].send(job).unwrap(); - //self.senders[self.next_sender].try_send(job).unwrap(); - self.next_sender += 1; - if self.next_sender == self.senders.len() { - self.next_sender = 0; - } - } -} - -impl Drop for ThreadPool { - fn drop(&mut self) { - self.senders.clear(); - - for worker in &mut self.workers { - println!("Shutting down worker {}", worker.id); - if let Some(thread) = worker.thread.take() { - thread.join().unwrap(); - } - } - } -} - -struct Worker { - id: usize, - thread: Option>, -} - -impl Worker { - fn new(id: usize, receiver: Receiver) -> Worker { - let thread = thread::spawn(move || Self::work(receiver)); - - Worker { - id, - thread: Some(thread), - } - } - - fn work(receiver: Receiver) { - loop { - let message = receiver.recv(); - match message { - Ok((handler, stream)) => { - // println!("Worker got a job; executing."); - handler(stream); - } - Err(_) => { - // println!("Worker disconnected; shutting down."); - break; - } - } - } - } -} diff --git a/wrk/rust/bythebook-improved/src/main.rs b/wrk/rust/bythebook-improved/src/main.rs deleted file mode 100644 index 8b22471..0000000 --- a/wrk/rust/bythebook-improved/src/main.rs +++ /dev/null @@ -1,34 +0,0 @@ -use hello::ThreadPool; -use std::io::prelude::*; -use std::net::TcpListener; -use std::net::TcpStream; - -fn main() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); - //Creating a massive amount of threads so we can always have one ready to go. - let mut pool = ThreadPool::new(128); - - for stream in listener.incoming() { - let stream = stream.unwrap(); - //handle_connection(stream); - pool.execute(handle_connection, stream); - } - - println!("Shutting down."); -} - -fn handle_connection(mut stream: TcpStream) { - stream.set_nodelay(true).expect("set_nodelay call failed"); - loop{ - let mut buffer = [0; 1024]; - match stream.read(&mut buffer){ - Err(_)=>return, - Ok(0)=>return, - Ok(_v)=>{}, - } - - let response_bytes = b"HTTP/1.1 200 OK\r\nContent-Length: 16\r\nConnection: keep-alive\r\n\r\nHELLO from RUST!"; - - stream.write_all(response_bytes).unwrap(); - } -} diff --git a/wrk/rust/bythebook/.gitignore b/wrk/rust/bythebook/.gitignore deleted file mode 100644 index 6985cf1..0000000 --- a/wrk/rust/bythebook/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb diff --git a/wrk/rust/bythebook/Cargo.toml b/wrk/rust/bythebook/Cargo.toml deleted file mode 100644 index fb1ec2c..0000000 --- a/wrk/rust/bythebook/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "hello" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] diff --git a/wrk/rust/bythebook/hello.html b/wrk/rust/bythebook/hello.html deleted file mode 100644 index a2b3773..0000000 --- a/wrk/rust/bythebook/hello.html +++ /dev/null @@ -1 +0,0 @@ -Hello from RUST! diff --git a/wrk/rust/bythebook/src/lib.rs b/wrk/rust/bythebook/src/lib.rs deleted file mode 100644 index 936d014..0000000 --- a/wrk/rust/bythebook/src/lib.rs +++ /dev/null @@ -1,92 +0,0 @@ -use std::{ - sync::{mpsc, Arc, Mutex}, - thread, -}; - -pub struct ThreadPool { - workers: Vec, - sender: Option>, -} - -type Job = Box; - -impl ThreadPool { - /// Create a new ThreadPool. - /// - /// The size is the number of threads in the pool. - /// - /// # Panics - /// - /// The `new` function will panic if the size is zero. - pub fn new(size: usize) -> ThreadPool { - assert!(size > 0); - - let (sender, receiver) = mpsc::channel(); - - let receiver = Arc::new(Mutex::new(receiver)); - - let mut workers = Vec::with_capacity(size); - - for id in 0..size { - workers.push(Worker::new(id, Arc::clone(&receiver))); - } - - ThreadPool { - workers, - sender: Some(sender), - } - } - - pub fn execute(&self, f: F) - where - F: FnOnce() + Send + 'static, - { - let job = Box::new(f); - - self.sender.as_ref().unwrap().send(job).unwrap(); - } -} - -impl Drop for ThreadPool { - fn drop(&mut self) { - drop(self.sender.take()); - - for worker in &mut self.workers { - println!("Shutting down worker {}", worker.id); - - if let Some(thread) = worker.thread.take() { - thread.join().unwrap(); - } - } - } -} - -struct Worker { - id: usize, - thread: Option>, -} - -impl Worker { - fn new(id: usize, receiver: Arc>>) -> Worker { - let thread = thread::spawn(move || loop { - let message = receiver.lock().unwrap().recv(); - - match message { - Ok(job) => { - // println!("Worker got a job; executing."); - - job(); - } - Err(_) => { - // println!("Worker disconnected; shutting down."); - break; - } - } - }); - - Worker { - id, - thread: Some(thread), - } - } -} diff --git a/wrk/rust/bythebook/src/main.rs b/wrk/rust/bythebook/src/main.rs deleted file mode 100644 index 34a7474..0000000 --- a/wrk/rust/bythebook/src/main.rs +++ /dev/null @@ -1,43 +0,0 @@ -use hello::ThreadPool; -// use std::fs; -use std::io::prelude::*; -use std::net::TcpListener; -use std::net::TcpStream; -// use std::thread; -// use std::time::Duration; - -fn main() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); - let pool = ThreadPool::new(4); - - // for stream in listener.incoming().take(2) { - for stream in listener.incoming() { - let stream = stream.unwrap(); - - pool.execute(|| { - handle_connection(stream); - }); - } - - println!("Shutting down."); -} - -fn handle_connection(mut stream: TcpStream) { - let mut buffer = [0; 1024]; - stream.read(&mut buffer).unwrap(); - - - let status_line = "HTTP/1.1 200 OK"; - - let contents = "HELLO from RUST!"; - - let response = format!( - "{}\r\nContent-Length: {}\r\n\r\n{}", - status_line, - contents.len(), - contents - ); - - stream.write_all(response.as_bytes()).unwrap(); - stream.flush().unwrap(); -} diff --git a/wrk/rust/clean/.gitignore b/wrk/rust/clean/.gitignore deleted file mode 100644 index 6985cf1..0000000 --- a/wrk/rust/clean/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb diff --git a/wrk/rust/clean/Cargo.toml b/wrk/rust/clean/Cargo.toml deleted file mode 100644 index cf97cf6..0000000 --- a/wrk/rust/clean/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "hello" -version = "0.1.0" -edition = "2021" - - -[dependencies] - diff --git a/wrk/rust/clean/hello.html b/wrk/rust/clean/hello.html deleted file mode 100644 index a2b3773..0000000 --- a/wrk/rust/clean/hello.html +++ /dev/null @@ -1 +0,0 @@ -Hello from RUST! diff --git a/wrk/rust/clean/src/lib.rs b/wrk/rust/clean/src/lib.rs deleted file mode 100644 index 6e5d88d..0000000 --- a/wrk/rust/clean/src/lib.rs +++ /dev/null @@ -1,101 +0,0 @@ -//Crossbeam should, but does not make this faster. -//use crossbeam::channel::bounded; -use std::{net::TcpStream, sync::mpsc, thread}; -type Job = (fn(TcpStream), TcpStream); - -type Sender = mpsc::Sender; -//type Sender = crossbeam::channel::Sender; - -type Receiver = mpsc::Receiver; -//type Receiver = crossbeam::channel::Receiver; - -pub struct ThreadPool { - workers: Vec, - senders: Vec, - - next_sender: usize, -} - -impl ThreadPool { - /// Create a new ThreadPool. - /// - /// The size is the number of threads in the pool. - /// - /// # Panics - /// - /// The `new` function will panic if the size is zero. - pub fn new(size: usize) -> ThreadPool { - assert!(size > 0); - - let mut workers = Vec::with_capacity(size); - let mut senders = Vec::with_capacity(size); - - for id in 0..size { - //let (sender, receiver) = bounded(2); - let (sender, receiver) = mpsc::channel(); - senders.push(sender); - workers.push(Worker::new(id, receiver)); - } - - ThreadPool { - workers, - senders, - next_sender: 0, - } - } - /// round robin over available workers to ensure we never have to buffer requests - pub fn execute(&mut self, handler: fn(TcpStream), stream: TcpStream) { - let job = (handler, stream); - self.senders[self.next_sender].send(job).unwrap(); - //self.senders[self.next_sender].try_send(job).unwrap(); - self.next_sender += 1; - if self.next_sender == self.senders.len() { - self.next_sender = 0; - } - } -} - -impl Drop for ThreadPool { - fn drop(&mut self) { - self.senders.clear(); - - for worker in &mut self.workers { - println!("Shutting down worker {}", worker.id); - if let Some(thread) = worker.thread.take() { - thread.join().unwrap(); - } - } - } -} - -struct Worker { - id: usize, - thread: Option>, -} - -impl Worker { - fn new(id: usize, receiver: Receiver) -> Worker { - let thread = thread::spawn(move || Self::work(receiver)); - - Worker { - id, - thread: Some(thread), - } - } - - fn work(receiver: Receiver) { - loop { - let message = receiver.recv(); - match message { - Ok((handler, stream)) => { - // println!("Worker got a job; executing."); - handler(stream); - } - Err(_) => { - // println!("Worker disconnected; shutting down."); - break; - } - } - } - } -} diff --git a/wrk/rust/clean/src/main.rs b/wrk/rust/clean/src/main.rs deleted file mode 100644 index cda767c..0000000 --- a/wrk/rust/clean/src/main.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::io::prelude::*; -use std::net::TcpListener; -use std::net::TcpStream; - -fn main() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); - - for stream in listener.incoming() { - let stream = stream.unwrap(); - //handle_connection(stream); - std::thread::spawn(||{handle_connection(stream)}); - } - - println!("Shutting down."); -} - -fn handle_connection(mut stream: TcpStream) { - stream.set_nodelay(true).expect("set_nodelay call failed"); - loop{ - let mut buffer = [0; 1024]; - match stream.read(&mut buffer){ - Err(_)=>return, - Ok(0)=>return, - Ok(_v)=>{}, - } - - let response_bytes = b"HTTP/1.1 200 OK\r\nContent-Length: 16\r\nConnection: keep-alive\r\n\r\nHELLO from RUST!"; - - stream.write_all(response_bytes).unwrap(); - } - -} diff --git a/wrk/samples/README_req_per_sec.png b/wrk/samples/README_req_per_sec.png deleted file mode 100644 index cfd7ce0762350037363765d0c9d41304d92700fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43673 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 pIr0hzLb0 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

Xb0vsO!olTOn!Qgh{ejv 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 diff --git a/wrk/samples/xfer_per_sec_graph.png b/wrk/samples/xfer_per_sec_graph.png deleted file mode 100644 index 73fe3cd718f634cf93a2466236393981e8c9d9dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47465 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 diff --git a/wrk/sanic/sanic-app.py b/wrk/sanic/sanic-app.py deleted file mode 100644 index a382282..0000000 --- a/wrk/sanic/sanic-app.py +++ /dev/null @@ -1,12 +0,0 @@ -from sanic import Sanic -from sanic.response import html - -app = Sanic("sanic-app") - - -@app.route('/') -async def test(request): - return html("Hello from sanic!", 200) - -if __name__ == '__main__': - app.run() diff --git a/wrk/zig/main.zig b/wrk/zig/main.zig deleted file mode 100644 index ef68612..0000000 --- a/wrk/zig/main.zig +++ /dev/null @@ -1,24 +0,0 @@ -const std = @import("std"); -const zap = @import("zap"); - -fn on_request_minimal(r: zap.Request) !void { - try r.sendBody("Hello from ZAP!!!"); -} - -pub fn main() !void { - var listener = zap.HttpListener.init(.{ - .port = 3000, - .on_request = on_request_minimal, - .log = false, - .max_clients = 100000, - }); - try listener.listen(); - - std.debug.print("Listening on 0.0.0.0:3000\n", .{}); - - // start worker threads - zap.start(.{ - .threads = 4, - .workers = 4, // empirical tests: yield best perf on my machine - }); -} diff --git a/wrk/zigstd/main.zig b/wrk/zigstd/main.zig deleted file mode 100644 index 45f9273..0000000 --- a/wrk/zigstd/main.zig +++ /dev/null @@ -1,33 +0,0 @@ -const std = @import("std"); - -pub fn main() !void { - // var gpa = std.heap.GeneralPurposeAllocator(.{ - // .thread_safe = true, - // }){}; - // const allocator = gpa.allocator(); - - const address = try std.net.Address.parseIp("127.0.0.1", 3000); - var http_server = try address.listen(.{ - .reuse_address = true, - }); - - var read_buffer: [2048]u8 = undefined; - - // const max_header_size = 8192; - - while (true) { - const connection = try http_server.accept(); - defer connection.stream.close(); - var server = std.http.Server.init(connection, &read_buffer); - - var request = try server.receiveHead(); - const server_body: []const u8 = "HI FROM ZIG STD!\n"; - - try request.respond(server_body, .{ - .extra_headers = &.{ - .{ .name = "content_type", .value = "text/plain" }, - .{ .name = "connection", .value = "close" }, - }, - }); - } -} From 43c411dcd76fc7b36345723d9fd84041de90225f Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 30 Mar 2025 15:03:35 +0200 Subject: [PATCH 49/57] fix router.zig type checking, remove wrk examples from build.zig --- build.zig | 2 -- src/router.zig | 18 ++++++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/build.zig b/build.zig index b13002a..e020e26 100644 --- a/build.zig +++ b/build.zig @@ -60,8 +60,6 @@ pub fn build(b: *std.Build) !void { .{ .name = "serve", .src = "examples/serve/serve.zig" }, .{ .name = "hello_json", .src = "examples/hello_json/hello_json.zig" }, .{ .name = "endpoint", .src = "examples/endpoint/main.zig" }, - .{ .name = "wrk", .src = "wrk/zig/main.zig" }, - .{ .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" }, diff --git a/src/router.zig b/src/router.zig index e6bfe23..ff35831 100644 --- a/src/router.zig +++ b/src/router.zig @@ -82,10 +82,10 @@ pub fn handle_func(self: *Router, path: []const u8, instance: *anyopaque, handle // Need to check: // 1) handler is function pointer const f = blk: { - if (hand_info == .Pointer) { - const inner = @typeInfo(hand_info.Pointer.child); - if (inner == .Fn) { - break :blk inner.Fn; + if (hand_info == .pointer) { + const inner = @typeInfo(hand_info.pointer.child); + if (inner == .@"fn") { + break :blk inner.@"fn"; } } @compileError("Expected handler to be a function pointer. Found " ++ @@ -104,8 +104,14 @@ pub fn handle_func(self: *Router, path: []const u8, instance: *anyopaque, handle // 3) handler returns void const ret_info = @typeInfo(f.return_type.?); - if (ret_info != .Void) { - @compileError("Expected handler's return type to be void. Found " ++ + if (ret_info != .error_union) { + @compileError("Expected handler's return type to be !void. Found " ++ + @typeName(f.return_type.?)); + } + + const payload = @typeInfo(ret_info.error_union.payload); + if (payload != .void) { + @compileError("Expected handler's return type to be !void. Found " ++ @typeName(f.return_type.?)); } } From cce02dd9f3696ea299410a818d145c6cabcaf4c9 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 30 Mar 2025 15:04:21 +0200 Subject: [PATCH 50/57] fix README, gh workflow desc --- .github/workflows/build-current-zig.yml | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-current-zig.yml b/.github/workflows/build-current-zig.yml index 0f9bffe..0c9684c 100644 --- a/.github/workflows/build-current-zig.yml +++ b/.github/workflows/build-current-zig.yml @@ -1,4 +1,4 @@ -name: Works with Zig 0.13.0 +name: Works with Zig 0.14.0 on: push: branches: diff --git a/README.md b/README.md index 2706bc6..4f10bef 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ proved to be: ## Here's what works -**NOTE:** I recommend checking out **the new App-based** or the Endpoint-based +I recommend checking out **the new App-based** or the Endpoint-based examples, as they reflect how I intended Zap to be used. Most of the examples are super stripped down to only include what's necessary to @@ -65,7 +65,7 @@ port and docs dir: `zig build docserver && zig-out/bin/docserver --port=8989 - **[app_basic](examples/app/basic.zig)**: Shows how to use zap.App with a simple Endpoint. -- **[app_basic](examples/app/auth.zig)**: Shows how to use zap.App with an +- **[app_auth](examples/app/auth.zig)**: Shows how to use zap.App with an Endpoint using an Authenticator. See the other examples for specific uses of Zap. From cc6d55fbf760e4de967a91ebf02f0e5e39769c35 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 30 Mar 2025 19:37:40 +0200 Subject: [PATCH 51/57] Improved & generalized checkEndpoint functions E.g. return type's error set does not need to be `anyerror` anymore. --- examples/endpoint/stopendpoint.zig | 12 +++--- examples/endpoint/userweb.zig | 13 +++--- examples/endpoint_auth/endpoint_auth.zig | 6 +++ src/App.zig | 50 +++++++++++++++++++++-- src/endpoint.zig | 51 +++++++++++++++++++++++- 5 files changed, 115 insertions(+), 17 deletions(-) diff --git a/examples/endpoint/stopendpoint.zig b/examples/endpoint/stopendpoint.zig index f96ec37..0e5d067 100644 --- a/examples/endpoint/stopendpoint.zig +++ b/examples/endpoint/stopendpoint.zig @@ -14,14 +14,14 @@ pub fn init(path: []const u8) StopEndpoint { }; } -pub fn get(e: *StopEndpoint, r: zap.Request) anyerror!void { +pub fn get(e: *StopEndpoint, r: zap.Request) !void { _ = e; _ = r; zap.stop(); } -pub fn post(_: *StopEndpoint, _: zap.Request) anyerror!void {} -pub fn put(_: *StopEndpoint, _: zap.Request) anyerror!void {} -pub fn delete(_: *StopEndpoint, _: zap.Request) anyerror!void {} -pub fn patch(_: *StopEndpoint, _: zap.Request) anyerror!void {} -pub fn options(_: *StopEndpoint, _: zap.Request) anyerror!void {} +pub fn post(_: *StopEndpoint, _: zap.Request) !void {} +pub fn put(_: *StopEndpoint, _: zap.Request) !void {} +pub fn delete(_: *StopEndpoint, _: zap.Request) !void {} +pub fn patch(_: *StopEndpoint, _: zap.Request) !void {} +pub fn options(_: *StopEndpoint, _: zap.Request) !void {} diff --git a/examples/endpoint/userweb.zig b/examples/endpoint/userweb.zig index d45370c..2d13bf9 100644 --- a/examples/endpoint/userweb.zig +++ b/examples/endpoint/userweb.zig @@ -43,8 +43,9 @@ fn userIdFromPath(self: *UserWeb, path: []const u8) ?usize { return null; } -pub fn put(_: *UserWeb, _: zap.Request) anyerror!void {} -pub fn get(self: *UserWeb, r: zap.Request) anyerror!void { +pub fn put(_: *UserWeb, _: zap.Request) !void {} + +pub fn get(self: *UserWeb, r: zap.Request) !void { if (r.path) |path| { // /users if (path.len == self.path.len) { @@ -69,7 +70,7 @@ fn listUsers(self: *UserWeb, r: zap.Request) !void { } } -pub fn post(self: *UserWeb, r: zap.Request) anyerror!void { +pub fn post(self: *UserWeb, r: zap.Request) !void { if (r.body) |body| { const maybe_user: ?std.json.Parsed(User) = std.json.parseFromSlice(User, self.alloc, body, .{}) catch null; if (maybe_user) |u| { @@ -86,7 +87,7 @@ pub fn post(self: *UserWeb, r: zap.Request) anyerror!void { } } -pub fn patch(self: *UserWeb, r: zap.Request) anyerror!void { +pub fn patch(self: *UserWeb, r: zap.Request) !void { if (r.path) |path| { if (self.userIdFromPath(path)) |id| { if (self._users.get(id)) |_| { @@ -109,7 +110,7 @@ pub fn patch(self: *UserWeb, r: zap.Request) anyerror!void { } } -pub fn delete(self: *UserWeb, r: zap.Request) anyerror!void { +pub fn delete(self: *UserWeb, r: zap.Request) !void { if (r.path) |path| { if (self.userIdFromPath(path)) |id| { var jsonbuf: [128]u8 = undefined; @@ -124,7 +125,7 @@ pub fn delete(self: *UserWeb, r: zap.Request) anyerror!void { } } -pub fn options(_: *UserWeb, r: zap.Request) anyerror!void { +pub fn options(_: *UserWeb, r: zap.Request) !void { try r.setHeader("Access-Control-Allow-Origin", "*"); try r.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); r.setStatus(zap.http.StatusCode.no_content); diff --git a/examples/endpoint_auth/endpoint_auth.zig b/examples/endpoint_auth/endpoint_auth.zig index 57b2a37..0cf4879 100644 --- a/examples/endpoint_auth/endpoint_auth.zig +++ b/examples/endpoint_auth/endpoint_auth.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build endpoint_auth`. +//! Run me with `zig build run-endpoint_auth`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/src/App.zig b/src/App.zig index 4699b77..d53cba3 100644 --- a/src/App.zig +++ b/src/App.zig @@ -153,12 +153,56 @@ pub fn Create(comptime Context: type) type { "patch", "options", }; + const params_to_check = [_]type{ + *T, + Allocator, + *Context, + Request, + }; inline for (methods_to_check) |method| { if (@hasDecl(T, method)) { const Method = @TypeOf(@field(T, method)); - const Expected = fn (_: *T, _: Allocator, _: *Context, _: Request) anyerror!void; - if (Method != Expected) { - @compileError(method ++ " method of " ++ @typeName(T) ++ " has wrong type:\n" ++ @typeName(Method) ++ "\nexpected:\n" ++ @typeName(Expected)); + const method_info = @typeInfo(Method); + if (method_info != .@"fn") { + @compileError("Expected `" ++ @typeName(T) ++ "." ++ method ++ "` to be a request handler method, got: " ++ @typeName(Method)); + } + + // now check parameters + const params = method_info.@"fn".params; + if (params.len != params_to_check.len) { + @compileError(std.fmt.comptimePrint( + "Expected method `{s}.{s}` to have {d} parameters, got {d}", + .{ + @typeName(T), + method, + params_to_check.len, + params.len, + }, + )); + } + + inline for (params_to_check, 0..) |param_type_expected, i| { + if (params[i].type.? != param_type_expected) { + @compileError(std.fmt.comptimePrint( + "Expected parameter {d} of method {s}.{s} to be {s}, got {s}", + .{ + i + 1, + @typeName(T), + method, + @typeName(param_type_expected), + @typeName(params[i].type.?), + }, + )); + } + } + + const ret_type = method_info.@"fn".return_type.?; + const ret_info = @typeInfo(ret_type); + if (ret_info != .error_union) { + @compileError("Expected return type of method `" ++ @typeName(T) ++ "." ++ method ++ "` to be !void, got: " ++ @typeName(ret_type)); + } + if (ret_info.error_union.payload != void) { + @compileError("Expected return type of method `" ++ @typeName(T) ++ "." ++ method ++ "` to be !void, got: !" ++ @typeName(ret_info.error_union.payload)); } } else { @compileError(@typeName(T) ++ " has no method named `" ++ method ++ "`"); diff --git a/src/endpoint.zig b/src/endpoint.zig index c32a489..ae67fbc 100644 --- a/src/endpoint.zig +++ b/src/endpoint.zig @@ -95,10 +95,57 @@ pub fn checkEndpointType(T: type) void { "patch", "options", }; + + const params_to_check = [_]type{ + *T, + Request, + }; + inline for (methods_to_check) |method| { if (@hasDecl(T, method)) { - if (@TypeOf(@field(T, method)) != fn (_: *T, _: Request) anyerror!void) { - @compileError(method ++ " method of " ++ @typeName(T) ++ " has wrong type:\n" ++ @typeName(@TypeOf(T.get)) ++ "\nexpected:\n" ++ @typeName(fn (_: *T, _: Request) anyerror!void)); + const Method = @TypeOf(@field(T, method)); + const method_info = @typeInfo(Method); + if (method_info != .@"fn") { + @compileError("Expected `" ++ @typeName(T) ++ "." ++ method ++ "` to be a request handler method, got: " ++ @typeName(Method)); + } + + // now check parameters + const params = method_info.@"fn".params; + if (params.len != params_to_check.len) { + @compileError(std.fmt.comptimePrint( + "Expected method `{s}.{s}` to have {d} parameters, got {d}", + .{ + @typeName(T), + method, + params_to_check.len, + params.len, + }, + )); + } + + inline for (params_to_check, 0..) |param_type_expected, i| { + if (params[i].type.? != param_type_expected) { + @compileError(std.fmt.comptimePrint( + "Expected parameter {d} of method {s}.{s} to be {s}, got {s}", + .{ + i + 1, + @typeName(T), + method, + @typeName(param_type_expected), + @typeName(params[i].type.?), + }, + )); + } + } + + // check return type + const ret_type = method_info.@"fn".return_type.?; + const ret_info = @typeInfo(ret_type); + if (ret_info != .error_union) { + @compileError("Expected return type of method `" ++ @typeName(T) ++ "." ++ method ++ "` to be !void, got: " ++ @typeName(ret_type)); + } + if (ret_info.error_union.payload != void) { + @compileError("Expected return type of method `" ++ @typeName(T) ++ "." ++ method ++ "` to be !void, got: !" ++ @typeName(ret_info.error_union.payload)); } } else { @compileError(@typeName(T) ++ " has no method named `" ++ method ++ "`"); From 4c59132a08ce3500bbee3b1a3f294b6e8a2d0cdc Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 30 Mar 2025 19:40:08 +0200 Subject: [PATCH 52/57] added header to example main source files showing how to build & run --- examples/accept/accept.zig | 6 +++++ examples/app/auth.zig | 6 +++++ examples/app/basic.zig | 18 ++++++++++----- examples/bindataformpost/bindataformpost.zig | 6 +++++ examples/cookies/cookies.zig | 6 +++++ examples/endpoint/error.zig | 22 +++++++++++++++++++ examples/endpoint/main.zig | 9 ++++++++ examples/hello/hello.zig | 6 +++++ examples/hello2/hello2.zig | 6 +++++ examples/hello_json/hello_json.zig | 6 +++++ examples/http_params/http_params.zig | 6 +++++ examples/https/https.zig | 6 +++++ examples/middleware/middleware.zig | 6 +++++ .../middleware_with_endpoint.zig | 6 +++++ examples/mustache/mustache.zig | 6 +++++ examples/routes/routes.zig | 6 +++++ examples/senderror/senderror.zig | 6 +++++ examples/sendfile/sendfile.zig | 6 +++++ examples/serve/serve.zig | 6 +++++ examples/simple_router/simple_router.zig | 6 +++++ .../userpass_session_auth.zig | 6 +++++ examples/websockets/websockets.zig | 6 +++++ 22 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 examples/endpoint/error.zig diff --git a/examples/accept/accept.zig b/examples/accept/accept.zig index 8e29350..2894337 100644 --- a/examples/accept/accept.zig +++ b/examples/accept/accept.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build accept`. +//! Run me with `zig build run-accept`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/examples/app/auth.zig b/examples/app/auth.zig index ef73219..ebb7103 100644 --- a/examples/app/auth.zig +++ b/examples/app/auth.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build app_auth`. +//! Run me with `zig build run-app_auth`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/examples/app/basic.zig b/examples/app/basic.zig index 752b0d0..318473d 100644 --- a/examples/app/basic.zig +++ b/examples/app/basic.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build app_basic`. +//! Run me with `zig build run-app_basic`. +//! const std = @import("std"); const Allocator = std.mem.Allocator; @@ -32,7 +38,7 @@ const SimpleEndpoint = struct { } // handle GET requests - pub fn get(e: *SimpleEndpoint, arena: Allocator, context: *MyContext, r: zap.Request) anyerror!void { + pub fn get(e: *SimpleEndpoint, arena: Allocator, context: *MyContext, r: zap.Request) !void { const thread_id = std.Thread.getCurrentId(); r.setStatus(.ok); @@ -55,11 +61,11 @@ const SimpleEndpoint = struct { } // empty stubs for all other request methods - pub fn post(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) anyerror!void {} - pub fn put(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) anyerror!void {} - pub fn delete(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) anyerror!void {} - pub fn patch(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) anyerror!void {} - pub fn options(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) anyerror!void {} + pub fn post(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} + pub fn put(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} + pub fn delete(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} + pub fn patch(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} + pub fn options(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} }; pub fn main() !void { diff --git a/examples/bindataformpost/bindataformpost.zig b/examples/bindataformpost/bindataformpost.zig index c1d5fe8..214def0 100644 --- a/examples/bindataformpost/bindataformpost.zig +++ b/examples/bindataformpost/bindataformpost.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build bindataformpost`. +//! Run me with `zig build run-bindataformpost`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/examples/cookies/cookies.zig b/examples/cookies/cookies.zig index 22d8e18..ca76a66 100644 --- a/examples/cookies/cookies.zig +++ b/examples/cookies/cookies.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build cookies`. +//! Run me with `zig build run-cookies`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/examples/endpoint/error.zig b/examples/endpoint/error.zig new file mode 100644 index 0000000..255c936 --- /dev/null +++ b/examples/endpoint/error.zig @@ -0,0 +1,22 @@ +const std = @import("std"); +const zap = @import("zap"); + +/// A simple endpoint listening on the /error route that causes an error on GET requests, which gets logged to the response (=browser) by default +pub const ErrorEndpoint = @This(); + +path: []const u8 = "/error", +error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response, + +pub fn get(e: *ErrorEndpoint, r: zap.Request) !void { + _ = e; + _ = r; + return error.@"Oh-no!"; +} + +pub fn post(_: *ErrorEndpoint, _: zap.Request) !void {} +// pub fn post(_: *ErrorEndpoint, _: zap.Request) !void {} + +pub fn put(_: *ErrorEndpoint, _: zap.Request) !void {} +pub fn delete(_: *ErrorEndpoint, _: zap.Request) !void {} +pub fn patch(_: *ErrorEndpoint, _: zap.Request) !void {} +pub fn options(_: *ErrorEndpoint, _: zap.Request) !void {} diff --git a/examples/endpoint/main.zig b/examples/endpoint/main.zig index a38803a..1f30a61 100644 --- a/examples/endpoint/main.zig +++ b/examples/endpoint/main.zig @@ -1,7 +1,14 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build endpoint`. +//! Run me with `zig build run-endpoint`. +//! const std = @import("std"); const zap = @import("zap"); const UserWeb = @import("userweb.zig"); const StopEndpoint = @import("stopendpoint.zig"); +const ErrorEndpoint = @import("error.zig"); // this is just to demo that we can catch arbitrary slugs as fallback fn on_request(r: zap.Request) !void { @@ -39,10 +46,12 @@ pub fn main() !void { defer userWeb.deinit(); var stopEp = StopEndpoint.init("/stop"); + var errorEp: ErrorEndpoint = .{}; // register endpoints with the listener try listener.register(&userWeb); try listener.register(&stopEp); + try listener.register(&errorEp); // fake some users var uid: usize = undefined; diff --git a/examples/hello/hello.zig b/examples/hello/hello.zig index c7db536..fde0d5c 100644 --- a/examples/hello/hello.zig +++ b/examples/hello/hello.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build hello`. +//! Run me with `zig build run-hello`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/examples/hello2/hello2.zig b/examples/hello2/hello2.zig index 5302c4c..fb29a67 100644 --- a/examples/hello2/hello2.zig +++ b/examples/hello2/hello2.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build hello2`. +//! Run me with `zig build run-hello2`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/examples/hello_json/hello_json.zig b/examples/hello_json/hello_json.zig index af693ef..412982b 100644 --- a/examples/hello_json/hello_json.zig +++ b/examples/hello_json/hello_json.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build hello_json`. +//! Run me with `zig build run-hello_json`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/examples/http_params/http_params.zig b/examples/http_params/http_params.zig index 4034eaf..d33a247 100644 --- a/examples/http_params/http_params.zig +++ b/examples/http_params/http_params.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build http_params`. +//! Run me with `zig build run-http_params`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/examples/https/https.zig b/examples/https/https.zig index bf9331b..2379f3c 100644 --- a/examples/https/https.zig +++ b/examples/https/https.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build -Dopenssl=true https`. +//! Run me with `zig build -Dopenssl=true run-https`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/examples/middleware/middleware.zig b/examples/middleware/middleware.zig index 844272e..ca91538 100644 --- a/examples/middleware/middleware.zig +++ b/examples/middleware/middleware.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build middleware`. +//! Run me with `zig build run-middleware`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/examples/middleware_with_endpoint/middleware_with_endpoint.zig b/examples/middleware_with_endpoint/middleware_with_endpoint.zig index 108cf28..e2cbf31 100644 --- a/examples/middleware_with_endpoint/middleware_with_endpoint.zig +++ b/examples/middleware_with_endpoint/middleware_with_endpoint.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build middleware_with_endpoint`. +//! Run me with `zig build run-middleware_with_endpoint`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/examples/mustache/mustache.zig b/examples/mustache/mustache.zig index 37ea7db..a123497 100644 --- a/examples/mustache/mustache.zig +++ b/examples/mustache/mustache.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build mustache`. +//! Run me with `zig build run-mustache`. +//! const std = @import("std"); const zap = @import("zap"); const Mustache = @import("zap").Mustache; diff --git a/examples/routes/routes.zig b/examples/routes/routes.zig index d605f3e..f44140a 100644 --- a/examples/routes/routes.zig +++ b/examples/routes/routes.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build routes`. +//! Run me with `zig build run-routes`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/examples/senderror/senderror.zig b/examples/senderror/senderror.zig index 3fb0c81..db06fba 100644 --- a/examples/senderror/senderror.zig +++ b/examples/senderror/senderror.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build senderror`. +//! Run me with `zig build run-senderror`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/examples/sendfile/sendfile.zig b/examples/sendfile/sendfile.zig index a4ac59e..09b1f74 100644 --- a/examples/sendfile/sendfile.zig +++ b/examples/sendfile/sendfile.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build sendfile`. +//! Run me with `zig build run-sendfile`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/examples/serve/serve.zig b/examples/serve/serve.zig index 789257e..14d8b08 100644 --- a/examples/serve/serve.zig +++ b/examples/serve/serve.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build serve`. +//! Run me with `zig build run-serve`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/examples/simple_router/simple_router.zig b/examples/simple_router/simple_router.zig index 72658ba..e976bb8 100644 --- a/examples/simple_router/simple_router.zig +++ b/examples/simple_router/simple_router.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build simple_router`. +//! Run me with `zig build run-simple_router`. +//! const std = @import("std"); const zap = @import("zap"); const Allocator = std.mem.Allocator; diff --git a/examples/userpass_session_auth/userpass_session_auth.zig b/examples/userpass_session_auth/userpass_session_auth.zig index fcf7744..e13c54d 100644 --- a/examples/userpass_session_auth/userpass_session_auth.zig +++ b/examples/userpass_session_auth/userpass_session_auth.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build userpass_session`. +//! Run me with `zig build run-userpass_session`. +//! const std = @import("std"); const zap = @import("zap"); diff --git a/examples/websockets/websockets.zig b/examples/websockets/websockets.zig index 7e44452..97ec250 100644 --- a/examples/websockets/websockets.zig +++ b/examples/websockets/websockets.zig @@ -1,3 +1,9 @@ +//! +//! Part of the Zap examples. +//! +//! Build me with `zig build websockets`. +//! Run me with `zig build run-websockets`. +//! const std = @import("std"); const zap = @import("zap"); const WebSockets = zap.WebSockets; From 3a2246ba5088d5bc7a46570a9e25eb25ef4ae953 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 30 Mar 2025 19:58:22 +0200 Subject: [PATCH 53/57] cleanup unused params where applicable (ie non-instructive) --- examples/endpoint/error.zig | 4 +--- examples/endpoint/stopendpoint.zig | 4 +--- examples/websockets/websockets.zig | 3 ++- src/endpoint.zig | 4 +--- src/router.zig | 3 +-- src/tests/test_auth.zig | 8 ++++---- 6 files changed, 10 insertions(+), 16 deletions(-) diff --git a/examples/endpoint/error.zig b/examples/endpoint/error.zig index 255c936..3e5ee2f 100644 --- a/examples/endpoint/error.zig +++ b/examples/endpoint/error.zig @@ -7,9 +7,7 @@ pub const ErrorEndpoint = @This(); path: []const u8 = "/error", error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response, -pub fn get(e: *ErrorEndpoint, r: zap.Request) !void { - _ = e; - _ = r; +pub fn get(_: *ErrorEndpoint, _: zap.Request) !void { return error.@"Oh-no!"; } diff --git a/examples/endpoint/stopendpoint.zig b/examples/endpoint/stopendpoint.zig index 0e5d067..2b12bd6 100644 --- a/examples/endpoint/stopendpoint.zig +++ b/examples/endpoint/stopendpoint.zig @@ -14,9 +14,7 @@ pub fn init(path: []const u8) StopEndpoint { }; } -pub fn get(e: *StopEndpoint, r: zap.Request) !void { - _ = e; - _ = r; +pub fn get(_: *StopEndpoint, _: zap.Request) !void { zap.stop(); } diff --git a/examples/websockets/websockets.zig b/examples/websockets/websockets.zig index 97ec250..e2d9f31 100644 --- a/examples/websockets/websockets.zig +++ b/examples/websockets/websockets.zig @@ -125,8 +125,9 @@ fn handle_websocket_message( message: []const u8, is_text: bool, ) void { - _ = is_text; _ = handle; + _ = is_text; + if (context) |ctx| { // send message const buflen = 128; // arbitrary len diff --git a/src/endpoint.zig b/src/endpoint.zig index ae67fbc..6a06093 100644 --- a/src/endpoint.zig +++ b/src/endpoint.zig @@ -43,9 +43,7 @@ //! pub fn patch(_: *StopEndpoint, _: zap.Request) void {} //! pub fn options(_: *StopEndpoint, _: zap.Request) void {} //! -//! pub fn get(self: *StopEndpoint, r: zap.Request) void { -//! _ = self; -//! _ = r; +//! pub fn get(_: *StopEndpoint, _: zap.Request) void { //! zap.stop(); //! } //! }; diff --git a/src/router.zig b/src/router.zig index ff35831..61223e4 100644 --- a/src/router.zig +++ b/src/router.zig @@ -63,8 +63,7 @@ pub fn handle_func_unbound(self: *Router, path: []const u8, h: zap.HttpRequestFn /// /// ```zig /// const HandlerType = struct { -/// pub fn getA(self: *HandlerType, r: zap.Request) void { -/// _ = self; +/// pub fn getA(_: *HandlerType, r: zap.Request) void { /// r.sendBody("hello\n\n") catch return; /// } /// } diff --git a/src/tests/test_auth.zig b/src/tests/test_auth.zig index 1dba9a1..a660b04 100644 --- a/src/tests/test_auth.zig +++ b/src/tests/test_auth.zig @@ -151,16 +151,14 @@ pub const Endpoint = struct { path: []const u8, error_strategy: zap.Endpoint.ErrorStrategy = .raise, - pub fn get(e: *Endpoint, r: zap.Request) !void { - _ = e; + pub fn get(_: *Endpoint, r: zap.Request) !void { r.sendBody(HTTP_RESPONSE) catch return; received_response = HTTP_RESPONSE; std.time.sleep(1 * std.time.ns_per_s); zap.stop(); } - pub fn unauthorized(e: *Endpoint, r: zap.Request) !void { - _ = e; + pub fn unauthorized(_: *Endpoint, r: zap.Request) !void { r.setStatus(.unauthorized); r.sendBody("UNAUTHORIZED ACCESS") catch return; received_response = "UNAUTHORIZED"; @@ -590,6 +588,8 @@ test "BasicAuth UserPass authenticateRequest test-unauthorized" { var encoder = std.base64.url_safe.Encoder; var buffer: [256]u8 = undefined; const encoded = encoder.encode(&buffer, token); + + // not interested in the encoded auth string; we want unauthorized _ = encoded; // create authenticator From 44ac2f2c3a1b2d40a9f4575f7ef9cb979b9c0944 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 30 Mar 2025 20:03:15 +0200 Subject: [PATCH 54/57] fix typo in README. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4f10bef..88c2978 100644 --- a/README.md +++ b/README.md @@ -177,12 +177,12 @@ claim any performance fame for itself. In this initial implementation of Zap, I didn't care about optimizations at all. But, how fast is it? Being blazingly fast is relative. When compared with a -simple GO HTTP server, a simple Zig Zap HTTP server performed really good on my +simple GO HTTP server, a simple Zig Zap HTTP server performed really well on my machine (x86_64-linux): - Zig Zap was nearly 30% faster than GO - Zig Zap had over 50% more throughput than GO -- **YMMV !!!** +- **YMMV!!!** So, being somewhere in the ballpark of basic GO performance, zig zap seems to be ... of reasonable performance 😎. @@ -190,7 +190,7 @@ So, being somewhere in the ballpark of basic GO performance, zig zap seems to be I can rest my case that developing ZAP was a good idea because it's faster than both alternatives: a) staying with Python, and b) creating a GO + Zig hybrid. -### On (now missing) Micro-Benchmakrs +### On (now missing) Micro-Benchmarks I used to have some micro-benchmarks in this repo, showing that Zap beat all the other things I tried, and eventually got tired of the meaningless discussions From a01c6146ec72ef4a10405d752488aa47fc936bb4 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 30 Mar 2025 20:32:33 +0200 Subject: [PATCH 55/57] Minor README & comment cosmetics --- README.md | 2 +- examples/endpoint/error.zig | 6 +++--- src/App.zig | 7 ------- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 88c2978..6f0e328 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,7 @@ really promising. - [jetzig](https://github.com/jetzig-framework/jetzig) : Comfortably develop modern web applications quickly, using http.zig under the hood - [zzz](https://github.com/tardy-org/zzz) : Super promising, super-fast, -especially for IO-heavy tasks, io_uring support - need I say more? + especially for IO-heavy tasks, io_uring support - need I say more? ## 💪 Robust diff --git a/examples/endpoint/error.zig b/examples/endpoint/error.zig index 3e5ee2f..178be67 100644 --- a/examples/endpoint/error.zig +++ b/examples/endpoint/error.zig @@ -1,7 +1,8 @@ const std = @import("std"); const zap = @import("zap"); -/// A simple endpoint listening on the /error route that causes an error on GET requests, which gets logged to the response (=browser) by default +/// A simple endpoint listening on the /error route that causes an error on GET +/// requests, which gets logged to the response (=browser) by default pub const ErrorEndpoint = @This(); path: []const u8 = "/error", @@ -11,9 +12,8 @@ pub fn get(_: *ErrorEndpoint, _: zap.Request) !void { return error.@"Oh-no!"; } +// unused: pub fn post(_: *ErrorEndpoint, _: zap.Request) !void {} -// pub fn post(_: *ErrorEndpoint, _: zap.Request) !void {} - pub fn put(_: *ErrorEndpoint, _: zap.Request) !void {} pub fn delete(_: *ErrorEndpoint, _: zap.Request) !void {} pub fn patch(_: *ErrorEndpoint, _: zap.Request) !void {} diff --git a/src/App.zig b/src/App.zig index d53cba3..e8a381d 100644 --- a/src/App.zig +++ b/src/App.zig @@ -1,10 +1,3 @@ -//! WIP: zap.App. -//! -//! - Per Request Arena(s) thread-local? -//! - Custom "State" Context, type-safe -//! - route handlers -//! - automatic error catching & logging, optional report to HTML - const std = @import("std"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; From 337276fa528819dc40d9617742ddb9b96b2fc5a8 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 30 Mar 2025 21:16:51 +0200 Subject: [PATCH 56/57] update basic app example with stop endpoint --- README.md | 2 +- examples/app/basic.zig | 39 +++++++++++++++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6f0e328..d89cd4f 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ really promising. ### 📣 Shout-Outs -- [httpz](https://github.com/karlseguin/http.zig) : Pure Zig! Closer to Zap's +- [httpz](https://github.com/karlseguin/http.zig) : Pure Zig! Close to Zap's model. Performance = good! - [jetzig](https://github.com/jetzig-framework/jetzig) : Comfortably develop modern web applications quickly, using http.zig under the hood diff --git a/examples/app/basic.zig b/examples/app/basic.zig index 318473d..44c8650 100644 --- a/examples/app/basic.zig +++ b/examples/app/basic.zig @@ -68,6 +68,27 @@ const SimpleEndpoint = struct { pub fn options(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} }; +const StopEndpoint = struct { + path: []const u8, + error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response, + + pub fn get(_: *StopEndpoint, _: Allocator, context: *MyContext, _: zap.Request) !void { + std.debug.print( + \\Before I stop, let me dump the app context: + \\db_connection='{s}' + \\ + \\ + , .{context.*.db_connection}); + zap.stop(); + } + + pub fn post(_: *StopEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} + pub fn put(_: *StopEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} + pub fn delete(_: *StopEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} + pub fn patch(_: *StopEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} + pub fn options(_: *StopEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} +}; + pub fn main() !void { // setup allocations var gpa: std.heap.GeneralPurposeAllocator(.{ @@ -85,11 +106,13 @@ pub fn main() !void { var app = try App.init(allocator, &my_context, .{}); defer app.deinit(); - // create the endpoint - var my_endpoint = SimpleEndpoint.init("/", "some endpoint specific data"); - - // register the endpoint with the app + // create the endpoints + var my_endpoint = SimpleEndpoint.init("/test", "some endpoint specific data"); + var stop_endpoint: StopEndpoint = .{ .path = "/stop" }; + // + // register the endpoints with the app try app.register(&my_endpoint); + try app.register(&stop_endpoint); // listen on the network try app.listen(.{ @@ -98,6 +121,14 @@ pub fn main() !void { }); std.debug.print("Listening on 0.0.0.0:3000\n", .{}); + std.debug.print( + \\ Try me via: + \\ curl http://localhost:3000/test + \\ Stop me via: + \\ curl http://localhost:3000/stop + \\ + , .{}); + // start worker threads -- only 1 process!!! zap.start(.{ .threads = 2, From 24dfcbaeaa6517824c377374dcdb573da8d68dab Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 30 Mar 2025 21:20:58 +0200 Subject: [PATCH 57/57] update README's zig fetch because announceybot was overwhelmed with the superlage release notes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d89cd4f..05d6044 100644 --- a/README.md +++ b/README.md @@ -298,7 +298,7 @@ In your zig project folder (where `build.zig` is located), run: ``` -zig fetch --save "git+https://github.com/zigzap/zap#v0.9.1" +zig fetch --save "git+https://github.com/zigzap/zap#v0.10.0" ```