jetzig/src/GenerateRoutes.zig
2024-02-25 14:20:22 +00:00

481 lines
17 KiB
Zig

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,
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);
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);
}
};
// An argument passed to a view function.
const Arg = 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 {
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();
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 view_routes = try self.generateRoutesForView(views_dir, entry.path);
for (view_routes.static) |view_route| {
try self.static_routes.append(view_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 {
\\ pub const static = .{
\\
);
for (self.static_routes.items) |static_route| {
try self.writeRoute(writer, static_route);
}
try writer.writeAll(
\\ };
\\
\\ pub const dynamic = .{
\\
);
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("};");
// std.debug.print("routes.zig\n{s}\n", .{self.buffer.items});
}
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},
\\ .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,
uri_path,
full_name,
route.path,
route.name,
params_buf.items,
});
defer self.allocator.free(output);
try writer.writeAll(output);
}
const RouteSet = struct {
dynamic: []Function,
static: []Function,
};
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);
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 (self.ast.nodes.items(.tag), 0..) |tag, index| {
switch (tag) {
.fn_proto_multi => {
const function = try self.parseFunction(index, path, source);
if (function) |capture| {
for (capture.args) |arg| {
if (std.mem.eql(u8, try arg.typeBasename(), "StaticRequest")) {
try static_routes.append(capture);
}
if (std.mem.eql(u8, try arg.typeBasename(), "Request")) {
try dynamic_routes.append(capture);
}
}
}
},
.simple_var_decl => {
const decl = self.ast.simpleVarDecl(asNodeIndex(index));
if (self.isStaticParamsDecl(decl)) {
const params = try self.data.object();
try self.parseStaticParamsDecl(decl, params);
static_params = self.data.value;
}
},
else => {},
}
}
for (static_routes.items) |*static_route| {
var encoded_params = std.ArrayList([]const u8).init(self.allocator);
defer encoded_params.deinit();
if (static_params) |capture| {
if (capture.get(static_route.name)) |params| {
for (params.array.array.items) |item| { // XXX: Use public interface for Data.Array here ?
var json_buf = std.ArrayList(u8).init(self.allocator);
defer json_buf.deinit();
const json_writer = json_buf.writer();
try item.toJson(json_writer);
var encoded_buf = std.ArrayList(u8).init(self.allocator);
defer encoded_buf.deinit();
const writer = encoded_buf.writer();
try std.json.encodeJsonString(json_buf.items, .{}, writer);
try static_route.params.append(try self.allocator.dupe(u8, encoded_buf.items));
}
}
}
}
return .{
.dynamic = dynamic_routes.items,
.static = static_routes.items,
};
}
// 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,
index: usize,
path: []const u8,
source: []const u8,
) !?Function {
const fn_proto = self.ast.fnProtoMulti(@as(u32, @intCast(index)));
if (fn_proto.name_token) |token| {
const function_name = try self.allocator.dupe(u8, self.ast.tokenSlice(token));
var it = fn_proto.iterate(&self.ast);
var args = std.ArrayList(Arg).init(self.allocator);
defer args.deinit();
while (it.next()) |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),
.args = try self.allocator.dupe(Arg, args.items),
.source = try self.allocator.dupe(u8, source),
.params = std.ArrayList([]const u8).init(self.allocator),
};
}
return null;
}
fn parseTypeExpr(self: *Self, 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..(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(self.ast.tokenSlice(@as(u32, @intCast(node.main_token + index))));
},
else => return try std.mem.concat(self.allocator, u8, buf.items),
}
}
},
else => {},
}
return error.JetzigAstParserError;
}
fn asNodeIndex(index: usize) std.zig.Ast.Node.Index {
return @as(std.zig.Ast.Node.Index, @intCast(index));
}