diff --git a/README.md b/README.md
index 2c3bbe2..f580af0 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@

-_Jetzig_ is a web framework written in [Zig](https://ziglang.org) :lizard:.
+_Jetzig_ is a web framework written in 100% pure [Zig](https://ziglang.org) :lizard: for _Linux_, _OS X_, _Windows_, and any _OS_ that can compile _Zig_ code.
The framework is under active development and is currently in an alpha state.
@@ -16,6 +16,7 @@ If you are interested in _Jetzig_ you will probably find these tools interesting
* [http.zig](https://github.com/karlseguin/http.zig)
* [zig-router](https://github.com/Cloudef/zig-router)
* [zig-webui](https://github.com/webui-dev/zig-webui/)
+* [ZTS](https://github.com/zigster64/zts)
## Checklist
@@ -28,7 +29,7 @@ If you are interested in _Jetzig_ you will probably find these tools interesting
* :white_check_mark: Cookies.
* :white_check_mark: Error handling.
* :white_check_mark: Static content from /public directory.
-* :white_check_mark: Headers (available but not yet wrapped).
+* :white_check_mark: Request/response headers.
* :white_check_mark: Stack trace output on error.
* :white_check_mark: Static content generation.
* :x: Param/JSON payload parsing/abstracting.
diff --git a/demo/src/app/views/root.zig b/demo/src/app/views/root.zig
index de8238d..e0ade83 100644
--- a/demo/src/app/views/root.zig
+++ b/demo/src/app/views/root.zig
@@ -1,37 +1,10 @@
-const std = @import("std");
const jetzig = @import("jetzig");
-pub const static_params = .{
- .index = .{
- .{ .params = .{ .foo = "hi", .bar = "bye" } },
- .{ .params = .{ .foo = "hello", .bar = "goodbye" } },
- },
- .get = .{
- .{ .id = "1", .params = .{ .foo = "hi", .bar = "bye" } },
- .{ .id = "2", .params = .{ .foo = "hello", .bar = "goodbye" } },
- },
-};
-
-pub fn index(request: *jetzig.StaticRequest, data: *jetzig.Data) !jetzig.View {
+pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
var root = try data.object();
+ try root.put("message", data.string("Welcome to Jetzig!"));
- const params = try request.params();
-
- if (params.get("foo")) |foo| try root.put("foo", foo);
+ try request.response.headers.append("x-example-header", "example header value");
return request.render(.ok);
}
-
-pub fn get(id: []const u8, request: *jetzig.StaticRequest, data: *jetzig.Data) !jetzig.View {
- var root = try data.object();
-
- const params = try request.params();
-
- if (std.mem.eql(u8, id, "1")) {
- try root.put("id", data.string("id is '1'"));
- }
-
- if (params.get("foo")) |foo| try root.put("foo", foo);
-
- return request.render(.created);
-}
diff --git a/demo/src/app/views/root/index.zmpl b/demo/src/app/views/root/index.zmpl
index fd884a1..57ee5cc 100644
--- a/demo/src/app/views/root/index.zmpl
+++ b/demo/src/app/views/root/index.zmpl
@@ -14,8 +14,6 @@
{.message}
- {.foo}
-
diff --git a/demo/src/app/views/static.zig b/demo/src/app/views/static.zig
new file mode 100644
index 0000000..e0d3b1c
--- /dev/null
+++ b/demo/src/app/views/static.zig
@@ -0,0 +1,68 @@
+/// This example demonstrates static site generation (SSG).
+///
+/// Any view function that receives `*jetzig.StaticRequest` is considered as a SSG view, which
+/// will be invoked at build time and its content (both JSON and HTML) rendered to `static/` in
+/// the root project directory.
+///
+/// Define `pub const static_params` as a struct with fields named after each view function, with
+/// the value for each field being an array of structs with fields `params` and, where
+/// applicable (i.e. `get`, `put`, `patch`, and `delete`), `id`.
+///
+/// For each item in the provided array, a separate JSON and HTML output will be generated. At
+/// run time, requests are matched to the relevant content by comparing the request params and
+/// resource ID to locate the relevant content.
+///
+/// Launch the demo app and try the following requests:
+///
+/// ```console
+/// curl -H "Accept: application/json" \
+/// --data-bin '{"foo":"hello", "bar":"goodbye"}' \
+/// --request GET \
+/// 'http://localhost:8080/static'
+/// ```
+///
+/// ```console
+/// curl 'http://localhost:8080/static.html?foo=hi&bar=bye'
+/// ```
+///
+/// ```console
+/// curl 'http://localhost:8080/static/123.html?foo=hi&bar=bye'
+/// ```
+const std = @import("std");
+const jetzig = @import("jetzig");
+
+pub const static_params = .{
+ .index = .{
+ .{ .params = .{ .foo = "hi", .bar = "bye" } },
+ .{ .params = .{ .foo = "hello", .bar = "goodbye" } },
+ },
+ .get = .{
+ .{ .id = "1", .params = .{ .foo = "hi", .bar = "bye" } },
+ .{ .id = "2", .params = .{ .foo = "hello", .bar = "goodbye" } },
+ },
+};
+
+pub fn index(request: *jetzig.StaticRequest, data: *jetzig.Data) !jetzig.View {
+ var root = try data.object();
+
+ const params = try request.params();
+
+ if (params.get("foo")) |foo| try root.put("foo", foo);
+
+ return request.render(.ok);
+}
+
+pub fn get(id: []const u8, request: *jetzig.StaticRequest, data: *jetzig.Data) !jetzig.View {
+ var root = try data.object();
+
+ const params = try request.params();
+
+ if (std.mem.eql(u8, id, "1")) {
+ try root.put("id", data.string("id is '1'"));
+ }
+
+ if (params.get("foo")) |foo| try root.put("foo", foo);
+ if (params.get("bar")) |bar| try root.put("bar", bar);
+
+ return request.render(.created);
+}
diff --git a/demo/src/app/views/zmpl.manifest.zig b/demo/src/app/views/zmpl.manifest.zig
index 54152c0..eb2409b 100644
--- a/demo/src/app/views/zmpl.manifest.zig
+++ b/demo/src/app/views/zmpl.manifest.zig
@@ -2,6 +2,8 @@
// This file is automatically generated at build time. Manual edits will be discarded.
// This file should _not_ be stored in version control.
pub const templates = struct {
+ pub const static_index = @import("static/.index.zmpl.compiled.zig");
+ pub const static_get = @import("static/.get.zmpl.compiled.zig");
pub const root_index = @import("root/.index.zmpl.compiled.zig");
pub const quotes_post = @import("quotes/.post.zmpl.compiled.zig");
pub const quotes_get = @import("quotes/.get.zmpl.compiled.zig");
diff --git a/src/GenerateRoutes.zig b/src/GenerateRoutes.zig
index 6314dc2..3ebbf6e 100644
--- a/src/GenerateRoutes.zig
+++ b/src/GenerateRoutes.zig
@@ -155,6 +155,8 @@ pub fn generateRoutes(self: *Self) !void {
try writer.writeAll(" };\n");
try writer.writeAll("};");
+
+ // std.debug.print("routes.zig\n{s}\n", .{self.buffer.items});
}
fn writeRoute(self: *Self, writer: std.ArrayList(u8).Writer, route: Function) !void {
diff --git a/src/jetzig.zig b/src/jetzig.zig
index 1801eee..fd8b756 100644
--- a/src/jetzig.zig
+++ b/src/jetzig.zig
@@ -8,12 +8,26 @@ pub const data = @import("jetzig/data.zig");
pub const caches = @import("jetzig/caches.zig");
pub const views = @import("jetzig/views.zig");
pub const colors = @import("jetzig/colors.zig");
+
+/// The primary interface for a Jetzig application. Create an `App` in your application's
+/// `src/main.zig` and call `start` to launch the application.
pub const App = @import("jetzig/App.zig");
-// Convenience for view function parameters.
+/// An HTTP request which is passed to (dynamic) view functions and provides access to params,
+/// headers, and functions to render a response.
pub const Request = http.Request;
+
+/// A build-time request. Provides a similar interface to a `Request` but outputs are generated
+/// when building the application and then returned immediately to the client for matching
+/// requests.
pub const StaticRequest = http.StaticRequest;
+
+/// Generic, JSON-compatible data type. Provides `Value` which in turn provides `Object`,
+/// `Array`, `String`, `Integer`, `Float`, `Boolean`, and `NullType`.
pub const Data = data.Data;
+
+/// The return value of all view functions. Call `request.render(.ok)` in a view function to
+/// generate a `View`.
pub const View = views.View;
pub const config = struct {
diff --git a/src/jetzig/App.zig b/src/jetzig/App.zig
index 419660c..2dee724 100644
--- a/src/jetzig/App.zig
+++ b/src/jetzig/App.zig
@@ -14,6 +14,9 @@ pub fn deinit(self: Self) void {
_ = self;
}
+/// Starts an application. `routes` should be `@import("routes").routes`, a generated file
+/// automatically created at build time. `templates` should be
+/// `@import("src/app/views/zmpl.manifest.zig").templates`, created by Zmpl at compile time.
pub fn start(self: Self, routes: []jetzig.views.Route, templates: []jetzig.TemplateFn) !void {
var server = jetzig.http.Server.init(
self.allocator,
diff --git a/src/jetzig/http/Headers.zig b/src/jetzig/http/Headers.zig
index 783d678..98167e3 100644
--- a/src/jetzig/http/Headers.zig
+++ b/src/jetzig/http/Headers.zig
@@ -13,14 +13,42 @@ pub fn deinit(self: *Self) void {
self.std_headers.deinit();
}
-pub fn getFirstValue(self: *Self, key: []const u8) ?[]const u8 {
- return self.std_headers.getFirstValue(key);
+// Gets the first value for a given header identified by `name`.
+pub fn getFirstValue(self: *Self, name: []const u8) ?[]const u8 {
+ return self.std_headers.getFirstValue(name);
}
-pub fn append(self: *Self, key: []const u8, value: []const u8) !void {
- try self.std_headers.append(key, value);
+/// Appends `name` and `value` to headers.
+pub fn append(self: *Self, name: []const u8, value: []const u8) !void {
+ try self.std_headers.append(name, value);
}
+/// Returns an iterator which implements `next()` returning each name/value of the stored headers.
+pub fn iterator(self: *Self) Iterator {
+ return Iterator{ .std_headers = self.std_headers };
+}
+
+const Iterator = struct {
+ std_headers: std.http.Headers,
+ index: usize = 0,
+
+ const Header = struct {
+ name: []const u8,
+ value: []const u8,
+ };
+
+ /// Returns the next item in the current iteration of headers.
+ pub fn next(self: *Iterator) ?Header {
+ if (self.std_headers.list.items.len > self.index) {
+ const std_header = self.std_headers.list.items[self.index];
+ self.index += 1;
+ return .{ .name = std_header.name, .value = std_header.value };
+ } else {
+ return null;
+ }
+ }
+};
+
test {
const allocator = std.testing.allocator;
var headers = std.http.Headers.init(allocator);
diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig
index 4236b9e..2346595 100644
--- a/src/jetzig/http/Request.zig
+++ b/src/jetzig/http/Request.zig
@@ -43,9 +43,22 @@ pub fn init(
_ => return error.JetzigUnsupportedHttpMethod,
};
+ // TODO: Replace all this with a `Path` type which exposes all components of the path in a
+ // sensible way:
+ // * Array of segments: "/foo/bar/baz" => .{ "foo", "bar", "baz" }
+ // * Resource ID: "/foo/bar/baz/1" => "1"
+ // * Extension: "/foo/bar/baz/1.json" => ".json"
+ // * Query params: "/foo/bar/baz?foo=bar&baz=qux" => .{ .foo = "bar", .baz => "qux" }
+ // * Anything else ?
var it = std.mem.splitScalar(u8, response.std_response.request.target, '/');
var segments = std.ArrayList([]const u8).init(allocator);
- while (it.next()) |segment| try segments.append(segment);
+ while (it.next()) |segment| {
+ if (std.mem.indexOfScalar(u8, segment, '?')) |query_index| {
+ try segments.append(segment[0..query_index]);
+ } else {
+ try segments.append(segment);
+ }
+ }
var cookies = try allocator.create(jetzig.http.Cookies);
cookies.* = jetzig.http.Cookies.init(
@@ -149,7 +162,7 @@ fn parseQueryString(self: *Self) !bool {
if (self.path.len - 1 < index + 1) return false;
self.query.* = jetzig.http.Query.init(
- self.server.allocator,
+ self.allocator,
self.path[index + 1 ..],
self.query_data,
);
@@ -214,7 +227,7 @@ pub fn resourceModifier(self: *Self) ?Modifier {
}
pub fn resourceName(self: *Self) []const u8 {
- if (self.segments.items.len == 0) return "default";
+ if (self.segments.items.len == 0) return "default"; // Should never happen ?
const basename = std.fs.path.basename(self.segments.items[self.segments.items.len - 1]);
if (std.mem.indexOfScalar(u8, basename, '?')) |index| {
@@ -264,7 +277,7 @@ fn isMatch(self: *Self, match_type: enum { exact, resource_id }, route: jetzig.v
.resource_id => self.pathWithoutExtensionAndResourceId(),
};
- return (std.mem.eql(u8, path, route.uri_path));
+ return std.mem.eql(u8, path, route.uri_path);
}
// TODO: Be a bit more deterministic in identifying extension, e.g. deal with `.` characters
diff --git a/src/jetzig/http/Response.zig b/src/jetzig/http/Response.zig
index 848e6c2..a40bf1d 100644
--- a/src/jetzig/http/Response.zig
+++ b/src/jetzig/http/Response.zig
@@ -30,8 +30,8 @@ pub fn init(
pub fn deinit(self: *const Self) void {
self.headers.deinit();
- // self.allocator.free(self.content);
- // self.allocator.free(self.content_type);
+ self.allocator.destroy(self.headers);
+ self.std_response.deinit();
}
const ResetState = enum { reset, closing };
@@ -49,11 +49,18 @@ pub fn wait(self: *const Self) !void {
try self.std_response.wait();
}
+/// Finalizes a request. Appends any stored headers, sets the response status code, and writes
+/// the response body.
pub fn finish(self: *const Self) !void {
self.std_response.status = switch (self.status_code) {
inline else => |status_code| @field(std.http.Status, @tagName(status_code)),
};
+ var it = self.headers.iterator();
+ while (it.next()) |header| {
+ try self.std_response.headers.append(header.name, header.value);
+ }
+
try self.std_response.send();
try self.std_response.writeAll(self.content);
try self.std_response.finish();
@@ -76,12 +83,3 @@ pub fn setTransferEncoding(self: *const Self, transfer_encoding: TransferEncodin
// TODO: Chunked encoding
self.std_response.transfer_encoding = .{ .content_length = transfer_encoding.content_length };
}
-
-pub fn dupe(self: *const Self) !Self {
- return .{
- .allocator = self.allocator,
- .status_code = self.status_code,
- .content_type = try self.allocator.dupe(u8, self.content_type),
- .content = try self.allocator.dupe(u8, self.content),
- };
-}
diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig
index e3aa3f2..f95b47e 100644
--- a/src/jetzig/http/Server.zig
+++ b/src/jetzig/http/Server.zig
@@ -67,18 +67,22 @@ pub fn listen(self: *Self) !void {
fn processRequests(self: *Self) !void {
while (true) {
- var std_response = try self.server.accept(.{ .allocator = self.allocator });
+ var arena = std.heap.ArenaAllocator.init(self.allocator);
+ const allocator = arena.allocator();
+
+ var std_response = try self.server.accept(.{ .allocator = allocator });
var response = try jetzig.http.Response.init(
- self.allocator,
+ allocator,
&std_response,
);
errdefer response.deinit();
+ errdefer arena.deinit();
try response.headers.append("Connection", "close");
while (response.reset() != .closing) {
- self.processNextRequest(&response) catch |err| {
+ self.processNextRequest(allocator, &response) catch |err| {
switch (err) {
error.EndOfStream, error.ConnectionResetByPeer => continue,
error.UnknownHttpMethod => continue, // TODO: Render 400 Bad Request here ?
@@ -88,10 +92,11 @@ fn processRequests(self: *Self) !void {
}
response.deinit();
+ arena.deinit();
}
}
-fn processNextRequest(self: *Self, response: *jetzig.http.Response) !void {
+fn processNextRequest(self: *Self, allocator: std.mem.Allocator, response: *jetzig.http.Response) !void {
try response.wait();
self.start_time = std.time.nanoTimestamp();
@@ -99,10 +104,7 @@ fn processNextRequest(self: *Self, response: *jetzig.http.Response) !void {
const body = try response.read();
defer self.allocator.free(body);
- var arena = std.heap.ArenaAllocator.init(self.allocator);
- defer arena.deinit();
-
- var request = try jetzig.http.Request.init(arena.allocator(), self, response, body);
+ var request = try jetzig.http.Request.init(allocator, self, response, body);
defer request.deinit();
var middleware_data = try jetzig.http.middleware.beforeMiddleware(&request);
@@ -180,16 +182,11 @@ fn renderHTML(
return;
}
}
-
- response.content = "";
- response.status_code = .not_found;
- response.content_type = "text/html";
- return;
- } else {
- response.content = "";
- response.status_code = .not_found;
- response.content_type = "text/html";
}
+
+ response.content = "";
+ response.status_code = .not_found;
+ response.content_type = "text/html";
}
fn renderJSON(
@@ -329,22 +326,10 @@ fn matchRoute(self: *Self, request: *jetzig.http.Request, static: bool) !?jetzig
return null;
}
-fn matchStaticParams(self: *Self, request: *jetzig.http.Request, route: jetzig.views.Route) !?usize {
- _ = self;
- const params = try request.params();
-
- for (route.params.items, 0..) |static_params, index| {
- if (try static_params.getValue("params")) |expected_params| {
- if (expected_params.eql(params)) return index;
- }
- }
- return null;
-}
-
const StaticResource = struct { content: []const u8, mime_type: []const u8 = "application/octet-stream" };
fn matchStaticResource(self: *Self, request: *jetzig.http.Request) !?StaticResource {
- const public_content = try self.matchPublicContent(request);
+ const public_content = try matchPublicContent(request);
if (public_content) |content| return .{ .content = content };
const static_content = try self.matchStaticContent(request);
@@ -359,9 +344,7 @@ fn matchStaticResource(self: *Self, request: *jetzig.http.Request) !?StaticResou
return null;
}
-fn matchPublicContent(self: *Self, request: *jetzig.http.Request) !?[]const u8 {
- _ = self;
-
+fn matchPublicContent(request: *jetzig.http.Request) !?[]const u8 {
if (request.path.len < 2) return null;
if (request.method != .GET) return null;
@@ -403,7 +386,8 @@ fn matchStaticContent(self: *Self, request: *jetzig.http.Request) !?[]const u8 {
const matched_route = try self.matchRoute(request, true);
if (matched_route) |route| {
- const static_path = try self.staticPath(request, route);
+ const static_path = try staticPath(request, route);
+
if (static_path) |capture| {
return static_dir.readFileAlloc(
request.allocator,
@@ -421,9 +405,10 @@ fn matchStaticContent(self: *Self, request: *jetzig.http.Request) !?[]const u8 {
return null;
}
-fn staticPath(self: *Self, request: *jetzig.http.Request, route: jetzig.views.Route) !?[]const u8 {
- _ = self;
+fn staticPath(request: *jetzig.http.Request, route: jetzig.views.Route) !?[]const u8 {
const params = try request.params();
+ defer params.deinit();
+
const extension = switch (request.requestFormat()) {
.HTML, .UNKNOWN => ".html",
.JSON => ".json",
@@ -439,8 +424,8 @@ fn staticPath(self: *Self, request: *jetzig.http.Request, route: jetzig.views.Ro
.string => |capture| {
if (!std.mem.eql(u8, capture.value, request.resourceId())) continue;
},
- // Should be unreachable but we want to avoid a runtime panic.
- inline else => continue,
+ // Should be unreachable - this means generated `routes.zig` is incoherent:
+ inline else => return error.JetzigRouteError,
}
}
},