jetzig/src/GenerateRoutes.zig
Bob Farrell 31927cdb6b Static routes
Generate views defined with `request: *jetzig.http.StaticRequest` as
static content into `static/` directory so that exported JSON and HTML
can be rendered direct from disk, skipping runtime rendering.
2024-02-17 15:28:27 +00:00

270 lines
8.8 KiB
Zig

const std = @import("std");
allocator: std.mem.Allocator,
views_path: []const u8,
buffer: std.ArrayList(u8),
dynamic_routes: std.ArrayList(Function),
static_routes: std.ArrayList(Function),
const Self = @This();
const Function = struct {
name: []const u8,
params: []Param,
path: []const u8,
source: []const u8,
pub fn fullName(self: @This(), allocator: std.mem.Allocator) ![]const u8 {
var path = try allocator.dupe(u8, self.path);
const extension = std.fs.path.extension(path);
defer allocator.free(path);
std.mem.replaceScalar(u8, path, std.fs.path.sep, '_');
return std.mem.concat(
allocator,
u8,
&[_][]const u8{ path[0 .. path.len - extension.len], "_", self.name },
);
}
pub fn uriPath(self: @This(), allocator: std.mem.Allocator) ![]const u8 {
if (std.mem.eql(u8, self.path, "root.zig")) return try allocator.dupe(u8, "/");
var path = try allocator.dupe(u8, self.path);
const extension = std.fs.path.extension(path);
defer allocator.free(path);
std.mem.replaceScalar(u8, path, std.fs.path.sep, '/');
return std.mem.concat(
allocator,
u8,
&[_][]const u8{ "/", path[0 .. path.len - extension.len] },
);
}
pub fn lessThanFn(context: void, lhs: @This(), rhs: @This()) bool {
_ = context;
return std.mem.order(u8, lhs.name, rhs.name).compare(std.math.CompareOperator.lt);
}
};
const Param = struct {
name: []const u8,
type_name: []const u8,
pub fn typeBasename(self: @This()) ![]const u8 {
if (std.mem.indexOfScalar(u8, self.type_name, '.')) |_| {
var it = std.mem.splitBackwardsScalar(u8, self.type_name, '.');
while (it.next()) |capture| {
return capture;
}
}
const pointer_start = std.mem.indexOfScalar(u8, self.type_name, '*');
if (pointer_start) |index| {
if (self.type_name.len < index + 1) return error.JetzigAstParserError;
return self.type_name[index + 1 ..];
} else {
return self.type_name;
}
}
};
pub fn init(allocator: std.mem.Allocator, views_path: []const u8) Self {
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),
};
}
pub fn deinit(self: *Self) void {
self.buffer.deinit();
self.static_routes.deinit();
self.dynamic_routes.deinit();
}
pub fn generateRoutes(self: *Self) !void {
const writer = self.buffer.writer();
var views_dir = try std.fs.cwd().openDir(self.views_path, .{ .iterate = true });
defer views_dir.close();
var walker = try views_dir.walk(self.allocator);
defer walker.deinit();
while (try walker.next()) |entry| {
if (entry.kind != .file) continue;
const extension = std.fs.path.extension(entry.path);
const basename = std.fs.path.basename(entry.path);
if (std.mem.eql(u8, basename, "routes.zig")) continue;
if (std.mem.eql(u8, basename, "zmpl.manifest.zig")) continue;
if (std.mem.startsWith(u8, basename, ".")) continue;
if (!std.mem.eql(u8, extension, ".zig")) continue;
const routes = try self.generateRoute(views_dir, entry.path);
for (routes.static) |route| {
try self.static_routes.append(route);
}
for (routes.dynamic) |route| {
try self.dynamic_routes.append(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");
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");
for (self.dynamic_routes.items) |dynamic_route| {
try self.writeRoute(writer, dynamic_route);
const name = try dynamic_route.fullName(self.allocator);
defer self.allocator.free(name);
std.debug.print("[jetzig] Imported route: {s}\n", .{name});
}
try writer.writeAll(" };\n");
try writer.writeAll("};");
}
fn writeRoute(self: *Self, writer: std.ArrayList(u8).Writer, route: Function) !void {
const full_name = try route.fullName(self.allocator);
defer self.allocator.free(full_name);
const uri_path = try route.uriPath(self.allocator);
defer self.allocator.free(uri_path);
const output_template =
\\ .{{
\\ .name = "{s}",
\\ .action = "{s}",
\\ .uri_path = "{s}",
\\ .template = "{s}",
\\ .function = @import("{s}").{s},
\\ }},
\\
;
const output = try std.fmt.allocPrint(self.allocator, output_template, .{
full_name,
route.name,
uri_path,
full_name,
route.path,
route.name,
});
defer self.allocator.free(output);
try writer.writeAll(output);
}
const RouteSet = struct {
dynamic: []Function,
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 ?
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);
var static_routes = std.ArrayList(Function).init(self.allocator);
var dynamic_routes = std.ArrayList(Function).init(self.allocator);
for (ast.nodes.items(.tag), 0..) |tag, index| {
const function = try self.parseTag(ast, tag, index, path, source);
if (function) |capture| {
for (capture.params) |param| {
if (std.mem.eql(u8, try param.typeBasename(), "StaticRequest")) {
try static_routes.append(capture);
}
if (std.mem.eql(u8, try param.typeBasename(), "Request")) {
try dynamic_routes.append(capture);
}
}
}
}
return .{ .dynamic = dynamic_routes.items, .static = static_routes.items };
}
fn parseTag(
self: *Self,
ast: std.zig.Ast,
tag: std.zig.Ast.Node.Tag,
index: usize,
path: []const u8,
source: []const u8,
) !?Function {
switch (tag) {
.fn_proto_multi => {
const fn_proto = ast.fnProtoMulti(@as(u32, @intCast(index)));
if (fn_proto.name_token) |token| {
const function_name = try self.allocator.dupe(u8, ast.tokenSlice(token));
var it = fn_proto.iterate(&ast);
var params = std.ArrayList(Param).init(self.allocator);
defer params.deinit();
while (it.next()) |param| {
if (param.name_token) |param_token| {
const param_name = ast.tokenSlice(param_token);
const node = ast.nodes.get(param.type_expr);
const type_name = try self.parseTypeExpr(ast, node);
try params.append(.{ .name = param_name, .type_name = type_name });
}
}
return .{
.name = function_name,
.path = try self.allocator.dupe(u8, path),
.params = try self.allocator.dupe(Param, params.items),
.source = try self.allocator.dupe(u8, source),
};
}
},
else => {},
}
return null;
}
fn parseTypeExpr(self: *Self, ast: std.zig.Ast, 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 => {},
.ptr_type_aligned => {
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);
switch (token.tag) {
.asterisk, .period, .identifier => {
try buf.append(ast.tokenSlice(@as(u32, @intCast(node.main_token + index))));
},
else => return try std.mem.concat(self.allocator, u8, buf.items),
}
}
},
else => {},
}
return error.JetzigAstParserError;
}