JSON and query param parsing/SSG params

Implement `jetzig.http.Request.params()` which parses either a JSON
request body or a query param string into a `jetzig.data.Value`.

Allow configuring params for static site generation - configure an array
of params for each endpoint which is then parsed at build time and a
separate JSON and HTML output is generated for each by invoking the
relevant view function and passing in resource ID/params. Params are
stored in generated `routes.zig` for route matching at run time.
This commit is contained in:
Bob Farrell 2024-02-15 22:41:06 +00:00
parent 280f1eaadd
commit 216b86c235
29 changed files with 941 additions and 340 deletions

View File

@ -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

View File

@ -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);

View File

@ -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",
},
},

View File

@ -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,
});

View File

@ -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 = "../",

View File

@ -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);
}

View File

@ -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,
},
};
};

View File

@ -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,

View File

@ -0,0 +1 @@
<div>{.param}</div>

View File

@ -1,6 +1,37 @@
const std = @import("std");
const jetzig = @import("jetzig");
pub fn index(request: *jetzig.http.StaticRequest, data: *jetzig.data.Data) !jetzig.views.View {
_ = data;
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);
return request.render(.created);
}

View File

@ -14,6 +14,8 @@
<h1 class="text-3xl text-center p-3 pb-6 font-bold">{.message}</h1>
</div>
<div>{.foo}</div>
<button hx-get="/quotes/random" hx-trigger="click" hx-target="#quote" class="bg-[#39b54a] text-white font-bold py-2 px-4 rounded">Click Me</button>
<div id="quote" class="p-7 mx-auto w-1/2">

View File

@ -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,
},
};
};

View File

@ -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");
};

View File

@ -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);

View File

@ -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);
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.params) |param| {
if (std.mem.eql(u8, try param.typeBasename(), "StaticRequest")) {
for (capture.args) |arg| {
if (std.mem.eql(u8, try arg.typeBasename(), "StaticRequest")) {
try static_routes.append(capture);
}
if (std.mem.eql(u8, try param.typeBasename(), "Request")) {
if (std.mem.eql(u8, try arg.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 => {},
}
}
return .{ .dynamic = dynamic_routes.items, .static = static_routes.items };
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));
}
}
}
}
fn parseTag(
return .{
.dynamic = dynamic_routes.items,
.static = static_routes.items,
};
}
// 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)));
const fn_proto = self.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 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 });
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 });
}
}
return .{
.name = function_name,
.path = try self.allocator.dupe(u8, path),
.params = try self.allocator.dupe(Param, params.items),
.args = try self.allocator.dupe(Arg, args.items),
.source = try self.allocator.dupe(u8, source),
.params = std.ArrayList([]const u8).init(self.allocator),
};
}
},
else => {},
}
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));
}

View File

@ -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,12 +39,43 @@ fn compileStaticRoutes(allocator: std.mem.Allocator) !void {
.static = true,
.uri_path = static_route.uri_path,
.template = static_route.template,
.json_params = &params,
};
var request = try jetzig.http.StaticRequest.init(allocator);
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);
// 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", .{});
@ -45,12 +84,14 @@ fn compileStaticRoutes(allocator: std.mem.Allocator) !void {
const json_path = try std.mem.concat(
allocator,
u8,
&[_][]const u8{ route.name, ".json" },
&[_][]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)) {
@ -58,7 +99,7 @@ fn compileStaticRoutes(allocator: std.mem.Allocator) !void {
const html_path = try std.mem.concat(
allocator,
u8,
&[_][]const u8{ route.name, ".html" },
&[_][]const u8{ route.name, index_suffix, ".html" },
);
defer allocator.free(html_path);
const html_file = try dir.createFile(html_path, .{ .truncate = true });
@ -67,4 +108,3 @@ fn compileStaticRoutes(allocator: std.mem.Allocator) !void {
std.debug.print("[jetzig] Compiled static route: {s}\n", .{html_path});
}
}
}

View File

@ -1,27 +0,0 @@
<html>
<head>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div class="text-center pt-10 m-auto">
<div><img class="p-3 mx-auto" src="/jetzig.png" /></div>
<div>
<h1 class="text-3xl text-center p-3 pb-6 font-bold">{.message}</h1>
</div>
<button hx-get="/quotes/random" hx-trigger="click" hx-target="#quote" class="bg-[#39b54a] text-white font-bold py-2 px-4 rounded">Click Me</button>
<div id="quote" class="p-7 mx-auto w-1/2">
<div hx-get="/quotes/init" hx-trigger="load"></div>
</div>
<div>Take a look at the <span class="font-mono">src/app/</span> directory to see how this application works.</div>
<div>Visit <a class="font-bold text-[#39b54a]" href="https://jetzig.dev/">jetzig.dev</a> to get started.</div>
</div>
</body>
</html>

View File

@ -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;
}

View File

@ -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);

View File

@ -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");

157
src/jetzig/http/Query.zig Normal file
View File

@ -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,
}
}

View File

@ -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| {

View File

@ -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,
}
}

View File

@ -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,
}

View File

@ -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();
}

View File

@ -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 },
);
}
}
}
}

View File

@ -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),
}
}

View File

@ -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;
}
};

View File

@ -1,4 +1,5 @@
test {
_ = @import("jetzig.zig");
_ = @import("jetzig/http/Query.zig");
@import("std").testing.refAllDeclsRecursive(@This());
}