1
0
Fork 0
mirror of https://github.com/zigzap/zap.git synced 2025-10-20 23:24:09 +00:00
Find a file
2023-01-15 02:45:59 +01:00
examples endpoints example works 2023-01-15 02:35:04 +01:00
src endpoint example: post user 2023-01-14 23:27:38 +01:00
.gitignore initial commit after adding facilio dep 2023-01-11 11:57:38 +01:00
.gitmodules moved deps into src 2023-01-11 12:17:27 +01:00
build.zig cleanup 2023-01-14 16:44:40 +01:00
LICENSE added LICENSE 2023-01-11 23:38:59 +01:00
README.md update README 2023-01-15 02:45:59 +01:00
shell.nix added wrk to nix shell 2023-01-14 16:13:29 +01:00

zap - blazingly fast backends

Zap is intended to become the zig replacement for the kind of REST APIs I used to write in python with Flask and mongodb, etc.

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 mind HTTPS. In production, I use nginx as a reverse proxy anyway.

Zap wraps and patches facil.io - the C web application framework.

ZAP IS SUPER ALPHA

Here's what works:

  • Super easy build process: zag's build.zig fetches git sub-modules, applies a patch to facil.io's logging for microsecond precision, builds and optionally runs everything.
    • tested on Linux and macOS (arm, M1)
  • hello: welcomes you with some static HTML
  • routes: a super easy example dispatching on the HTTP path
  • serve: the traditional static web server with optional dynamic request handling
  • hello_json: serves you json dependent on HTTP path
  • endpoints: a simple JSON REST API example featuring a /users endpoint for PUTting/DELETE-ing/GET-ting/POST-ing users and a /list endpoint returning the entire user list on GET, together with a static HTML and JavaScript frontend to play around with.

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.

Getting started

$ git clone https://github.com/renerocksai/zap.git
$ cd zap 
$ zig build run-hello
$ # open http://localhost:3000 in your browser

... and open http://localhost:3000 in your browser.

Using zap in your own projects

If you don't have an existing zig project, create one like this:

$ mkdir zaptest && cd zaptest
$ zig init-exe
$ git init 

With an existing zig project, adding zap to it is easy:

  1. Add zap as a git submodule
  2. Add zap to your build.zig

To add zap as a git submodule:

$ mkdir libs
$ git submodule add https://github.com/renerocksai/zap.git libs/zap

Then, add the following at the top of your build.zig:

const zap_builder = @import("./libs/zap/build.zig");

const zap = std.build.Pkg{
    .name = "zap",
    .source = std.build.FileSource{ .path = "./libs/zap/src/zap.zig" },
};

In the build function, add the following before exe.install():

    exe.addPackage(zap);
    zap_builder.addZap(exe, "./libs/zap/") catch unreachable;

From then on, you can use the zap package in your project. Check out the examples to see how to use zap.

Examples

You build and run the examples via:

$ zig build [EXAMPLE]
$ ./zig-out/bin/[EXAMPLE]

... where [EXAMPLE] is one of hello, routes, or serve.

Example: building and running the hello example:

$ zig build hello 
$ ./zig-out/bin/hello

To just run an example, like routes, without generating an executable, run:

$ zig build run-[EXAMPLE]

Example: building and running the routes example:

$ zig build run-routes

hello

const std = @import("std");
const zap = @import("zap");

fn on_request(r: zap.SimpleRequest) void {
    if (r.path) |the_path| {
        std.debug.print("PATH: {s}\n", .{the_path});
    }

    if (r.query) |the_query| {
        std.debug.print("QUERY: {s}\n", .{the_query});
    }
    _ = r.sendBody("<html><body><h1>Hello from ZAP!!!</h1></body></html>");
}

pub fn main() !void {
    var listener = zap.SimpleHttpListener.init(.{
        .port = 3000,
        .on_request = on_request,
        .log = false,
    });
    try listener.listen();

    std.debug.print("Listening on 0.0.0.0:3000\n", .{});

    // start worker threads
    zap.start(.{
        .threads = 2,
        .workers = 2,
    });
}

