Browse Source

Added Zig Httpz framework (#9298)

* Added Zig httpz framework

* Zig httpz fortunes
Dragos Varovici 9 months ago
parent
commit
540de89f04

+ 2 - 0
frameworks/Zig/httpz/.gitignore

@@ -0,0 +1,2 @@
+zig-cache/**/*',
+zig-out: 'zig-out/**/*',

+ 25 - 0
frameworks/Zig/httpz/README.md

@@ -0,0 +1,25 @@
+
+# [Httpz](https://github.com/karlseguin/http.zig) - An HTTP/1.1 server for Zig
+
+## Description
+
+Native Zig framework and zig http replacement 
+
+## Test URLs
+
+### Test 1: JSON Encoding
+
+    http://localhost:3000/json
+
+### Test 2: Plaintext
+
+    http://localhost:3000/plaintext
+
+### Test 2: Single Row Query
+
+    http://localhost:3000/db
+
+### Test 4: Fortunes (Template rendering)
+
+    http://localhost:3000/fortunes
+

+ 26 - 0
frameworks/Zig/httpz/benchmark_config.json

@@ -0,0 +1,26 @@
+{
+  "framework": "httpz",
+  "tests": [{
+    "default": {
+      "json_url": "/json",
+      "plaintext_url": "/plaintext",
+      "db_url": "/db",
+      "fortune_url": "/fortunes",
+      "port": 3000,
+      "approach": "Realistic",
+      "classification": "Fullstack",
+      "database": "Postgres",
+      "framework": "httpz",
+      "language": "Zig",
+      "flavor": "None",
+      "orm": "raw",
+      "platform": "None",
+      "webserver": "None",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "Httpz (Zig)",
+      "notes": "",
+      "versus": ""
+    }
+  }]
+}

+ 78 - 0
frameworks/Zig/httpz/build.zig

@@ -0,0 +1,78 @@
+const std = @import("std");
+const ModuleMap = std.StringArrayHashMap(*std.Build.Module);
+var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+const allocator = gpa.allocator();
+
+// Although this function looks imperative, note that its job is to
+// declaratively construct a build graph that will be executed by an external
+// runner.
+pub fn build(b: *std.Build) !void {
+    // Standard target options allows the person running `zig build` to choose
+    // what target to build for. Here we do not override the defaults, which
+    // means any target is allowed, and the default is native. Other options
+    // for restricting supported target set are available.
+    const target = b.standardTargetOptions(.{});
+
+    // Standard optimization options allow the person running `zig build` to select
+    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do nots
+    // set a preferred release mode, allowing the user to decide how to optimize.
+    const optimize = b.standardOptimizeOption(.{});
+
+    const dep_opts = .{ .target = target, .optimize = optimize };
+
+    const exe = b.addExecutable(.{
+        .name = "httpz",
+        // In this case the main source file is merely a path, however, in more
+        // complicated build scripts, this could be a generated file.
+        .root_source_file = b.path("src/main.zig"),
+        .target = target,
+        .optimize = optimize,
+    });
+
+    var modules = ModuleMap.init(allocator);
+    defer modules.deinit();
+
+    const httpz_module = b.dependency("httpz", dep_opts).module("httpz");
+    const pg_module = b.dependency("pg", dep_opts).module("pg");
+    const datetimez_module = b.dependency("datetimez", dep_opts).module("zig-datetime");
+    const mustache_module = b.dependency("mustache", dep_opts).module("mustache");
+
+    try modules.put("httpz", httpz_module);
+    try modules.put("pg", pg_module);
+    try modules.put("datetimez", datetimez_module);
+    try modules.put("mustache", mustache_module);
+
+    //     // Expose this as a module that others can import
+    exe.root_module.addImport("httpz", httpz_module);
+    exe.root_module.addImport("pg", pg_module);
+    exe.root_module.addImport("datetimez", datetimez_module);
+    exe.root_module.addImport("mustache", mustache_module);
+
+    // This declares intent for the executable to be installed into the
+    // standard location when the user invokes the "install" step (the default
+    // step when running `zig build`).
+    b.installArtifact(exe);
+
+    // This *creates* a Run step in the build graph, to be executed when another
+    // step is evaluated that depends on it. The next line below will establish
+    // such a dependency.
+    const run_cmd = b.addRunArtifact(exe);
+
+    // By making the run step depend on the install step, it will be run from the
+    // installation directory rather than directly from within the cache directory.
+    // This is not necessary, however, if the application depends on other installed
+    // files, this ensures they will be present and in the expected location.
+    run_cmd.step.dependOn(b.getInstallStep());
+
+    // This allows the user to pass arguments to the application in the build
+    // command itself, like this: `zig build run -- arg1 arg2 etc`
+    if (b.args) |args| {
+        run_cmd.addArgs(args);
+    }
+
+    // This creates a build step. It will be visible in the `zig build --help` menu,
+    // and can be selected like this: `zig build run`
+    // This will evaluate the `run` step rather than the default, which is "install".
+    const run_step = b.step("run", "Run the app");
+    run_step.dependOn(&run_cmd.step);
+}

+ 19 - 0
frameworks/Zig/httpz/build.zig.zon

@@ -0,0 +1,19 @@
+.{ .name = "Zap testing", .version = "0.1.1", .paths = .{
+    "build.zig",
+    "build.zig.zon",
+    "src",
+}, .dependencies = .{
+    .pg = .{ .url = "https://github.com/karlseguin/pg.zig/archive/239a4468163a49d8c0d03285632eabe96003e9e2.tar.gz", .hash = "1220a1d7e51e2fa45e547c76a9e099c09d06e14b0b9bfc6baa89367f56f1ded399a0" },
+    .httpz = .{
+        .url = "git+https://github.com/karlseguin/http.zig?ref=zig-0.13#7d2ddae87af9b110783085c0ea6b03985faa4584",
+        .hash = "12208c1f2c5f730c4c03aabeb0632ade7e21914af03e6510311b449458198d0835d6",
+    },
+    .datetimez = .{
+        .url = "git+https://github.com/frmdstryr/zig-datetime#70aebf28fb3e137cd84123a9349d157a74708721",
+        .hash = "122077215ce36e125a490e59ec1748ffd4f6ba00d4d14f7308978e5360711d72d77f",
+    },
+    .mustache = .{
+            .url = "git+https://github.com/batiati/mustache-zig#ae5ecc1522da983dc39bb0d8b27f5d1b1d7956e3",
+            .hash = "1220ac9e3316ce71ad9cd66c7f215462bf5c187828b50bb3d386549bf6af004e3bb0",
+        },
+} }

+ 23 - 0
frameworks/Zig/httpz/httpz.dockerfile

@@ -0,0 +1,23 @@
+FROM fedora:40
+
+WORKDIR /httpz
+
+ENV PG_USER=benchmarkdbuser
+ENV PG_PASS=benchmarkdbpass
+ENV PG_DB=hello_world
+ENV PG_HOST=tfb-database
+ENV PG_PORT=5432
+
+COPY src src
+COPY build.zig.zon build.zig.zon
+COPY build.zig build.zig
+COPY run.sh run.sh
+
+RUN dnf install -y zig
+RUN zig version
+RUN zig build -Doptimize=ReleaseFast 
+RUN cp /httpz/zig-out/bin/httpz /usr/local/bin
+
+EXPOSE 3000
+
+CMD ["sh", "run.sh"]

+ 3 - 0
frameworks/Zig/httpz/run.sh

@@ -0,0 +1,3 @@
+echo "Waiting for Httpz framework to start..."
+
+httpz

+ 192 - 0
frameworks/Zig/httpz/src/endpoints.zig

@@ -0,0 +1,192 @@
+const std = @import("std");
+const httpz = @import("httpz");
+const pg = @import("pg");
+const datetimez = @import("datetimez");
+const mustache = @import("mustache");
+
+const Allocator = std.mem.Allocator;
+const Thread = std.Thread;
+const Mutex = Thread.Mutex;
+const template = "<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>{{#fortunes}}<tr><td>{{id}}</td><td>{{message}}</td></tr>{{/fortunes}}</table></body></html>";
+
+pub const Global = struct {
+    pool: *pg.Pool,
+    prng: *std.rand.DefaultPrng,
+    allocator: Allocator,
+    mutex: std.Thread.Mutex = .{},
+};
+
+const Message = struct {
+    message: []const u8,
+};
+
+const World = struct {
+    id: i32,
+    randomNumber: i32,
+};
+
+const Fortune = struct {
+    id: i32,
+    message: []const u8,
+};
+
+pub fn plaintext(global: *Global, _: *httpz.Request, res: *httpz.Response) !void {
+    try setHeaders(global.allocator, res);
+
+    res.content_type = .TEXT;
+    res.body = "Hello, World!";
+}
+
+pub fn json(global: *Global, _: *httpz.Request, res: *httpz.Response) !void {
+    try setHeaders(global.allocator, res);
+
+    const message = Message{ .message = "Hello, World!" };
+
+    try res.json(message, .{});
+}
+
+pub fn db(global: *Global, _: *httpz.Request, res: *httpz.Response) !void {
+    try setHeaders(global.allocator, res);
+
+    global.mutex.lock();
+    const random_number = 1 + (global.prng.random().uintAtMost(u32, 9999));
+    global.mutex.unlock();
+
+    const world = getWorld(global.pool, random_number) catch |err| {
+        std.debug.print("Error querying database: {}\n", .{err});
+        return;
+    };
+
+    try res.json(world, .{});
+}
+
+pub fn fortune(global: *Global, _: *httpz.Request, res: *httpz.Response) !void {
+    try setHeaders(global.allocator, res);
+
+    const fortunes_html = try getFortunesHtml(global.allocator, global.pool);
+
+    res.header("content-type", "text/html; charset=utf-8");
+    res.body = fortunes_html;
+}
+
+fn getWorld(pool: *pg.Pool, random_number: u32) !World{
+    var conn = try pool.acquire();
+    defer conn.release();
+
+    const row_result = try conn.row("SELECT id, randomNumber FROM World WHERE id = $1", .{random_number});
+
+    var row = row_result.?;
+    defer row.deinit() catch {};
+
+    return World{ .id = row.get(i32, 0), .randomNumber = row.get(i32, 1) };
+}
+
+fn setHeaders(allocator: Allocator, res: *httpz.Response) !void {
+    res.header("Server", "Httpz");
+
+    const now = datetimez.datetime.Date.now();
+    const time = datetimez.datetime.Time.now();
+
+    // Wed, 17 Apr 2013 12:00:00 GMT
+    // Return date in ISO format YYYY-MM-DD
+    const TB_DATE_FMT = "{s:0>3}, {d:0>2} {s:0>3} {d:0>4} {d:0>2}:{d:0>2}:{d:0>2} GMT";
+    const now_str = try std.fmt.allocPrint(allocator, TB_DATE_FMT, .{ now.weekdayName()[0..3], now.day, now.monthName()[0..3], now.year, time.hour, time.minute, time.second });
+
+    //defer allocator.free(now_str);
+
+    res.header("Date", now_str);
+}
+
+fn getFortunesHtml(allocator: Allocator, pool: *pg.Pool) ![]const u8 {
+    const fortunes = try getFortunes(allocator, pool);
+
+    const raw = try mustache.allocRenderText(allocator, template,.{ .fortunes = fortunes });
+
+    // std.debug.print("mustache output {s}\n", .{raw});
+
+    const html = try deescapeHtml(allocator, raw);
+
+    // std.debug.print("html output {s}\n", .{html});
+
+    return html;
+}
+
+fn getFortunes(allocator: Allocator, pool: *pg.Pool) ![]const Fortune {
+    var conn = try pool.acquire();
+    defer conn.release();
+
+    var rows = try conn.query("SELECT id, message FROM Fortune", .{});
+    defer rows.deinit();
+
+    var fortunes = std.ArrayList(Fortune).init(allocator);
+    defer fortunes.deinit();
+
+    while (try rows.next()) |row| {
+        const current_fortune = Fortune{ .id = row.get(i32, 0), .message = row.get([]const u8, 1) };
+        try fortunes.append(current_fortune);
+    }
+
+    const zero_fortune = Fortune{ .id = 0, .message = "Additional fortune added at request time." };
+    try fortunes.append(zero_fortune);
+
+    const fortunes_slice = try fortunes.toOwnedSlice();
+    std.mem.sort(Fortune, fortunes_slice, {}, cmpFortuneByMessage);
+
+    return fortunes_slice;
+}
+
+fn cmpFortuneByMessage(_: void, a: Fortune, b: Fortune) bool {
+    return std.mem.order(u8, a.message, b.message).compare(std.math.CompareOperator.lt);
+}
+
+fn deescapeHtml(allocator: Allocator, input: []const u8) ![]const u8 {
+    var output = std.ArrayList(u8).init(allocator);
+    defer output.deinit();
+
+    var i: usize = 0;
+    while (i < input.len) {
+        if (std.mem.startsWith(u8, input[i..], "&#32;")) {
+            try output.append(' ');
+            i += 5;
+        } else if (std.mem.startsWith(u8, input[i..], "&#34;")) {
+            try output.append('"');
+            i += 5;
+        } else if (std.mem.startsWith(u8, input[i..], "&#38;")) {
+            try output.append('&');
+            i += 5;
+        } else if (std.mem.startsWith(u8, input[i..], "&#39;")) {
+            try output.append('\'');
+            i += 5;
+        } else if (std.mem.startsWith(u8, input[i..], "&#40;")) {
+            try output.append('(');
+            i += 5;
+        } else if (std.mem.startsWith(u8, input[i..], "&#41;")) {
+            try output.append(')');
+            i += 5;
+        } else if (std.mem.startsWith(u8, input[i..], "&#43;")) {
+            try output.append('+');
+            i += 5;
+        } else if (std.mem.startsWith(u8, input[i..], "&#44;")) {
+            try output.append(',');
+            i += 5;
+        } else if (std.mem.startsWith(u8, input[i..], "&#46;")) {
+            try output.append('.');
+            i += 5;
+        } else if (std.mem.startsWith(u8, input[i..], "&#47;")) {
+            try output.append('/');
+            i += 5;
+        } else if (std.mem.startsWith(u8, input[i..], "&#58;")) {
+            try output.append(':');
+            i += 5;
+        } else if (std.mem.startsWith(u8, input[i..], "&#59;")) {
+            try output.append(';');
+            i += 5;
+        } else {
+            try output.append(input[i]);
+            i += 1;
+        }
+    }
+
+    return output.toOwnedSlice();
+}
+

+ 71 - 0
frameworks/Zig/httpz/src/main.zig

@@ -0,0 +1,71 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const httpz = @import("httpz");
+const pg = @import("pg");
+const datetimez = @import("datetimez");
+const pool = @import("pool.zig");
+
+const endpoints = @import("endpoints.zig");
+
+const RndGen = std.rand.DefaultPrng;
+const Allocator = std.mem.Allocator;
+const Pool = pg.Pool;
+
+var server: httpz.ServerCtx(*endpoints.Global,*endpoints.Global) = undefined;
+
+pub fn main() !void {
+    var gpa = std.heap.GeneralPurposeAllocator(.{
+        .thread_safe = true,
+    }){};
+
+    const allocator = gpa.allocator();
+
+    var pg_pool = try pool.initPool(allocator);
+    defer pg_pool.deinit();
+
+    var prng = std.rand.DefaultPrng.init(@as(u64, @bitCast(std.time.milliTimestamp())));
+
+    var global = endpoints.Global{ .pool = pg_pool, .prng = &prng, .allocator = allocator };
+
+    server = try httpz.ServerApp(*endpoints.Global).init(allocator, .{
+        .port = 3000, .address = "0.0.0.0", }, &global);
+    defer server.deinit();
+
+    // now that our server is up, we register our intent to handle SIGINT
+    try std.posix.sigaction(std.posix.SIG.INT, &.{
+        .handler = .{.handler = shutdown},
+        .mask = std.posix.empty_sigset,
+        .flags = 0,
+    }, null);
+
+    var router = server.router();
+    router.get("/json", endpoints.json);
+    router.get("/plaintext", endpoints.plaintext);
+    router.get("/db", endpoints.db);
+    router.get("/fortunes", endpoints.fortune);
+
+    std.debug.print("Httpz listening at 0.0.0.0:{d}\n", .{3000});
+
+    try server.listen();
+}
+
+fn shutdown(_: c_int) callconv(.C) void {
+    // this will unblock the server.listen()
+    server.stop();
+}
+
+fn notFound(_: *httpz.Request, res: *httpz.Response) !void {
+    res.status = 404;
+
+    // you can set the body directly to a []u8, but note that the memory
+    // must be valid beyond your handler. Use the res.arena if you need to allocate
+    // memory for the body.
+    res.body = "Not Found";
+}
+
+// note that the error handler return `void` and not `!void`
+fn errorHandler(req: *httpz.Request, res: *httpz.Response, err: anyerror) void {
+    res.status = 500;
+    res.body = "Internal Server Error";
+    std.log.warn("httpz: unhandled exception for request: {s}\nErr: {}", .{req.url.raw, err});
+}

+ 87 - 0
frameworks/Zig/httpz/src/pool.zig

@@ -0,0 +1,87 @@
+const std = @import("std");
+const regex = @import("regex");
+const pg = @import("pg");
+
+const Allocator = std.mem.Allocator;
+const Pool = pg.Pool;
+const ArrayList = std.ArrayList;
+
+pub fn initPool(allocator: Allocator) !*pg.Pool {
+    const info = try parsePostgresConnStr(allocator);
+    //std.debug.print("Connection: {s}:{s}@{s}:{d}/{s}\n", .{ info.username, info.password, info.hostname, info.port, info.database });
+
+    const pg_pool = try Pool.init(allocator, .{
+        .size = 28,
+        .connect = .{
+            .port = info.port,
+            .host = info.hostname,
+        },
+        .auth = .{
+            .username = info.username,
+            .database = info.database,
+            .password = info.password,
+        },
+        .timeout = 10_000,
+    });
+
+    return pg_pool;
+}
+
+pub const ConnectionInfo = struct {
+    username: []const u8,
+    password: []const u8,
+    hostname: []const u8,
+    port: u16,
+    database: []const u8,
+};
+
+fn addressAsString(address: std.net.Address) ![]const u8 {
+    const bytes = @as(*const [4]u8, @ptrCast(&address.in.sa.addr));
+
+    var buffer: [256]u8 = undefined;
+    var source = std.io.StreamSource{ .buffer = std.io.fixedBufferStream(&buffer) };
+    var writer = source.writer();
+
+    //try writer.writeAll("Hello, World!");
+
+    try writer.print("{}.{}.{}.{}", .{
+        bytes[0],
+        bytes[1],
+        bytes[2],
+        bytes[3],
+    });
+
+    const output = source.buffer.getWritten();
+
+    return output;
+}
+
+fn parsePostgresConnStr(allocator: Allocator) !ConnectionInfo {
+    const pg_port = try getEnvVar(allocator, "PG_PORT", "5432");
+    // std.debug.print("tfb port {s}\n", .{pg_port});
+    var port = try std.fmt.parseInt(u16, pg_port, 0);
+
+    if (port == 0) {
+        port = 5432;
+    }
+
+    return ConnectionInfo{
+        .username = try getEnvVar(allocator, "PG_USER", "benchmarkdbuser"),
+        .password = try getEnvVar(allocator, "PG_PASS", "benchmarkdbpass"),
+        .hostname = try getEnvVar(allocator, "PG_HOST", "localhost"),
+        .port = port,
+        .database = try getEnvVar(allocator, "PG_DB", "hello_world"),
+    };
+}
+
+fn getEnvVar(allocator: Allocator, name: []const u8, default: []const u8) ![]const u8 {
+    const env_var = std.process.getEnvVarOwned(allocator, name) catch |err| switch (err) {
+        error.EnvironmentVariableNotFound => return default,
+        error.OutOfMemory => return err,
+        error.InvalidWtf8 => return err,
+    };
+
+    if (env_var.len == 0) return default;
+
+    return env_var;
+}