Merge pull request #118 from jetzig-framework/deprecate-data-arg

Database CLI improvements
This commit is contained in:
bobf 2024-11-19 22:17:47 +00:00 committed by GitHub
commit d887cd5bd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 611 additions and 146 deletions

View File

@ -7,8 +7,8 @@
.hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163", .hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163",
}, },
.zmpl = .{ .zmpl = .{
.url = "https://github.com/jetzig-framework/zmpl/archive/7b7452bc7fdb0bd2f8a6a9c4e9312900b486aeba.tar.gz", .url = "https://github.com/jetzig-framework/zmpl/archive/25b91d030b992631d319adde1cf01baecd9f3934.tar.gz",
.hash = "1220ed127f38fa51df53a85b3cc2030a7555e34058db7fd374ebaef817abb43d35f7", .hash = "12208dd5a4bf0c6c7efc4e9f37a5d8ed80d6004d5680176d1fc2114bfa593e927baf",
}, },
.jetkv = .{ .jetkv = .{
.url = "https://github.com/jetzig-framework/jetkv/archive/2b1130a48979ea2871c8cf6ca89c38b1e7062839.tar.gz", .url = "https://github.com/jetzig-framework/jetkv/archive/2b1130a48979ea2871c8cf6ca89c38b1e7062839.tar.gz",

208
cli/colors.zig Normal file
View File

@ -0,0 +1,208 @@
const std = @import("std");
// Must be consistent with `std.io.tty.Color` for Windows compatibility.
pub const codes = .{
.escape = "\x1b[",
.black = "30m",
.red = "31m",
.green = "32m",
.yellow = "33m",
.blue = "34m",
.magenta = "35m",
.cyan = "36m",
.white = "37m",
.bright_black = "90m",
.bright_red = "91m",
.bright_green = "92m",
.bright_yellow = "93m",
.bright_blue = "94m",
.bright_magenta = "95m",
.bright_cyan = "96m",
.bright_white = "97m",
.bold = "1m",
.dim = "2m",
.reset = "0m",
};
/// Map color codes generated by `std.io.tty.Config.setColor` back to `std.io.tty.Color`. Used by
/// `jetzig.util.writeAnsi` to parse escape codes so they can be passed to
/// `std.io.tty.Config.setColor` (using Windows API to set console color mode).
const ansi_colors = .{
.{ "30", .black },
.{ "31", .red },
.{ "32", .green },
.{ "33", .yellow },
.{ "34", .blue },
.{ "35", .magenta },
.{ "36", .cyan },
.{ "37", .white },
.{ "90", .bright_black },
.{ "91", .bright_red },
.{ "92", .bright_green },
.{ "93", .bright_yellow },
.{ "94", .bright_blue },
.{ "95", .bright_magenta },
.{ "96", .bright_cyan },
.{ "97", .bright_white },
.{ "1", .bold },
.{ "2", .dim },
.{ "0", .reset },
};
pub const codes_map = if (@hasDecl(std, "ComptimeStringMap"))
std.ComptimeStringMap(std.io.tty.Color, ansi_colors)
else if (@hasDecl(std, "StaticStringMap"))
std.StaticStringMap(std.io.tty.Color).initComptime(ansi_colors)
else
unreachable;
// Map basic ANSI color codes to Windows TextAttribute colors
// used by std.os.windows.SetConsoleTextAttribute()
const windows_colors = .{
.{ "30", 0 },
.{ "31", 4 },
.{ "32", 2 },
.{ "33", 6 },
.{ "34", 1 },
.{ "35", 5 },
.{ "36", 3 },
.{ "37", 7 },
.{ "90", 8 },
.{ "91", 12 },
.{ "92", 10 },
.{ "93", 14 },
.{ "94", 9 },
.{ "95", 13 },
.{ "96", 11 },
.{ "97", 15 },
.{ "1", 7 },
.{ "2", 7 },
.{ "0", 7 },
};
pub const windows_map = if (@hasDecl(std, "ComptimeStringMap"))
std.ComptimeStringMap(u16, windows_colors)
else if (@hasDecl(std, "StaticStringMap"))
std.StaticStringMap(u16).initComptime(windows_colors)
else
unreachable;
/// Colorize a log message. Note that we force `.escape_codes` when we are a TTY even on Windows.
/// `jetzig.loggers.LogQueue` parses the ANSI codes and uses `std.io.tty.Config.setColor` to
/// invoke the appropriate Windows API call to set the terminal color before writing each token.
/// We must do it this way because Windows colors are set by API calls at the time of write, not
/// encoded into the message string.
pub fn colorize(color: std.io.tty.Color, buf: []u8, input: []const u8, is_colorized: bool) ![]const u8 {
if (!is_colorized) return input;
const config: std.io.tty.Config = .escape_codes;
var stream = std.io.fixedBufferStream(buf);
const writer = stream.writer();
try config.setColor(writer, color);
try writer.writeAll(input);
try config.setColor(writer, .reset);
return stream.getWritten();
}
fn wrap(comptime attribute: []const u8, comptime message: []const u8) []const u8 {
return codes.escape ++ attribute ++ message ++ codes.escape ++ codes.reset;
}
fn runtimeWrap(allocator: std.mem.Allocator, attribute: []const u8, message: []const u8) ![]const u8 {
return try std.mem.join(
allocator,
"",
&[_][]const u8{ codes.escape, attribute, message, codes.escape, codes.reset },
);
}
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);
}
pub fn runtimeBlack(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.black, message);
}
pub fn red(comptime message: []const u8) []const u8 {
return wrap(codes.red, message);
}
pub fn runtimeRed(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.red, message);
}
pub fn green(comptime message: []const u8) []const u8 {
return wrap(codes.green, message);
}
pub fn runtimeGreen(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.green, message);
}
pub fn yellow(comptime message: []const u8) []const u8 {
return wrap(codes.yellow, message);
}
pub fn runtimeYellow(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.yellow, message);
}
pub fn blue(comptime message: []const u8) []const u8 {
return wrap(codes.blue, message);
}
pub fn runtimeBlue(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.blue, message);
}
pub fn magenta(comptime message: []const u8) []const u8 {
return wrap(codes.magenta, message);
}
pub fn runtimeMagenta(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.magenta, message);
}
pub fn cyan(comptime message: []const u8) []const u8 {
return wrap(codes.cyan, message);
}
pub fn runtimeCyan(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.cyan, message);
}
pub fn white(comptime message: []const u8) []const u8 {
return wrap(codes.white, message);
}
pub fn runtimeWhite(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.white, message);
}
pub fn duration(buf: *[256]u8, delta: i64, is_colorized: bool) ![]const u8 {
if (!is_colorized) {
return try std.fmt.bufPrint(
buf,
"{}",
.{std.fmt.fmtDurationSigned(delta)},
);
}
const color: std.io.tty.Color = if (delta < 1000000)
.green
else if (delta < 5000000)
.yellow
else
.red;
var duration_buf: [256]u8 = undefined;
const formatted_duration = try std.fmt.bufPrint(
&duration_buf,
"{}",
.{std.fmt.fmtDurationSigned(delta)},
);
return try colorize(color, buf, formatted_duration, true);
}

