Skip to content

Commit

Permalink
start to improve the examples
Browse files Browse the repository at this point in the history
  • Loading branch information
karlseguin committed Aug 30, 2024
1 parent 49755ee commit 0872ba1
Show file tree
Hide file tree
Showing 12 changed files with 368 additions and 26 deletions.
56 changes: 34 additions & 22 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,6 @@ pub fn build(b: *std.Build) !void {
httpz_module.addOptions("build", options);
}

{
// demo
const exe = b.addExecutable(.{
.name = "http.zig demo",
.root_source_file = b.path("example/main.zig"),
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("httpz", httpz_module);
exe.root_module.addImport("metrics", metrics_module);
exe.root_module.addImport("websocket", websocket_module);
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 the app");
run_step.dependOn(&run_cmd.step);
}

{
const tests = b.addTest(.{
.root_source_file = b.path("src/httpz.zig"),
Expand Down Expand Up @@ -71,4 +49,38 @@ pub fn build(b: *std.Build) !void {
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_test.step);
}

const examples = [_]struct{
file: []const u8,
name: []const u8,
} {
.{.file = "examples/01_basic.zig", .name = "example_1"},
.{.file = "examples/02_handler.zig", .name = "example_2"},
.{.file = "examples/03_dispatch.zig", .name = "example_3"},
.{.file = "examples/04_action_context.zig", .name = "example_4"},
};

{
for (examples) |ex| {
const exe = b.addExecutable(.{
.name = ex.name,
.target = target,
.optimize = optimize,
.root_source_file = b.path(ex.file),
});
exe.root_module.addImport("httpz", httpz_module);
exe.root_module.addImport("metrics", metrics_module);
// exe.root_module.addImport("websocket", websocket_module);
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(ex.name, ex.file);
run_step.dependOn(&run_cmd.step);
}
}
}
3 changes: 0 additions & 3 deletions example/index.html

This file was deleted.

109 changes: 109 additions & 0 deletions examples/01_basic.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const std = @import("std");
const httpz = @import("httpz");
const Allocator = std.mem.Allocator;

const PORT = 8800;

// This example demonstrates basic httpz usage, with focus on using the
// httpz.Request and httpz.Response objects.

pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();

// We pass a "void" handler. This is the simplest, but limits what we can do
// The last parameter is an instance of our handler. Since we have
// a void handler, we pass a void value: i.e. {}.
var server = try httpz.Server(void).init(allocator, .{
.port = PORT,
.request = .{
// httpz has a number of tweakable configuration settings (see readme)
// by default, it won't read form data. We need to configure a max
// field count (since one of our examples reads form data)
.max_form_count = 20,
},
}, {});
defer server.deinit();

var router = server.router();

// Register routes. The last parameter is a Route Config. For these basic
// examples, we aren't using it.
// Other support methods: post, put, delete, head, trace, options and all
router.get("/", index, .{});
router.get("/hello", hello, .{});
router.get("/json/hello/:name", json, .{});
router.get("/writer/hello/:name", writer, .{});
router.get("/metrics", metrics, .{});
router.get("/form_data", formShow, .{});
router.post("/form_data", formPost, .{});

std.debug.print("listening http://localhost:{d}/\n", .{PORT});
// Starts the server, this is blocking.
try server.listen();
}

fn index(_: *httpz.Request, res: *httpz.Response) !void {
res.body =
\\<!DOCTYPE html>
\\ <ul>
\\ <li><a href="/hello?name=Teg">Querystring + text output</a>
\\ <li><a href="/writer/hello/Ghanima">Path parameter + serialize json object</a>
\\ <li><a href="/json/hello/Duncan">Path parameter + json writer</a>
\\ <li><a href="/metrics">Internal metrics</a>
\\ <li><a href="/form_data">Form Data</a>
;
}

fn hello(req: *httpz.Request, res: *httpz.Response) !void {
const query = try req.query();
const name = query.get("name") orelse "stranger";

// Could also see res.writer(), see the writer endpoint for an example
res.body = try std.fmt.allocPrint(res.arena, "Hello {s}", .{name});
}

fn json(req: *httpz.Request, res: *httpz.Response) !void {
const name = req.param("name").?;

// the last parameter to res.json is an std.json.StringifyOptions
try res.json(.{ .hello = name }, .{});
}

fn writer(req: *httpz.Request, res: *httpz.Response) !void {
res.content_type = httpz.ContentType.JSON;

const name = req.param("name").?;
var ws = std.json.writeStream(res.writer(), .{ .whitespace = .indent_4 });
try ws.beginObject();
try ws.objectField("name");
try ws.write(name);
try ws.endObject();
}

fn metrics(_: *httpz.Request, res: *httpz.Response) !void {
// httpz exposes some prometheus-style metrics
return httpz.writeMetrics(res.writer());
}

fn formShow(_: *httpz.Request, res: *httpz.Response) !void {
res.body =
\\ <html>
\\ <form method=post>
\\ <p><input name=name value=goku></p>
\\ <p><input name=power value=9001></p>
\\ <p><input type=submit value=submit></p>
\\ </form>
;
}

fn formPost(req: *httpz.Request, res: *httpz.Response) !void {
var it = (try req.formData()).iterator();

res.content_type = .TEXT;

const w = res.writer();
while (it.next()) |kv| {
try std.fmt.format(w, "{s}={s}\n", .{kv.key, kv.value});
}
}
84 changes: 84 additions & 0 deletions examples/02_handler.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const std = @import("std");
const httpz = @import("httpz");
const Allocator = std.mem.Allocator;

const PORT = 8801;

// This example demonstrates using a custom Handler. It shows how to have
// global state (here we show a counter, but it could be a more complex struct
// including things such as a DB pool) and how to define not found and error
// handlers.

pub fn main() !void {

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();

// We specify our "Handler" and, as the last parameter to init, pass an
// instance of it.
var handler = Handler{};
var server = try httpz.Server(*Handler).init(allocator, .{.port = PORT}, &handler);
defer server.deinit();

var router = server.router();

// Register routes.

router.get("/", index, .{});
router.get("/hits", hits, .{});
router.get("/error", @"error", .{});

std.debug.print("listening http://localhost:{d}/\n", .{PORT});

// Starts the server, this is blocking.
try server.listen();
}

const Handler = struct {
_hits: usize = 0,

// If the handler defines a special "notFound" function, it'll be called
// when a request is made and no route matches.
pub fn notFound(_: *Handler, _: *httpz.Request, res: *httpz.Response) !void {
res.status = 404;
res.body = "NOPE!";
}

// If the handler defines the special "uncaughtError" function, it'll be
// called when an action returns an error.
// Note that this function takes an additional parameter (the error) and
// returns a `void` rather than a `!void`.
pub fn uncaughtError(_: *Handler, req: *httpz.Request, res: *httpz.Response, err: anyerror) void {
std.debug.print("uncaught http error at {s}: {}\n", .{req.url.path, err});

// Alternative to res.content_type = .TYPE
// useful for dynamic content types, or content types not defined in
// httpz.ContentType
res.headers.add("content-type", "text/html; charset=utf-8");

res.status = 505;
res.body = "<!DOCTYPE html>(╯°□°)╯︵ ┻━┻";
}
};

fn index(_: *Handler, _: *httpz.Request, res: *httpz.Response) !void {
res.body =
\\<!DOCTYPE html>
\\ <ul>
\\ <li><a href="/hits">Shared global hit counter</a>
\\ <li><a href="/not_found">Custom not found handler</a>
\\ <li><a href="/error">Custom error handler</a>
;
}

pub fn hits(h: *Handler, _: *httpz.Request, res: *httpz.Response) !void {
const count = @atomicRmw(usize, &h._hits, .Add, 1, .monotonic);

// @atomicRmw returns the previous version so we need to +1 it
// to display the count includin this hit
return res.json(.{.hits = count + 1}, .{});
}

fn @"error"(_: *Handler, _: *httpz.Request, _: *httpz.Response) !void {
return error.ActionError;
}
46 changes: 46 additions & 0 deletions examples/03_dispatch.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const std = @import("std");
const httpz = @import("httpz");
const Allocator = std.mem.Allocator;

const PORT = 8802;

// This example uses a custom dispatch method on our handler for greater control
// in how actions are executed.

pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();

var handler = Handler{};
var server = try httpz.Server(*Handler).init(allocator, .{.port = PORT}, &handler);
defer server.deinit();

var router = server.router();

router.get("/", index, .{});
std.debug.print("listening http://localhost:{d}/\n", .{PORT});

// Starts the server, this is blocking.
try server.listen();
}

const Handler = struct {
// In addition to the special "notFound" and "uncaughtError" shown in example 2
// the special "dispatch" method can be used to gain more control over request handling.
pub fn dispatch(self: *Handler, action: httpz.Action(*Handler), req: *httpz.Request, res: *httpz.Response) !void {
// Our custom dispatch lets us add a log + timing for every request
// httpz supports middlewares, but in many cases, having a dispatch is good
// enough and is much more straightforward.

var start = try std.time.Timer.start();
// We don't _have_ to call the action if we don't want to. For example
// we could do authentication and set the response directly on error.
try action(self, req, res);

std.debug.print("{d}\t{d}us\t{s}\n", .{std.time.timestamp(), start.lap() / 1000, req.url.path});
}
};

fn index(_: *Handler, _: *httpz.Request, res: *httpz.Response) !void {
res.body = "see the console logs";
}
85 changes: 85 additions & 0 deletions examples/04_action_context.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const std = @import("std");
const httpz = @import("httpz");
const Allocator = std.mem.Allocator;

const PORT = 8803;

// This example is very similar to 03_dispatch.zig, but shows how the action
// state can be a different type than the handler.

pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();

var handler = Handler{};
var server = try httpz.Server(*Handler).init(allocator, .{.port = PORT}, &handler);
defer server.deinit();

var router = server.router();

const restricted_route = &RouteData{.restricted = true};

// We can register arbitrary data to a route, which we can retrieve
// via req.route_data. This is stored as a `*const anyopaque`.
router.get("/", index, .{});
router.get("/admin", admin, .{.data = restricted_route});

std.debug.print("listening http://localhost:{d}/\n", .{PORT});

// Starts the server, this is blocking.
try server.listen();
}