serve

const std = @import("std");
const zap = @import("zap");

fn on_request(r: zap.SimpleRequest) void {
    // TODO: send 404 response
    _ = r.sendBody("<html><body><h1>404 - File not found</h1></body></html>");
}

pub fn main() !void {
    var listener = zap.SimpleHttpListener.init(.{
        .port = 3000,
        .on_request = on_request,
        .public_folder = std.mem.span("examples/serve"),
        .log = true,
    });
    try listener.listen();

    std.debug.print("Listening on 0.0.0.0:3000\n", .{});

    // start worker threads
    zap.start(.{
        .threads = 2,
        .workers = 2,
    });
}

routes

const std = @import("std");
const zap = @import("zap");

fn dispatch_routes(r: zap.SimpleRequest) void {
    // dispatch
    if (r.path) |the_path| {
        if (routes.get(the_path)) |foo| {
            foo(r);
        }
    }
    // or default: present menu
    _ = r.sendBody(
        \\ <html>
        \\   <body>
        \\     <p><a href="/static">static</a></p>
        \\     <p><a href="/dynamic">dynamic</a></p>
        \\   </body>
        \\ </html>
    );
}

fn static_site(r: zap.SimpleRequest) void {
    _ = r.sendBody("<html><body><h1>Hello from STATIC ZAP!</h1></body></html>");
}

var dynamic_counter: i32 = 0;
fn dynamic_site(r: zap.SimpleRequest) void {
    dynamic_counter += 1;
    var buf: [128]u8 = undefined;
    const filled_buf = std.fmt.bufPrintZ(
        &buf,
        "<html><body><h1>Hello # {d} from DYNAMIC ZAP!!!</h1></body></html>",
        .{dynamic_counter},
    ) catch "ERROR";
    _ = r.sendBody(std.mem.span(filled_buf));
}

fn setup_routes(a: std.mem.Allocator) !void {
    routes = std.StringHashMap(zap.SimpleHttpRequestFn).init(a);
    try routes.put("/static", static_site);
    try routes.put("/dynamic", dynamic_site);
}

var routes: std.StringHashMap(zap.SimpleHttpRequestFn) = undefined;

pub fn main() !void {
    try setup_routes(std.heap.page_allocator);
    var listener = zap.SimpleHttpListener.init(.{
        .port = 3000,
        .on_request = dispatch_routes,
        .log = true,
    });
    try listener.listen();

    std.debug.print("Listening on 0.0.0.0:3000\n", .{});

    zap.start(.{
        .threads = 2,
        .workers = 2,
    });
}

hello_json

const std = @import("std");
const zap = @import("zap");

const User = struct {
    first_name: ?[]const u8 = null,
    last_name: ?[]const u8 = null,
};

fn on_request(r: zap.SimpleRequest) void {
    if (!std.mem.eql(u8, r.method.?, "GET"))
        return;

    // /user/n
    if (r.path) |the_path| {
        if (the_path.len < 7 or !std.mem.startsWith(u8, the_path, "/user/"))
            return;

        const user_id: usize = @intCast(usize, the_path[6] - 0x30);
        const user = users.get(user_id);

        var json_to_send: []const u8 = undefined;
        if (stringify(user, .{})) |json| {
            json_to_send = json;
        } else {
            json_to_send = "null";
        }
        std.debug.print("<< json: {s}\n", .{json_to_send});
        r.setContentType(.JSON);
        _ = r.sendBody(json_to_send);
    }
}

const UserMap = std.AutoHashMap(usize, User);

var users: UserMap = undefined;
fn setupUserData(a: std.mem.Allocator) !void {
    users = UserMap.init(a);
    try users.put(1, .{ .first_name = "renerocksai" });
    try users.put(2, .{ .first_name = "Your", .last_name = "Mom" });
}

fn stringify(value: anytype, options: std.json.StringifyOptions) ?[]const u8 {
    var buf: [100]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buf);
    var string = std.ArrayList(u8).init(fba.allocator());
    if (std.json.stringify(value, options, string.writer())) {
        return string.items;
    } else |_| { // error
        return null;
    }
}

