diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 68d7e8f..1f6df02 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@v3 with: submodules: true - + - name: Setup Zig # You may pin to the exact commit or the version. # uses: goto-bus-stop/setup-zig@41ae19e72e21b9a1380e86ff9f058db709fc8fc6 @@ -30,10 +30,10 @@ jobs: with: version: master cache: true # Let's see how this behaves - + - run: zig version - run: zig env - + - name: Build run: zig build --verbose @@ -49,13 +49,13 @@ jobs: cd cli for target in "${targets[@]}"; do mkdir -p $root/artifacts/$target - echo "Building target ${target}..." + echo "Building target ${target}..." zig build -Dtarget=${target} -Doptimize=ReleaseSafe --prefix $root/artifacts/${target}/ & sed -e '1,5d' < $root/README.md > $root/artifacts/${target}/README.md cp $root/LICENSE $root/artifacts/${target}/ done wait - + - name: Upload artifacts Target Windows if: ${{ matrix.os == 'ubuntu-latest' }} uses: actions/upload-artifact@v2 @@ -72,7 +72,7 @@ jobs: if: ${{ matrix.os == 'ubuntu-latest' }} uses: actions/upload-artifact@v2 with: - name: builds-macos-x86 + name: build-macos-x86 path: artifacts/x86_64-macos - name: Upload artifacts Target MacOS 2 if: ${{ matrix.os == 'ubuntu-latest' }} diff --git a/cli/cli.zig b/cli/cli.zig index 65ef87d..61b9384 100644 --- a/cli/cli.zig +++ b/cli/cli.zig @@ -1,6 +1,7 @@ const std = @import("std"); const args = @import("args"); -const init = @import("init.zig"); +const init = @import("commands/init.zig"); +const generate = @import("commands/generate.zig"); const Options = struct { help: bool = false, @@ -10,8 +11,10 @@ const Options = struct { }; pub const meta = .{ + .usage_summary = "[COMMAND]", .option_docs = .{ .init = "Initialize a new project", + .generate = "Generate scaffolding", .help = "Print help and exit", }, }; @@ -19,6 +22,8 @@ const Options = struct { const Verb = union(enum) { init: init.Options, + generate: generate.Options, + g: generate.Options, }; /// 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(); - if (options.verb) |verb| { - switch (verb) { - .init => |opts| return init.run( - allocator, - opts, - writer, - options.positionals, - .{ .help = options.options.help }, - ), + run(allocator, options, writer) catch |err| { + switch (err) { + error.JetzigCommandError => std.os.exit(1), + else => return err, } - } + }; - if (options.options.help) { + if (options.options.help or options.verb == null) { try args.printHelp(Options, "jetzig", writer); try writer.writeAll( \\ \\Commands: \\ \\ init Initialize a new project. + \\ generate Generate scaffolding. \\ \\ 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 }, + ), + }; + } +} diff --git a/cli/commands/generate.zig b/cli/commands/generate.zig new file mode 100644 index 0000000..7898aa5 --- /dev/null +++ b/cli/commands/generate.zig @@ -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; + } +} diff --git a/cli/commands/generate/middleware.zig b/cli/commands/generate/middleware.zig new file mode 100644 index 0000000..ff9aca3 --- /dev/null +++ b/cli/commands/generate/middleware.zig @@ -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); + \\} + \\ +; diff --git a/cli/commands/generate/partial.zig b/cli/commands/generate/partial.zig new file mode 100644 index 0000000..dfc9983 --- /dev/null +++ b/cli/commands/generate/partial.zig @@ -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( + \\
Partial content goes here.
+ \\ + ); + + file.close(); + + const realpath = try dir.realpathAlloc(allocator, filename); + defer allocator.free(realpath); + std.debug.print("Generated partial template: {s}\n", .{realpath}); +} diff --git a/cli/commands/generate/view.zig b/cli/commands/generate/view.zig new file mode 100644 index 0000000..e9d7c55 --- /dev/null +++ b/cli/commands/generate/view.zig @@ -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( + \\
+ \\ Content goes here + \\
+ \\ + ); + + file.close(); + + const realpath = try view_dir.realpathAlloc(allocator, filename); + defer allocator.free(realpath); + std.debug.print("Generated template: {s}\n", .{realpath}); +} diff --git a/cli/init.zig b/cli/commands/init.zig similarity index 89% rename from cli/init.zig rename to cli/commands/init.zig index c718948..4244088 100644 --- a/cli/init.zig +++ b/cli/commands/init.zig @@ -1,18 +1,10 @@ const std = @import("std"); const args = @import("args"); +const util = @import("../util.zig"); + const init_data = @import("init_data").init_data; -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; -} - +/// Command line options for the `init` command. pub const Options = struct { path: ?[]const u8 = null, @@ -50,7 +42,7 @@ pub fn run( for (positionals) |arg| { if (install_path != null) { std.debug.print("Unexpected positional argument: {s}\n", .{arg}); - return error.JetzigUnexpectedPositionalArgumentsError; + return error.JetzigCommandError; } 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}); if (result.term.Exited != 0) { - printFailure(); + util.printFailure(); std.debug.print( \\ \\Error running command: {s} @@ -244,9 +236,9 @@ fn runCommand(allocator: std.mem.Allocator, install_path: []const u8, argv: []co \\{s} \\ , .{ command, result.stdout, result.stderr }); - return error.JetzigRunCommandError; + return error.JetzigCommandError; } else { - printSuccess(); + util.printSuccess(); } } @@ -267,7 +259,7 @@ fn copySourceFile( var content: []const u8 = undefined; if (replace) |capture| { const initial = readSourceFile(allocator, src) catch |err| { - printFailure(); + util.printFailure(); return err; }; defer allocator.free(initial); @@ -276,25 +268,25 @@ fn copySourceFile( } } else { content = readSourceFile(allocator, src) catch |err| { - printFailure(); + util.printFailure(); return err; }; } defer allocator.free(content); writeSourceFile(install_dir, dest, content) catch |err| { - printFailure(); + util.printFailure(); return err; }; - printSuccess(); + util.printSuccess(); } // Read a file from Jetzig source code. fn readSourceFile(allocator: std.mem.Allocator, path: []const u8) ![]const u8 { 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. @@ -335,7 +327,7 @@ fn githubUrl(allocator: std.mem.Allocator) ![]const u8 { if (fetch_result.status != .ok) { std.debug.print("Error fetching from GitHub: {s}\n", .{url}); - return error.JetzigGitHubFetchError; + return error.JetzigCommandError; } const parsed_response = try std.json.parseFromSlice( @@ -381,20 +373,15 @@ fn promptInput( const input = try reader.readUntilDelimiterOrEofAlloc(allocator, '\n', max_read_bytes); if (input) |capture| { defer allocator.free(capture); - const stripped_input = strip(capture); + const stripped_input = util.strip(capture); 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); } } } -// 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). fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void { 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", }); } - -/// Print a success confirmation. -fn printSuccess() void { - std.debug.print(" ✅\n", .{}); -} - -/// Print a failure confirmation. -fn printFailure() void { - std.debug.print(" ❌\n", .{}); -} diff --git a/cli/util.zig b/cli/util.zig new file mode 100644 index 0000000..449b90e --- /dev/null +++ b/cli/util.zig @@ -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; +} diff --git a/demo/src/app/lib/example.zig b/demo/src/app/lib/example.zig new file mode 100644 index 0000000..eb10c1f --- /dev/null +++ b/demo/src/app/lib/example.zig @@ -0,0 +1,3 @@ +pub fn exampleFunction() []const u8 { + return "example value"; +} diff --git a/src/cli.gitignore b/src/cli.gitignore new file mode 100644 index 0000000..ee7098f --- /dev/null +++ b/src/cli.gitignore @@ -0,0 +1,2 @@ +zig-out/ +zig-cache/