mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 14:06:08 +00:00
Merge pull request #85 from jetzig-framework/routes-command
Add `jetzig routes` command
This commit is contained in:
commit
aafcd4cddc
21
build.zig
21
build.zig
@ -155,7 +155,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
&[_][]const u8{ root_path, "src", "app", "mailers" },
|
||||
);
|
||||
|
||||
var generate_routes = try Routes.init(
|
||||
var routes = try Routes.init(
|
||||
b.allocator,
|
||||
root_path,
|
||||
templates_path,
|
||||
@ -163,11 +163,11 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
jobs_path,
|
||||
mailers_path,
|
||||
);
|
||||
try generate_routes.generateRoutes();
|
||||
const generated_routes = try routes.generateRoutes();
|
||||
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_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 });
|
||||
|
||||
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_static_routes_cmd.step);
|
||||
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 {
|
||||
|
12
cli/cli.zig
12
cli/cli.zig
@ -4,6 +4,7 @@ const init = @import("commands/init.zig");
|
||||
const update = @import("commands/update.zig");
|
||||
const generate = @import("commands/generate.zig");
|
||||
const server = @import("commands/server.zig");
|
||||
const routes = @import("commands/routes.zig");
|
||||
const bundle = @import("commands/bundle.zig");
|
||||
const tests = @import("commands/tests.zig");
|
||||
|
||||
@ -21,6 +22,7 @@ const Options = struct {
|
||||
.update = "Update current project to latest version of Jetzig",
|
||||
.generate = "Generate scaffolding",
|
||||
.server = "Run a development server",
|
||||
.routes = "List all routes in your app",
|
||||
.bundle = "Create a deployment bundle",
|
||||
.@"test" = "Run app tests",
|
||||
.help = "Print help and exit",
|
||||
@ -33,10 +35,12 @@ const Verb = union(enum) {
|
||||
update: update.Options,
|
||||
generate: generate.Options,
|
||||
server: server.Options,
|
||||
routes: routes.Options,
|
||||
bundle: bundle.Options,
|
||||
@"test": tests.Options,
|
||||
g: generate.Options,
|
||||
s: server.Options,
|
||||
r: routes.Options,
|
||||
b: bundle.Options,
|
||||
t: tests.Options,
|
||||
};
|
||||
@ -70,6 +74,7 @@ pub fn main() !void {
|
||||
\\ update Update current project to latest version of Jetzig.
|
||||
\\ generate Generate scaffolding.
|
||||
\\ server Run a development server.
|
||||
\\ routes List all routes in your app.
|
||||
\\ bundle Create a deployment bundle.
|
||||
\\ test Run app tests.
|
||||
\\
|
||||
@ -110,6 +115,13 @@ fn run(allocator: std.mem.Allocator, options: args.ParseArgsResult(Options, Verb
|
||||
options.positionals,
|
||||
.{ .help = options.options.help },
|
||||
),
|
||||
.r, .routes => |opts| routes.run(
|
||||
allocator,
|
||||
opts,
|
||||
writer,
|
||||
options.positionals,
|
||||
.{ .help = options.options.help },
|
||||
),
|
||||
.b, .bundle => |opts| bundle.run(
|
||||
allocator,
|
||||
opts,
|
||||
|
45
cli/commands/routes.zig
Normal file
45
cli/commands/routes.zig
Normal 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",
|
||||
});
|
||||
}
|
@ -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 {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const allocator = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator;
|
||||
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();
|
||||
|
||||
// Example custom route:
|
||||
// app.route(.GET, "/custom/:id/foo/bar", @import("app/views/custom/foo.zig"), .bar);
|
||||
|
||||
try app.start(routes, .{});
|
||||
}
|
||||
|
@ -121,7 +121,7 @@ pub fn deinit(self: *Routes) void {
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
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});
|
||||
}
|
||||
|
||||
@ -270,6 +271,7 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
|
||||
\\ .action = .{1s},
|
||||
\\ .view_name = "{2s}",
|
||||
\\ .view = jetzig.Route.ViewType{{ .{3s} = .{{ .{1s} = @import("{7s}").{1s} }} }},
|
||||
\\ .path = "{7s}",
|
||||
\\ .static = {4s},
|
||||
\\ .uri_path = "{5s}",
|
||||
\\ .template = "{6s}",
|
||||
|
@ -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
|
||||
/// `start(@import("routes").routes)` on the returned value.
|
||||
pub fn init(allocator: std.mem.Allocator) !App {
|
||||
@ -221,5 +223,6 @@ pub fn init(allocator: std.mem.Allocator) !App {
|
||||
.environment = environment,
|
||||
.allocator = allocator,
|
||||
.custom_routes = std.ArrayList(views.Route).init(allocator),
|
||||
.initHook = initHook,
|
||||
};
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ const App = @This();
|
||||
environment: jetzig.Environment,
|
||||
allocator: std.mem.Allocator,
|
||||
custom_routes: std.ArrayList(jetzig.views.Route),
|
||||
initHook: ?*const fn (*App) anyerror!void,
|
||||
|
||||
pub fn deinit(self: *const App) void {
|
||||
@constCast(self).custom_routes.deinit();
|
||||
@ -22,9 +23,11 @@ const AppOptions = struct {};
|
||||
/// Starts an application. `routes` should be `@import("routes").routes`, a generated file
|
||||
/// automatically created at build time. `templates` should be
|
||||
/// `@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`
|
||||
|
||||
if (self.initHook) |hook| try hook(@constCast(self));
|
||||
|
||||
var mime_map = jetzig.http.mime.MimeMap.init(self.allocator);
|
||||
defer mime_map.deinit();
|
||||
try mime_map.build();
|
||||
@ -137,7 +140,7 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void {
|
||||
}
|
||||
|
||||
pub fn route(
|
||||
self: *const App,
|
||||
self: *App,
|
||||
comptime method: jetzig.http.Request.Method,
|
||||
comptime path: []const u8,
|
||||
comptime module: type,
|
||||
@ -155,11 +158,11 @@ pub fn route(
|
||||
@memcpy(&view_name, module_name);
|
||||
std.mem.replaceScalar(u8, &view_name, '.', '/');
|
||||
|
||||
@constCast(self).custom_routes.append(.{
|
||||
self.custom_routes.append(.{
|
||||
.name = member,
|
||||
.action = .custom,
|
||||
.method = method,
|
||||
.view_name = self.allocator.dupe(u8, &view_name) catch @panic("OOM"),
|
||||
.view_name = module_name,
|
||||
.uri_path = path,
|
||||
.layout = if (@hasDecl(module, "layout")) module.layout else null,
|
||||
.view = comptime switch (viewType(path)) {
|
||||
|
@ -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 {
|
||||
return wrap(codes.black, message);
|
||||
}
|
||||
|
@ -170,23 +170,26 @@ pub const Reader = struct {
|
||||
self.queue.writer.mutex.lock();
|
||||
defer self.queue.writer.mutex.unlock();
|
||||
|
||||
const writer = switch (event.target) {
|
||||
.stdout => blk: {
|
||||
switch (event.target) {
|
||||
.stdout => {
|
||||
stdout_written = true;
|
||||
if (builtin.os.tag == .windows) {
|
||||
file = self.stdout_file;
|
||||
colorize = self.queue.stdout_colorize;
|
||||
}
|
||||
break :blk stdout_writer;
|
||||
},
|
||||
.stderr => blk: {
|
||||
.stderr => {
|
||||
stderr_written = true;
|
||||
if (builtin.os.tag == .windows) {
|
||||
file = self.stderr_file;
|
||||
colorize = self.queue.stderr_colorize;
|
||||
}
|
||||
break :blk stderr_writer;
|
||||
},
|
||||
}
|
||||
|
||||
const writer = switch (event.target) {
|
||||
.stdout => stdout_writer,
|
||||
.stderr => stderr_writer,
|
||||
};
|
||||
|
||||
if (event.ptr) |ptr| {
|
||||
@ -196,11 +199,7 @@ pub const Reader = struct {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (builtin.os.tag == .windows and colorize) {
|
||||
try writeWindows(file, writer, event);
|
||||
} else {
|
||||
try writer.writeAll(event.message[0..event.len]);
|
||||
}
|
||||
try jetzig.util.writeAnsi(file, writer, event.message[0..event.len]);
|
||||
|
||||
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 {
|
||||
var info: std.os.windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
|
||||
_ = std.os.windows.kernel32.GetConsoleScreenBufferInfo(
|
||||
file.handle,
|
||||
&info
|
||||
);
|
||||
_ = std.os.windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info);
|
||||
|
||||
var it = std.mem.tokenizeSequence(u8, event.message[0..event.len], "\x1b[");
|
||||
while (it.next()) |token| {
|
||||
if (std.mem.indexOfScalar(u8, token, 'm')) |index| {
|
||||
if (index > 0 and index + 1 < token.len) {
|
||||
if (jetzig.colors.windows_map.get(token[0..index])) |color| {
|
||||
try std.os.windows.SetConsoleTextAttribute(
|
||||
file.handle,
|
||||
color
|
||||
);
|
||||
try std.os.windows.SetConsoleTextAttribute(file.handle, color);
|
||||
try writer.writeAll(token[index + 1 ..]);
|
||||
continue;
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ store: *jetzig.kv.Store,
|
||||
cache: *jetzig.kv.Store,
|
||||
job_queue: *jetzig.kv.Store,
|
||||
|
||||
const initHook = jetzig.root.initHook;
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, routes_module: type) !App {
|
||||
switch (jetzig.testing.state) {
|
||||
.ready => {},
|
||||
|
@ -1,4 +1,7 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const colors = @import("colors.zig");
|
||||
|
||||
/// Compare two strings with case-insensitive matching.
|
||||
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];
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ action: Action,
|
||||
method: jetzig.http.Request.Method = undefined, // Used by custom routes only
|
||||
view_name: []const u8,
|
||||
uri_path: []const u8,
|
||||
path: ?[]const u8 = null,
|
||||
view: ViewType,
|
||||
render: RenderFn = renderFn,
|
||||
renderStatic: RenderStaticFn = renderStaticFn,
|
||||
|
69
src/routes.zig
Normal file
69
src/routes.zig
Normal 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);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user