Merge pull request #85 from jetzig-framework/routes-command

Add `jetzig routes` command
This commit is contained in:
bobf 2024-06-05 21:50:55 +01:00 committed by GitHub
commit aafcd4cddc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 210 additions and 31 deletions

View File

@ -155,7 +155,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
&[_][]const u8{ root_path, "src", "app", "mailers" }, &[_][]const u8{ root_path, "src", "app", "mailers" },
); );
var generate_routes = try Routes.init( var routes = try Routes.init(
b.allocator, b.allocator,
root_path, root_path,
templates_path, templates_path,
@ -163,11 +163,11 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
jobs_path, jobs_path,
mailers_path, mailers_path,
); );
try generate_routes.generateRoutes(); const generated_routes = try routes.generateRoutes();
const routes_write_files = b.addWriteFiles(); const routes_write_files = b.addWriteFiles();
const routes_file = routes_write_files.add("routes.zig", generate_routes.buffer.items); const routes_file = routes_write_files.add("routes.zig", generated_routes);
const tests_write_files = b.addWriteFiles(); const tests_write_files = b.addWriteFiles();
const tests_file = tests_write_files.add("tests.zig", generate_routes.buffer.items); const tests_file = tests_write_files.add("tests.zig", generated_routes);
const routes_module = b.createModule(.{ .root_source_file = routes_file }); const routes_module = b.createModule(.{ .root_source_file = routes_file });
var src_dir = try std.fs.openDirAbsolute(b.pathFromRoot("src"), .{ .iterate = true }); var src_dir = try std.fs.openDirAbsolute(b.pathFromRoot("src"), .{ .iterate = true });
@ -229,6 +229,19 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
test_step.dependOn(&run_exe_unit_tests.step); test_step.dependOn(&run_exe_unit_tests.step);
test_step.dependOn(&run_static_routes_cmd.step); test_step.dependOn(&run_static_routes_cmd.step);
exe_unit_tests.root_module.addImport("routes", routes_module); exe_unit_tests.root_module.addImport("routes", routes_module);
const routes_step = b.step("jetzig:routes", "List all routes in your app");
const exe_routes = b.addExecutable(.{
.name = "routes",
.root_source_file = jetzig_dep.path("src/routes.zig"),
.target = target,
.optimize = optimize,
});
exe_routes.root_module.addImport("jetzig", jetzig_module);
exe_routes.root_module.addImport("routes", routes_module);
exe_routes.root_module.addImport("app", &exe.root_module);
const run_routes_cmd = b.addRunArtifact(exe_routes);
routes_step.dependOn(&run_routes_cmd.step);
} }
fn generateMarkdownFragments(b: *std.Build) ![]const u8 { fn generateMarkdownFragments(b: *std.Build) ![]const u8 {

View File

@ -4,6 +4,7 @@ const init = @import("commands/init.zig");
const update = @import("commands/update.zig"); const update = @import("commands/update.zig");
const generate = @import("commands/generate.zig"); const generate = @import("commands/generate.zig");
const server = @import("commands/server.zig"); const server = @import("commands/server.zig");
const routes = @import("commands/routes.zig");
const bundle = @import("commands/bundle.zig"); const bundle = @import("commands/bundle.zig");
const tests = @import("commands/tests.zig"); const tests = @import("commands/tests.zig");
@ -21,6 +22,7 @@ const Options = struct {
.update = "Update current project to latest version of Jetzig", .update = "Update current project to latest version of Jetzig",
.generate = "Generate scaffolding", .generate = "Generate scaffolding",
.server = "Run a development server", .server = "Run a development server",
.routes = "List all routes in your app",
.bundle = "Create a deployment bundle", .bundle = "Create a deployment bundle",
.@"test" = "Run app tests", .@"test" = "Run app tests",
.help = "Print help and exit", .help = "Print help and exit",
@ -33,10 +35,12 @@ const Verb = union(enum) {
update: update.Options, update: update.Options,
generate: generate.Options, generate: generate.Options,
server: server.Options, server: server.Options,
routes: routes.Options,
bundle: bundle.Options, bundle: bundle.Options,
@"test": tests.Options, @"test": tests.Options,
g: generate.Options, g: generate.Options,
s: server.Options, s: server.Options,
r: routes.Options,
b: bundle.Options, b: bundle.Options,
t: tests.Options, t: tests.Options,
}; };
@ -70,6 +74,7 @@ pub fn main() !void {
\\ update Update current project to latest version of Jetzig. \\ update Update current project to latest version of Jetzig.
\\ generate Generate scaffolding. \\ generate Generate scaffolding.
\\ server Run a development server. \\ server Run a development server.
\\ routes List all routes in your app.
\\ bundle Create a deployment bundle. \\ bundle Create a deployment bundle.
\\ test Run app tests. \\ test Run app tests.
\\ \\
@ -110,6 +115,13 @@ fn run(allocator: std.mem.Allocator, options: args.ParseArgsResult(Options, Verb
options.positionals, options.positionals,
.{ .help = options.options.help }, .{ .help = options.options.help },
), ),
.r, .routes => |opts| routes.run(
allocator,
opts,
writer,
options.positionals,
.{ .help = options.options.help },
),
.b, .bundle => |opts| bundle.run( .b, .bundle => |opts| bundle.run(
allocator, allocator,
opts, opts,

45
cli/commands/routes.zig Normal file
View File

@ -0,0 +1,45 @@
const std = @import("std");
const args = @import("args");
const util = @import("../util.zig");
/// Command line options for the `routes` command.
pub const Options = struct {
pub const meta = .{
.usage_summary = "",
.full_text =
\\Output all available routes for this app.
\\
\\Example:
\\
\\ jetzig routes
,
};
};
/// Run the `jetzig routes` command.
pub fn run(
allocator: std.mem.Allocator,
options: Options,
writer: anytype,
positionals: [][]const u8,
other_options: struct { help: bool },
) !void {
_ = positionals;
_ = options;
if (other_options.help) {
try args.printHelp(Options, "jetzig routes", writer);
return;
}
var cwd = try util.detectJetzigProjectDir();
defer cwd.close();
const realpath = try std.fs.realpathAlloc(allocator, ".");
defer allocator.free(realpath);
try util.runCommandStreaming(allocator, realpath, &[_][]const u8{
"zig",
"build",
"jetzig:routes",
});
}

View File

@ -175,16 +175,18 @@ pub const jetzig_options = struct {
}; };
}; };
pub fn init(app: *jetzig.App) !void {
// Example custom route:
app.route(.GET, "/custom/:id/foo/bar", @import("app/views/custom/foo.zig"), .bar);
}
pub fn main() !void { pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator; const allocator = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator;
defer if (builtin.mode == .Debug) std.debug.assert(gpa.deinit() == .ok); defer if (builtin.mode == .Debug) std.debug.assert(gpa.deinit() == .ok);
const app = try jetzig.init(allocator); var app = try jetzig.init(allocator);
defer app.deinit(); defer app.deinit();
// Example custom route:
// app.route(.GET, "/custom/:id/foo/bar", @import("app/views/custom/foo.zig"), .bar);
try app.start(routes, .{}); try app.start(routes, .{});
} }

View File

@ -121,7 +121,7 @@ pub fn deinit(self: *Routes) void {
} }
/// Generates the complete route set for the application /// Generates the complete route set for the application
pub fn generateRoutes(self: *Routes) !void { pub fn generateRoutes(self: *Routes) ![]const u8 {
const writer = self.buffer.writer(); const writer = self.buffer.writer();
try writer.writeAll( try writer.writeAll(
@ -177,6 +177,7 @@ pub fn generateRoutes(self: *Routes) !void {
\\ \\
); );
return try self.buffer.toOwnedSlice();
// std.debug.print("routes.zig\n{s}\n", .{self.buffer.items}); // std.debug.print("routes.zig\n{s}\n", .{self.buffer.items});
} }
@ -270,6 +271,7 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
\\ .action = .{1s}, \\ .action = .{1s},
\\ .view_name = "{2s}", \\ .view_name = "{2s}",
\\ .view = jetzig.Route.ViewType{{ .{3s} = .{{ .{1s} = @import("{7s}").{1s} }} }}, \\ .view = jetzig.Route.ViewType{{ .{3s} = .{{ .{1s} = @import("{7s}").{1s} }} }},
\\ .path = "{7s}",
\\ .static = {4s}, \\ .static = {4s},
\\ .uri_path = "{5s}", \\ .uri_path = "{5s}",
\\ .template = "{6s}", \\ .template = "{6s}",

View File

@ -209,6 +209,8 @@ pub const config = struct {
} }
}; };
pub const initHook: ?*const fn (*App) anyerror!void = if (@hasDecl(root, "init")) root.init else null;
/// Initialize a new Jetzig app. Call this from `src/main.zig` and then call /// Initialize a new Jetzig app. Call this from `src/main.zig` and then call
/// `start(@import("routes").routes)` on the returned value. /// `start(@import("routes").routes)` on the returned value.
pub fn init(allocator: std.mem.Allocator) !App { pub fn init(allocator: std.mem.Allocator) !App {
@ -221,5 +223,6 @@ pub fn init(allocator: std.mem.Allocator) !App {
.environment = environment, .environment = environment,
.allocator = allocator, .allocator = allocator,
.custom_routes = std.ArrayList(views.Route).init(allocator), .custom_routes = std.ArrayList(views.Route).init(allocator),
.initHook = initHook,
}; };
} }

