mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-15 06:26:07 +00:00
Merge pull request #14 from jetzig-framework/cli-generate
CLI generate command
This commit is contained in:
commit
647833ca5a
2
.github/workflows/CI.yml
vendored
2
.github/workflows/CI.yml
vendored
@ -74,7 +74,7 @@ jobs:
|
|||||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: builds-macos-x86
|
name: build-macos-x86
|
||||||
path: artifacts/x86_64-macos
|
path: artifacts/x86_64-macos
|
||||||
- name: Upload artifacts Target MacOS 2
|
- name: Upload artifacts Target MacOS 2
|
||||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||||
|
46
cli/cli.zig
46
cli/cli.zig
@ -1,6 +1,7 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const args = @import("args");
|
const args = @import("args");
|
||||||
const init = @import("init.zig");
|
const init = @import("commands/init.zig");
|
||||||
|
const generate = @import("commands/generate.zig");
|
||||||
|
|
||||||
const Options = struct {
|
const Options = struct {
|
||||||
help: bool = false,
|
help: bool = false,
|
||||||
@ -10,8 +11,10 @@ const Options = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const meta = .{
|
pub const meta = .{
|
||||||
|
.usage_summary = "[COMMAND]",
|
||||||
.option_docs = .{
|
.option_docs = .{
|
||||||
.init = "Initialize a new project",
|
.init = "Initialize a new project",
|
||||||
|
.generate = "Generate scaffolding",
|
||||||
.help = "Print help and exit",
|
.help = "Print help and exit",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -19,6 +22,8 @@ const Options = struct {
|
|||||||
|
|
||||||
const Verb = union(enum) {
|
const Verb = union(enum) {
|
||||||
init: init.Options,
|
init: init.Options,
|
||||||
|
generate: generate.Options,
|
||||||
|
g: generate.Options,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Main entrypoint for `jetzig` executable. Parses command line args and generates a new
|
/// Main entrypoint for `jetzig` executable. Parses command line args and generates a new
|
||||||
@ -33,28 +38,45 @@ pub fn main() !void {
|
|||||||
|
|
||||||
const writer = std.io.getStdErr().writer();
|
const writer = std.io.getStdErr().writer();
|
||||||
|
|
||||||
if (options.verb) |verb| {
|
run(allocator, options, writer) catch |err| {
|
||||||
switch (verb) {
|
switch (err) {
|
||||||
.init => |opts| return init.run(
|
error.JetzigCommandError => std.os.exit(1),
|
||||||
allocator,
|
else => return err,
|
||||||
opts,
|
|
||||||
writer,
|
|
||||||
options.positionals,
|
|
||||||
.{ .help = options.options.help },
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (options.options.help) {
|
if (options.options.help or options.verb == null) {
|
||||||
try args.printHelp(Options, "jetzig", writer);
|
try args.printHelp(Options, "jetzig", writer);
|
||||||
try writer.writeAll(
|
try writer.writeAll(
|
||||||
\\
|
\\
|
||||||
\\Commands:
|
\\Commands:
|
||||||
\\
|
\\
|
||||||
\\ init Initialize a new project.
|
\\ init Initialize a new project.
|
||||||
|
\\ generate Generate scaffolding.
|
||||||
\\
|
\\
|
||||||
\\ Pass --help to any command for more information, e.g. `jetzig init --help`
|
\\ Pass --help to any command for more information, e.g. `jetzig init --help`
|
||||||
\\
|
\\
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run(allocator: std.mem.Allocator, options: args.ParseArgsResult(Options, Verb), writer: anytype) !void {
|
||||||
|
if (options.verb) |verb| {
|
||||||
|
return switch (verb) {
|
||||||
|
.init => |opts| init.run(
|
||||||
|
allocator,
|
||||||
|
opts,
|
||||||
|
writer,
|
||||||
|
options.positionals,
|
||||||
|
.{ .help = options.options.help },
|
||||||
|
),
|
||||||
|
.g, .generate => |opts| generate.run(
|
||||||
|
allocator,
|
||||||
|
opts,
|
||||||
|
writer,
|
||||||
|
options.positionals,
|
||||||
|
.{ .help = options.options.help },
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
81
cli/commands/generate.zig
Normal file
81
cli/commands/generate.zig
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const args = @import("args");
|
||||||
|
const view = @import("generate/view.zig");
|
||||||
|
const partial = @import("generate/partial.zig");
|
||||||
|
const middleware = @import("generate/middleware.zig");
|
||||||
|
const util = @import("../util.zig");
|
||||||
|
|
||||||
|
/// Command line options for the `generate` command.
|
||||||
|
pub const Options = struct {
|
||||||
|
path: ?[]const u8 = null,
|
||||||
|
|
||||||
|
pub const shorthands = .{
|
||||||
|
.p = "path",
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const meta = .{
|
||||||
|
.usage_summary = "[view|middleware] [options]",
|
||||||
|
.full_text =
|
||||||
|
\\Generates scaffolding for views, middleware, and other objects in future.
|
||||||
|
\\
|
||||||
|
\\When generating a view, by default all actions will be included.
|
||||||
|
\\Optionally pass one or more of the following arguments to specify desired actions:
|
||||||
|
\\
|
||||||
|
\\ index, get, post, patch, put, delete
|
||||||
|
\\
|
||||||
|
\\Each view action can be qualified with a `:static` option to mark the view content
|
||||||
|
\\as statically generated at build time.
|
||||||
|
\\
|
||||||
|
\\e.g. generate a view named `iguanas` with a static `index` action:
|
||||||
|
\\
|
||||||
|
\\ jetzig generate view iguanas index:static get post delete
|
||||||
|
,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Run the `jetzig init` command.
|
||||||
|
pub fn run(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
options: Options,
|
||||||
|
writer: anytype,
|
||||||
|
positionals: [][]const u8,
|
||||||
|
other_options: struct { help: bool },
|
||||||
|
) !void {
|
||||||
|
var cwd = try util.detectJetzigProjectDir();
|
||||||
|
defer cwd.close();
|
||||||
|
|
||||||
|
_ = options;
|
||||||
|
if (other_options.help) {
|
||||||
|
try args.printHelp(Options, "jetzig generate", writer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var generate_type: ?enum { view, partial, middleware } = null;
|
||||||
|
var sub_args = std.ArrayList([]const u8).init(allocator);
|
||||||
|
defer sub_args.deinit();
|
||||||
|
|
||||||
|
for (positionals) |arg| {
|
||||||
|
if (generate_type == null and std.mem.eql(u8, arg, "view")) {
|
||||||
|
generate_type = .view;
|
||||||
|
} else if (generate_type == null and std.mem.eql(u8, arg, "partial")) {
|
||||||
|
generate_type = .partial;
|
||||||
|
} else if (generate_type == null and std.mem.eql(u8, arg, "middleware")) {
|
||||||
|
generate_type = .middleware;
|
||||||
|
} else if (generate_type == null) {
|
||||||
|
std.debug.print("Unknown generator command: {s}\n", .{arg});
|
||||||
|
return error.JetzigCommandError;
|
||||||
|
} else {
|
||||||
|
try sub_args.append(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generate_type) |capture| {
|
||||||
|
return switch (capture) {
|
||||||
|
.view => view.run(allocator, cwd, sub_args.items),
|
||||||
|
.partial => partial.run(allocator, cwd, sub_args.items),
|
||||||
|
.middleware => middleware.run(allocator, cwd, sub_args.items),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
std.debug.print("Missing sub-command. Expected: [view|middleware]\n", .{});
|
||||||
|
return error.JetzigCommandError;
|
||||||
|
}
|
||||||
|
}
|
101
cli/commands/generate/middleware.zig
Normal file
101
cli/commands/generate/middleware.zig
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const util = @import("../../util.zig");
|
||||||
|
|
||||||
|
/// Run the middleware generator. Create a middleware file in `src/app/middleware/`
|
||||||
|
pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8) !void {
|
||||||
|
if (args.len != 1 or !util.isCamelCase(args[0])) {
|
||||||
|
std.debug.print(
|
||||||
|
\\Expected a middleware name in CamelCase.
|
||||||
|
\\
|
||||||
|
\\Example:
|
||||||
|
\\
|
||||||
|
\\ jetzig generate middleware IguanaBrain
|
||||||
|
\\
|
||||||
|
, .{});
|
||||||
|
return error.JetzigCommandError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dir_path = try std.fs.path.join(allocator, &[_][]const u8{ "src", "app", "middleware" });
|
||||||
|
defer allocator.free(dir_path);
|
||||||
|
|
||||||
|
var dir = try cwd.makeOpenPath(dir_path, .{});
|
||||||
|
defer dir.close();
|
||||||
|
|
||||||
|
const filename = try std.mem.concat(allocator, u8, &[_][]const u8{ args[0], ".zig" });
|
||||||
|
defer allocator.free(filename);
|
||||||
|
|
||||||
|
const file = dir.createFile(filename, .{ .exclusive = true }) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.PathAlreadyExists => {
|
||||||
|
std.debug.print("Middleware already exists: {s}\n", .{filename});
|
||||||
|
return error.JetzigCommandError;
|
||||||
|
},
|
||||||
|
else => return err,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try file.writeAll(middleware_content);
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
const realpath = try dir.realpathAlloc(allocator, filename);
|
||||||
|
defer allocator.free(realpath);
|
||||||
|
std.debug.print(
|
||||||
|
\\Generated middleware: {s}
|
||||||
|
\\
|
||||||
|
\\Edit `src/main.zig` and add the new middleware to the `jetzig_options.middleware` declaration:
|
||||||
|
\\
|
||||||
|
\\ pub const jetzig_options = struct {{
|
||||||
|
\\ pub const middleware: []const type = &.{{
|
||||||
|
\\ @import("app/middleware/{s}.zig"),
|
||||||
|
\\ }};
|
||||||
|
\\ }};
|
||||||
|
\\
|
||||||
|
\\Middleware are invoked in the order they appear in `jetzig_options.middleware`.
|
||||||
|
\\
|
||||||
|
\\
|
||||||
|
, .{ realpath, args[0] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const middleware_content =
|
||||||
|
\\const std = @import("std");
|
||||||
|
\\const jetzig = @import("jetzig");
|
||||||
|
\\
|
||||||
|
\\/// Define any custom data fields you want to store here. Assigning to these fields in the `init`
|
||||||
|
\\/// function allows you to access them in the `beforeRequest` and `afterRequest` functions, where
|
||||||
|
\\/// they can also be modified.
|
||||||
|
\\my_custom_value: []const u8,
|
||||||
|
\\
|
||||||
|
\\const Self = @This();
|
||||||
|
\\
|
||||||
|
\\/// Initialize middleware.
|
||||||
|
\\pub fn init(request: *jetzig.http.Request) !*Self {
|
||||||
|
\\ var middleware = try request.allocator.create(Self);
|
||||||
|
\\ middleware.my_custom_value = "initial value";
|
||||||
|
\\ return middleware;
|
||||||
|
\\}
|
||||||
|
\\
|
||||||
|
\\/// Invoked immediately after the request head has been processed, before relevant view function
|
||||||
|
\\/// is processed. This gives you access to request headers but not the request body.
|
||||||
|
\\pub fn beforeRequest(self: *Self, request: *jetzig.http.Request) !void {
|
||||||
|
\\ request.server.logger.debug("[middleware] my_custom_value: {s}", .{self.my_custom_value});
|
||||||
|
\\ self.my_custom_value = @tagName(request.method);
|
||||||
|
\\}
|
||||||
|
\\
|
||||||
|
\\/// Invoked immediately after the request has finished responding. Provides full access to the
|
||||||
|
\\/// response as well as the request.
|
||||||
|
\\pub fn afterRequest(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
|
||||||
|
\\ request.server.logger.debug(
|
||||||
|
\\ "[middleware] my_custom_value: {s}, response status: {s}",
|
||||||
|
\\ .{ self.my_custom_value, @tagName(response.status_code) },
|
||||||
|
\\ );
|
||||||
|
\\}
|
||||||
|
\\
|
||||||
|
\\/// Invoked after `afterRequest` is called, use this function to do any clean-up.
|
||||||
|
\\/// Note that `request.allocator` is an arena allocator, so any allocations are automatically
|
||||||
|
\\/// done before the next request starts processing.
|
||||||
|
\\pub fn deinit(self: *Self, request: *jetzig.http.Request) void {
|
||||||
|
\\ request.allocator.destroy(self);
|
||||||
|
\\}
|
||||||
|
\\
|
||||||
|
;
|
46
cli/commands/generate/partial.zig
Normal file
46
cli/commands/generate/partial.zig
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// Run the partial generator. Create a partial template in `src/app/views/`
|
||||||
|
pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8) !void {
|
||||||
|
if (args.len != 2) {
|
||||||
|
std.debug.print(
|
||||||
|
\\Expected a view name and a name for a partial.
|
||||||
|
\\
|
||||||
|
\\Example:
|
||||||
|
\\
|
||||||
|
\\ jetzig generate partial iguanas ziglet
|
||||||
|
\\
|
||||||
|
, .{});
|
||||||
|
return error.JetzigCommandError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dir_path = try std.fs.path.join(allocator, &[_][]const u8{ "src", "app", "views", args[0] });
|
||||||
|
defer allocator.free(dir_path);
|
||||||
|
|
||||||
|
var dir = try cwd.makeOpenPath(dir_path, .{});
|
||||||
|
defer dir.close();
|
||||||
|
|
||||||
|
const filename = try std.mem.concat(allocator, u8, &[_][]const u8{ "_", args[1], ".zmpl" });
|
||||||
|
defer allocator.free(filename);
|
||||||
|
|
||||||
|
const file = dir.createFile(filename, .{ .exclusive = true }) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.PathAlreadyExists => {
|
||||||
|
std.debug.print("Partial already exists: {s}\n", .{filename});
|
||||||
|
return error.JetzigCommandError;
|
||||||
|
},
|
||||||
|
else => return err,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try file.writeAll(
|
||||||
|
\\<div>Partial content goes here.</div>
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
const realpath = try dir.realpathAlloc(allocator, filename);
|
||||||
|
defer allocator.free(realpath);
|
||||||
|
std.debug.print("Generated partial template: {s}\n", .{realpath});
|
||||||
|
}
|
220
cli/commands/generate/view.zig
Normal file
220
cli/commands/generate/view.zig
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const util = @import("../../util.zig");
|
||||||
|
|
||||||
|
/// Run the view generator. Create a view in `src/app/views/`
|
||||||
|
pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8) !void {
|
||||||
|
if (args.len == 0) {
|
||||||
|
std.debug.print(".\n", .{});
|
||||||
|
std.debug.print(
|
||||||
|
\\Expected view name followed by optional actions.
|
||||||
|
\\
|
||||||
|
\\Example:
|
||||||
|
\\
|
||||||
|
\\ jetzig generate view iguanas index:static get post delete
|
||||||
|
\\
|
||||||
|
, .{});
|
||||||
|
return error.JetzigCommandError;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf = std.ArrayList(u8).init(allocator);
|
||||||
|
defer buf.deinit();
|
||||||
|
|
||||||
|
const writer = buf.writer();
|
||||||
|
|
||||||
|
try writer.writeAll(
|
||||||
|
\\const std = @import("std");
|
||||||
|
\\const jetzig = @import("jetzig");
|
||||||
|
\\
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
|
||||||
|
const filename = try std.mem.concat(allocator, u8, &[_][]const u8{ args[0], ".zig" });
|
||||||
|
defer allocator.free(filename);
|
||||||
|
const action_args = if (args.len > 1)
|
||||||
|
args[1..]
|
||||||
|
else
|
||||||
|
&[_][]const u8{ "index", "get", "post", "put", "patch", "delete" };
|
||||||
|
|
||||||
|
var actions = std.ArrayList(Action).init(allocator);
|
||||||
|
defer actions.deinit();
|
||||||
|
|
||||||
|
var static_actions = std.ArrayList(Action).init(allocator);
|
||||||
|
defer static_actions.deinit();
|
||||||
|
|
||||||
|
for (action_args) |arg| {
|
||||||
|
if (parseAction(arg)) |action| {
|
||||||
|
try actions.append(action);
|
||||||
|
if (action.static) try static_actions.append(action);
|
||||||
|
} else {
|
||||||
|
std.debug.print("Unexpected argument: {s}\n", .{arg});
|
||||||
|
return error.JetzigCommandError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (static_actions.items.len > 0) try writeStaticParams(allocator, static_actions.items, writer);
|
||||||
|
|
||||||
|
for (actions.items) |action| {
|
||||||
|
try writeAction(allocator, writer, action);
|
||||||
|
try writeTemplate(allocator, cwd, args[0], action);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dir = try cwd.openDir("src/app/views", .{});
|
||||||
|
defer dir.close();
|
||||||
|
|
||||||
|
const file = dir.createFile(filename, .{ .exclusive = true }) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.PathAlreadyExists => {
|
||||||
|
std.debug.print("Path already exists, skipping view creation: {s}\n", .{filename});
|
||||||
|
return error.JetzigCommandError;
|
||||||
|
},
|
||||||
|
else => return err,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try file.writeAll(util.strip(buf.items));
|
||||||
|
try file.writeAll("\n");
|
||||||
|
file.close();
|
||||||
|
const realpath = try dir.realpathAlloc(allocator, filename);
|
||||||
|
defer allocator.free(realpath);
|
||||||
|
std.debug.print("Generated view: {s}\n", .{realpath});
|
||||||
|
}
|
||||||
|
|
||||||
|
const Method = enum { index, get, post, put, patch, delete };
|
||||||
|
const Action = struct {
|
||||||
|
method: Method,
|
||||||
|
static: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse a view arg. Grammar:
|
||||||
|
// [index[:static]|get[:static]|post[:static]|put[:static]|patch[:static]|delete[:static]]
|
||||||
|
fn parseAction(arg: []const u8) ?Action {
|
||||||
|
inline for (@typeInfo(Method).Enum.fields) |tag| {
|
||||||
|
const with_static = tag.name ++ ":static";
|
||||||
|
const method: Method = @enumFromInt(tag.value);
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, tag.name, arg)) return .{ .method = method, .static = false };
|
||||||
|
if (std.mem.eql(u8, with_static, arg)) return .{ .method = method, .static = true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a view function to the output buffer.
|
||||||
|
fn writeAction(allocator: std.mem.Allocator, writer: anytype, action: Action) !void {
|
||||||
|
const function = try std.fmt.allocPrint(
|
||||||
|
allocator,
|
||||||
|
\\pub fn {s}({s}request: *jetzig.{s}, data: *jetzig.Data) !jetzig.View {{
|
||||||
|
\\ _ = data;{s}
|
||||||
|
\\ return request.render({s});
|
||||||
|
\\}}
|
||||||
|
\\
|
||||||
|
\\
|
||||||
|
,
|
||||||
|
.{
|
||||||
|
@tagName(action.method),
|
||||||
|
switch (action.method) {
|
||||||
|
.index, .post => "",
|
||||||
|
.get, .put, .patch, .delete => "id: []const u8, ",
|
||||||
|
},
|
||||||
|
if (action.static) "StaticRequest" else "Request",
|
||||||
|
switch (action.method) {
|
||||||
|
.index, .post => "",
|
||||||
|
.get, .put, .patch, .delete => "\n _ = id;",
|
||||||
|
},
|
||||||
|
switch (action.method) {
|
||||||
|
.index, .get => ".ok",
|
||||||
|
.post => ".created",
|
||||||
|
.put, .patch, .delete => ".ok",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
defer allocator.free(function);
|
||||||
|
try writer.writeAll(function);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output static params example. Only invoked if one or more static routes are created.
|
||||||
|
fn writeStaticParams(allocator: std.mem.Allocator, actions: []Action, writer: anytype) !void {
|
||||||
|
try writer.writeAll(
|
||||||
|
\\// Define an array of params for each static view function.
|
||||||
|
\\// At build time, static outputs are generated for each set of params.
|
||||||
|
\\// At run time, requests matching the provided params will render the pre-rendered content.
|
||||||
|
\\pub const static_params = .{
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
|
||||||
|
for (actions) |action| {
|
||||||
|
switch (action.method) {
|
||||||
|
.index, .post => {
|
||||||
|
const output = try std.fmt.allocPrint(
|
||||||
|
allocator,
|
||||||
|
\\ .{s} = .{{
|
||||||
|
\\ .{{ .params = .{{ .foo = "bar", .baz = "qux" }} }},
|
||||||
|
\\ }},
|
||||||
|
\\
|
||||||
|
,
|
||||||
|
.{@tagName(action.method)},
|
||||||
|
);
|
||||||
|
defer allocator.free(output);
|
||||||
|
try writer.writeAll(output);
|
||||||
|
},
|
||||||
|
.get, .put, .patch, .delete => {
|
||||||
|
const output = try std.fmt.allocPrint(
|
||||||
|
allocator,
|
||||||
|
\\ .{s} = .{{
|
||||||
|
\\ .{{ .id = "1", .params = .{{ .foo = "bar", .baz = "qux" }} }},
|
||||||
|
\\ }},
|
||||||
|
\\
|
||||||
|
,
|
||||||
|
.{@tagName(action.method)},
|
||||||
|
);
|
||||||
|
defer allocator.free(output);
|
||||||
|
try writer.writeAll(output);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try writer.writeAll(
|
||||||
|
\\};
|
||||||
|
\\
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a Zmpl template for a corresponding view + action.
|
||||||
|
fn writeTemplate(allocator: std.mem.Allocator, cwd: std.fs.Dir, name: []const u8, action: Action) !void {
|
||||||
|
const path = try std.fs.path.join(allocator, &[_][]const u8{
|
||||||
|
"src",
|
||||||
|
"app",
|
||||||
|
"views",
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
defer allocator.free(path);
|
||||||
|
|
||||||
|
var view_dir = try cwd.makeOpenPath(path, .{});
|
||||||
|
defer view_dir.close();
|
||||||
|
|
||||||
|
const filename = try std.mem.concat(allocator, u8, &[_][]const u8{ @tagName(action.method), ".zmpl" });
|
||||||
|
defer allocator.free(filename);
|
||||||
|
|
||||||
|
const file = view_dir.createFile(filename, .{ .exclusive = true }) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.PathAlreadyExists => {
|
||||||
|
std.debug.print("Path already exists, skipping template creation: {s}\n", .{filename});
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
else => return err,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try file.writeAll(
|
||||||
|
\\<div>
|
||||||
|
\\ <span>Content goes here</span>
|
||||||
|
\\</div>
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
const realpath = try view_dir.realpathAlloc(allocator, filename);
|
||||||
|
defer allocator.free(realpath);
|
||||||
|
std.debug.print("Generated template: {s}\n", .{realpath});
|
||||||
|
}
|
@ -1,18 +1,10 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const args = @import("args");
|
const args = @import("args");
|
||||||
|
const util = @import("../util.zig");
|
||||||
|
|
||||||
const init_data = @import("init_data").init_data;
|
const init_data = @import("init_data").init_data;
|
||||||
|
|
||||||
fn base64Decode(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
|
/// Command line options for the `init` command.
|
||||||
const decoder = std.base64.Base64Decoder.init(
|
|
||||||
std.base64.url_safe_no_pad.alphabet_chars,
|
|
||||||
std.base64.url_safe_no_pad.pad_char,
|
|
||||||
);
|
|
||||||
const size = try decoder.calcSizeForSlice(input);
|
|
||||||
const ptr = try allocator.alloc(u8, size);
|
|
||||||
try decoder.decode(ptr, input);
|
|
||||||
return ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const Options = struct {
|
pub const Options = struct {
|
||||||
path: ?[]const u8 = null,
|
path: ?[]const u8 = null,
|
||||||
|
|
||||||
@ -50,7 +42,7 @@ pub fn run(
|
|||||||
for (positionals) |arg| {
|
for (positionals) |arg| {
|
||||||
if (install_path != null) {
|
if (install_path != null) {
|
||||||
std.debug.print("Unexpected positional argument: {s}\n", .{arg});
|
std.debug.print("Unexpected positional argument: {s}\n", .{arg});
|
||||||
return error.JetzigUnexpectedPositionalArgumentsError;
|
return error.JetzigCommandError;
|
||||||
}
|
}
|
||||||
install_path = arg;
|
install_path = arg;
|
||||||
}
|
}
|
||||||
@ -230,7 +222,7 @@ fn runCommand(allocator: std.mem.Allocator, install_path: []const u8, argv: []co
|
|||||||
std.debug.print("[exec] {s}", .{command});
|
std.debug.print("[exec] {s}", .{command});
|
||||||
|
|
||||||
if (result.term.Exited != 0) {
|
if (result.term.Exited != 0) {
|
||||||
printFailure();
|
util.printFailure();
|
||||||
std.debug.print(
|
std.debug.print(
|
||||||
\\
|
\\
|
||||||
\\Error running command: {s}
|
\\Error running command: {s}
|
||||||
@ -244,9 +236,9 @@ fn runCommand(allocator: std.mem.Allocator, install_path: []const u8, argv: []co
|
|||||||
\\{s}
|
\\{s}
|
||||||
\\
|
\\
|
||||||
, .{ command, result.stdout, result.stderr });
|
, .{ command, result.stdout, result.stderr });
|
||||||
return error.JetzigRunCommandError;
|
return error.JetzigCommandError;
|
||||||
} else {
|
} else {
|
||||||
printSuccess();
|
util.printSuccess();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,7 +259,7 @@ fn copySourceFile(
|
|||||||
var content: []const u8 = undefined;
|
var content: []const u8 = undefined;
|
||||||
if (replace) |capture| {
|
if (replace) |capture| {
|
||||||
const initial = readSourceFile(allocator, src) catch |err| {
|
const initial = readSourceFile(allocator, src) catch |err| {
|
||||||
printFailure();
|
util.printFailure();
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
defer allocator.free(initial);
|
defer allocator.free(initial);
|
||||||
@ -276,25 +268,25 @@ fn copySourceFile(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
content = readSourceFile(allocator, src) catch |err| {
|
content = readSourceFile(allocator, src) catch |err| {
|
||||||
printFailure();
|
util.printFailure();
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
defer allocator.free(content);
|
defer allocator.free(content);
|
||||||
|
|
||||||
writeSourceFile(install_dir, dest, content) catch |err| {
|
writeSourceFile(install_dir, dest, content) catch |err| {
|
||||||
printFailure();
|
util.printFailure();
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
printSuccess();
|
util.printSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read a file from Jetzig source code.
|
// Read a file from Jetzig source code.
|
||||||
fn readSourceFile(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
|
fn readSourceFile(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
|
||||||
inline for (init_data) |file| {
|
inline for (init_data) |file| {
|
||||||
if (std.mem.eql(u8, path, file.path)) return try base64Decode(allocator, file.data);
|
if (std.mem.eql(u8, path, file.path)) return try util.base64Decode(allocator, file.data);
|
||||||
}
|
}
|
||||||
return error.SourceFileNotFound;
|
return error.JetzigCommandError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write a file to the new project's directory.
|
// Write a file to the new project's directory.
|
||||||
@ -335,7 +327,7 @@ fn githubUrl(allocator: std.mem.Allocator) ![]const u8 {
|
|||||||
|
|
||||||
if (fetch_result.status != .ok) {
|
if (fetch_result.status != .ok) {
|
||||||
std.debug.print("Error fetching from GitHub: {s}\n", .{url});
|
std.debug.print("Error fetching from GitHub: {s}\n", .{url});
|
||||||
return error.JetzigGitHubFetchError;
|
return error.JetzigCommandError;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed_response = try std.json.parseFromSlice(
|
const parsed_response = try std.json.parseFromSlice(
|
||||||
@ -381,20 +373,15 @@ fn promptInput(
|
|||||||
const input = try reader.readUntilDelimiterOrEofAlloc(allocator, '\n', max_read_bytes);
|
const input = try reader.readUntilDelimiterOrEofAlloc(allocator, '\n', max_read_bytes);
|
||||||
if (input) |capture| {
|
if (input) |capture| {
|
||||||
defer allocator.free(capture);
|
defer allocator.free(capture);
|
||||||
const stripped_input = strip(capture);
|
const stripped_input = util.strip(capture);
|
||||||
|
|
||||||
if (std.mem.eql(u8, stripped_input, "")) {
|
if (std.mem.eql(u8, stripped_input, "")) {
|
||||||
if (options.default) |default| return try allocator.dupe(u8, strip(default));
|
if (options.default) |default| return try allocator.dupe(u8, util.strip(default));
|
||||||
} else return try allocator.dupe(u8, stripped_input);
|
} else return try allocator.dupe(u8, stripped_input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip leading and trailing whitespace from a u8 slice.
|
|
||||||
fn strip(input: []const u8) []const u8 {
|
|
||||||
return std.mem.trim(u8, input, &std.ascii.whitespace);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize a new Git repository when setting up a new project (optional).
|
// Initialize a new Git repository when setting up a new project (optional).
|
||||||
fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void {
|
fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void {
|
||||||
try runCommand(allocator, install_dir, &[_][]const u8{
|
try runCommand(allocator, install_dir, &[_][]const u8{
|
||||||
@ -416,13 +403,3 @@ fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void {
|
|||||||
"Initialize Jetzig project",
|
"Initialize Jetzig project",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print a success confirmation.
|
|
||||||
fn printSuccess() void {
|
|
||||||
std.debug.print(" ✅\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Print a failure confirmation.
|
|
||||||
fn printFailure() void {
|
|
||||||
std.debug.print(" ❌\n", .{});
|
|
||||||
}
|
|
96
cli/util.zig
Normal file
96
cli/util.zig
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// Decode a base64 string, used for parsing out build artifacts generated by the CLI program's
|
||||||
|
/// build.zig which are stored in the executable as a module.
|
||||||
|
pub fn base64Decode(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
|
||||||
|
const decoder = std.base64.Base64Decoder.init(
|
||||||
|
std.base64.url_safe_no_pad.alphabet_chars,
|
||||||
|
std.base64.url_safe_no_pad.pad_char,
|
||||||
|
);
|
||||||
|
const size = try decoder.calcSizeForSlice(input);
|
||||||
|
const ptr = try allocator.alloc(u8, size);
|
||||||
|
try decoder.decode(ptr, input);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print a success confirmation.
|
||||||
|
pub fn printSuccess() void {
|
||||||
|
std.debug.print(" ✅\n", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print a failure confirmation.
|
||||||
|
pub fn printFailure() void {
|
||||||
|
std.debug.print(" ❌\n", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifies that cwd is the root of a Jetzig project
|
||||||
|
pub fn detectJetzigProjectDir() !std.fs.Dir {
|
||||||
|
var dir = try std.fs.cwd().openDir(".", .{});
|
||||||
|
const max_parent_dirs: usize = 100; // Prevent symlink loops or other weird stuff.
|
||||||
|
|
||||||
|
for (0..max_parent_dirs) |_| {
|
||||||
|
if (try isPath(dir, "build.zig", .file) and try isPath(dir, "src/app/views", .dir)) return dir;
|
||||||
|
|
||||||
|
dir = dir.openDir("..", .{}) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.FileNotFound, error.NotDir => {
|
||||||
|
std.debug.print(
|
||||||
|
"Encountered unexpected detecting Jetzig project directory: {s}\n",
|
||||||
|
.{@errorName(err)},
|
||||||
|
);
|
||||||
|
return error.JetzigCommandError;
|
||||||
|
},
|
||||||
|
else => return err,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std.debug.print(
|
||||||
|
\\Exceeded maximum parent directory depth.
|
||||||
|
\\Unable to detect Jetzig project directory.
|
||||||
|
\\
|
||||||
|
,
|
||||||
|
.{},
|
||||||
|
);
|
||||||
|
return error.JetzigCommandError;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isPath(dir: std.fs.Dir, sub_path: []const u8, path_type: enum { file, dir }) !bool {
|
||||||
|
switch (path_type) {
|
||||||
|
.file => {
|
||||||
|
_ = dir.statFile(sub_path) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.FileNotFound => return false,
|
||||||
|
else => return err,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
.dir => {
|
||||||
|
var test_dir = dir.openDir(sub_path, .{}) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.FileNotFound, error.NotDir => return false,
|
||||||
|
else => return err,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
test_dir.close();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip leading and trailing whitespace from a u8 slice.
|
||||||
|
pub fn strip(input: []const u8) []const u8 {
|
||||||
|
return std.mem.trim(u8, input, &std.ascii.whitespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to confirm if a string input is in CamelCase. False if the first character is
|
||||||
|
/// not alphabetic lower-case or if the input contains underscores.
|
||||||
|
pub fn isCamelCase(input: []const u8) bool {
|
||||||
|
if (input.len == 0) return false;
|
||||||
|
if (!std.mem.containsAtLeast(u8, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1, &[_]u8{input[0]})) return false;
|
||||||
|
if (std.mem.containsAtLeast(u8, input, 1, "_")) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
3
demo/src/app/lib/example.zig
Normal file
3
demo/src/app/lib/example.zig
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub fn exampleFunction() []const u8 {
|
||||||
|
return "example value";
|
||||||
|
}
|
2
src/cli.gitignore
Normal file
2
src/cli.gitignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
zig-out/
|
||||||
|
zig-cache/
|
Loading…
x
Reference in New Issue
Block a user