mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 22:16:08 +00:00
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:
parent
280f1eaadd
commit
216b86c235
@ -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
|
||||
|
||||
|
12
build.zig
12
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);
|
||||
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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 = "../",
|
||||
|
26
demo/src/DemoMiddleware.zig
Normal file
26
demo/src/DemoMiddleware.zig
Normal 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);
|
||||
}
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
@ -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,
|
||||
|
1
demo/src/app/views/quotes/post.zmpl
Normal file
1
demo/src/app/views/quotes/post.zmpl
Normal file
@ -0,0 +1 @@
|
||||
<div>{.param}</div>
|
@ -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);
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
@ -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");
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)));
|
||||
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));
|
||||
}
|
||||
|
@ -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 = ¶ms,
|
||||
};
|
||||
|
||||
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 });
|
||||
@ -66,5 +107,4 @@ fn compileStaticRoutes(allocator: std.mem.Allocator) !void {
|
||||
defer html_file.close();
|
||||
std.debug.print("[jetzig] Compiled static route: {s}\n", .{html_path});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
157
src/jetzig/http/Query.zig
Normal 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,
|
||||
}
|
||||
}
|
@ -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| {
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
73
src/jetzig/http/middleware.zig
Normal file
73
src/jetzig/http/middleware.zig
Normal 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
59
src/main.zig
59
src/main.zig
@ -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;
|
||||
}
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
test {
|
||||
_ = @import("jetzig.zig");
|
||||
_ = @import("jetzig/http/Query.zig");
|
||||
@import("std").testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user