Skip to content

Commit

Permalink
Email framework
Browse files Browse the repository at this point in the history
Create mailers with `jetzig generate mailer <name>`. Mailers define
default values for email fields (e.g. subject, from address, etc.).
Mailers use Zmpl for rendering text/HTML parts.

Send an email from a request with `request.mail()`. Call
`deliver(.background, .{})` on the return value to use the built-in
mail job to send the email asynchronously.

Improve query/HTTP request body param parsing - unescape `+` and `%XX`
characters.
  • Loading branch information
bobf committed Apr 21, 2024
1 parent 3f6c1a4 commit 47c3563
Show file tree
Hide file tree
Showing 38 changed files with 1,195 additions and 148 deletions.
72 changes: 38 additions & 34 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,13 @@ const zmpl_build = @import("zmpl");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const template_path_option = b.option([]const u8, "zmpl_templates_path", "Path to templates") orelse
"src/app/views/";
const template_path: []const u8 = if (std.fs.path.isAbsolute(template_path_option))
try b.allocator.dupe(u8, template_path_option)
else
std.fs.cwd().realpathAlloc(b.allocator, template_path_option) catch |err| switch (err) {
error.FileNotFound => "",
else => return err,
};
const templates_paths = try zmpl_build.templatesPaths(
b.allocator,
&.{
.{ .prefix = "views", .path = &.{ "src", "app", "views" } },
.{ .prefix = "mailers", .path = &.{ "src", "app", "mailers" } },
},
);