pub fn main() !void {
    var a = std.heap.page_allocator;
    try setupUserData(a);
    var listener = zap.SimpleHttpListener.init(.{
        .port = 3000,
        .on_request = on_request,
        .log = false,
    });
    try listener.listen();

    std.debug.print("Listening on 0.0.0.0:3000\n", .{});

    // start worker threads
    zap.start(.{
        .threads = 2,
        .workers = 2,
    });
}

endpoints

The following only shows the GET functionality implemented on both the /user/ and the /list endpoints. See endpoints for a complete example, including a HTML + JavaScript frontend.

main.zig:

const std = @import("std");
const zap = @import("zap");
const Endpoints = @import("endpoints.zig");

pub fn main() !void {
    const allocator = std.heap.page_allocator;
    // setup listener
    var listener = zap.SimpleEndpointListener.init(
        allocator,
        .{
            .port = 3000,
            .on_request = null,
            .log = true,
            .public_folder = "./examples/endpoints/html",
        },
    );

    Endpoints.init(allocator, "/user", "/list");

    // add endpoints
    try listener.addEndpoint(Endpoints.getUserEndpoint());
    try listener.addEndpoint(Endpoints.getUserListEndpoint());

    // fake some users
    var uid: usize = undefined;
    uid = try Endpoints.getUsers().addByName("renerocksai", null);
    uid = try Endpoints.getUsers().addByName("renerocksai", "your mom");

    // listen
    try listener.listen();

    std.debug.print("Listening on 0.0.0.0:3000\n", .{});

    // and run
    zap.start(.{
        .threads = 2,
        .workers = 2,
    });
}

endpoints.zig:

const std = @import("std");
const zap = @import("zap");
const Users = @import("users.zig");
const User = Users.User;

// the Endpoints
pub const Self = @This();

var alloc: std.mem.Allocator = undefined;
var endpoint: zap.SimpleEndpoint = undefined;
var list_endpoint: zap.SimpleEndpoint = undefined;
var users: Users = undefined;

pub fn init(
    a: std.mem.Allocator,
    user_path: []const u8,
    userlist_path: []const u8,
) void {
    users = Users.init(a);
    alloc = a;
    endpoint = zap.SimpleEndpoint.init(.{
        .path = user_path,
        .get = getUser,
        .post = null,
        .put = null,
        .delete = null,
    });
    list_endpoint = zap.SimpleEndpoint.init(.{
        .path = userlist_path,
        .get = listUsers,
        .post = null,
        .put = null,
        .delete = null,
    });
}

pub fn getUsers() *Users {
    return &users;
}

pub fn getUserEndpoint() *zap.SimpleEndpoint {
    return &endpoint;
}

pub fn getUserListEndpoint() *zap.SimpleEndpoint {
    return &list_endpoint;
}

fn userIdFromPath(path: []const u8) ?usize {
    if (path.len >= endpoint.settings.path.len + 2) {
        if (path[endpoint.settings.path.len] != '/') {
            return null;
        }
        const idstr = path[endpoint.settings.path.len + 1 ..];
        return std.fmt.parseUnsigned(usize, idstr, 10) catch null;
    }
    return null;
}

pub fn getUser(e: *zap.SimpleEndpoint, r: zap.SimpleRequest) void {
    _ = e;
    if (r.path) |path| {
        if (userIdFromPath(path)) |id| {
            if (users.get(id)) |user| {
                if (zap.stringify(user, .{})) |json| {
                    _ = r.sendJson(json);
                }
            }
        }
    }
}

pub fn listUsers(e: *zap.SimpleEndpoint, r: zap.SimpleRequest) void {
    _ = e;
    var l: std.ArrayList(User) = std.ArrayList(User).init(alloc);
    if (users.list(&l)) {} else |_| {
        return;
    }
    if (zap.stringifyArrayList(std.ArrayList(User, &l, .{})) |maybe_json| {
        if (maybe_json) |json| {
            _ = r.sendJson(json);
        }
    } else |_| {
        return;
    }
}