diff --git a/README.md b/README.md
index 2cd1b6a..2c3bbe2 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@ If you are interested in _Jetzig_ you will probably find these tools interesting
* [Zap](https://github.com/zigzap/zap)
* [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/)
## Checklist
diff --git a/build.zig b/build.zig
index fb3650c..9915cb0 100644
--- a/build.zig
+++ b/build.zig
@@ -39,7 +39,8 @@ pub fn build(b: *std.Build) !void {
jetzig_module.addImport("zmpl", zmpl_dep.module("zmpl"));
// This is the way to make it look nice in the zig build script
- // If we would do it the other way around, we would have to do b.dependency("jetzig",.{}).builder.dependency("zmpl",.{}).module("zmpl");
+ // If we would do it the other way around, we would have to do
+ // b.dependency("jetzig",.{}).builder.dependency("zmpl",.{}).module("zmpl");
b.modules.put("zmpl", zmpl_dep.module("zmpl")) catch @panic("Out of memory");
const main_tests = b.addTest(.{
@@ -48,6 +49,15 @@ pub fn build(b: *std.Build) !void {
.optimize = optimize,
});
+ const docs_step = b.step("docs", "Generate documentation");
+ const docs_install = b.addInstallDirectory(.{
+ .source_dir = lib.getEmittedDocs(),
+ .install_dir = .prefix,
+ .install_subdir = "docs",
+ });
+
+ docs_step.dependOn(&docs_install.step);
+
main_tests.root_module.addImport("zmpl", zmpl_dep.module("zmpl"));
const run_main_tests = b.addRunArtifact(main_tests);
diff --git a/build.zig.zon b/build.zig.zon
index dd0a464..403ae25 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -3,8 +3,8 @@
.version = "0.0.0",
.dependencies = .{
.zmpl = .{
- .url = "https://github.com/jetzig-framework/zmpl/archive/621fcc531cb469788b9e887b58c7e76529a81d50.tar.gz",
- .hash = "12200896800328d9e3335b4e3244b84b7664fe2ab9ff2930781be90b9ae658855846",
+ .url = "https://github.com/jetzig-framework/zmpl/archive/41ac97a026e124f93d316eb838fa015a5a52bb15.tar.gz",
+ .hash = "122053b4c76247572201afbbec8fbde8c014c25984a3ca780ed4f6d514cc939038df",
},
},
diff --git a/demo/build.zig b/demo/build.zig
index 3f79ec3..c0d560f 100644
--- a/demo/build.zig
+++ b/demo/build.zig
@@ -36,7 +36,7 @@ pub fn build(b: *std.Build) !void {
b.installArtifact(exe);
- var generate_routes = GenerateRoutes.init(b.allocator, "src/app/views");
+ var generate_routes = try GenerateRoutes.init(b.allocator, "src/app/views");
try generate_routes.generateRoutes();
const write_files = b.addWriteFiles();
const routes_file = write_files.add("routes.zig", generate_routes.buffer.items);
@@ -77,7 +77,7 @@ pub fn build(b: *std.Build) !void {
run_step.dependOn(&run_cmd.step);
const lib_unit_tests = b.addTest(.{
- .root_source_file = .{ .path = "src/root.zig" },
+ .root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
diff --git a/demo/build.zig.zon b/demo/build.zig.zon
index e73a90a..2e43c0f 100644
--- a/demo/build.zig.zon
+++ b/demo/build.zig.zon
@@ -1,19 +1,7 @@
.{
.name = "sandbox-jetzig",
- // This is a [Semantic Version](https://semver.org/).
- // In a future version of Zig it will be used for package deduplication.
.version = "0.0.0",
-
- // This field is optional.
- // This is currently advisory only; Zig does not yet do anything
- // with this value.
- //.minimum_zig_version = "0.11.0",
-
- // This field is optional.
- // Each dependency must either provide a `url` and `hash`, or a `path`.
- // `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
- // Once all dependencies are fetched, `zig build` no longer requires
- // internet connectivity.
+ .minimum_zig_version = "0.12.0",
.dependencies = .{
.jetzig = .{
.path = "../",
diff --git a/demo/src/DemoMiddleware.zig b/demo/src/DemoMiddleware.zig
new file mode 100644
index 0000000..ad8f60e
--- /dev/null
+++ b/demo/src/DemoMiddleware.zig
@@ -0,0 +1,26 @@
+const std = @import("std");
+const jetzig = @import("jetzig");
+
+my_data: u8,
+
+const Self = @This();
+
+pub fn init(request: *jetzig.http.Request) !*Self {
+ var middleware = try request.allocator.create(Self);
+ middleware.my_data = 42;
+ return middleware;
+}
+
+pub fn beforeRequest(self: *Self, request: *jetzig.http.Request) !void {
+ request.server.logger.debug("[middleware] Before request, custom data: {d}", .{self.my_data});
+ self.my_data = 43;
+}
+
+pub fn afterRequest(self: *Self, request: *jetzig.http.Request, result: *jetzig.caches.Result) !void {
+ request.server.logger.debug("[middleware] After request, custom data: {d}", .{self.my_data});
+ request.server.logger.debug("[middleware] content-type: {s}", .{result.value.content_type});
+}
+
+pub fn deinit(self: *Self, request: *jetzig.http.Request) void {
+ request.allocator.destroy(self);
+}
diff --git a/demo/src/app/routes.zig b/demo/src/app/routes.zig
deleted file mode 100644
index d2e49c2..0000000
--- a/demo/src/app/routes.zig
+++ /dev/null
@@ -1,21 +0,0 @@
-pub const routes = struct {
- pub const static = .{
- .{
- .name = "root_index",
- .action = "index",
- .uri_path = "/",
- .template = "root_index",
- .function = @import("root.zig").index,
- },
- };
-
- pub const dynamic = .{
- .{
- .name = "quotes_get",
- .action = "get",
- .uri_path = "/quotes",
- .template = "quotes_get",
- .function = @import("quotes.zig").get,
- },
- };
-};
\ No newline at end of file
diff --git a/demo/src/app/views/quotes.zig b/demo/src/app/views/quotes.zig
index f8f566e..d682c3c 100644
--- a/demo/src/app/views/quotes.zig
+++ b/demo/src/app/views/quotes.zig
@@ -1,11 +1,7 @@
const std = @import("std");
const jetzig = @import("jetzig");
-const Request = jetzig.http.Request;
-const Data = jetzig.data.Data;
-const View = jetzig.views.View;
-
-pub fn get(id: []const u8, request: *Request, data: *Data) !View {
+pub fn get(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
var body = try data.object();
const random_quote = try randomQuote(request.allocator);
@@ -21,6 +17,13 @@ pub fn get(id: []const u8, request: *Request, data: *Data) !View {
return request.render(.ok);
}
+pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ const params = try request.params();
+ std.debug.print("{}\n", .{params});
+ return request.render(.ok);
+}
+
const Quote = struct {
quote: []const u8,
author: []const u8,
diff --git a/demo/src/app/views/quotes/post.zmpl b/demo/src/app/views/quotes/post.zmpl
new file mode 100644
index 0000000..0c69b5d
--- /dev/null
+++ b/demo/src/app/views/quotes/post.zmpl
@@ -0,0 +1 @@
+
diff --git a/demo/src/app/views/routes.zig b/demo/src/app/views/routes.zig
deleted file mode 100644
index d2e49c2..0000000
--- a/demo/src/app/views/routes.zig
+++ /dev/null
@@ -1,21 +0,0 @@
-pub const routes = struct {
- pub const static = .{
- .{
- .name = "root_index",
- .action = "index",
- .uri_path = "/",
- .template = "root_index",
- .function = @import("root.zig").index,
- },
- };
-
- pub const dynamic = .{
- .{
- .name = "quotes_get",
- .action = "get",
- .uri_path = "/quotes",
- .template = "quotes_get",
- .function = @import("quotes.zig").get,
- },
- };
-};
\ No newline at end of file
diff --git a/demo/src/app/views/zmpl.manifest.zig b/demo/src/app/views/zmpl.manifest.zig
index 68e3a4c..54152c0 100644
--- a/demo/src/app/views/zmpl.manifest.zig
+++ b/demo/src/app/views/zmpl.manifest.zig
@@ -3,5 +3,6 @@
// This file should _not_ be stored in version control.
pub const templates = struct {
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/demo/src/main.zig b/demo/src/main.zig
index fcbfd08..d5fcd2b 100644
--- a/demo/src/main.zig
+++ b/demo/src/main.zig
@@ -4,6 +4,10 @@ pub const jetzig = @import("jetzig");
pub const templates = @import("app/views/zmpl.manifest.zig").templates;
pub const routes = @import("routes").routes;
+pub const jetzig_options = struct {
+ pub const middleware: []const type = &.{@import("DemoMiddleware.zig")};
+};
+
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer std.debug.assert(gpa.deinit() == .ok);
diff --git a/src/GenerateRoutes.zig b/src/GenerateRoutes.zig
index 7b897ae..6314dc2 100644
--- a/src/GenerateRoutes.zig
+++ b/src/GenerateRoutes.zig
@@ -1,18 +1,22 @@
const std = @import("std");
+const jetzig = @import("jetzig.zig");
+ast: std.zig.Ast = undefined,
allocator: std.mem.Allocator,
views_path: []const u8,
buffer: std.ArrayList(u8),
dynamic_routes: std.ArrayList(Function),
static_routes: std.ArrayList(Function),
+data: *jetzig.data.Data,
const Self = @This();
const Function = struct {
name: []const u8,
- params: []Param,
+ args: []Arg,
path: []const u8,
source: []const u8,
+ params: std.ArrayList([]const u8),
pub fn fullName(self: @This(), allocator: std.mem.Allocator) ![]const u8 {
var path = try allocator.dupe(u8, self.path);
@@ -46,7 +50,8 @@ const Function = struct {
}
};
-const Param = struct {
+// An argument passed to a view function.
+const Arg = struct {
name: []const u8,
type_name: []const u8,
@@ -68,22 +73,28 @@ const Param = struct {
}
};
-pub fn init(allocator: std.mem.Allocator, views_path: []const u8) Self {
+pub fn init(allocator: std.mem.Allocator, views_path: []const u8) !Self {
+ const data = try allocator.create(jetzig.data.Data);
+ data.* = jetzig.data.Data.init(allocator);
+
return .{
.allocator = allocator,
.views_path = views_path,
.buffer = std.ArrayList(u8).init(allocator),
.static_routes = std.ArrayList(Function).init(allocator),
.dynamic_routes = std.ArrayList(Function).init(allocator),
+ .data = data,
};
}
pub fn deinit(self: *Self) void {
+ self.ast.deinit(self.allocator);
self.buffer.deinit();
self.static_routes.deinit();
self.dynamic_routes.deinit();
}
+/// Generates the complete route set for the application
pub fn generateRoutes(self: *Self) !void {
const writer = self.buffer.writer();
@@ -104,29 +115,36 @@ pub fn generateRoutes(self: *Self) !void {
if (std.mem.startsWith(u8, basename, ".")) continue;
if (!std.mem.eql(u8, extension, ".zig")) continue;
- const routes = try self.generateRoute(views_dir, entry.path);
+ const view_routes = try self.generateRoutesForView(views_dir, entry.path);
- for (routes.static) |route| {
- try self.static_routes.append(route);
+ for (view_routes.static) |view_route| {
+ try self.static_routes.append(view_route);
}
- for (routes.dynamic) |route| {
- try self.dynamic_routes.append(route);
+ for (view_routes.dynamic) |view_route| {
+ try self.dynamic_routes.append(view_route);
}
}
std.sort.pdq(Function, self.static_routes.items, {}, Function.lessThanFn);
std.sort.pdq(Function, self.dynamic_routes.items, {}, Function.lessThanFn);
- try writer.writeAll("pub const routes = struct {\n");
- try writer.writeAll(" pub const static = .{\n");
+ try writer.writeAll(
+ \\pub const routes = struct {
+ \\ pub const static = .{
+ \\
+ );
for (self.static_routes.items) |static_route| {
try self.writeRoute(writer, static_route);
}
- try writer.writeAll(" };\n\n");
- try writer.writeAll(" pub const dynamic = .{\n");
+ try writer.writeAll(
+ \\ };
+ \\
+ \\ pub const dynamic = .{
+ \\
+ );
for (self.dynamic_routes.items) |dynamic_route| {
try self.writeRoute(writer, dynamic_route);
@@ -153,10 +171,26 @@ fn writeRoute(self: *Self, writer: std.ArrayList(u8).Writer, route: Function) !v
\\ .uri_path = "{s}",
\\ .template = "{s}",
\\ .function = @import("{s}").{s},
+ \\ .params = {s},
\\ }},
\\
;
+ var params_buf = std.ArrayList(u8).init(self.allocator);
+ const params_writer = params_buf.writer();
+ defer params_buf.deinit();
+ if (route.params.items.len > 0) {
+ try params_writer.writeAll(".{\n");
+ for (route.params.items) |item| {
+ try params_writer.writeAll(" ");
+ try params_writer.writeAll(item);
+ try params_writer.writeAll(",\n");
+ }
+ try params_writer.writeAll(" }");
+ } else {
+ try params_writer.writeAll(".{}");
+ }
+
const output = try std.fmt.allocPrint(self.allocator, output_template, .{
full_name,
route.name,
@@ -164,6 +198,7 @@ fn writeRoute(self: *Self, writer: std.ArrayList(u8).Writer, route: Function) !v
full_name,
route.path,
route.name,
+ params_buf.items,
});
defer self.allocator.free(output);
@@ -175,76 +210,246 @@ const RouteSet = struct {
static: []Function,
};
-fn generateRoute(self: *Self, views_dir: std.fs.Dir, path: []const u8) !RouteSet {
- // REVIEW: Choose a sensible upper limit or allow user to take their own risks here ?
+fn generateRoutesForView(self: *Self, views_dir: std.fs.Dir, path: []const u8) !RouteSet {
const stat = try views_dir.statFile(path);
const source = try views_dir.readFileAllocOptions(self.allocator, path, stat.size, null, @alignOf(u8), 0);
defer self.allocator.free(source);
- var ast = try std.zig.Ast.parse(self.allocator, source, .zig);
- defer ast.deinit(self.allocator);
+ self.ast = try std.zig.Ast.parse(self.allocator, source, .zig);
var static_routes = std.ArrayList(Function).init(self.allocator);
var dynamic_routes = std.ArrayList(Function).init(self.allocator);
+ var static_params: ?*jetzig.data.Value = null;
- for (ast.nodes.items(.tag), 0..) |tag, index| {
- const function = try self.parseTag(ast, tag, index, path, source);
- if (function) |capture| {
- for (capture.params) |param| {
- if (std.mem.eql(u8, try param.typeBasename(), "StaticRequest")) {
- try static_routes.append(capture);
+ for (self.ast.nodes.items(.tag), 0..) |tag, index| {
+ switch (tag) {
+ .fn_proto_multi => {
+ const function = try self.parseFunction(index, path, source);
+ if (function) |capture| {
+ for (capture.args) |arg| {
+ if (std.mem.eql(u8, try arg.typeBasename(), "StaticRequest")) {
+ try static_routes.append(capture);
+ }
+ if (std.mem.eql(u8, try arg.typeBasename(), "Request")) {
+ try dynamic_routes.append(capture);
+ }
+ }
}
- if (std.mem.eql(u8, try param.typeBasename(), "Request")) {
- try dynamic_routes.append(capture);
+ },
+ .simple_var_decl => {
+ const decl = self.ast.simpleVarDecl(asNodeIndex(index));
+ if (self.isStaticParamsDecl(decl)) {
+ const params = try self.data.object();
+ try self.parseStaticParamsDecl(decl, params);
+ static_params = self.data.value;
+ }
+ },
+ else => {},
+ }
+ }
+
+ for (static_routes.items) |*static_route| {
+ var encoded_params = std.ArrayList([]const u8).init(self.allocator);
+ defer encoded_params.deinit();
+
+ if (static_params) |capture| {
+ if (capture.get(static_route.name)) |params| {
+ for (params.array.array.items) |item| { // XXX: Use public interface for Data.Array here ?
+ var json_buf = std.ArrayList(u8).init(self.allocator);
+ defer json_buf.deinit();
+ const json_writer = json_buf.writer();
+ try item.toJson(json_writer);
+ var encoded_buf = std.ArrayList(u8).init(self.allocator);
+ defer encoded_buf.deinit();
+ const writer = encoded_buf.writer();
+ try std.json.encodeJsonString(json_buf.items, .{}, writer);
+ try static_route.params.append(try self.allocator.dupe(u8, encoded_buf.items));
}
}
}
}
- return .{ .dynamic = dynamic_routes.items, .static = static_routes.items };
+ return .{
+ .dynamic = dynamic_routes.items,
+ .static = static_routes.items,
+ };
}
-fn parseTag(
+// Parse the `pub const static_params` definition and into a `jetzig.data.Value`.
+fn parseStaticParamsDecl(self: *Self, decl: std.zig.Ast.full.VarDecl, params: *jetzig.data.Value) !void {
+ const init_node = self.ast.nodes.items(.tag)[decl.ast.init_node];
+ switch (init_node) {
+ .struct_init_dot_two, .struct_init_dot_two_comma => {
+ try self.parseStruct(decl.ast.init_node, params);
+ },
+ else => return,
+ }
+}
+// Recursively parse a struct into a jetzig.data.Value so it can be serialized as JSON and stored
+// in `routes.zig` - used for static param comparison at runtime.
+fn parseStruct(self: *Self, node: std.zig.Ast.Node.Index, params: *jetzig.data.Value) anyerror!void {
+ var struct_buf: [2]std.zig.Ast.Node.Index = undefined;
+ const maybe_struct_init = self.ast.fullStructInit(&struct_buf, node);
+
+ if (maybe_struct_init == null) {
+ std.debug.print("Expected struct node.\n", .{});
+ return error.JetzigAstParserError;
+ }
+
+ const struct_init = maybe_struct_init.?;
+
+ for (struct_init.ast.fields) |field| try self.parseField(field, params);
+}
+
+// Array of param sets for a route, e.g. `.{ .{ .foo = "bar" } }
+fn parseArray(self: *Self, node: std.zig.Ast.Node.Index, params: *jetzig.data.Value) anyerror!void {
+ var array_buf: [2]std.zig.Ast.Node.Index = undefined;
+ const maybe_array = self.ast.fullArrayInit(&array_buf, node);
+
+ if (maybe_array == null) {
+ std.debug.print("Expected array node.\n", .{});
+ return error.JetzigAstParserError;
+ }
+
+ const array = maybe_array.?;
+
+ const main_token = self.ast.nodes.items(.main_token)[node];
+ const field_name = self.ast.tokenSlice(main_token - 3);
+
+ const params_array = try self.data.createArray();
+ try params.put(field_name, params_array);
+
+ for (array.ast.elements) |element| {
+ const elem = self.ast.nodes.items(.tag)[element];
+ switch (elem) {
+ .struct_init_dot, .struct_init_dot_two, .struct_init_dot_two_comma => {
+ const route_params = try self.data.createObject();
+ try params_array.append(route_params);
+ try self.parseStruct(element, route_params);
+ },
+ .array_init_dot, .array_init_dot_two, .array_init_dot_comma, .array_init_dot_two_comma => {
+ const route_params = try self.data.createObject();
+ try params_array.append(route_params);
+ try self.parseField(element, route_params);
+ },
+ .string_literal => {
+ const string_token = self.ast.nodes.items(.main_token)[element];
+ const string_value = self.ast.tokenSlice(string_token);
+
+ // Strip quotes: `"foo"` -> `foo`
+ try params_array.append(self.data.string(string_value[1 .. string_value.len - 1]));
+ },
+ .number_literal => {
+ const number_token = self.ast.nodes.items(.main_token)[element];
+ const number_value = self.ast.tokenSlice(number_token);
+ try params_array.append(try parseNumber(number_value, self.data));
+ },
+ inline else => {
+ const tag = self.ast.nodes.items(.tag)[element];
+ std.debug.print("Unexpected token: {}\n", .{tag});
+ return error.JetzigStaticParamsParseError;
+ },
+ }
+ }
+}
+
+// Parse the value of a param field (recursively when field is a struct/array)
+fn parseField(self: *Self, node: std.zig.Ast.Node.Index, params: *jetzig.data.Value) anyerror!void {
+ const tag = self.ast.nodes.items(.tag)[node];
+ switch (tag) {
+ // Route params, e.g. `.index = .{ ... }`
+ .array_init_dot, .array_init_dot_two, .array_init_dot_comma, .array_init_dot_two_comma => {
+ try self.parseArray(node, params);
+ },
+ .struct_init_dot, .struct_init_dot_two, .struct_init_dot_two_comma => {
+ const nested_params = try self.data.createObject();
+ const main_token = self.ast.nodes.items(.main_token)[node];
+ const field_name = self.ast.tokenSlice(main_token - 3);
+ try params.put(field_name, nested_params);
+ try self.parseStruct(node, nested_params);
+ },
+ // Individual param in a params struct, e.g. `.foo = "bar"`
+ .string_literal => {
+ const main_token = self.ast.nodes.items(.main_token)[node];
+ const field_name = self.ast.tokenSlice(main_token - 2);
+ const field_value = self.ast.tokenSlice(main_token);
+
+ try params.put(
+ field_name,
+ // strip outer quotes
+ self.data.string(field_value[1 .. field_value.len - 1]),
+ );
+ },
+ .number_literal => {
+ const main_token = self.ast.nodes.items(.main_token)[node];
+ const field_name = self.ast.tokenSlice(main_token - 2);
+ const field_value = self.ast.tokenSlice(main_token);
+
+ try params.put(field_name, try parseNumber(field_value, self.data));
+ },
+ else => {
+ std.debug.print("Unexpected token: {}\n", .{tag});
+ return error.JetzigStaticParamsParseError;
+ },
+ }
+}
+
+fn parseNumber(value: []const u8, data: *jetzig.data.Data) !*jetzig.data.Value {
+ if (std.mem.containsAtLeast(u8, value, 1, ".")) {
+ return data.float(try std.fmt.parseFloat(f64, value));
+ } else {
+ return data.integer(try std.fmt.parseInt(i64, value, 10));
+ }
+}
+
+fn isStaticParamsDecl(self: *Self, decl: std.zig.Ast.full.VarDecl) bool {
+ if (decl.visib_token) |token_index| {
+ const visibility = self.ast.tokenSlice(token_index);
+ const mutability = self.ast.tokenSlice(decl.ast.mut_token);
+ const identifier = self.ast.tokenSlice(decl.ast.mut_token + 1); // FIXME
+ return (std.mem.eql(u8, visibility, "pub") and
+ std.mem.eql(u8, mutability, "const") and
+ std.mem.eql(u8, identifier, "static_params"));
+ } else {
+ return false;
+ }
+}
+
+fn parseFunction(
self: *Self,
- ast: std.zig.Ast,
- tag: std.zig.Ast.Node.Tag,
index: usize,
path: []const u8,
source: []const u8,
) !?Function {
- switch (tag) {
- .fn_proto_multi => {
- const fn_proto = ast.fnProtoMulti(@as(u32, @intCast(index)));
- if (fn_proto.name_token) |token| {
- const function_name = try self.allocator.dupe(u8, ast.tokenSlice(token));
- var it = fn_proto.iterate(&ast);
- var params = std.ArrayList(Param).init(self.allocator);
- defer params.deinit();
+ const fn_proto = self.ast.fnProtoMulti(@as(u32, @intCast(index)));
+ if (fn_proto.name_token) |token| {
+ const function_name = try self.allocator.dupe(u8, self.ast.tokenSlice(token));
+ var it = fn_proto.iterate(&self.ast);
+ var args = std.ArrayList(Arg).init(self.allocator);
+ defer args.deinit();
- while (it.next()) |param| {
- if (param.name_token) |param_token| {
- const param_name = ast.tokenSlice(param_token);
- const node = ast.nodes.get(param.type_expr);
- const type_name = try self.parseTypeExpr(ast, node);
- try params.append(.{ .name = param_name, .type_name = type_name });
- }
- }
-
- return .{
- .name = function_name,
- .path = try self.allocator.dupe(u8, path),
- .params = try self.allocator.dupe(Param, params.items),
- .source = try self.allocator.dupe(u8, source),
- };
+ while (it.next()) |arg| {
+ if (arg.name_token) |arg_token| {
+ const arg_name = self.ast.tokenSlice(arg_token);
+ const node = self.ast.nodes.get(arg.type_expr);
+ const type_name = try self.parseTypeExpr(node);
+ try args.append(.{ .name = arg_name, .type_name = type_name });
}
- },
- else => {},
+ }
+
+ return .{
+ .name = function_name,
+ .path = try self.allocator.dupe(u8, path),
+ .args = try self.allocator.dupe(Arg, args.items),
+ .source = try self.allocator.dupe(u8, source),
+ .params = std.ArrayList([]const u8).init(self.allocator),
+ };
}
return null;
}
-fn parseTypeExpr(self: *Self, ast: std.zig.Ast, node: std.zig.Ast.Node) ![]const u8 {
+fn parseTypeExpr(self: *Self, node: std.zig.Ast.Node) ![]const u8 {
switch (node.tag) {
// Currently all expected params are pointers, keeping this here in case that changes in future:
.identifier => {},
@@ -252,11 +457,11 @@ fn parseTypeExpr(self: *Self, ast: std.zig.Ast, node: std.zig.Ast.Node) ![]const
var buf = std.ArrayList([]const u8).init(self.allocator);
defer buf.deinit();
- for (0..(ast.tokens.len - node.main_token)) |index| {
- const token = ast.tokens.get(node.main_token + index);
+ for (0..(self.ast.tokens.len - node.main_token)) |index| {
+ const token = self.ast.tokens.get(node.main_token + index);
switch (token.tag) {
.asterisk, .period, .identifier => {
- try buf.append(ast.tokenSlice(@as(u32, @intCast(node.main_token + index))));
+ try buf.append(self.ast.tokenSlice(@as(u32, @intCast(node.main_token + index))));
},
else => return try std.mem.concat(self.allocator, u8, buf.items),
}
@@ -267,3 +472,7 @@ fn parseTypeExpr(self: *Self, ast: std.zig.Ast, node: std.zig.Ast.Node) ![]const
return error.JetzigAstParserError;
}
+
+fn asNodeIndex(index: usize) std.zig.Ast.Node.Index {
+ return @as(std.zig.Ast.Node.Index, @intCast(index));
+}
diff --git a/src/compile_static_routes.zig b/src/compile_static_routes.zig
index 207dc19..cf03fca 100644
--- a/src/compile_static_routes.zig
+++ b/src/compile_static_routes.zig
@@ -16,6 +16,8 @@ pub fn main() !void {
}
fn compileStaticRoutes(allocator: std.mem.Allocator) !void {
+ std.fs.cwd().deleteTree("static") catch {};
+
inline for (routes.static) |static_route| {
const static_view = jetzig.views.Route.ViewType{
.static = @unionInit(
@@ -24,6 +26,12 @@ fn compileStaticRoutes(allocator: std.mem.Allocator) !void {
static_route.function,
),
};
+
+ comptime var static_params_len = 0;
+ inline for (static_route.params) |_| static_params_len += 1;
+ comptime var params: [static_params_len][]const u8 = undefined;
+ inline for (static_route.params, 0..) |json, index| params[index] = json;
+
const route = jetzig.views.Route{
.name = static_route.name,
.action = @field(jetzig.views.Route.Action, static_route.action),
@@ -31,40 +39,72 @@ fn compileStaticRoutes(allocator: std.mem.Allocator) !void {
.static = true,
.uri_path = static_route.uri_path,
.template = static_route.template,
+ .json_params = ¶ms,
};
- var request = try jetzig.http.StaticRequest.init(allocator);
- defer request.deinit();
+ if (static_params_len > 0) {
+ inline for (static_route.params, 0..) |json, index| {
+ var request = try jetzig.http.StaticRequest.init(allocator, json);
+ defer request.deinit();
+ try writeContent(allocator, route, &request, index);
+ }
+ }
- const view = try route.renderStatic(route, &request);
- defer view.deinit();
-
- var dir = try std.fs.cwd().makeOpenPath("static", .{});
- defer dir.close();
-
- const json_path = try std.mem.concat(
- allocator,
- u8,
- &[_][]const u8{ route.name, ".json" },
- );
- defer allocator.free(json_path);
- const json_file = try dir.createFile(json_path, .{ .truncate = true });
- try json_file.writeAll(try view.data.toJson());
- defer json_file.close();
- std.debug.print("[jetzig] Compiled static route: {s}\n", .{json_path});
-
- if (@hasDecl(templates, route.template)) {
- const template = @field(templates, route.template);
- const html_path = try std.mem.concat(
- allocator,
- u8,
- &[_][]const u8{ route.name, ".html" },
- );
- defer allocator.free(html_path);
- const html_file = try dir.createFile(html_path, .{ .truncate = true });
- try html_file.writeAll(try template.render(view.data));
- defer html_file.close();
- std.debug.print("[jetzig] Compiled static route: {s}\n", .{html_path});
+ // Always provide a fallback for non-resource routes (i.e. `index`, `post`) if params
+ // do not match any of the configured param sets.
+ switch (route.action) {
+ .index, .post => {
+ var request = try jetzig.http.StaticRequest.init(allocator, "{}");
+ defer request.deinit();
+ try writeContent(allocator, route, &request, null);
+ },
+ inline else => {},
}
}
}
+
+fn writeContent(
+ allocator: std.mem.Allocator,
+ comptime route: jetzig.views.Route,
+ request: *jetzig.http.StaticRequest,
+ index: ?usize,
+) !void {
+ const index_suffix = if (index) |capture|
+ try std.fmt.allocPrint(allocator, "_{}", .{capture})
+ else
+ try allocator.dupe(u8, "");
+ defer allocator.free(index_suffix);
+
+ const view = try route.renderStatic(route, request);
+ defer view.deinit();
+
+ var dir = try std.fs.cwd().makeOpenPath("static", .{});
+ defer dir.close();
+
+ const json_path = try std.mem.concat(
+ allocator,
+ u8,
+ &[_][]const u8{ route.name, index_suffix, ".json" },
+ );
+ defer allocator.free(json_path);
+
+ const json_file = try dir.createFile(json_path, .{ .truncate = true });
+ try json_file.writeAll(try view.data.toJson());
+ defer json_file.close();
+
+ std.debug.print("[jetzig] Compiled static route: {s}\n", .{json_path});
+
+ if (@hasDecl(templates, route.template)) {
+ const template = @field(templates, route.template);
+ const html_path = try std.mem.concat(
+ allocator,
+ u8,
+ &[_][]const u8{ route.name, index_suffix, ".html" },
+ );
+ defer allocator.free(html_path);
+ const html_file = try dir.createFile(html_path, .{ .truncate = true });
+ try html_file.writeAll(try template.render(view.data));
+ defer html_file.close();
+ std.debug.print("[jetzig] Compiled static route: {s}\n", .{html_path});
+ }
+}
diff --git a/src/init/src/app/views/index.zmpl b/src/init/src/app/views/index.zmpl
deleted file mode 100644
index a8e4bfa..0000000
--- a/src/init/src/app/views/index.zmpl
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{.message}
-
-
-
-
-
-
-
Take a look at the src/app/ directory to see how this application works.
-
-
-
-
diff --git a/src/jetzig.zig b/src/jetzig.zig
index a2b8d77..1801eee 100644
--- a/src/jetzig.zig
+++ b/src/jetzig.zig
@@ -10,6 +10,12 @@ pub const views = @import("jetzig/views.zig");
pub const colors = @import("jetzig/colors.zig");
pub const App = @import("jetzig/App.zig");
+// Convenience for view function parameters.
+pub const Request = http.Request;
+pub const StaticRequest = http.StaticRequest;
+pub const Data = data.Data;
+pub const View = views.View;
+
pub const config = struct {
pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 16);
pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 16);
@@ -97,6 +103,7 @@ pub fn route(comptime routes: anytype) []views.Route {
.static = false,
.uri_path = dynamic_route.uri_path,
.template = dynamic_route.template,
+ .json_params = &.{},
};
index += 1;
}
@@ -110,6 +117,11 @@ pub fn route(comptime routes: anytype) []views.Route {
),
};
+ comptime var params_size = 0;
+ inline for (static_route.params) |_| params_size += 1;
+ comptime var static_params: [params_size][]const u8 = undefined;
+ inline for (static_route.params, 0..) |json, params_index| static_params[params_index] = json;
+
detected[index] = .{
.name = static_route.name,
.action = @field(views.Route.Action, static_route.action),
@@ -117,6 +129,7 @@ pub fn route(comptime routes: anytype) []views.Route {
.static = true,
.uri_path = static_route.uri_path,
.template = static_route.template,
+ .json_params = &static_params,
};
index += 1;
}
diff --git a/src/jetzig/App.zig b/src/jetzig/App.zig
index 061c173..419660c 100644
--- a/src/jetzig/App.zig
+++ b/src/jetzig/App.zig
@@ -24,6 +24,15 @@ pub fn start(self: Self, routes: []jetzig.views.Route, templates: []jetzig.Templ
templates,
);
+ for (routes) |*route| {
+ var mutable = @constCast(route); // FIXME
+ try mutable.initParams(self.allocator);
+ }
+ defer for (routes) |*route| {
+ var mutable = @constCast(route); // FIXME
+ mutable.deinitParams();
+ };
+
defer server.deinit();
defer self.allocator.free(self.root_path);
defer self.allocator.free(self.host);
diff --git a/src/jetzig/http.zig b/src/jetzig/http.zig
index 1519888..98c2b63 100644
--- a/src/jetzig/http.zig
+++ b/src/jetzig/http.zig
@@ -7,4 +7,6 @@ pub const Response = @import("http/Response.zig");
pub const Session = @import("http/Session.zig");
pub const Cookies = @import("http/Cookies.zig");
pub const Headers = @import("http/Headers.zig");
+pub const Query = @import("http/Query.zig");
pub const status_codes = @import("http/status_codes.zig");
+pub const middleware = @import("http/middleware.zig");
diff --git a/src/jetzig/http/Query.zig b/src/jetzig/http/Query.zig
new file mode 100644
index 0000000..5faaaff
--- /dev/null
+++ b/src/jetzig/http/Query.zig
@@ -0,0 +1,157 @@
+const std = @import("std");
+const jetzig = @import("../../jetzig.zig");
+
+const Self = @This();
+
+allocator: std.mem.Allocator,
+query_string: []const u8,
+query_items: std.ArrayList(QueryItem),
+data: *jetzig.data.Data,
+
+pub const QueryItem = struct {
+ key: []const u8,
+ value: []const u8,
+};
+
+pub fn init(allocator: std.mem.Allocator, query_string: []const u8, data: *jetzig.data.Data) Self {
+ return .{
+ .allocator = allocator,
+ .query_string = query_string,
+ .query_items = std.ArrayList(QueryItem).init(allocator),
+ .data = data,
+ };
+}
+
+pub fn deinit(self: *Self) void {
+ self.query_items.deinit();
+ self.data.deinit();
+}
+
+pub fn parse(self: *Self) !void {
+ var pairs_it = std.mem.splitScalar(u8, self.query_string, '&');
+
+ while (pairs_it.next()) |pair| {
+ var key_value_it = std.mem.splitScalar(u8, pair, '=');
+ var count: u2 = 0;
+ var key: []const u8 = undefined;
+ var value: []const u8 = undefined;
+
+ while (key_value_it.next()) |key_or_value| {
+ switch (count) {
+ 0 => key = key_or_value,
+ 1 => value = key_or_value,
+ else => return error.JetzigQueryParseError,
+ }
+ count += 1;
+ }
+ try self.query_items.append(.{ .key = key, .value = value });
+ }
+
+ var params = try self.data.object();
+ for (self.query_items.items) |item| {
+ // TODO: Allow nested array/mapping params (`foo[bar][baz]=abc`)
+ if (arrayParam(item.key)) |key| {
+ if (params.get(key)) |value| {
+ switch (value.*) {
+ .array => try value.array.append(self.data.string(item.value)),
+ else => return error.JetzigQueryParseError,
+ }
+ } else {
+ var array = try self.data.createArray();
+ try array.append(self.data.string(item.value));
+ try params.put(key, array);
+ }
+ } else if (mappingParam(item.key)) |mapping| {
+ if (params.get(mapping.key)) |value| {
+ switch (value.*) {
+ .object => try value.object.put(mapping.field, self.data.string(item.value)),
+ else => return error.JetzigQueryParseError,
+ }
+ } else {
+ var object = try self.data.createObject();
+ try object.put(mapping.field, self.data.string(item.value));
+ try params.put(mapping.key, object);
+ }
+ } else {
+ try params.put(item.key, self.data.string(item.value));
+ }
+ }
+}
+
+fn arrayParam(key: []const u8) ?[]const u8 {
+ if (key.len >= 3 and std.mem.eql(u8, key[key.len - 2 ..], "[]")) {
+ return key[0 .. key.len - 2];
+ } else {
+ return null;
+ }
+}
+
+fn mappingParam(input: []const u8) ?struct { key: []const u8, field: []const u8 } {
+ if (input.len < 4) return null; // Must be at least `a[b]`
+
+ const open = std.mem.indexOfScalar(u8, input, '[');
+ const close = std.mem.lastIndexOfScalar(u8, input, ']');
+ if (open == null or close == null) return null;
+
+ const open_index = open.?;
+ const close_index = close.?;
+ if (close_index < open_index) return null;
+
+ return .{
+ .key = input[0..open_index],
+ .field = input[open_index + 1 .. close_index],
+ };
+}
+
+test "simple query string" {
+ const allocator = std.testing.allocator;
+ const query_string = "foo=bar&baz=qux";
+ var data = jetzig.data.Data.init(allocator);
+
+ var query = init(allocator, query_string, &data);
+ defer query.deinit();
+
+ try query.parse();
+ try std.testing.expectEqualStrings((try data.get("foo")).string.value, "bar");
+ try std.testing.expectEqualStrings((try data.get("baz")).string.value, "qux");
+}
+
+test "query string with array values" {
+ const allocator = std.testing.allocator;
+ const query_string = "foo[]=bar&foo[]=baz";
+ var data = jetzig.data.Data.init(allocator);
+
+ var query = init(allocator, query_string, &data);
+ defer query.deinit();
+
+ try query.parse();
+
+ const value = try data.get("foo");
+ switch (value.*) {
+ .array => |array| {
+ try std.testing.expectEqualStrings(array.get(0).?.string.value, "bar");
+ try std.testing.expectEqualStrings(array.get(1).?.string.value, "baz");
+ },
+ else => unreachable,
+ }
+}
+
+test "query string with mapping values" {
+ const allocator = std.testing.allocator;
+ const query_string = "foo[bar]=baz&foo[qux]=quux";
+ var data = jetzig.data.Data.init(allocator);
+
+ var query = init(allocator, query_string, &data);
+ defer query.deinit();
+
+ try query.parse();
+
+ const value = try data.get("foo");
+ switch (value.*) {
+ .object => |object| {
+ try std.testing.expectEqualStrings(object.get("bar").?.string.value, "baz");
+ try std.testing.expectEqualStrings(object.get("qux").?.string.value, "quux");
+ },
+ else => unreachable,
+ }
+}
diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig
index 76dc220..cda2934 100644
--- a/src/jetzig/http/Request.zig
+++ b/src/jetzig/http/Request.zig
@@ -18,29 +18,18 @@ server: *jetzig.http.Server,
session: *jetzig.http.Session,
status_code: jetzig.http.status_codes.StatusCode = undefined,
response_data: *jetzig.data.Data,
+query_data: *jetzig.data.Data,
+query: *jetzig.http.Query,
cookies: *jetzig.http.Cookies,
+body: []const u8,
pub fn init(
allocator: std.mem.Allocator,
server: *jetzig.http.Server,
- response: ?*std.http.Server.Response,
+ response: *std.http.Server.Response,
+ body: []const u8,
) !Self {
- if (response) |_| {} else {
- return .{
- .allocator = allocator,
- .path = "/",
- .method = .GET,
- .headers = jetzig.http.Headers.init(allocator, std.http.Headers{ .allocator = allocator }),
- .server = server,
- .segments = std.ArrayList([]const u8).init(allocator),
- .cookies = try allocator.create(jetzig.http.Cookies),
- .session = try allocator.create(jetzig.http.Session),
- .response_data = try allocator.create(jetzig.data.Data),
- };
- }
-
- const resp = response.?;
- const method = switch (resp.request.method) {
+ const method = switch (response.request.method) {
.DELETE => Method.DELETE,
.GET => Method.GET,
.PATCH => Method.PATCH,
@@ -53,14 +42,14 @@ pub fn init(
_ => return error.JetzigUnsupportedHttpMethod,
};
- var it = std.mem.splitScalar(u8, resp.request.target, '/');
+ var it = std.mem.splitScalar(u8, response.request.target, '/');
var segments = std.ArrayList([]const u8).init(allocator);
while (it.next()) |segment| try segments.append(segment);
var cookies = try allocator.create(jetzig.http.Cookies);
cookies.* = jetzig.http.Cookies.init(
allocator,
- resp.request.headers.getFirstValue("Cookie") orelse "",
+ response.request.headers.getFirstValue("Cookie") orelse "",
);
try cookies.parse();
@@ -79,16 +68,24 @@ pub fn init(
const response_data = try allocator.create(jetzig.data.Data);
response_data.* = jetzig.data.Data.init(allocator);
+ const query_data = try allocator.create(jetzig.data.Data);
+ query_data.* = jetzig.data.Data.init(allocator);
+
+ const query = try allocator.create(jetzig.http.Query);
+
return .{
.allocator = allocator,
- .path = resp.request.target,
+ .path = response.request.target,
.method = method,
- .headers = jetzig.http.Headers.init(allocator, resp.request.headers),
+ .headers = jetzig.http.Headers.init(allocator, response.request.headers),
.server = server,
.segments = segments,
.cookies = cookies,
.session = session,
.response_data = response_data,
+ .query_data = query_data,
+ .query = query,
+ .body = body,
};
}
@@ -111,6 +108,56 @@ pub fn getHeader(self: *Self, key: []const u8) ?[]const u8 {
return self.headers.getFirstValue(key);
}
+/// Provides a `Value` representing request parameters. Parameters are normalized, meaning that
+/// both the JSON request body and query parameters are accessed via the same interface.
+/// Note that query parameters are supported for JSON requests if no request body is present,
+/// otherwise the parsed JSON request body will take precedence and query parameters will be
+/// ignored.
+pub fn params(self: *Self) !*jetzig.data.Value {
+ switch (self.requestFormat()) {
+ .JSON => {
+ if (self.body.len == 0) return self.queryParams();
+
+ var data = try self.allocator.create(jetzig.data.Data);
+ data.* = jetzig.data.Data.init(self.allocator);
+ data.fromJson(self.body) catch |err| {
+ switch (err) {
+ error.UnexpectedEndOfInput => return error.JetzigBodyParseError,
+ else => return err,
+ }
+ };
+ return data.value.?;
+ },
+ .HTML, .UNKNOWN => return self.queryParams(),
+ }
+}
+
+fn queryParams(self: *Self) !*jetzig.data.Value {
+ if (!try self.parseQueryString()) {
+ self.query.data = try self.allocator.create(jetzig.data.Data);
+ self.query.data.* = jetzig.data.Data.init(self.allocator);
+ _ = try self.query.data.object();
+ }
+ return self.query.data.value.?;
+}
+
+fn parseQueryString(self: *Self) !bool {
+ const delimiter_index = std.mem.indexOfScalar(u8, self.path, '?');
+ if (delimiter_index) |index| {
+ if (self.path.len - 1 < index + 1) return false;
+
+ self.query.* = jetzig.http.Query.init(
+ self.server.allocator,
+ self.path[index + 1 ..],
+ self.query_data,
+ );
+ try self.query.parse();
+ return true;
+ }
+
+ return false;
+}
+
fn extensionFormat(self: *Self) ?jetzig.http.Request.Format {
const extension = std.fs.path.extension(self.path);
@@ -168,6 +215,9 @@ pub fn resourceName(self: *Self) []const u8 {
if (self.segments.items.len == 0) return "default";
const basename = std.fs.path.basename(self.segments.items[self.segments.items.len - 1]);
+ if (std.mem.indexOfScalar(u8, basename, '?')) |index| {
+ return basename[0..index];
+ }
const extension = std.fs.path.extension(basename);
return basename[0 .. basename.len - extension.len];
}
@@ -181,35 +231,53 @@ pub fn resourcePath(self: *Self) ![]const u8 {
return try std.mem.concat(self.allocator, u8, &[_][]const u8{ "/", path });
}
+/// For a path `/foo/bar/baz/123.json`, returns `"123"`.
pub fn resourceId(self: *Self) []const u8 {
return self.resourceName();
}
+// Determine if a given route matches the current request.
pub fn match(self: *Self, route: jetzig.views.Route) !bool {
switch (self.method) {
.GET => {
return switch (route.action) {
- .index => std.mem.eql(u8, self.pathWithoutExtension(), route.uri_path),
- .get => std.mem.eql(u8, self.pathWithoutResourceId(), route.uri_path),
+ .index => self.isMatch(.exact, route),
+ .get => self.isMatch(.resource_id, route),
else => false,
};
},
- .POST => return route.action == .post,
- .PUT => return route.action == .put,
- .PATCH => return route.action == .patch,
- .DELETE => return route.action == .delete,
+ .POST => return self.isMatch(.exact, route),
+ .PUT => return self.isMatch(.resource_id, route),
+ .PATCH => return self.isMatch(.resource_id, route),
+ .DELETE => return self.isMatch(.resource_id, route),
else => return false,
}
return false;
}
-fn pathWithoutExtension(self: *Self) []const u8 {
- const index = std.mem.indexOfScalar(u8, self.path, '.');
- if (index) |capture| return self.path[0..capture] else return self.path;
+fn isMatch(self: *Self, match_type: enum { exact, resource_id }, route: jetzig.views.Route) bool {
+ const path = switch (match_type) {
+ .exact => self.pathWithoutExtension(),
+ .resource_id => self.pathWithoutExtensionAndResourceId(),
+ };
+
+ return (std.mem.eql(u8, path, route.uri_path));
}
-fn pathWithoutResourceId(self: *Self) []const u8 {
+// TODO: Be a bit more deterministic in identifying extension, e.g. deal with `.` characters
+// elsewhere in the path (e.g. in query string).
+fn pathWithoutExtension(self: *Self) []const u8 {
+ const extension_index = std.mem.lastIndexOfScalar(u8, self.path, '.');
+ if (extension_index) |capture| return self.path[0..capture];
+
+ const query_index = std.mem.indexOfScalar(u8, self.path, '?');
+ if (query_index) |capture| return self.path[0..capture];
+
+ return self.path;
+}
+
+fn pathWithoutExtensionAndResourceId(self: *Self) []const u8 {
const path = self.pathWithoutExtension();
const index = std.mem.lastIndexOfScalar(u8, self.path, '/');
if (index) |capture| {
diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig
index 5a2ae5c..670f7a5 100644
--- a/src/jetzig/http/Server.zig
+++ b/src/jetzig/http/Server.zig
@@ -3,8 +3,11 @@ const std = @import("std");
const jetzig = @import("../../jetzig.zig");
const root_file = @import("root");
-const jetzig_server_options = if (@hasDecl(root_file, "jetzig_options")) root_file.jetzig_options else struct {};
-const middlewares: []const type = if (@hasDecl(jetzig_server_options, "middlewares")) jetzig_server_options.middlewares else &.{};
+
+pub const jetzig_server_options = if (@hasDecl(root_file, "jetzig_options"))
+ root_file.jetzig_options
+else
+ struct {};
pub const ServerOptions = struct {
cache: jetzig.caches.Cache,
@@ -94,38 +97,15 @@ fn processNextRequest(self: *Self, response: *std.http.Server.Response) !void {
var arena = std.heap.ArenaAllocator.init(self.allocator);
defer arena.deinit();
- var request = try jetzig.http.Request.init(arena.allocator(), self, response);
+ var request = try jetzig.http.Request.init(arena.allocator(), self, response, body);
defer request.deinit();
- var middleware_data = std.BoundedArray(*anyopaque, middlewares.len).init(0) catch unreachable;
- inline for (middlewares, 0..) |middleware, index| {
- if (comptime !@hasDecl(middleware, "init")) continue;
- const data = try @call(.always_inline, middleware.init, .{&request});
- middleware_data.insert(index, data) catch unreachable; // We cannot overflow here because we know the length of the array
- }
-
- inline for (middlewares, 0..) |middleware, index| {
- if (comptime !@hasDecl(middleware, "beforeRequest")) continue;
- if (comptime @hasDecl(middleware, "init")) {
- const data = middleware_data.get(index);
- try @call(.always_inline, middleware.beforeRequest, .{ @as(*middleware, @ptrCast(@alignCast(data))), &request });
- } else {
- try @call(.always_inline, middleware.beforeRequest, .{&request});
- }
- }
+ var middleware_data = try jetzig.http.middleware.beforeMiddleware(&request);
var result = try self.pageContent(&request);
defer result.deinit();
- inline for (middlewares, 0..) |middleware, index| {
- if (comptime !@hasDecl(middleware, "afterRequest")) continue;
- if (comptime @hasDecl(middleware, "init")) {
- const data = middleware_data.get(index);
- try @call(.always_inline, middleware.afterRequest, .{ @as(*middleware, @ptrCast(@alignCast(data))), &request, &result });
- } else {
- try @call(.always_inline, middleware.afterRequest, .{ &request, &result });
- }
- }
+ try jetzig.http.middleware.afterMiddleware(&middleware_data, &request, &result);
response.transfer_encoding = .{ .content_length = result.value.content.len };
var cookie_it = request.cookies.headerIterator();
@@ -133,28 +113,22 @@ fn processNextRequest(self: *Self, response: *std.http.Server.Response) !void {
// FIXME: Skip setting cookies that are already present ?
try response.headers.append("Set-Cookie", header);
}
+
try response.headers.append("Content-Type", result.value.content_type);
+
response.status = switch (result.value.status_code) {
inline else => |status_code| @field(std.http.Status, @tagName(status_code)),
};
try response.send();
try response.writeAll(result.value.content);
-
try response.finish();
const log_message = try self.requestLogMessage(&request, result);
defer self.allocator.free(log_message);
self.logger.debug("{s}", .{log_message});
- inline for (middlewares, 0..) |middleware, index| {
- if (comptime @hasDecl(middleware, "init")) {
- if (comptime @hasDecl(middleware, "deinit")) {
- const data = middleware_data.get(index);
- @call(.always_inline, middleware.deinit, .{ @as(*middleware, @ptrCast(@alignCast(data))), &request });
- }
- }
- }
+ jetzig.http.middleware.deinit(&middleware_data, &request);
}
fn pageContent(self: *Self, request: *jetzig.http.Request) !jetzig.caches.Result {
@@ -171,7 +145,7 @@ fn pageContent(self: *Self, request: *jetzig.http.Request) !jetzig.caches.Result
fn renderResponse(self: *Self, request: *jetzig.http.Request) !jetzig.http.Response {
const static = self.matchStaticResource(request) catch |err| {
if (isUnhandledError(err)) return err;
- const rendered = try self.internalServerError(request, err);
+ const rendered = try self.renderInternalServerError(request, err);
return .{
.allocator = self.allocator,
.status_code = .internal_server_error,
@@ -182,7 +156,7 @@ fn renderResponse(self: *Self, request: *jetzig.http.Request) !jetzig.http.Respo
if (static) |resource| return try self.renderStatic(request, resource);
- const route = try self.matchRoute(request);
+ const route = try self.matchRoute(request, false);
switch (request.requestFormat()) {
.HTML => return self.renderHTML(request, route),
@@ -273,13 +247,21 @@ fn renderView(
const view = route.render(route, request) catch |err| {
self.logger.debug("Encountered error: {s}", .{@errorName(err)});
if (isUnhandledError(err)) return err;
- return try self.internalServerError(request, err);
+ if (isBadRequest(err)) return try self.renderBadRequest(request);
+ return try self.renderInternalServerError(request, err);
};
const content = if (template) |capture| try capture.render(view.data) else "";
return .{ .view = view, .content = content };
}
+fn isBadRequest(err: anyerror) bool {
+ return switch (err) {
+ error.JetzigBodyParseError, error.JetzigQueryParseError => true,
+ else => false,
+ };
+}
+
fn isUnhandledError(err: anyerror) bool {
return switch (err) {
error.OutOfMemory => true,
@@ -287,7 +269,7 @@ fn isUnhandledError(err: anyerror) bool {
};
}
-fn internalServerError(self: *Self, request: *jetzig.http.Request, err: anyerror) !RenderedView {
+fn renderInternalServerError(self: *Self, request: *jetzig.http.Request, err: anyerror) !RenderedView {
request.response_data.reset();
var object = try request.response_data.object();
@@ -301,6 +283,20 @@ fn internalServerError(self: *Self, request: *jetzig.http.Request, err: anyerror
.content = "Internal Server Error\n",
};
}
+
+fn renderBadRequest(self: *Self, request: *jetzig.http.Request) !RenderedView {
+ _ = self;
+ request.response_data.reset();
+
+ var object = try request.response_data.object();
+ try object.put("error", request.response_data.string("Bad Request"));
+
+ return .{
+ .view = jetzig.views.View{ .data = request.response_data, .status_code = .bad_request },
+ .content = "Bad Request\n",
+ };
+}
+
fn logStackTrace(
self: *Self,
stack: *std.builtin.StackTrace,
@@ -341,18 +337,31 @@ fn duration(self: *Self) i64 {
return @intCast(std.time.nanoTimestamp() - self.start_time);
}
-fn matchRoute(self: *Self, request: *jetzig.http.Request) !?jetzig.views.Route {
+fn matchRoute(self: *Self, request: *jetzig.http.Request, static: bool) !?jetzig.views.Route {
for (self.routes) |route| {
- if (route.action == .index and try request.match(route)) return route;
+ // .index routes always take precedence.
+ if (route.static == static and route.action == .index and try request.match(route)) return route;
}
for (self.routes) |route| {
- if (try request.match(route)) return route;
+ if (route.static == static and try request.match(route)) return route;
}
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 {
@@ -383,8 +392,10 @@ fn matchPublicContent(self: *Self, request: *jetzig.http.Request) !?[]const u8 {
else => return err,
}
};
+ defer iterable_dir.close();
var walker = try iterable_dir.walk(request.allocator);
+ defer walker.deinit();
while (try walker.next()) |file| {
if (file.kind != .file) continue;
@@ -410,19 +421,14 @@ fn matchStaticContent(self: *Self, request: *jetzig.http.Request) !?[]const u8 {
};
defer static_dir.close();
- // TODO: Use a hashmap to avoid O(n)
- for (self.routes) |route| {
- if (route.static and try request.match(route)) {
- const extension = switch (request.requestFormat()) {
- .HTML, .UNKNOWN => ".html",
- .JSON => ".json",
- };
-
- const path = try std.mem.concat(request.allocator, u8, &[_][]const u8{ route.name, extension });
+ const matched_route = try self.matchRoute(request, true);
+ if (matched_route) |route| {
+ const static_path = try self.staticPath(request, route);
+ if (static_path) |capture| {
return static_dir.readFileAlloc(
request.allocator,
- path,
+ capture,
jetzig.config.max_bytes_static_content,
) catch |err| {
switch (err) {
@@ -430,8 +436,55 @@ fn matchStaticContent(self: *Self, request: *jetzig.http.Request) !?[]const u8 {
else => return err,
}
};
- }
+ } else return null;
}
return null;
}
+
+fn staticPath(self: *Self, request: *jetzig.http.Request, route: jetzig.views.Route) !?[]const u8 {
+ _ = self;
+ const params = try request.params();
+ const extension = switch (request.requestFormat()) {
+ .HTML, .UNKNOWN => ".html",
+ .JSON => ".json",
+ };
+
+ for (route.params.items, 0..) |static_params, index| {
+ if (try static_params.getValue("params")) |expected_params| {
+ switch (route.action) {
+ .index, .post => {},
+ inline else => {
+ if (try static_params.getValue("id")) |id| {
+ switch (id.*) {
+ .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,
+ }
+ }
+ },
+ }
+ if (!expected_params.eql(params)) continue;
+
+ const index_fmt = try std.fmt.allocPrint(request.allocator, "{}", .{index});
+ defer request.allocator.free(index_fmt);
+
+ return try std.mem.concat(
+ request.allocator,
+ u8,
+ &[_][]const u8{ route.name, "_", index_fmt, extension },
+ );
+ }
+ }
+
+ switch (route.action) {
+ .index, .post => return try std.mem.concat(
+ request.allocator,
+ u8,
+ &[_][]const u8{ route.name, "_", extension },
+ ),
+ else => return null,
+ }
+}
diff --git a/src/jetzig/http/Session.zig b/src/jetzig/http/Session.zig
index 777e1ed..9dd55e3 100644
--- a/src/jetzig/http/Session.zig
+++ b/src/jetzig/http/Session.zig
@@ -65,21 +65,21 @@ pub fn deinit(self: *Self) void {
if (self.cookie) |*ptr| self.allocator.free(ptr.*.value);
}
-pub fn get(self: *Self, key: []const u8) !?jetzig.data.Value {
+pub fn get(self: *Self, key: []const u8) !?*jetzig.data.Value {
if (self.state != .parsed) return error.UnparsedSessionCookie;
- return switch (self.data.value.?) {
+ return switch (self.data.value.?.*) {
.object => self.data.value.?.object.get(key),
else => unreachable,
};
}
-pub fn put(self: *Self, key: []const u8, value: jetzig.data.Value) !void {
+pub fn put(self: *Self, key: []const u8, value: *jetzig.data.Value) !void {
if (self.state != .parsed) return error.UnparsedSessionCookie;
- switch (self.data.value.?) {
+ switch (self.data.value.?.*) {
.object => |*object| {
- try object.put(key, value);
+ try object.*.put(key, value);
},
else => unreachable,
}
diff --git a/src/jetzig/http/StaticRequest.zig b/src/jetzig/http/StaticRequest.zig
index 674283b..261d73c 100644
--- a/src/jetzig/http/StaticRequest.zig
+++ b/src/jetzig/http/StaticRequest.zig
@@ -4,11 +4,13 @@ const jetzig = @import("../../jetzig.zig");
response_data: *jetzig.data.Data,
allocator: std.mem.Allocator,
+json: []const u8,
-pub fn init(allocator: std.mem.Allocator) !Self {
+pub fn init(allocator: std.mem.Allocator, json: []const u8) !Self {
return .{
.allocator = allocator,
.response_data = try allocator.create(jetzig.data.Data),
+ .json = json,
};
}
@@ -20,7 +22,23 @@ pub fn render(self: *Self, status_code: jetzig.http.status_codes.StatusCode) jet
return .{ .data = self.response_data, .status_code = status_code };
}
-pub fn resourceId(self: *Self) []const u8 {
- _ = self;
- return "TODO";
+pub fn resourceId(self: *Self) ![]const u8 {
+ var data = try self.allocator.create(jetzig.data.Data);
+ data.* = jetzig.data.Data.init(self.allocator);
+ defer self.allocator.destroy(data);
+ defer data.deinit();
+
+ try data.fromJson(self.json);
+ // Routes generator rejects missing `.id` option so this should always be present.
+ // Note that static requests are never rendered at runtime so we can be unsafe here and risk
+ // failing a build (which would not be coherent if we allowed it to complete).
+ return data.value.?.get("id").?.string.value;
+}
+
+/// Returns the static params defined by `pub const static_params` in the relevant view.
+pub fn params(self: *Self) !*jetzig.data.Value {
+ var data = try self.allocator.create(jetzig.data.Data);
+ data.* = jetzig.data.Data.init(self.allocator);
+ try data.fromJson(self.json);
+ return data.value.?.get("params") orelse data.object();
}
diff --git a/src/jetzig/http/middleware.zig b/src/jetzig/http/middleware.zig
new file mode 100644
index 0000000..f0f817f
--- /dev/null
+++ b/src/jetzig/http/middleware.zig
@@ -0,0 +1,73 @@
+const std = @import("std");
+const jetzig = @import("../../jetzig.zig");
+
+const server_options = jetzig.http.Server.jetzig_server_options;
+
+const middlewares: []const type = if (@hasDecl(server_options, "middleware"))
+ server_options.middleware
+else
+ &.{};
+
+const MiddlewareData = std.BoundedArray(*anyopaque, middlewares.len);
+
+pub fn beforeMiddleware(request: *jetzig.http.Request) !MiddlewareData {
+ var middleware_data = MiddlewareData.init(0) catch unreachable;
+
+ inline for (middlewares, 0..) |middleware, index| {
+ if (comptime !@hasDecl(middleware, "init")) continue;
+ const data = try @call(.always_inline, middleware.init, .{request});
+ // We cannot overflow here because we know the length of the array
+ middleware_data.insert(index, data) catch unreachable;
+ }
+
+ inline for (middlewares, 0..) |middleware, index| {
+ if (comptime !@hasDecl(middleware, "beforeRequest")) continue;
+ if (comptime @hasDecl(middleware, "init")) {
+ const data = middleware_data.get(index);
+ try @call(
+ .always_inline,
+ middleware.beforeRequest,
+ .{ @as(*middleware, @ptrCast(@alignCast(data))), request },
+ );
+ } else {
+ try @call(.always_inline, middleware.beforeRequest, .{request});
+ }
+ }
+
+ return middleware_data;
+}
+
+pub fn afterMiddleware(
+ middleware_data: *MiddlewareData,
+ request: *jetzig.http.Request,
+ result: *jetzig.caches.Result,
+) !void {
+ inline for (middlewares, 0..) |middleware, index| {
+ if (comptime !@hasDecl(middleware, "afterRequest")) continue;
+ if (comptime @hasDecl(middleware, "init")) {
+ const data = middleware_data.get(index);
+ try @call(
+ .always_inline,
+ middleware.afterRequest,
+ .{ @as(*middleware, @ptrCast(@alignCast(data))), request, result },
+ );
+ } else {
+ try @call(.always_inline, middleware.afterRequest, .{ request, result });
+ }
+ }
+}
+
+pub fn deinit(middleware_data: *MiddlewareData, request: *jetzig.http.Request) void {
+ inline for (middlewares, 0..) |middleware, index| {
+ if (comptime @hasDecl(middleware, "init")) {
+ if (comptime @hasDecl(middleware, "deinit")) {
+ const data = middleware_data.get(index);
+ @call(
+ .always_inline,
+ middleware.deinit,
+ .{ @as(*middleware, @ptrCast(@alignCast(data))), request },
+ );
+ }
+ }
+ }
+}
diff --git a/src/jetzig/views/Route.zig b/src/jetzig/views/Route.zig
index 91f7b3c..75061e5 100644
--- a/src/jetzig/views/Route.zig
+++ b/src/jetzig/views/Route.zig
@@ -45,6 +45,25 @@ static: bool,
render: RenderFn = renderFn,
renderStatic: RenderStaticFn = renderStaticFn,
template: []const u8,
+json_params: [][]const u8,
+params: std.ArrayList(*jetzig.data.Data) = undefined,
+
+/// Initializes a route's static params on server launch. Converts static params (JSON strings)
+/// to `jetzig.data.Data` values. Memory is owned by caller (`App.start()`).
+pub fn initParams(self: *Self, allocator: std.mem.Allocator) !void {
+ self.params = std.ArrayList(*jetzig.data.Data).init(allocator);
+ for (self.json_params) |params| {
+ var data = try allocator.create(jetzig.data.Data);
+ data.* = jetzig.data.Data.init(allocator);
+ try self.params.append(data);
+ try data.fromJson(params);
+ }
+}
+
+pub fn deinitParams(self: *const Self) void {
+ for (self.params.items) |data| data.deinit();
+ self.params.deinit();
+}
fn renderFn(self: Self, request: *jetzig.http.Request) anyerror!jetzig.views.View {
switch (self.view.?) {
@@ -70,10 +89,10 @@ fn renderStaticFn(self: Self, request: *jetzig.http.StaticRequest) anyerror!jetz
switch (self.view.?.static) {
.index => |view| return try view(request, request.response_data),
- .get => |view| return try view(request.resourceId(), request, request.response_data),
+ .get => |view| return try view(try request.resourceId(), request, request.response_data),
.post => |view| return try view(request, request.response_data),
- .patch => |view| return try view(request.resourceId(), request, request.response_data),
- .put => |view| return try view(request.resourceId(), request, request.response_data),
- .delete => |view| return try view(request.resourceId(), request, request.response_data),
+ .patch => |view| return try view(try request.resourceId(), request, request.response_data),
+ .put => |view| return try view(try request.resourceId(), request, request.response_data),
+ .delete => |view| return try view(try request.resourceId(), request, request.response_data),
}
}
diff --git a/src/main.zig b/src/main.zig
deleted file mode 100644
index 228e1dd..0000000
--- a/src/main.zig
+++ /dev/null
@@ -1,59 +0,0 @@
-const std = @import("std");
-
-pub const jetzig = @import("jetzig.zig");
-pub const templates = @import("app/views/zmpl.manifest.zig").templates;
-pub const routes = @import("routes").routes;
-
-pub const jetzig_options = struct {
- pub const middleware: []const type = &.{ TestMiddleware, IncompleteMiddleware, IncompleteMiddleware2 };
-};
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer std.debug.assert(gpa.deinit() == .ok);
- const allocator = gpa.allocator();
-
- const app = try jetzig.init(allocator);
- defer app.deinit();
-
- try app.start(
- comptime jetzig.route(routes),
- comptime jetzig.loadTemplates(templates),
- );
-}
-
-const TestMiddleware = struct {
- my_data: u8,
- pub fn init(request: *jetzig.http.Request) !*TestMiddleware {
- var mw = try request.allocator.create(TestMiddleware);
- mw.my_data = 42;
- return mw;
- }
-
- pub fn beforeRequest(middleware: *TestMiddleware, request: *jetzig.http.Request) !void {
- request.server.logger.debug("Before request, custom data: {d}", .{middleware.my_data});
- middleware.my_data = 43;
- }
-
- pub fn afterRequest(middleware: *TestMiddleware, request: *jetzig.http.Request, result: *jetzig.caches.Result) !void {
- request.server.logger.debug("After request, custom data: {d}", .{middleware.my_data});
- request.server.logger.debug("{s}", .{result.value.content_type});
- }
-
- pub fn deinit(middleware: *TestMiddleware, request: *jetzig.http.Request) void {
- request.allocator.destroy(middleware);
- }
-};
-
-const IncompleteMiddleware = struct {
- pub fn beforeRequest(request: *jetzig.http.Request) !void {
- request.server.logger.debug("Before request", .{});
- }
-};
-
-const IncompleteMiddleware2 = struct {
- pub fn afterRequest(request: *jetzig.http.Request, result: *jetzig.caches.Result) !void {
- request.server.logger.debug("After request", .{});
- _ = result;
- }
-};
diff --git a/src/tests.zig b/src/tests.zig
index 8b51ac2..6bf0a52 100644
--- a/src/tests.zig
+++ b/src/tests.zig
@@ -1,4 +1,5 @@
test {
_ = @import("jetzig.zig");
+ _ = @import("jetzig/http/Query.zig");
@import("std").testing.refAllDeclsRecursive(@This());
}