const lib = b.addStaticLibrary(.{
.name = "jetzig",
Expand All @@ -40,32 +38,13 @@ pub fn build(b: *std.Build) !void {
jetzig_module.addImport("mime_types", mime_module);
lib.root_module.addImport("jetzig", jetzig_module);

const zmpl_version = b.option(
enum { v1, v2 },
"zmpl_version",
"Zmpl syntax version (default: v1)",
) orelse .v2;

if (zmpl_version == .v1) {
std.debug.print(
\\[WARN] Zmpl v1 is deprecated and will soon be removed.
\\ Update to v2 by modifying `jetzigInit` in your `build.zig`:
\\
\\ try jetzig.jetzigInit(b, exe, .{{ .zmpl_version = .v2 }});
\\
\\ See https://jetzig.dev/documentation.html for information on migrating to Zmpl v2.
\\
, .{});
}

const zmpl_dep = b.dependency(
"zmpl",
.{
.target = target,
.optimize = optimize,
.zmpl_templates_path = template_path,
.zmpl_templates_paths = templates_paths,
.zmpl_auto_build = false,
.zmpl_version = zmpl_version,
.zmpl_constants = try zmpl_build.addTemplateConstants(b, struct {
jetzig_view: []const u8,
jetzig_action: []const u8,
Expand All @@ -84,11 +63,17 @@ pub fn build(b: *std.Build) !void {

const zmd_dep = b.dependency("zmd", .{ .target = target, .optimize = optimize });

const smtp_client_dep = b.dependency("smtp_client", .{
.target = target,
.optimize = optimize,
});

lib.root_module.addImport("zmpl", zmpl_module);
jetzig_module.addImport("zmpl", zmpl_module);
jetzig_module.addImport("args", zig_args_dep.module("args"));
jetzig_module.addImport("zmd", zmd_dep.module("zmd"));
jetzig_module.addImport("jetkv", jetkv_dep.module("jetkv"));
jetzig_module.addImport("smtp", smtp_client_dep.module("smtp_client"));

const main_tests = b.addTest(.{
.root_source_file = .{ .path = "src/tests.zig" },
Expand All @@ -115,15 +100,20 @@ pub fn build(b: *std.Build) !void {

/// Build-time options for Jetzig.
pub const JetzigInitOptions = struct {
zmpl_version: enum { v1, v2 } = .v1,
zmpl_version: enum { v1, v2 } = .v2,
};

pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigInitOptions) !void {
if (options.zmpl_version == .v1) {
std.debug.print("Zmpl v1 has now been removed. Please upgrade to v2.\n", .{});
return error.ZmplVersionNotSupported;
}

const target = b.host;
const optimize = exe.root_module.optimize orelse .Debug;
const jetzig_dep = b.dependency(
"jetzig",
.{ .optimize = optimize, .target = target, .zmpl_version = options.zmpl_version },
.{ .optimize = optimize, .target = target },
);
const jetzig_module = jetzig_dep.module("jetzig");
const zmpl_module = jetzig_dep.module("zmpl");
Expand All @@ -140,17 +130,31 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
}

const root_path = b.build_root.path orelse try std.fs.cwd().realpathAlloc(b.allocator, ".");
const templates_path = try std.fs.path.join(
const templates_path: []const u8 = try std.fs.path.join(
b.allocator,
&[_][]const u8{ root_path, "src", "app" },
);
const views_path: []const u8 = try std.fs.path.join(
b.allocator,
&[_][]const u8{ root_path, "src", "app", "views" },
);

const jobs_path = try std.fs.path.join(
b.allocator,
&[_][]const u8{ root_path, "src", "app", "jobs" },
);
const mailers_path = try std.fs.path.join(
b.allocator,
&[_][]const u8{ root_path, "src", "app", "mailers" },
);

var generate_routes = try Routes.init(b.allocator, root_path, templates_path, jobs_path);
var generate_routes = try Routes.init(
b.allocator,
root_path,
templates_path,
views_path,
jobs_path,
mailers_path,
);
try generate_routes.generateRoutes();
const write_files = b.addWriteFiles();
const routes_file = write_files.add("routes.zig", generate_routes.buffer.items);
Expand Down
8 changes: 6 additions & 2 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
.hash = "12207d49df326e0c180a90fa65d9993898e0a0ffd8e79616b4b81f81769261858856",
},
.zmpl = .{
.url = "https://github.com/jetzig-framework/zmpl/archive/1fcdfc42444224d43eb6e5389d9b4aa239fcd787.tar.gz",
.hash = "1220f7bc1e2b2317790db37b7dec685c7d9a2ece9708108a4b636477d996be002c71",
.url = "https://github.com/jetzig-framework/zmpl/archive/4511ae706e8679385d38cc1366497082f8f53afb.tar.gz",
.hash = "1220d493e6fdfaccbafff41df2b7b407728ed11619bebb198c90dae9420f03a6d29d",
},
.args = .{
.url = "https://github.com/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz",
Expand All @@ -18,6 +18,10 @@
.url = "https://github.com/jetzig-framework/jetkv/archive/a6fcc2df220c1a40094e167eeb567bb5888404e9.tar.gz",
.hash = "12207bd2d7465b33e745a5b0567172377f94a221d1fc9aab238bb1b372c64f4ec1a0",
},
.smtp_client = .{
.url = "https://github.com/karlseguin/smtp_client.zig/archive/e79e411862d4f4d41657bf41efb884efca3d67dd.tar.gz",
.hash = "12209907c69891a38e6923308930ac43bfb40135bc609ea370b5759fc2e1c4f57284",
},
},

.paths = .{
Expand Down
56 changes: 30 additions & 26 deletions cli/commands/generate.zig
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
const std = @import("std");
const args = @import("args");
const secret = @import("generate/secret.zig");
const util = @import("../util.zig");

const view = @import("generate/view.zig");
const partial = @import("generate/partial.zig");
const layout = @import("generate/layout.zig");
const middleware = @import("generate/middleware.zig");
const job = @import("generate/job.zig");
const secret = @import("generate/secret.zig");
const util = @import("../util.zig");
const mailer = @import("generate/mailer.zig");

/// Command line options for the `generate` command.
pub const Options = struct {
pub const meta = .{
.usage_summary = "[view|partial|layout|middleware|job|secret] [options]",
.usage_summary = "[view|partial|layout|mailer|middleware|job|secret] [options]",
.full_text =
\\Generate scaffolding for views, middleware, and other objects.
\\
Expand All @@ -36,47 +38,49 @@ pub fn run(

_ = options;

var generate_type: ?enum { view, partial, layout, middleware, job, secret } = null;
const Generator = enum { view, partial, layout, mailer, middleware, job, secret };
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, "layout")) {
generate_type = .layout;
} else if (generate_type == null and std.mem.eql(u8, arg, "job")) {
generate_type = .job;
} else if (generate_type == null and std.mem.eql(u8, arg, "middleware")) {
generate_type = .middleware;
} else if (generate_type == null and std.mem.eql(u8, arg, "secret")) {
generate_type = .secret;
} else if (generate_type == null) {
std.debug.print("Unknown generator command: {s}\n", .{arg});
return error.JetzigCommandError;
} else {
try sub_args.append(arg);
}
const map = std.ComptimeStringMap(Generator, .{
.{ "view", .view },
.{ "partial", .partial },
.{ "layout", .layout },
.{ "job", .job },
.{ "mailer", .mailer },
.{ "middleware", .middleware },
.{ "secret", .secret },
});

var available_buf = std.ArrayList([]const u8).init(allocator);
defer available_buf.deinit();
for (map.kvs) |kv| try available_buf.append(kv.key);
const available_help = try std.mem.join(allocator, "|", available_buf.items);
defer allocator.free(available_help);

const generate_type: ?Generator = if (positionals.len > 0) map.get(positionals[0]) else null;

if (positionals.len > 1) {
for (positionals[1..]) |arg| try sub_args.append(arg);
}

if (other_options.help and generate_type == null) {
try args.printHelp(Options, "jetzig generate", writer);
return;
} else if (generate_type == null) {
std.debug.print("Missing sub-command. Expected: [{s}]\n", .{available_help});
return error.JetzigCommandError;
}

if (generate_type) |capture| {
return switch (capture) {
.view => view.run(allocator, cwd, sub_args.items, other_options.help),
.partial => partial.run(allocator, cwd, sub_args.items, other_options.help),
.layout => layout.run(allocator, cwd, sub_args.items, other_options.help),
.mailer => mailer.run(allocator, cwd, sub_args.items, other_options.help),
.job => job.run(allocator, cwd, sub_args.items, other_options.help),
.middleware => middleware.run(allocator, cwd, sub_args.items, other_options.help),
.secret => secret.run(allocator, cwd, sub_args.items, other_options.help),
};
} else {
std.debug.print("Missing sub-command. Expected: [view|partial|layout|job|middleware|secret]\n", .{});
return error.JetzigCommandError;
}
}
18 changes: 14 additions & 4 deletions cli/commands/generate/job.zig
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he
const file = dir.createFile(filename, .{ .exclusive = true }) catch |err| {
switch (err) {
error.PathAlreadyExists => {
std.debug.print("Partial already exists: {s}\n", .{filename});
std.debug.print("Job already exists: {s}\n", .{filename});
return error.JetzigCommandError;
},
else => return err,
Expand All @@ -42,10 +42,20 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he
\\const std = @import("std");
\\const jetzig = @import("jetzig");
\\
\\/// The `run` function for all jobs receives an arena allocator, a logger, and the params
\\/// passed to the job when it was created.
\\// The `run` function for a job is invoked every time the job is processed by a queue worker
\\// (or by the Jetzig server if the job is processed in-line).
\\//
\\// Arguments:
\\// * allocator: Arena allocator for use during the job execution process.
\\// * params: Params assigned to a job (from a request, any values added to `data`).
\\// * env: Provides the following fields:
\\// - logger: Logger attached to the same stream as the Jetzig server.
\\// - environment: Enum of `{ production, development }`.
\\pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, logger: jetzig.Logger) !void {
\\ // Job execution code
\\ _ = allocator;
\\ _ = params;
\\ // Job execution code goes here. Add any code that you would like to run in the background.
\\ try env.logger.INFO("Running a job.", .{});
\\}
\\
);
Expand Down
Loading

0 comments on commit 47c3563

Please sign in to comment.