mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 22:16:08 +00:00
481 lines
17 KiB
Zig
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));
|
|
}
|