View File

@ -5,17 +5,16 @@ const util = @import("../util.zig");
/// Command line options for the `update` command. /// Command line options for the `update` command.
pub const Options = struct { pub const Options = struct {
pub const meta = .{ pub const meta = .{
.usage_summary = "[password]", .usage_summary = "[init|create]",
.full_text = .full_text =
\\Generates a password \\Manage user authentication. Initialize with `init` to generate a users table migration.
\\
\\Example: \\Example:
\\ \\
\\ jetzig update \\ jetzig auth init
\\ jetzig update web \\ jetzig auth create bob@jetzig.dev
, ,
.option_docs = .{ .option_docs = .{},
.path = "Set the output path relative to the current directory (default: current directory)",
},
}; };
}; };
@ -32,9 +31,10 @@ pub fn run(
defer arena.deinit(); defer arena.deinit();
const allocator = arena.allocator(); const allocator = arena.allocator();
const Action = enum { user_create }; const Action = enum { init, create };
const map = std.StaticStringMap(Action).initComptime(.{ const map = std.StaticStringMap(Action).initComptime(.{
.{ "user:create", .user_create }, .{ "init", .init },
.{ "create", .create },
}); });
const action = if (main_options.positionals.len > 0) const action = if (main_options.positionals.len > 0)
@ -55,7 +55,20 @@ pub fn run(
break :blk error.JetzigCommandError; break :blk error.JetzigCommandError;
} else if (action) |capture| } else if (action) |capture|
switch (capture) { switch (capture) {
.user_create => blk: { .init => {
const argv = [_][]const u8{
"jetzig",
"generate",
"migration",
"create_users",
"table:users",
"column:email:string:index:unique",
"column:password_hash:string",
};
try util.runCommand(allocator, &argv);
try util.print(.success, "Migration created. Run `jetzig database update` to run migration and reflect database.", .{});
},
.create => blk: {
if (sub_args.len < 1) { if (sub_args.len < 1) {
std.debug.print("Missing argument. Expected an email/username parameter.\n", .{}); std.debug.print("Missing argument. Expected an email/username parameter.\n", .{});
break :blk error.JetzigCommandError; break :blk error.JetzigCommandError;

View File

@ -149,7 +149,7 @@ pub fn run(
const tmpdir_real_path = try tmpdir.realpathAlloc(allocator, "."); const tmpdir_real_path = try tmpdir.realpathAlloc(allocator, ".");
defer allocator.free(tmpdir_real_path); defer allocator.free(tmpdir_real_path);
try util.runCommand(allocator, tmpdir_real_path, tar_argv.items); try util.runCommandInDir(allocator, tar_argv.items, .{ .path = tmpdir_real_path });
switch (builtin.os.tag) { switch (builtin.os.tag) {
.windows => {}, .windows => {},
@ -215,7 +215,7 @@ fn zig_build_install(allocator: std.mem.Allocator, path: []const u8, options: Op
defer project_dir.close(); defer project_dir.close();
project_dir.makePath(".bundle") catch {}; project_dir.makePath(".bundle") catch {};
try util.runCommand(allocator, path, install_argv.items); try util.runCommandInDir(allocator, install_argv.items, .{ .path = path });
const install_bin_path = try std.fs.path.join(allocator, &[_][]const u8{ ".bundle", "bin" }); const install_bin_path = try std.fs.path.join(allocator, &[_][]const u8{ ".bundle", "bin" });
defer allocator.free(install_bin_path); defer allocator.free(install_bin_path);

View File

@ -9,12 +9,15 @@ const rollback = @import("database/rollback.zig");
const create = @import("database/create.zig"); const create = @import("database/create.zig");
const drop = @import("database/drop.zig"); const drop = @import("database/drop.zig");
const reflect = @import("database/reflect.zig"); const reflect = @import("database/reflect.zig");
const update = @import("database/update.zig");
const setup = @import("database/setup.zig");
pub const confirm_drop_env = "JETZIG_DROP_PRODUCTION_DATABASE"; pub const confirm_drop_env = "JETZIG_DROP_PRODUCTION_DATABASE";
/// Command line options for the `database` command. /// Command line options for the `database` command.
pub const Options = struct { pub const Options = struct {
pub const meta = .{ pub const meta = .{
.usage_summary = "[migrate|rollback|create|drop|reflect]", .usage_summary = "[setup|create|drop|migrate|rollback|reflect|update]",
.full_text = .full_text =
\\Manage the application's database. \\Manage the application's database.
\\ \\
@ -38,13 +41,15 @@ pub fn run(
defer arena.deinit(); defer arena.deinit();
const alloc = arena.allocator(); const alloc = arena.allocator();
const Action = enum { migrate, rollback, create, drop, reflect }; const Action = enum { migrate, rollback, create, drop, reflect, update, setup };
const map = std.StaticStringMap(Action).initComptime(.{ const map = std.StaticStringMap(Action).initComptime(.{
.{ "migrate", .migrate }, .{ "migrate", .migrate },
.{ "rollback", .rollback }, .{ "rollback", .rollback },
.{ "create", .create }, .{ "create", .create },
.{ "drop", .drop }, .{ "drop", .drop },
.{ "reflect", .reflect }, .{ "reflect", .reflect },
.{ "update", .update },
.{ "setup", .setup },
}); });
const action = if (main_options.positionals.len > 0) const action = if (main_options.positionals.len > 0)
@ -73,6 +78,8 @@ pub fn run(
.create => create.run(alloc, cwd, sub_args, options, T, main_options), .create => create.run(alloc, cwd, sub_args, options, T, main_options),
.drop => drop.run(alloc, cwd, sub_args, options, T, main_options), .drop => drop.run(alloc, cwd, sub_args, options, T, main_options),
.reflect => reflect.run(alloc, cwd, sub_args, options, T, main_options), .reflect => reflect.run(alloc, cwd, sub_args, options, T, main_options),
.update => update.run(alloc, cwd, sub_args, options, T, main_options),
.setup => setup.run(alloc, cwd, sub_args, options, T, main_options),
}; };
}; };
} }

View File

@ -0,0 +1,54 @@
const std = @import("std");
const cli = @import("../../cli.zig");
const util = @import("../../util.zig");
pub fn run(
allocator: std.mem.Allocator,
cwd: std.fs.Dir,
args: []const []const u8,
options: cli.database.Options,
T: type,
main_options: T,
) !void {
_ = cwd;
_ = options;
if (main_options.options.help or args.len != 0) {
std.debug.print(
\\Set up a database: create a database, run migrations, reflect schema.
\\
\\Convenience wrapper for:
\\
\\* jetzig database create
\\* jetzig database update
\\
\\Example:
\\
\\ jetzig database setup
\\ jetzig --environment=testing setup
\\
, .{});
return if (main_options.options.help) {} else error.JetzigCommandError;
}
const env = main_options.options.environment;
try runCommand(allocator, env, "create");
try runCommand(allocator, env, "migrate");
try runCommand(allocator, env, "reflect");
try util.print(
.success,
"Database created, migrations applied, and Schema generated successfully.",
.{},
);
}
fn runCommand(allocator: std.mem.Allocator, environment: anytype, comptime action: []const u8) !void {
try util.runCommand(allocator, &.{
"zig",
"build",
util.environmentBuildOption(environment),
"jetzig:database:" ++ action,
});
}

View File

@ -0,0 +1,45 @@
const std = @import("std");
const cli = @import("../../cli.zig");
const util = @import("../../util.zig");
pub fn run(
allocator: std.mem.Allocator,
cwd: std.fs.Dir,
args: []const []const u8,
options: cli.database.Options,
T: type,
main_options: T,
) !void {
_ = cwd;
_ = options;
if (main_options.options.help or args.len != 0) {
std.debug.print(
\\Update a database: run migrations and reflect schema.
\\
\\Convenience wrapper for `jetzig database migrate` and `jetzig database reflect`.
\\
\\Example:
\\
\\ jetzig database update
\\ jetzig --environment=testing update
\\
, .{});
return if (main_options.options.help) {} else error.JetzigCommandError;
}
try util.runCommand(allocator, &.{
"zig",
"build",
util.environmentBuildOption(main_options.options.environment),
"jetzig:database:migrate",
});
try util.runCommand(allocator, &.{
"zig",
"build",
util.environmentBuildOption(main_options.options.environment),
"jetzig:database:reflect",
});
}

View File

@ -47,7 +47,7 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he
\\// \\//
\\// Arguments: \\// Arguments:
\\// * allocator: Arena allocator for use during the job execution process. \\// * allocator: Arena allocator for use during the job execution process.
\\// * params: Params assigned to a job (from a request, any values added to `data`). \\// * params: Params assigned to a job (from a request, values added to response data).
\\// * env: Provides the following fields: \\// * env: Provides the following fields:
\\// - logger: Logger attached to the same stream as the Jetzig server. \\// - logger: Logger attached to the same stream as the Jetzig server.
\\// - environment: Enum of `{ production, development }`. \\// - environment: Enum of `{ production, development }`.

View File

@ -194,12 +194,16 @@ pub fn run(
null, null,
); );
try util.runCommand(allocator, realpath, &[_][]const u8{ try util.runCommandInDir(
allocator,
&[_][]const u8{
"zig", "zig",
"fetch", "fetch",
"--save", "--save",
github_url, github_url,
}); },
.{ .dir = install_dir },
);
// TODO: Use arg or interactive prompt to do Git setup in net project, default to no. // TODO: Use arg or interactive prompt to do Git setup in net project, default to no.
// const git_setup = false; // const git_setup = false;
@ -322,22 +326,34 @@ fn promptInput(
// 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 util.runCommand(allocator, install_dir, &[_][]const u8{ try util.runCommandInDir(
allocator,
&[_][]const u8{
"git", "git",
"init", "init",
".", ".",
}); },
.{ .path = install_dir },
);
try util.runCommand(allocator, install_dir, &[_][]const u8{ try util.runCommandInDir(
allocator,
&[_][]const u8{
"git", "git",
"add", "add",
".", ".",
}); },
.{ .path = install_dir },
);
try util.runCommand(allocator, install_dir, &[_][]const u8{ try util.runCommandInDir(
allocator,
&[_][]const u8{
"git", "git",
"commit", "commit",
"-m", "-m",
"Initialize Jetzig project", "Initialize Jetzig project",
}); },
.{ .path = install_dir },
);
} }

View File

@ -63,9 +63,8 @@ pub fn run(
); );
while (true) { while (true) {
util.runCommand( util.runCommandInDir(
allocator, allocator,
realpath,
&.{ &.{
"zig", "zig",
"build", "build",
@ -75,6 +74,7 @@ pub fn run(
"--color", "--color",
"on", "on",
}, },
.{ .path = realpath },
) catch { ) catch {
std.debug.print("Build failed, waiting for file change...\n", .{}); std.debug.print("Build failed, waiting for file change...\n", .{});
try awaitFileChange(allocator, cwd, &mtime); try awaitFileChange(allocator, cwd, &mtime);

View File

@ -55,18 +55,15 @@ pub fn run(
const save_arg = try std.mem.concat(allocator, u8, &[_][]const u8{ "--save=", name }); const save_arg = try std.mem.concat(allocator, u8, &[_][]const u8{ "--save=", name });
defer allocator.free(save_arg); defer allocator.free(save_arg);
var cwd = try util.detectJetzigProjectDir(); try util.runCommand(
defer cwd.close(); allocator,
&[_][]const u8{
const realpath = try std.fs.realpathAlloc(allocator, ".");
defer allocator.free(realpath);
try util.runCommand(allocator, realpath, &[_][]const u8{
"zig", "zig",
"fetch", "fetch",
save_arg, save_arg,
github_url, github_url,
}); },
);
std.debug.print( std.debug.print(
\\Update complete. \\Update complete.

View File

@ -2,6 +2,7 @@ const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const cli = @import("cli.zig"); const cli = @import("cli.zig");
const colors = @import("colors.zig");
/// Decode a base64 string, used for parsing out build artifacts generated by the CLI program's /// 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. /// build.zig which are stored in the executable as a module.
@ -16,14 +17,35 @@ pub fn base64Decode(allocator: std.mem.Allocator, input: []const u8) ![]const u8
return ptr; return ptr;
} }
const icons = .{
.check = "",
.cross = "",
};
/// Print a success confirmation. /// Print a success confirmation.
pub fn printSuccess() void { pub fn printSuccess() void {
std.debug.print("\n", .{}); std.debug.print(" " ++ icons.check ++ "\n", .{});
} }
/// Print a failure confirmation. /// Print a failure confirmation.
pub fn printFailure() void { pub fn printFailure() void {
std.debug.print("\n", .{}); std.debug.print(" " ++ icons.cross ++ "\n", .{});
}
const PrintContext = enum { success, failure };
/// Print some output in with a given context to stderr.
pub fn print(comptime context: PrintContext, comptime message: []const u8, args: anytype) !void {
const writer = std.io.getStdErr().writer();
switch (context) {
.success => try writer.print(
std.fmt.comptimePrint("{s} {s}\n", .{ icons.check, colors.green(message) }),
args,
),
.failure => try writer.print(
std.fmt.comptimePrint("{s} {s}\n", .{ icons.cross, colors.red(message) }),
args,
),
}
} }
/// Detects a Jetzig project directory either in the current directory or one of its parent /// Detects a Jetzig project directory either in the current directory or one of its parent
@ -126,9 +148,32 @@ pub fn runCommandStreaming(allocator: std.mem.Allocator, install_path: []const u
_ = try child.wait(); _ = try child.wait();
} }
/// Runs a command as a child process and verifies successful exit code. /// Runs a command as a child process in Jetzig project directory and verifies successful exit
pub fn runCommand(allocator: std.mem.Allocator, install_path: []const u8, argv: []const []const u8) !void { /// code.
const result = try std.process.Child.run(.{ .allocator = allocator, .argv = argv, .cwd = install_path }); pub fn runCommand(allocator: std.mem.Allocator, argv: []const []const u8) !void {
var dir = try detectJetzigProjectDir();
defer dir.close();
try runCommandInDir(allocator, argv, .{ .dir = dir });
}
const Dir = union(enum) {
path: []const u8,
dir: std.fs.Dir,
};
/// Runs a command as a child process in the given directory and verifies successful exit code.
pub fn runCommandInDir(allocator: std.mem.Allocator, argv: []const []const u8, dir: Dir) !void {
const cwd_path = switch (dir) {
.path => |capture| capture,
.dir => |capture| try capture.realpathAlloc(allocator, "."),
};
defer if (dir == .dir) allocator.free(cwd_path);
const result = try std.process.Child.run(.{
.allocator = allocator,
.argv = argv,
.cwd = cwd_path,
});
defer allocator.free(result.stdout); defer allocator.free(result.stdout);
defer allocator.free(result.stderr); defer allocator.free(result.stderr);

View File

@ -28,12 +28,11 @@ pub const defaults: jetzig.mail.DefaultMailParams = .{
pub fn deliver( pub fn deliver(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
mail: *jetzig.mail.MailParams, mail: *jetzig.mail.MailParams,
data: *jetzig.data.Data,
params: *jetzig.data.Value, params: *jetzig.data.Value,
env: jetzig.jobs.JobEnv, env: jetzig.jobs.JobEnv,
) !void { ) !void {
_ = allocator; _ = allocator;
try params.put("email_message", data.string("Custom email message")); try params.put("email_message", "Custom email message");
try env.logger.INFO("Delivering email with subject: '{?s}'", .{mail.get(.subject)}); try env.logger.INFO("Delivering email with subject: '{?s}'", .{mail.get(.subject)});
} }

View File

@ -1,7 +1,7 @@
const jetzig = @import("jetzig"); const jetzig = @import("jetzig");
pub fn bar(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { pub fn bar(id: []const u8, request: *jetzig.Request) !jetzig.View {
var root = try data.object(); var root = try request.data(.object);
try root.put("id", data.string(id)); try root.put("id", id);
return request.render(.ok); return request.render(.ok);
} }

View File

@ -1,13 +1,12 @@
const std = @import("std"); const std = @import("std");
const jetzig = @import("jetzig"); const jetzig = @import("jetzig");
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { pub fn index(request: *jetzig.Request) !jetzig.View {
_ = data;
return request.render(.ok); return request.render(.ok);
} }
pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { pub fn post(request: *jetzig.Request) !jetzig.View {
var root = try data.root(.object); var root = try request.data(.object);
const params = try request.params(); const params = try request.params();

View File

@ -2,8 +2,8 @@ const std = @import("std");
const jetzig = @import("jetzig"); const jetzig = @import("jetzig");
/// This example demonstrates usage of Jetzig's KV store. /// This example demonstrates usage of Jetzig's KV store.
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { pub fn index(request: *jetzig.Request) !jetzig.View {
var root = try data.object(); var root = try request.data(.object);
// Fetch a string from the KV store. If it exists, store it in the root data object, // Fetch a string from the KV store. If it exists, store it in the root data object,
// otherwise store a string value to be picked up by the next request. // otherwise store a string value to be picked up by the next request.
@ -11,7 +11,7 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
try root.put("stored_string", capture); try root.put("stored_string", capture);
} else { } else {
try root.put("stored_string", null); try root.put("stored_string", null);
try request.store.put("example-key", data.string("example-value")); try request.store.put("example-key", "example-value");
} }
// Left-pop an item from an array and store it in the root data object. This will empty the // Left-pop an item from an array and store it in the root data object. This will empty the
@ -21,9 +21,9 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
try root.put("popped", value); try root.put("popped", value);
} else { } else {
// Store some values in an array in the KV store. // Store some values in an array in the KV store.
try request.store.append("example-array", data.string("hello")); try request.store.append("example-array", "hello");
try request.store.append("example-array", data.string("goodbye")); try request.store.append("example-array", "goodbye");
try request.store.append("example-array", data.string("hello again")); try request.store.append("example-array", "hello again");
try root.put("popped", null); try root.put("popped", null);
} }

View File

@ -1,8 +1,7 @@
const std = @import("std"); const std = @import("std");
const jetzig = @import("jetzig"); const jetzig = @import("jetzig");
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { pub fn index(request: *jetzig.Request) !jetzig.View {
_ = data;
return request.render(.ok); return request.render(.ok);
} }
@ -13,9 +12,9 @@ pub const static_params = .{
}, },
}; };
pub fn get(id: []const u8, request: *jetzig.StaticRequest, data: *jetzig.Data) !jetzig.View { pub fn get(id: []const u8, request: *jetzig.StaticRequest) !jetzig.View {
var object = try data.object(); var object = try request.data(.object);
try object.put("id", data.string(id)); try object.put("id", id);
const params = try request.params(); const params = try request.params();
if (params.get("foo")) |value| try object.put("foo", value); if (params.get("foo")) |value| try object.put("foo", value);
return request.render(.ok); return request.render(.ok);

View File

@ -42,6 +42,14 @@ test "post query params" {
}, },
}); });
try response2.expectStatus(.unprocessable_entity); try response2.expectStatus(.unprocessable_entity);
const response3 = try app.request(.POST, "/params", .{
.params = .{
.name = "", // empty param
.favorite_animal = "raccoon",
},
});
try response3.expectStatus(.unprocessable_entity);
} }
test "post json" { test "post json" {

View File

@ -1,14 +1,13 @@
const std = @import("std"); const std = @import("std");
const jetzig = @import("jetzig"); const jetzig = @import("jetzig");
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { pub fn index(request: *jetzig.Request) !jetzig.View {
_ = data;
const params = try request.params(); const params = try request.params();
if (params.get("redirect")) |location| { if (params.get("redirect")) |location| {
switch (location.*) { switch (location.*) {
// Value is `.Null` when param is empty, e.g.: // Value is `.null` when param is empty, e.g.:
// `http://localhost:8080/redirect?redirect` // `http://localhost:8080/redirect?redirect`
.Null => return request.redirect("http://www.example.com/", .moved_permanently), .null => return request.redirect("http://www.example.com/", .moved_permanently),
// Value is `.string` when param is present, e.g.: // Value is `.string` when param is present, e.g.:
// `http://localhost:8080/redirect?redirect=https://jetzig.dev/` // `http://localhost:8080/redirect?redirect=https://jetzig.dev/`
.string => |string| return request.redirect(string.value, .moved_permanently), .string => |string| return request.redirect(string.value, .moved_permanently),

View File

@ -7,6 +7,7 @@ const jetzig = @import("jetzig");
const Migrate = @import("jetquery_migrate").Migrate; const Migrate = @import("jetquery_migrate").Migrate;
const MigrateSchema = @import("jetquery_migrate").MigrateSchema; const MigrateSchema = @import("jetquery_migrate").MigrateSchema;
const Schema = @import("Schema"); const Schema = @import("Schema");
const util = @import("util.zig");
const confirm_drop_env = "JETZIG_DROP_PRODUCTION_DATABASE"; const confirm_drop_env = "JETZIG_DROP_PRODUCTION_DATABASE";
const production_drop_failure_message = "To drop a production database, " ++ const production_drop_failure_message = "To drop a production database, " ++
@ -20,6 +21,15 @@ pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer std.debug.assert(gpa.deinit() == .ok); defer std.debug.assert(gpa.deinit() == .ok);
if (comptime !@hasField(@TypeOf(config), "adapter") or config.adapter == .null) {
try util.print(
.failure,
"Database is currently not configured. Update `config/database.zig` before running database commands.",
.{},
);
std.process.exit(1);
}
const gpa_allocator = gpa.allocator(); const gpa_allocator = gpa.allocator();
var arena = std.heap.ArenaAllocator.init(gpa_allocator); var arena = std.heap.ArenaAllocator.init(gpa_allocator);
defer arena.deinit(); defer arena.deinit();

33
src/commands/util.zig Normal file
View File

@ -0,0 +1,33 @@
const std = @import("std");
const colors = @import("jetzig").colors;
const icons = .{
.check = "",
.cross = "",
};
/// Print a success confirmation.
pub fn printSuccess() void {
std.debug.print(" " ++ icons.check ++ "\n", .{});
}
/// Print a failure confirmation.
pub fn printFailure() void {
std.debug.print(" " ++ icons.cross ++ "\n", .{});
}
const PrintContext = enum { success, failure };
/// Print some output in with a given context to stderr.
pub fn print(comptime context: PrintContext, comptime message: []const u8, args: anytype) !void {
const writer = std.io.getStdErr().writer();
switch (context) {
.success => try writer.print(
std.fmt.comptimePrint("{s} {s}\n", .{ icons.check, colors.green(message) }),
args,
),
.failure => try writer.print(
std.fmt.comptimePrint("{s} {s}\n", .{ icons.cross, colors.red(message) }),
args,
),
}
}

View File

@ -186,13 +186,13 @@ test "query string with param without value" {
const foo = data.get("foo").?; const foo = data.get("foo").?;
try switch (foo.*) { try switch (foo.*) {
.Null => {}, .null => {},
else => std.testing.expect(false), else => std.testing.expect(false),
}; };
const bar = data.get("bar").?; const bar = data.get("bar").?;
try switch (bar.*) { try switch (bar.*) {
.Null => {}, .null => {},
else => std.testing.expect(false), else => std.testing.expect(false),
}; };
} }

View File

@ -60,8 +60,9 @@ pub const RequestStore = struct {
} }
/// Get a String from the store. /// Get a String from the store.
pub fn put(self: RequestStore, key: []const u8, value: *jetzig.data.Value) !void { pub fn put(self: RequestStore, key: []const u8, value: anytype) !void {
try self.store.put(key, value); const alloc = (try self.data()).allocator();
try self.store.put(key, try jetzig.Data.zmplValue(value, alloc));
} }
/// Remove a String to from the key-value store and return it if found. /// Remove a String to from the key-value store and return it if found.
@ -75,13 +76,15 @@ pub const RequestStore = struct {
} }
/// Append a Value to the end of an Array in the key-value store. /// Append a Value to the end of an Array in the key-value store.
pub fn append(self: RequestStore, key: []const u8, value: *jetzig.data.Value) !void { pub fn append(self: RequestStore, key: []const u8, value: anytype) !void {
try self.store.append(key, value); const alloc = (try self.data()).allocator();
try self.store.append(key, try jetzig.Data.zmplValue(value, alloc));
} }
/// Prepend a Value to the start of an Array in the key-value store. /// Prepend a Value to the start of an Array in the key-value store.
pub fn prepend(self: RequestStore, key: []const u8, value: *jetzig.data.Value) !void { pub fn prepend(self: RequestStore, key: []const u8, value: anytype) !void {
try self.store.prepend(key, value); const alloc = (try self.data()).allocator();
try self.store.prepend(key, try jetzig.Data.zmplValue(value, alloc));
} }
/// Pop a String from an Array in the key-value store. /// Pop a String from an Array in the key-value store.
@ -660,7 +663,7 @@ pub fn joinPath(self: *const Request, args: anytype) ![]const u8 {
@compileError("Cannot coerce type `" ++ @typeName(field.type) ++ "` to string."), @compileError("Cannot coerce type `" ++ @typeName(field.type) ++ "` to string."),
else => args[index], // Assume []const u8, let Zig do the work. else => args[index], // Assume []const u8, let Zig do the work.
}, },
.int, .float => try std.fmt.allocPrint(self.allocator, "{d}", args[index]), .int, .float => try std.fmt.allocPrint(self.allocator, "{d}", .{args[index]}),
else => @compileError("Cannot coerce type `" ++ @typeName(field.type) ++ "` to string."), else => @compileError("Cannot coerce type `" ++ @typeName(field.type) ++ "` to string."),
}; };
} }

View File

@ -166,7 +166,7 @@ fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
const static_resource = self.matchStaticResource(request) catch |err| { const static_resource = self.matchStaticResource(request) catch |err| {
if (isUnhandledError(err)) return err; if (isUnhandledError(err)) return err;
const rendered = try self.renderInternalServerError(request, err); const rendered = try self.renderInternalServerError(request, @errorReturnTrace(), err);
request.setResponse(rendered, .{}); request.setResponse(rendered, .{});
return; return;
}; };
@ -220,7 +220,11 @@ fn renderHTML(
if (zmpl.findPrefixed("views", matched_route.template)) |template| { if (zmpl.findPrefixed("views", matched_route.template)) |template| {
const rendered = self.renderView(matched_route, request, template) catch |err| { const rendered = self.renderView(matched_route, request, template) catch |err| {
if (isUnhandledError(err)) return err; if (isUnhandledError(err)) return err;
const rendered_error = try self.renderInternalServerError(request, err); const rendered_error = try self.renderInternalServerError(
request,
@errorReturnTrace(),
err,
);
return request.setResponse(rendered_error, .{}); return request.setResponse(rendered_error, .{});
}; };
return request.setResponse(rendered, .{}); return request.setResponse(rendered, .{});
@ -229,7 +233,7 @@ fn renderHTML(
// assigned in a view. // assigned in a view.
const rendered = self.renderView(matched_route, request, null) catch |err| { const rendered = self.renderView(matched_route, request, null) catch |err| {
if (isUnhandledError(err)) return err; if (isUnhandledError(err)) return err;
const rendered_error = try self.renderInternalServerError(request, err); const rendered_error = try self.renderInternalServerError(request, @errorReturnTrace(), err);
return request.setResponse(rendered_error, .{}); return request.setResponse(rendered_error, .{});
}; };
@ -296,10 +300,9 @@ fn renderView(
// `return request.render(.ok)`, but the actual rendered view is stored in // `return request.render(.ok)`, but the actual rendered view is stored in
// `request.rendered_view`. // `request.rendered_view`.
_ = route.render(route, request) catch |err| { _ = route.render(route, request) catch |err| {
try self.logger.ERROR("Encountered error: {s}", .{@errorName(err)});
if (isUnhandledError(err)) return err; if (isUnhandledError(err)) return err;
if (isBadRequest(err)) return try self.renderBadRequest(request); if (isBadRequest(err)) return try self.renderBadRequest(request);
return try self.renderInternalServerError(request, err); return try self.renderInternalServerError(request, @errorReturnTrace(), err);
}; };
if (request.failed) { if (request.failed) {
@ -414,10 +417,15 @@ fn isBadHttpError(err: anyerror) bool {
}; };
} }
fn renderInternalServerError(self: *Server, request: *jetzig.http.Request, err: anyerror) !RenderedView { fn renderInternalServerError(
self: *Server,
request: *jetzig.http.Request,
stack_trace: ?*std.builtin.StackTrace,
err: anyerror,
) !RenderedView {
request.response_data.reset(); request.response_data.reset();
try self.logger.logError(err); try self.logger.logError(stack_trace, err);
const status = .internal_server_error; const status = .internal_server_error;
return try self.renderError(request, status); return try self.renderError(request, status);
@ -460,13 +468,11 @@ fn renderErrorView(
_ = route.render(route.*, request) catch |err| { _ = route.render(route.*, request) catch |err| {
if (isUnhandledError(err)) return err; if (isUnhandledError(err)) return err;
try self.logger.logError(err); try self.logger.logError(@errorReturnTrace(), err);
try self.logger.ERROR( try self.logger.ERROR(
"Unexepected error occurred while rendering error page: {s}", "Unexepected error occurred while rendering error page: {s}",
.{@errorName(err)}, .{@errorName(err)},
); );
const stack = @errorReturnTrace();
if (stack) |capture| try self.logStackTrace(capture, request.allocator);
return try renderDefaultError(request, status_code); return try renderDefaultError(request, status_code);
}; };

View File

@ -1,12 +1,12 @@
const std = @import("std"); const std = @import("std");
const Self = @This(); const StaticRequest = @This();
const jetzig = @import("../../jetzig.zig"); const jetzig = @import("../../jetzig.zig");
response_data: *jetzig.data.Data, response_data: *jetzig.data.Data,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
json: []const u8, json: []const u8,
pub fn init(allocator: std.mem.Allocator, json: []const u8) !Self { pub fn init(allocator: std.mem.Allocator, json: []const u8) !StaticRequest {
return .{ return .{
.allocator = allocator, .allocator = allocator,
.response_data = try allocator.create(jetzig.data.Data), .response_data = try allocator.create(jetzig.data.Data),
@ -14,31 +14,35 @@ pub fn init(allocator: std.mem.Allocator, json: []const u8) !Self {
}; };
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *StaticRequest) void {
_ = self; _ = self;
} }
pub fn render(self: *Self, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View { pub fn render(self: *StaticRequest, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
return .{ .data = self.response_data, .status_code = status_code }; return .{ .data = self.response_data, .status_code = status_code };
} }
pub fn resourceId(self: *Self) ![]const u8 { pub fn data(self: *StaticRequest, comptime root: @TypeOf(.enum_literal)) !*jetzig.Data.Value {
var data = try self.allocator.create(jetzig.data.Data); return try self.response_data.root(root);
data.* = jetzig.data.Data.init(self.allocator); }
defer self.allocator.destroy(data);
defer data.deinit();
try data.fromJson(self.json); pub fn resourceId(self: *StaticRequest) ![]const u8 {
var params_data = try self.allocator.create(jetzig.data.Data);
params_data.* = jetzig.data.Data.init(self.allocator);
defer self.allocator.destroy(params_data);
defer params_data.deinit();
try params_data.fromJson(self.json);
// Routes generator rejects missing `.id` option so this should always be present. // Routes generator rejects missing `.id` option so this should always be present.
// Note that static requests are never rendered at runtime so we can be unsafe here and risk // Note that static requests are never rendered at runtime so we can be unsafe here and risk
// failing a build (which would not be coherent if we allowed it to complete). // failing a build (which would not be coherent if we allowed it to complete).
return try self.allocator.dupe(u8, data.value.?.get("id").?.string.value); return try self.allocator.dupe(u8, params_data.value.?.get("id").?.string.value);
} }
/// Returns the static params defined by `pub const static_params` in the relevant view. /// Returns the static params defined by `pub const static_params` in the relevant view.
pub fn params(self: *Self) !*jetzig.data.Value { pub fn params(self: *StaticRequest) !*jetzig.data.Value {
var data = try self.allocator.create(jetzig.data.Data); var params_data = try self.allocator.create(jetzig.data.Data);
data.* = jetzig.data.Data.init(self.allocator); params_data.* = jetzig.data.Data.init(self.allocator);
try data.fromJson(self.json); try params_data.fromJson(self.json);
return data.value.?.get("params") orelse data.object(); return params_data.value.?.get("params") orelse params_data.object();
} }

View File

@ -13,7 +13,13 @@ pub fn expectParams(request: *jetzig.http.Request, T: type) !?T {
var failed = false; var failed = false;
inline for (fields, 0..) |field, index| { inline for (fields, 0..) |field, index| {
if (actual_params.get(field.name)) |value| { var maybe_value = actual_params.get(field.name);
if (isBlank(maybe_value)) {
maybe_value = null;
}
if (maybe_value) |value| {
switch (@typeInfo(field.type)) { switch (@typeInfo(field.type)) {
.optional => |info| if (value.coerce(info.child)) |coerced| { .optional => |info| if (value.coerce(info.child)) |coerced| {
@field(t, field.name) = coerced; @field(t, field.name) = coerced;
@ -66,6 +72,12 @@ pub fn expectParams(request: *jetzig.http.Request, T: type) !?T {
return t; return t;
} }
fn isBlank(maybe_value: ?*const jetzig.Data.Value) bool {
if (maybe_value) |value| {
return value.* == .string and jetzig.util.strip(value.string.value).len == 0;
} else return true;
}
/// See `Request.paramsInfo`. /// See `Request.paramsInfo`.
pub const ParamsInfo = struct { pub const ParamsInfo = struct {
params: std.BoundedArray(ParamInfo, 1024), params: std.BoundedArray(ParamInfo, 1024),

View File

@ -44,7 +44,7 @@ pub fn deinit(self: *Store) void {
self.store.deinit(); self.store.deinit();
} }
/// Put a Value or into the key-value store. /// Put a or into the key-value store.
pub fn put(self: *Store, key: []const u8, value: *jetzig.data.Value) !void { pub fn put(self: *Store, key: []const u8, value: *jetzig.data.Value) !void {
try self.store.put(key, try value.toJson()); try self.store.put(key, try value.toJson());
} }

View File

@ -83,9 +83,13 @@ pub const Logger = union(enum) {
} }
} }
pub fn logError(self: *const Logger, err: anyerror) !void { pub fn logError(
self: *const Logger,
stack_trace: ?*std.builtin.StackTrace,
err: anyerror,
) !void {
switch (self.*) { switch (self.*) {
inline else => |*logger| try logger.logError(err), inline else => |*logger| try logger.logError(stack_trace, err),
} }
} }

View File

@ -238,17 +238,13 @@ fn printSql(self: *const DevelopmentLogger, sql: []const u8) !void {
try self.print(.INFO, "{s}", .{stream.getWritten()}); try self.print(.INFO, "{s}", .{stream.getWritten()});
} }
pub fn logError(self: *const DevelopmentLogger, err: anyerror) !void { pub fn logError(self: *const DevelopmentLogger, stack_trace: ?*std.builtin.StackTrace, err: anyerror) !void {
if (@errorReturnTrace()) |stack| { if (stack_trace) |stack| {
try self.log(.ERROR, "\nStack Trace:\n{}", .{stack});
var buf = std.ArrayList(u8).init(self.allocator);
defer buf.deinit();
const writer = buf.writer();
try stack.format("", .{}, writer);
try self.logger.ERROR("{s}\n", .{buf.items});
}
try self.log(.ERROR, "Encountered Error: {s}", .{@errorName(err)}); try self.log(.ERROR, "Encountered Error: {s}", .{@errorName(err)});
try self.log(.ERROR, "Stack trace:\n{}", .{stack});
} else {
try self.log(.ERROR, "Encountered Error: {s}", .{@errorName(err)});
}
} }
fn logFile(self: DevelopmentLogger, comptime level: jetzig.loggers.LogLevel) std.fs.File { fn logFile(self: DevelopmentLogger, comptime level: jetzig.loggers.LogLevel) std.fs.File {

View File

@ -112,7 +112,13 @@ pub fn logSql(self: *const JsonLogger, event: jetzig.jetquery.events.Event) !voi
try self.log_queue.print("{s}\n", .{stream.getWritten()}, .stdout); try self.log_queue.print("{s}\n", .{stream.getWritten()}, .stdout);
} }
pub fn logError(self: *const JsonLogger, err: anyerror) !void { pub fn logError(
self: *const JsonLogger,
stack_trace: ?*std.builtin.StackTrace,
err: anyerror,
) !void {
// TODO: Format this as JSON and include line number/column if available.
_ = stack_trace;
try self.log(.ERROR, "Encountered error: {s}", .{@errorName(err)}); try self.log(.ERROR, "Encountered error: {s}", .{@errorName(err)});
} }

View File

@ -19,7 +19,8 @@ pub inline fn logRequest(self: @This(), request: *const jetzig.http.Request) !vo
_ = request; _ = request;
} }
pub inline fn logError(self: @This(), err: anyerror) !void { pub inline fn logError(self: @This(), stack_trace: ?*std.builtin.StackTrace, err: anyerror) !void {
_ = self; _ = self;
_ = stack_trace;
std.debug.print("Error: {s}\n", .{@errorName(err)}); std.debug.print("Error: {s}\n", .{@errorName(err)});
} }

View File

@ -141,6 +141,8 @@ const sql_tokens = .{
"VALUES", "VALUES",
}; };
pub fn logError(self: *const ProductionLogger, err: anyerror) !void { pub fn logError(self: *const ProductionLogger, stack_trace: ?*std.builtin.StackTrace, err: anyerror) !void {
// TODO: Include line number/column if available.
_ = stack_trace;
try self.log(.ERROR, "Encountered Error: {s}", .{@errorName(err)}); try self.log(.ERROR, "Encountered Error: {s}", .{@errorName(err)});
} }

View File

@ -46,7 +46,9 @@ pub fn logSql(self: TestLogger, event: jetzig.jetquery.events.Event) !void {
try self.log(.INFO, "[database] {?s}", .{event.sql}); try self.log(.INFO, "[database] {?s}", .{event.sql});
} }
pub fn logError(self: TestLogger, err: anyerror) !void { pub fn logError(self: TestLogger, stack_trace: ?*std.builtin.StackTrace, err: anyerror) !void {
// TODO: Output useful debug info from stack trace
_ = stack_trace;
try self.log(.ERROR, "Encountered error: {s}", .{@errorName(err)}); try self.log(.ERROR, "Encountered error: {s}", .{@errorName(err)});
} }

View File

@ -4,7 +4,7 @@ const jetzig = @import("../../jetzig.zig");
/// Default Mail Job. Send an email with the given params in the background. /// Default Mail Job. Send an email with the given params in the background.
pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig.jobs.JobEnv) !void { pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig.jobs.JobEnv) !void {
const mailer_name = if (params.get("mailer_name")) |param| switch (param.*) { const mailer_name = if (params.get("mailer_name")) |param| switch (param.*) {
.Null => null, .null => null,
.string => |string| string.value, .string => |string| string.value,
else => null, else => null,
} else null; } else null;
@ -34,9 +34,7 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig
.defaults = mailer.defaults, .defaults = mailer.defaults,
}; };
var data = jetzig.data.Data.init(allocator); try mailer.deliverFn(allocator, &mail_params, params.get("params").?, env);
try mailer.deliverFn(allocator, &mail_params, &data, params.get("params").?, env);
const mail = jetzig.mail.Mail.init(allocator, env, .{ const mail = jetzig.mail.Mail.init(allocator, env, .{
.subject = mail_params.get(.subject) orelse "(No subject)", .subject = mail_params.get(.subject) orelse "(No subject)",
@ -62,7 +60,7 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig
fn resolveSubject(subject: ?*const jetzig.data.Value) ?[]const u8 { fn resolveSubject(subject: ?*const jetzig.data.Value) ?[]const u8 {
if (subject) |capture| { if (subject) |capture| {
return switch (capture.*) { return switch (capture.*) {
.Null => null, .null => null,
.string => |string| string.value, .string => |string| string.value,
else => unreachable, else => unreachable,
}; };
@ -73,7 +71,7 @@ fn resolveSubject(subject: ?*const jetzig.data.Value) ?[]const u8 {
fn resolveFrom(from: ?*const jetzig.data.Value) ?[]const u8 { fn resolveFrom(from: ?*const jetzig.data.Value) ?[]const u8 {
return if (from) |capture| switch (capture.*) { return if (from) |capture| switch (capture.*) {
.Null => null, .null => null,
.string => |string| string.value, .string => |string| string.value,
else => unreachable, else => unreachable,
} else null; } else null;
@ -97,7 +95,7 @@ fn resolveText(
) !?[]const u8 { ) !?[]const u8 {
if (text) |capture| { if (text) |capture| {
return switch (capture.*) { return switch (capture.*) {
.Null => try defaultText(allocator, mailer, params), .null => try defaultText(allocator, mailer, params),
.string => |string| string.value, .string => |string| string.value,
else => unreachable, else => unreachable,
}; };
@ -114,7 +112,7 @@ fn resolveHtml(
) !?[]const u8 { ) !?[]const u8 {
if (text) |capture| { if (text) |capture| {
return switch (capture.*) { return switch (capture.*) {
.Null => try defaultHtml(allocator, mailer, params), .null => try defaultHtml(allocator, mailer, params),
.string => |string| string.value, .string => |string| string.value,
else => unreachable, else => unreachable,
}; };

View File

@ -4,7 +4,6 @@ const jetzig = @import("../../jetzig.zig");
pub const DeliverFn = *const fn ( pub const DeliverFn = *const fn (
std.mem.Allocator, std.mem.Allocator,
*jetzig.mail.MailParams, *jetzig.mail.MailParams,
*jetzig.data.Data,
*jetzig.data.Value, *jetzig.data.Value,
jetzig.jobs.JobEnv, jetzig.jobs.JobEnv,
) anyerror!void; ) anyerror!void;

View File

@ -205,7 +205,7 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
}, },
else => {}, else => {},
}, },
.Null => switch (@typeInfo(@TypeOf(expected_value))) { .null => switch (@typeInfo(@TypeOf(expected_value))) {
.optional => { .optional => {
if (expected_value == null) return; if (expected_value == null) return;
}, },
@ -264,7 +264,7 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
else => unreachable, else => unreachable,
} }
}, },
.Null => { .null => {
logFailure( logFailure(
"Expected value in " ++ jetzig.colors.cyan("{s}") ++ ", found " ++ jetzig.colors.green("null") ++ "\nJSON:" ++ json_banner, "Expected value in " ++ jetzig.colors.cyan("{s}") ++ ", found " ++ jetzig.colors.green("null") ++ "\nJSON:" ++ json_banner,
.{ expected_path, try jsonPretty(response) }, .{ expected_path, try jsonPretty(response) },