const Handler = struct {
// In example_3, our action type was: httpz.Action(*Handler).
// In this example, we've changed it to: httpz.Action(*Env)
// This allows our handler to be a general app-wide "state" while our actions
// received a request-specific context
pub fn dispatch(self: *Handler, action: httpz.Action(*Env), req: *httpz.Request, res: *httpz.Response) !void {
const user = (try req.query()).get("auth");

// RouteData can be anything, but since it's stored as a *const anyopaque
// you'll need to restore the type/alignment.

// (You could also use a per-route handler, or middleware, to achieve
// the same thing. Using route data is a bit ugly due to the type erasure
// but it can be convenient!).
if (req.route_data) |rd| {
const route_data: *const RouteData = @ptrCast(@alignCast(rd));
if (route_data.restricted and (user == null or user.?.len == 0)) {
res.status = 401;
res.body = "permission denied";
return;
}
}

var env = Env{
.user = user, // todo: this is not very good security!
.handler = self,
};

try action(&env, req, res);
}
};

const RouteData = struct{
restricted: bool,
};

const Env = struct{
handler: *Handler,
user: ?[]const u8,
};

fn index(_: *Env, _: *httpz.Request, res: *httpz.Response) !void {
res.body =
\\<!DOCTYPE html>
\\ <ul>
\\ <li><a href="/admin?auth=sudo">admin</a>
;
}

// because of our dispatch method, this can only be called when env.user != null
fn admin(env: *Env, _: *httpz.Request, res: *httpz.Response) !void {
res.body = try std.fmt.allocPrint(res.arena, "Welcome to the admin portal, {s}", .{env.user.?});
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 0872ba1

Please sign in to comment.