View File

@ -10,6 +10,7 @@ const App = @This();
environment: jetzig.Environment, environment: jetzig.Environment,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
custom_routes: std.ArrayList(jetzig.views.Route), custom_routes: std.ArrayList(jetzig.views.Route),
initHook: ?*const fn (*App) anyerror!void,
pub fn deinit(self: *const App) void { pub fn deinit(self: *const App) void {
@constCast(self).custom_routes.deinit(); @constCast(self).custom_routes.deinit();
@ -22,9 +23,11 @@ const AppOptions = struct {};
/// Starts an application. `routes` should be `@import("routes").routes`, a generated file /// Starts an application. `routes` should be `@import("routes").routes`, a generated file
/// automatically created at build time. `templates` should be /// automatically created at build time. `templates` should be
/// `@import("src/app/views/zmpl.manifest.zig").templates`, created by Zmpl at compile time. /// `@import("src/app/views/zmpl.manifest.zig").templates`, created by Zmpl at compile time.
pub fn start(self: App, routes_module: type, options: AppOptions) !void { pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
_ = options; // See `AppOptions` _ = options; // See `AppOptions`
if (self.initHook) |hook| try hook(@constCast(self));
var mime_map = jetzig.http.mime.MimeMap.init(self.allocator); var mime_map = jetzig.http.mime.MimeMap.init(self.allocator);
defer mime_map.deinit(); defer mime_map.deinit();
try mime_map.build(); try mime_map.build();
@ -137,7 +140,7 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void {
} }
pub fn route( pub fn route(
self: *const App, self: *App,
comptime method: jetzig.http.Request.Method, comptime method: jetzig.http.Request.Method,
comptime path: []const u8, comptime path: []const u8,
comptime module: type, comptime module: type,
@ -155,11 +158,11 @@ pub fn route(
@memcpy(&view_name, module_name); @memcpy(&view_name, module_name);
std.mem.replaceScalar(u8, &view_name, '.', '/'); std.mem.replaceScalar(u8, &view_name, '.', '/');
@constCast(self).custom_routes.append(.{ self.custom_routes.append(.{
.name = member, .name = member,
.action = .custom, .action = .custom,
.method = method, .method = method,
.view_name = self.allocator.dupe(u8, &view_name) catch @panic("OOM"), .view_name = module_name,
.uri_path = path, .uri_path = path,
.layout = if (@hasDecl(module, "layout")) module.layout else null, .layout = if (@hasDecl(module, "layout")) module.layout else null,
.view = comptime switch (viewType(path)) { .view = comptime switch (viewType(path)) {

View File

@ -119,6 +119,10 @@ fn runtimeWrap(allocator: std.mem.Allocator, attribute: []const u8, message: []c
); );
} }
pub fn bold(comptime message: []const u8) []const u8 {
return codes.escape ++ codes.bold ++ message ++ codes.escape ++ codes.reset;
}
pub fn black(comptime message: []const u8) []const u8 { pub fn black(comptime message: []const u8) []const u8 {
return wrap(codes.black, message); return wrap(codes.black, message);
} }

View File

@ -170,23 +170,26 @@ pub const Reader = struct {
self.queue.writer.mutex.lock(); self.queue.writer.mutex.lock();
defer self.queue.writer.mutex.unlock(); defer self.queue.writer.mutex.unlock();
const writer = switch (event.target) { switch (event.target) {
.stdout => blk: { .stdout => {
stdout_written = true; stdout_written = true;
if (builtin.os.tag == .windows) { if (builtin.os.tag == .windows) {
file = self.stdout_file; file = self.stdout_file;
colorize = self.queue.stdout_colorize; colorize = self.queue.stdout_colorize;
} }
break :blk stdout_writer;
}, },
.stderr => blk: { .stderr => {
stderr_written = true; stderr_written = true;
if (builtin.os.tag == .windows) { if (builtin.os.tag == .windows) {
file = self.stderr_file; file = self.stderr_file;
colorize = self.queue.stderr_colorize; colorize = self.queue.stderr_colorize;
} }
break :blk stderr_writer;
}, },
}
const writer = switch (event.target) {
.stdout => stdout_writer,
.stderr => stderr_writer,
}; };
if (event.ptr) |ptr| { if (event.ptr) |ptr| {
@ -196,11 +199,7 @@ pub const Reader = struct {
continue; continue;
} }
if (builtin.os.tag == .windows and colorize) { try jetzig.util.writeAnsi(file, writer, event.message[0..event.len]);
try writeWindows(file, writer, event);
} else {
try writer.writeAll(event.message[0..event.len]);
}
self.queue.writer.position -= 1; self.queue.writer.position -= 1;
@ -273,20 +272,14 @@ fn initPool(allocator: std.mem.Allocator, T: type) std.heap.MemoryPool(T) {
fn writeWindows(file: std.fs.File, writer: anytype, event: Event) !void { fn writeWindows(file: std.fs.File, writer: anytype, event: Event) !void {
var info: std.os.windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; var info: std.os.windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
_ = std.os.windows.kernel32.GetConsoleScreenBufferInfo( _ = std.os.windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info);
file.handle,
&info
);
var it = std.mem.tokenizeSequence(u8, event.message[0..event.len], "\x1b["); var it = std.mem.tokenizeSequence(u8, event.message[0..event.len], "\x1b[");
while (it.next()) |token| { while (it.next()) |token| {
if (std.mem.indexOfScalar(u8, token, 'm')) |index| { if (std.mem.indexOfScalar(u8, token, 'm')) |index| {
if (index > 0 and index + 1 < token.len) { if (index > 0 and index + 1 < token.len) {
if (jetzig.colors.windows_map.get(token[0..index])) |color| { if (jetzig.colors.windows_map.get(token[0..index])) |color| {
try std.os.windows.SetConsoleTextAttribute( try std.os.windows.SetConsoleTextAttribute(file.handle, color);
file.handle,
color
);
try writer.writeAll(token[index + 1 ..]); try writer.writeAll(token[index + 1 ..]);
continue; continue;
} }

View File

@ -12,6 +12,8 @@ store: *jetzig.kv.Store,
cache: *jetzig.kv.Store, cache: *jetzig.kv.Store,
job_queue: *jetzig.kv.Store, job_queue: *jetzig.kv.Store,
const initHook = jetzig.root.initHook;
pub fn init(allocator: std.mem.Allocator, routes_module: type) !App { pub fn init(allocator: std.mem.Allocator, routes_module: type) !App {
switch (jetzig.testing.state) { switch (jetzig.testing.state) {
.ready => {}, .ready => {},

View File

@ -1,4 +1,7 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin");
const colors = @import("colors.zig");
/// Compare two strings with case-insensitive matching. /// Compare two strings with case-insensitive matching.
pub fn equalStringsCaseInsensitive(expected: []const u8, actual: []const u8) bool { pub fn equalStringsCaseInsensitive(expected: []const u8, actual: []const u8) bool {
@ -85,3 +88,30 @@ pub fn generateVariableName(buf: *[32]u8) []const u8 {
} }
return buf[0..32]; return buf[0..32];
} }
/// Write a string of bytes, possibly containing ANSI escape codes. Translate ANSI escape codes
/// into Windows API console commands. Allow building an ANSI string and writing at once to a
/// Windows console. In non-Windows environments, output ANSI bytes directly.
pub fn writeAnsi(file: std.fs.File, writer: anytype, text: []const u8) !void {
if (builtin.os.tag != .windows) {
try writer.writeAll(text);
} else {
var info: std.os.windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
_ = std.os.windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info);
var it = std.mem.tokenizeSequence(u8, text, "\x1b[");
while (it.next()) |token| {
if (std.mem.indexOfScalar(u8, token, 'm')) |index| {
if (index > 0 and index + 1 < token.len) {
if (colors.windows_map.get(token[0..index])) |color| {
try std.os.windows.SetConsoleTextAttribute(file.handle, color);
try writer.writeAll(token[index + 1 ..]);
continue;
}
}
}
// Fallback
try writer.writeAll(token);
}
}
}

View File

@ -51,6 +51,7 @@ action: Action,
method: jetzig.http.Request.Method = undefined, // Used by custom routes only method: jetzig.http.Request.Method = undefined, // Used by custom routes only
view_name: []const u8, view_name: []const u8,
uri_path: []const u8, uri_path: []const u8,
path: ?[]const u8 = null,
view: ViewType, view: ViewType,
render: RenderFn = renderFn, render: RenderFn = renderFn,
renderStatic: RenderStaticFn = renderStaticFn, renderStatic: RenderStaticFn = renderStaticFn,

69
src/routes.zig Normal file
View File

@ -0,0 +1,69 @@
const std = @import("std");
const routes = @import("routes");
const app = @import("app");
const jetzig = @import("jetzig");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
comptime var max_uri_path_len: usize = 0;
log("Jetzig Routes:", .{});
const environment = jetzig.Environment.init(undefined);
const initHook: ?*const fn (*jetzig.App) anyerror!void = if (@hasDecl(app, "init")) app.init else null;
inline for (routes.routes) |route| max_uri_path_len = @max(route.uri_path.len + 5, max_uri_path_len);
const padded_path = std.fmt.comptimePrint("{{s: <{}}}", .{max_uri_path_len});
inline for (routes.routes) |route| {
const action = comptime switch (route.action) {
.get => jetzig.colors.cyan("{s: <7}"),
.index => jetzig.colors.blue("{s: <7}"),
.post => jetzig.colors.yellow("{s: <7}"),
.put => jetzig.colors.magenta("{s: <7}"),
.patch => jetzig.colors.purple("{s: <7}"),
.delete => jetzig.colors.red("{s: <7}"),
.custom => unreachable,
};
log(" " ++ action ++ " " ++ padded_path ++ " {?s}", .{
@tagName(route.action),
route.uri_path ++ switch (route.action) {
.index, .post => "",
.get, .put, .patch, .delete => "/:id",
.custom => "",
},
route.path,
});
}
var jetzig_app = jetzig.App{
.environment = environment,
.allocator = allocator,
.custom_routes = std.ArrayList(jetzig.views.Route).init(allocator),
.initHook = initHook,
};
if (initHook) |hook| try hook(&jetzig_app);
for (jetzig_app.custom_routes.items) |route| {
log(
" " ++ jetzig.colors.bold(jetzig.colors.white("{s: <7}")) ++ " " ++ padded_path ++ " {s}:{s}",
.{ route.name, route.uri_path, route.view_name, route.name },
);
}
}
fn log(comptime message: []const u8, args: anytype) void {
std.debug.print(message ++ "\n", args);
}
fn sortedRoutes(comptime unordered_routes: []const jetzig.views.Route) void {
comptime std.sort.pdq(jetzig.views.Route, unordered_routes, {}, lessThanFn);
}
pub fn lessThanFn(context: void, lhs: jetzig.views.Route, rhs: jetzig.views.Route) bool {
_ = context;
return std.mem.order(u8, lhs.uri_path, rhs.uri_path).compare(std.math.CompareOperator.lt);
}