Compare commits

..

79 Commits

Author SHA1 Message Date
Bob Farrell
1feb18fb74 Add note to README about Zig compatibility 2025-04-26 18:14:50 +01:00
Bob Farrell
86d82026ab Fix incorrect email address format 2025-04-21 14:46:53 +01:00
bobf
01d0862043
Merge pull request #190 from PotatoesMaster/htmx-middleware-vary-header
HtmxMiddleware: add Vary header to prevent caching issues
2025-04-20 15:41:26 +01:00
Emanuel Guével
95d3f10bc9 HtmxMiddleware: add Vary header to prevent caching issues
See https://htmx.org/docs/#caching
2025-04-20 16:06:55 +02:00
Bob Farrell
dee5701b4a Never sync stdout/stderr
Detecting if stdout/stderr is tty seems to cause issues when running in
Docker (in particular with fly.io)
2025-04-19 21:41:27 +01:00
Bob Farrell
b7c3c0045a Fix missing @tagName after nameCast changes 2025-04-19 20:16:31 +01:00
bobf
6d9fa8bff4
Merge pull request #189 from jetzig-framework/zig-alignment-changes
Use Alignment arg instead of integer
2025-04-19 19:57:34 +01:00
Bob Farrell
4bd9d8b990 Eradicate std.enums.nameCast
Deprecated here: 530228d953
2025-04-19 19:35:55 +01:00
bobf
8171ab5b5d
Merge pull request #188 from uzyn/custom-session-name
Overriding of default session cookie name
2025-04-17 07:01:16 +01:00
Bob Farrell
13907aad7c Use Alignment arg instead of integer
See f32a5d349d
2025-04-16 19:59:05 +01:00
Bob Farrell
aae9fd182b Use Server.env.vars to get session cookie name 2025-04-16 19:29:44 +01:00
U-Zyn Chua
4c568f2606
Minor typo fix 2025-04-16 15:59:45 +08:00
U-Zyn Chua
94cf122847
Allow setting of custom session cookie name. 2025-04-16 15:59:18 +08:00
bobf
b32d24ad80
Merge pull request #187 from uzyn/ci-use-minver
CI now uses minimum_zig_version as defined in build.zig.zon
2025-04-16 06:09:42 +01:00
U-Zyn Chua
3fd42f2ebb
CI now uses minimum_zig_version as defined in build.zig.zon 2025-04-15 17:15:29 +08:00
bobf
ace82985ef
Merge pull request #186 from uzyn/min-zig-ver
Defines minimum supported Zig version.
2025-04-15 08:36:57 +01:00
U-Zyn Chua
e96a3d36b6
Defines specific version at CI. 2025-04-15 15:10:46 +08:00
U-Zyn Chua
dc982cf125
Defines minimum supported zig version. 2025-04-15 14:52:47 +08:00
bobf
6d522c07c5
Merge pull request #185 from jetzig-framework/zig-0.15
Zig 0.15 compatibility
2025-04-09 20:15:03 +01:00
Bob Farrell
c26d563898 Zig 0.15 compatibility
Various dependencies updated after changes to `std.zig.Ast` and
`std.LinkedList` and other things.
2025-04-09 20:08:38 +01:00
bobf
2c52792217
Merge pull request #174 from sc68cal/login_example
Add an example for using auth for logging in
2025-03-23 14:05:38 +00:00
Sean M. Collins
327b0390de
Merge pull request #1 from bobf/login_example
Adjust test, fix memory leak in hashPassword, deinit repo on test app…
2025-03-23 09:36:56 -04:00
Bob Farrell
2cb1b4e959 Adjust test, fix memory leak in hashPassword, deinit repo on test app deinit 2025-03-23 13:28:09 +00:00
bobf
a46e944499
Merge pull request #178 from uzyn/fix/ci-zig-setup
Switches zig-setup workflow allowing building on latest stable Zig
2025-03-23 12:32:48 +00:00
Sean M. Collins
79574ed861 Use defer to free hashed_pass 2025-03-20 13:54:13 -04:00
U-Zyn Chua
00ed3067dd
Switches zig-setup workflow 2025-03-15 10:16:45 +08:00
Sean M. Collins
aae890998f fix spelling mistake 2025-03-10 22:07:12 -04:00
Sean M. Collins
21bf237437 Shrink database.zig and comment 2025-03-10 21:57:02 -04:00
Sean M. Collins
b81c6dfe91 update paths to database template 2025-03-10 21:55:20 -04:00
Sean M. Collins
9e4e8529c5 Create a template file for init
This allows us to use database.zig for CI, while still creating a blank
database.zig for new projects that are created via jetzig init
2025-03-10 21:49:36 -04:00
Sean M. Collins
359b4fc2eb Add migration step 2025-03-10 21:31:50 -04:00
Sean M. Collins
70a8b0f698 Add tests for login view 2025-03-10 21:20:33 -04:00
Sean M. Collins
f94db09551 Do not pre-create database
Let jetzig create it via the commands
2025-03-10 17:57:48 -04:00
Sean M. Collins
46a311174f Use actual YAML syntax for env vars
I always mess this up when moving from shell to YAML configs
2025-03-10 17:54:46 -04:00
Sean M. Collins
48e7139e26 Set JETQUERY_POOL_SIZE in CI runs 2025-03-10 17:53:50 -04:00
Sean M. Collins
c6ce935ce1 Run postgres inside the test runner 2025-03-10 17:25:40 -04:00
Sean M. Collins
22688ccaa3 Set adapter to postgresql 2025-03-10 17:13:08 -04:00
Sean M. Collins
f3788567bb Fix argument to first migration 2025-03-10 17:05:09 -04:00
Sean M. Collins
58f7e0e76a Add User to schema 2025-03-10 16:50:11 -04:00
Sean M. Collins
b070d281e3 Add user table 2025-03-10 16:32:27 -04:00
Sean M. Collins
9012ec3aaf Add an example for using auth for logging in 2025-03-10 16:26:58 -04:00
bobf
182ceee17f
Merge pull request #173 from jetzig-framework/jetquery-group-by-synthetic-columns
JetQuery: groupBy synthetic columns
2025-03-09 11:45:15 +00:00
Bob Farrell
034ecddfa0 JetQuery: groupBy synthetic columns 2025-03-09 11:38:01 +00:00
bobf
19e17bc447
Merge pull request #171 from sc68cal/issue_170
Set same_site to correct type
2025-03-08 20:06:16 +00:00
Sean M. Collins
d8a992ce38 Set same_site to correct type
Fixes #170
2025-03-08 07:52:02 -05:00
bobf
b8af924f8e
Merge pull request #169 from blurrycat/fix/thread_anon_struct
Fix anonymous struct literal in log_thread spawn
2025-03-07 21:00:18 +00:00
bobf
29612e463f
Merge pull request #168 from blurrycat/fix/cli_memleak
Fix memory leak in CLI
2025-03-07 20:53:52 +00:00
blurrycat
23d0102ae8 Fix memory leak in CLI 2025-03-06 19:59:51 +01:00
blurrycat
544b33e279 Fix anonymous struct literal in log_thread spawn 2025-03-06 19:59:01 +01:00
bobf
d3157e4247
Merge pull request #163 from dongsu8142/middleware-fix
Fix compression middleware
2025-03-03 09:00:58 +00:00
dongsu8142
501c6184a5
Merge branch 'jetzig-framework:main' into middleware-fix 2025-03-03 12:20:07 +09:00
bobf
d6226bd4ef
Merge pull request #165 from jetzig-framework/no-llvm
No llvm
2025-03-02 14:23:26 +00:00
Bob Farrell
7eea1e3f0a Minor CLI improvements
Show some progress "..." when running external commands to give the user
a bit of extra feedback while commands run.
2025-03-02 14:14:42 +00:00
Bob Farrell
3418698220 Use LLVM everywhere except Linux
Zig Compiler seems not ready for Windows yet (builds failing),
restricting to Linux for now, we can add other OSes as the compiler
matures.
2025-03-02 14:05:01 +00:00
Bob Farrell
9c4b6198cd Use llvm on MacOS
Experimental feature currently only tested with Linux, builds failing
for Mac.
2025-03-02 14:00:18 +00:00
Bob Farrell
06674db51f Use fork of zig-args (temporary)
Revert when [this PR](https://github.com/ikskuh/zig-args/pull/67) is
merged.
2025-03-02 13:12:29 +00:00
Bob Farrell
099a2a5349 Use Zig compiler
Default to not using LLVM for application compilation. This gives more
than 2x performance improvement for compilation stage.
2025-03-01 14:33:17 +00:00
Bob Farrell
c0d6fb162b Latest Zig build.zig.zon format compatibility 2025-03-01 12:28:08 +00:00
bobf
a16e1c8716
Merge pull request #164 from IbrahimOuhamou/subhana-allah-db-exe
added option 'database_exe' to install the database exe thanks to Allah
2025-02-27 21:30:27 +00:00
IbrahimOuhamou
1dfe58fb64 added option 'database_exe' thanks to Allah 2025-02-27 12:16:24 +00:00
dongsu8142
719bd300a7 Fix compression middleware 2025-02-26 22:24:01 +09:00
bobf
8a4f91f26a
Merge pull request #160 from Cohors1316/main
Switched HX-Target header to HX-Request in HtmxMiddleware
2025-02-19 08:15:57 +00:00
Ben Jordan
0a4690e001
Switched HX-Target header to HX-Request
Using a target like `hx-target="next div"` will not generate an HX-Target header if the target element does not have an ID. Judging by the comments it used to be HX-Request.
2025-02-17 19:29:42 -05:00
bobf
da2978ed04
Merge pull request #158 from jetzig-framework/jetquery-insert-returning
Update JetQuery
2025-02-15 18:11:06 +00:00
Bob Farrell
fb969d9b81 Update JetQuery
Provides "INSERT ... RETURNING" thanks to @Cohors1316
2025-02-15 18:01:28 +00:00
bobf
492c1833b2
Merge pull request #157 from jetzig-framework/fix-email-recipients
Fix email recipients
2025-02-15 17:58:49 +00:00
Bob Farrell
87c7adb50f Fix email recipients
Prevent breaking out of "To" recipient resolving when `name` is null -
this is a valid value as we only need the `email` field to deliver an
email successfully.
2025-02-15 17:52:30 +00:00
bobf
ae356c0ebf
Merge pull request #156 from jetzig-framework/zig-nightly-fixes-2025-02-13
Fixes for misc. Zig nightly changes
2025-02-13 18:41:25 +00:00
Bob Farrell
ecd66db3dc Fixes for misc. Zig nightly changes
* Removal of std.ArrayList.popOrNull
* Removal of std.mem.page_size
2025-02-13 18:23:04 +00:00
bobf
b71e738ec0
Merge pull request #152 from talvdav/update-httpz
updated http.zig to newer version, containig fix for 0.14.0-dev.3026+…
2025-02-03 19:39:22 +00:00
Thomas A. Alvarado Davalos
1ccaa45d34 updated http.zig to newer version, containig fix for 0.14.0-dev.3026+c225b780e 2025-02-03 20:18:12 +01:00
bobf
492ac4a999
Merge pull request #151 from jetzig-framework/0.14-fixes-for-test-runners
Zig 0.14 test runner config changes
2025-02-02 11:37:00 +00:00
Bob Farrell
a5430ff380 Zig 0.14 test runner config changes
Update dependencies to match new Zig build config API for test runners,
misc. fixes needed after upgrading dependencies.
2025-02-02 11:31:40 +00:00
bobf
1b5c72b75f
Merge pull request #143 from erg/fix-142-jetzig-init
util.zig: more helpful error message when `zig` is not in path
2025-02-01 11:53:50 +00:00
bobf
6223f256d8
Merge pull request #147 from jetzig-framework/misc-zig-updates
Misc. Zig updates
2025-01-25 12:27:43 +00:00
Bob Farrell
a99999af52 Misc. Zig updates
Changes to latest Zig nightly build required several minor fixes across
multiple dependencies.
2025-01-25 12:22:19 +00:00
Doug Coleman
b5ae4ec181 util.zig: more helpful error message when zig is not in path 2025-01-16 14:55:01 -06:00
bobf
5ebb60d759
Merge pull request #135 from jetzig-framework/zig-build-fixes
Latest Zig changes accommodation (missed one)
2024-12-22 13:16:44 +00:00
bobf
413fd30aee
Merge pull request #134 from jetzig-framework/zig-build-fixes
Zig build fixes for latest Zig nightly build
2024-12-22 12:41:48 +00:00
56 changed files with 798 additions and 370 deletions

View File

@ -18,18 +18,17 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
# Create postgres server
# https://github.com/marketplace/actions/setup-postgresql-for-linux-macos-windows
- uses: ikalnytskyi/action-setup-postgres@v7
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- 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
uses: goto-bus-stop/setup-zig@v2
with:
version: master
cache: true # Let's see how this behaves
uses: mlugg/setup-zig@main
- run: zig version
- run: zig env
@ -43,7 +42,17 @@ jobs:
- name: Run App Tests
run: |
cd demo
zig build -Denvironment=testing jetzig:database:create
zig build -Denvironment=testing jetzig:database:migrate
zig build -Denvironment=testing jetzig:test
env:
JETQUERY_HOSTNAME: 'localhost'
JETQUERY_USERNAME: 'postgres'
JETQUERY_PASSWORD: 'postgres'
JETQUERY_DATABASE: 'jetzig_demo_test'
# Assume a small amount of connections are allowed
# into postgres
JETQUERY_POOL_SIZE: 2
- name: Build artifacts
if: ${{ matrix.os == 'ubuntu-latest' }}

View File

@ -6,6 +6,8 @@ _Jetzig_ is a web framework written in 100% pure [Zig](https://ziglang.org) :liz
Official website: [jetzig.dev](https://www.jetzig.dev/)
Please note that _Jetzig_'s `main` branch aims to be compatible with the latest [Zig nightly master build](https://ziglang.org/download/) and older versions of _Zig_ are not supported.
_Jetzig_ aims to provide a rich set of user-friendly tools for building modern web applications quickly. See the checklist below.
Join us on Discord ! [https://discord.gg/eufqssz7X6](https://discord.gg/eufqssz7X6).

View File

@ -5,6 +5,9 @@ pub const GenerateMimeTypes = @import("src/GenerateMimeTypes.zig");
const zmpl_build = @import("zmpl");
const Environment = enum { development, testing, production };
const builtin = @import("builtin");
const use_llvm_default = builtin.os.tag != .linux;
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
@ -36,6 +39,7 @@ pub fn build(b: *std.Build) !void {
.{
.target = target,
.optimize = optimize,
.use_llvm = b.option(bool, "use_llvm", "Use LLVM") orelse use_llvm_default,
.zmpl_templates_paths = templates_paths,
.zmpl_auto_build = false,
.zmpl_markdown_fragments = try generateMarkdownFragments(b),
@ -46,6 +50,11 @@ pub fn build(b: *std.Build) !void {
},
);
const zmpl_steps = zmpl_dep.builder.top_level_steps;
const zmpl_compile_step = zmpl_steps.get("compile").?;
const compile_step = b.step("compile", "Compile Zmpl templates");
compile_step.dependOn(&zmpl_compile_step.step);
const zmpl_module = zmpl_dep.module("zmpl");
const jetkv_dep = b.dependency("jetkv", .{ .target = target, .optimize = optimize });
@ -58,18 +67,15 @@ pub fn build(b: *std.Build) !void {
const jetcommon_dep = b.dependency("jetcommon", .{ .target = target, .optimize = optimize });
const zmd_dep = b.dependency("zmd", .{ .target = target, .optimize = optimize });
const httpz_dep = b.dependency("httpz", .{ .target = target, .optimize = optimize });
const pg_dep = b.dependency("pg", .{ .target = target, .optimize = optimize });
// This is the way to make it look nice in the zig build script
// If we would do it the other way around, we would have to do
// b.dependency("jetzig",.{}).builder.dependency("zmpl",.{}).module("zmpl");
b.modules.put("zmpl", zmpl_dep.module("zmpl")) catch @panic("Out of memory");
b.modules.put("zmd", zmd_dep.module("zmd")) catch @panic("Out of memory");
b.modules.put("pg", pg_dep.module("pg")) catch @panic("Out of memory");
b.modules.put("jetquery", jetquery_dep.module("jetquery")) catch @panic("Out of memory");
b.modules.put("jetcommon", jetcommon_dep.module("jetcommon")) catch @panic("Out of memory");
b.modules.put("jetquery_migrate", jetquery_dep.module("jetquery_migrate")) catch @panic("Out of memory");
jetquery_dep.module("jetquery").addImport("pg", pg_dep.module("pg"));
const smtp_client_dep = b.dependency("smtp_client", .{
.target = target,
@ -132,6 +138,8 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
const target = exe.root_module.resolved_target orelse @panic("Unable to detect compile target.");
const optimize = exe.root_module.optimize orelse .Debug;
exe.use_llvm = exe.use_llvm orelse use_llvm_default;
if (optimize != .Debug) exe.linkLibC();
const environment = b.option(
@ -172,7 +180,6 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
const jetzig_module = jetzig_dep.module("jetzig");
const zmpl_module = jetzig_dep.module("zmpl");
const zmd_module = jetzig_dep.module("zmd");
const pg_module = jetzig_dep.module("pg");
const jetquery_module = jetzig_dep.module("jetquery");
const jetcommon_module = jetzig_dep.module("jetcommon");
const jetquery_migrate_module = jetzig_dep.module("jetquery_migrate");
@ -187,7 +194,6 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
exe.root_module.addImport("jetzig", jetzig_module);
exe.root_module.addImport("zmpl", zmpl_module);
exe.root_module.addImport("zmd", zmd_module);
exe.root_module.addImport("pg", pg_module);
if (b.option(bool, "jetzig_runner", "Used internally by `jetzig server` command.")) |jetzig_runner| {
if (jetzig_runner) {
@ -197,7 +203,10 @@ 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 root_path = if (b.build_root.path) |build_root_path|
try std.fs.path.join(b.allocator, &.{ build_root_path, "." })
else
try std.fs.cwd().realpathAlloc(b.allocator, ".");
const templates_path: []const u8 = try std.fs.path.join(
b.allocator,
&[_][]const u8{ root_path, "src", "app" },
@ -220,6 +229,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
.root_source_file = jetzig_dep.path("src/routes_file.zig"),
.target = target,
.optimize = optimize,
.use_llvm = exe.use_llvm,
});
exe_routes_file.root_module.addImport("jetzig", jetzig_module);
@ -247,6 +257,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
.root_source_file = jetzig_dep.path("src/compile_static_routes.zig"),
.target = target,
.optimize = optimize,
.use_llvm = exe.use_llvm,
});
const main_module = b.createModule(.{ .root_source_file = b.path("src/main.zig") });
@ -315,7 +326,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
.root_source_file = tests_file_path,
.target = target,
.optimize = optimize,
.test_runner = jetzig_dep.path("src/test_runner.zig"),
.test_runner = .{ .mode = .simple, .path = jetzig_dep.path("src/test_runner.zig") },
});
exe_unit_tests.root_module.addImport("jetzig", jetzig_module);
exe_unit_tests.root_module.addImport("static", static_module);
@ -355,6 +366,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
.root_source_file = jetzig_dep.path("src/commands/routes.zig"),
.target = target,
.optimize = optimize,
.use_llvm = exe.use_llvm,
});
const auth_user_create_step = b.step("jetzig:auth:user:create", "List all routes in your app");
@ -363,6 +375,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
.root_source_file = jetzig_dep.path("src/commands/auth.zig"),
.target = target,
.optimize = optimize,
.use_llvm = exe.use_llvm,
});
exe_auth.root_module.addImport("jetquery", jetquery_module);
exe_auth.root_module.addImport("jetzig", jetzig_module);
@ -384,6 +397,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
.root_source_file = jetzig_dep.path("src/commands/database.zig"),
.target = target,
.optimize = optimize,
.use_llvm = exe.use_llvm,
});
exe_database.root_module.addImport("jetquery", jetquery_module);
exe_database.root_module.addImport("jetzig", jetzig_module);
@ -394,6 +408,9 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
registerDatabaseSteps(b, exe_database);
const option_db_exe = b.option(bool, "database_exe", "option to install 'database' executable default is false") orelse false;
if (option_db_exe) b.installArtifact(exe_database);
exe_routes.root_module.addImport("jetzig", jetzig_module);
exe_routes.root_module.addImport("routes", routes_module);
exe_routes.root_module.addImport("app", exe.root_module);
@ -408,6 +425,8 @@ fn registerDatabaseSteps(b: *std.Build, exe_database: *std.Build.Step.Compile) v
.{ "create", "Create a database for your Jetzig app." },
.{ "drop", "Drop your Jetzig app's database." },
.{ "reflect", "Read your app's database and generate a JetQuery schema." },
.{ "setup", "Create the database, run migrations, and generate schema." },
.{ "update", "Run migrations and generate schema." },
};
inline for (commands) |command| {
@ -428,7 +447,7 @@ fn generateMarkdownFragments(b: *std.Build) ![]const u8 {
}
};
const stat = try file.stat();
const source = try file.readToEndAllocOptions(b.allocator, @intCast(stat.size), null, @alignOf(u8), 0);
const source = try file.readToEndAllocOptions(b.allocator, @intCast(stat.size), null, .of(u8), 0);
if (try getMarkdownFragmentsSource(b.allocator, source)) |markdown_fragments_source| {
return try std.fmt.allocPrint(b.allocator,
\\const std = @import("std");
@ -449,10 +468,10 @@ fn getMarkdownFragmentsSource(allocator: std.mem.Allocator, source: [:0]const u8
for (ast.nodes.items(.tag), 0..) |tag, index| {
switch (tag) {
.simple_var_decl => {
const decl = ast.simpleVarDecl(@intCast(index));
const decl = ast.simpleVarDecl(@enumFromInt(index));
const identifier = ast.tokenSlice(decl.ast.mut_token + 1);
if (std.mem.eql(u8, identifier, "markdown_fragments")) {
return ast.getNodeSource(@intCast(index));
return ast.getNodeSource(@enumFromInt(index));
}
},
else => continue,

View File

@ -1,42 +1,40 @@
.{
.name = "jetzig",
.name = .jetzig,
.version = "0.0.0",
.fingerprint = 0x93ad8bfa2d209022,
.minimum_zig_version = "0.15.0-dev.355+206bd1ced",
.dependencies = .{
.jetcommon = .{
.url = "https://github.com/jetzig-framework/jetcommon/archive/fb4edc13759d87bfcd9b1f5fcefdf93f8c9c62dd.tar.gz",
.hash = "jetcommon-0.1.0-jPY_DS1HAAAP8xp5HSWB_ZY7m9JEYUmm8adQFrse0lwB",
},
.zmd = .{
.url = "https://github.com/jetzig-framework/zmd/archive/d6c8aa9a9cde99674ccb096d8f94ed09cba8dab.tar.gz",
.hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163",
},
.zmpl = .{
.url = "https://github.com/jetzig-framework/zmpl/archive/f9a3c602d060d6b337312b820c852376cd111766.tar.gz",
.hash = "1220f61c70456b8bb7f407ff539d1d8569acea64f68db12f9245f1d4113495c33907",
},
.jetkv = .{
.url = "https://github.com/jetzig-framework/jetkv/archive/acaa30db281f1c331d20c48cfe6539186549ad45.tar.gz",
.hash = "1220b260b20cb65d801a00a39dc6506387f5faa1a225f85160e011bd2aabd2ce6e0b",
},
.jetquery = .{
.url = "https://github.com/jetzig-framework/jetquery/archive/387ad885bb29a16d38dbb71f5b4b5b083fa320e3.tar.gz",
.hash = "12206089a3d9de179a0d40cf35c48786eb4525a203ebf8cc131e23e430625713c402",
},
.jetcommon = .{
.url = "https://github.com/jetzig-framework/jetcommon/archive/86f24cfdf2aaa0e8ada4539a6edef882708ced2b.tar.gz",
.hash = "12200439fc28aa7fa08f0e8fea100f6724c34c9dbfaaae4feec482c80e5ac08ea4f6",
.smtp_client = .{
.url = "https://github.com/karlseguin/smtp_client.zig/archive/5163c66cc42cdd93176a6b1cad45f3db3a291a6a.tar.gz",
.hash = "smtp_client-0.0.1-AAAAAIJkAQCngHtRYVUMsMuncmicSHK_7ugwWibDzQ4S",
},
.args = .{
.url = "https://github.com/ikskuh/zig-args/archive/0abdd6947a70e6d8cc83b66228cea614aa856206.tar.gz",
.hash = "1220411a8c46d95bbf3b6e2059854bcb3c5159d428814099df5294232b9980517e9c",
.url = "https://github.com/bobf/zig-args/archive/88cbade9a517a4014824f8f53f3c48c8a0b2ffe1.tar.gz",
.hash = "zig_args-0.0.0-jqtN6P_NAAC97fGpk9hS2K681jkiqPsWP6w3ucb_ctGH",
},
.pg = .{
.url = "https://github.com/karlseguin/pg.zig/archive/a03737e8c9404d1642e9b2fdea8e719dfae5cda8.tar.gz",
.hash = "1220eff52824f32e7ab225cd50bf962324ca14c498c7c197a554dc55e5bb612f119f",
.jetkv = .{
.url = "https://github.com/jetzig-framework/jetkv/archive/5a94e3bac0a6e291efc9d6534beb2d311671ff17.tar.gz",
.hash = "jetkv-0.0.0-zCv0fmCGAgCyYqwHjk0P5KrYVRew1MJAtbtAcIO-WPpT",
},
.smtp_client = .{
.url = "https://github.com/karlseguin/smtp_client.zig/archive/3cbe8f269e4c3a6bce407e7ae48b2c76307c559f.tar.gz",
.hash = "1220de146446d0cae4396e346cb8283dd5e086491f8577ddbd5e03ad0928111d8bc6",
.zmpl = .{
.url = "https://github.com/jetzig-framework/zmpl/archive/c57fc9b83027e8c1459d9625c3509f59f0fb89f3.tar.gz",
.hash = "zmpl-0.0.1-SYFGBgdqAwDeA6xm4KAhpKoNrWs5CMQK6x447zhWclCs",
},
.httpz = .{
.url = "https://github.com/karlseguin/http.zig/archive/da9e944de0be6e5c67ca711dd238ce82d81558b4.tar.gz",
.hash = "12201df692f62d526fdf94e6000cf8de2142edf27484887e2e8f1ec5db4c9b808e5c",
.url = "https://github.com/karlseguin/http.zig/archive/37d7cb9819b804ade5f4b974b82f8dd0622225ed.tar.gz",
.hash = "httpz-0.0.0-PNVzrEK4BgBpHQGA2m0RPqPGEjnTdDXHodBwzjYDrmps",
},
.jetquery = .{
.url = "https://github.com/jetzig-framework/jetquery/archive/e1f969f2e3e0e1ad9cc30d56fde9739aa692fdc3.tar.gz",
.hash = "jetquery-0.0.0-TNf3zo2ABgBgcsIAvJ1Ud2B2zDzrBy9GQ31kKmTYZ7Ya",
},
},

View File

@ -1,16 +1,17 @@
.{
.name = "jetzig-cli",
.name = .jetzig_cli,
.fingerprint = 0x73894a3e0616c96a,
.version = "0.0.0",
.minimum_zig_version = "0.12.0",
.dependencies = .{
.args = .{
.url = "https://github.com/ikskuh/zig-args/archive/0abdd6947a70e6d8cc83b66228cea614aa856206.tar.gz",
.hash = "1220411a8c46d95bbf3b6e2059854bcb3c5159d428814099df5294232b9980517e9c",
.url = "https://github.com/bobf/zig-args/archive/88cbade9a517a4014824f8f53f3c48c8a0b2ffe1.tar.gz",
.hash = "zig_args-0.0.0-jqtN6P_NAAC97fGpk9hS2K681jkiqPsWP6w3ucb_ctGH",
},
.jetquery = .{
.url = "https://github.com/jetzig-framework/jetquery/archive/387ad885bb29a16d38dbb71f5b4b5b083fa320e3.tar.gz",
.hash = "12206089a3d9de179a0d40cf35c48786eb4525a203ebf8cc131e23e430625713c402",
.url = "https://github.com/jetzig-framework/jetquery/archive/e1f969f2e3e0e1ad9cc30d56fde9739aa692fdc3.tar.gz",
.hash = "jetquery-0.0.0-TNf3zo2ABgBgcsIAvJ1Ud2B2zDzrBy9GQ31kKmTYZ7Ya",
},
},
.paths = .{

View File

@ -149,13 +149,13 @@ pub fn run(
const tmpdir_real_path = try tmpdir.realpathAlloc(allocator, ".");
defer allocator.free(tmpdir_real_path);
try util.runCommandInDir(allocator, tar_argv.items, .{ .path = tmpdir_real_path });
try util.runCommandInDir(allocator, tar_argv.items, .{ .path = tmpdir_real_path }, .{});
switch (builtin.os.tag) {
.windows => {},
else => std.debug.print("Bundle `bundle.tar.gz` generated successfully.", .{}),
}
util.printSuccess();
util.printSuccess(null);
}
fn locateMarkdownFiles(allocator: std.mem.Allocator, dir: std.fs.Dir, views_path: []const u8, paths: *std.ArrayList([]const u8)) !void {
@ -215,7 +215,7 @@ fn zig_build_install(allocator: std.mem.Allocator, path: []const u8, options: Op
defer project_dir.close();
project_dir.makePath(".bundle") catch {};
try util.runCommandInDir(allocator, install_argv.items, .{ .path = path });
try util.runCommandInDir(allocator, install_argv.items, .{ .path = path }, .{});
const install_bin_path = try std.fs.path.join(allocator, &[_][]const u8{ ".bundle", "bin" });
defer allocator.free(install_bin_path);

View File

@ -56,21 +56,16 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he
\\// * allocator: Arena allocator for use during the mail delivery process.
\\// * mail: Mail parameters (from, to, subject, etc.). Inspect or override any values
\\// assigned when the mail was created.
\\// * data: Provides `data.string()` etc. for generating Jetzig Values.
\\// * params: Template data for `text.zmpl` and `html.zmpl`. Inherits all response data
\\// assigned in a view function and can be modified for email-specific content.
\\// * env: Provides the following fields:
\\// - logger: Logger attached to the same stream as the Jetzig server.
\\// - environment: Enum of `{ production, development }`.
\\// * env: Provides various information about the environment. See `jetzig.jobs.JobEnv`.
\\pub fn deliver(
\\ allocator: std.mem.Allocator,
\\ mail: *jetzig.mail.MailParams,
\\ data: *jetzig.data.Data,
\\ params: *jetzig.data.Value,
\\ env: jetzig.jobs.JobEnv,
\\) !void {
\\ _ = allocator;
\\ _ = data;
\\ _ = params;
\\ try env.logger.INFO("Delivering email with subject: '{?s}'", .{mail.get(.subject)});
\\}

View File

@ -106,7 +106,7 @@ pub fn run(
try copySourceFile(
allocator,
install_dir,
"demo/config/database.zig",
"demo/config/database_template.zig",
"config/database.zig",
null,
);
@ -202,6 +202,7 @@ pub fn run(
github_url,
},
.{ .dir = install_dir },
.{},
);
// TODO: Use arg or interactive prompt to do Git setup in net project, default to no.
@ -260,7 +261,7 @@ fn copySourceFile(
util.printFailure();
return err;
};
util.printSuccess();
util.printSuccess(null);
}
// Read a file from Jetzig source code.
@ -333,6 +334,7 @@ fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void {
".",
},
.{ .path = install_dir },
.{},
);
try util.runCommandInDir(
@ -343,6 +345,7 @@ fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void {
".",
},
.{ .path = install_dir },
.{},
);
try util.runCommandInDir(
@ -354,5 +357,6 @@ fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void {
"Initialize Jetzig project",
},
.{ .path = install_dir },
.{},
);
}

View File

@ -78,11 +78,7 @@ pub fn run(
});
while (true) {
util.runCommandInDir(
allocator,
argv.items,
.{ .path = realpath },
) catch {
util.runCommandInDir(allocator, argv.items, .{ .path = realpath }, .{}) catch {
std.debug.print("Build failed, waiting for file change...\n", .{});
try awaitFileChange(allocator, cwd, &mtime);
std.debug.print("Changes detected, restarting server...\n", .{});

View File

@ -30,7 +30,7 @@ pub fn initDataModule(build: *std.Build) !*std.Build.Module {
"demo/public/zmpl.png",
"demo/public/favicon.ico",
"demo/public/styles.css",
"demo/config/database.zig",
"demo/config/database_template.zig",
".gitignore",
};

View File

@ -23,8 +23,8 @@ const icons = .{
};
/// Print a success confirmation.
pub fn printSuccess() void {
std.debug.print(" " ++ icons.check ++ "\n", .{});
pub fn printSuccess(message: ?[]const u8) void {
std.debug.print(" " ++ icons.check ++ " {s}\n", .{message orelse ""});
}
/// Print a failure confirmation.
@ -153,7 +153,7 @@ pub fn runCommandStreaming(allocator: std.mem.Allocator, install_path: []const u
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 });
try runCommandInDir(allocator, argv, .{ .dir = dir }, .{});
}
const Dir = union(enum) {
@ -161,48 +161,118 @@ const Dir = union(enum) {
dir: std.fs.Dir,
};
pub const RunOptions = struct {
output: enum { stream, capture } = .capture,
wait: bool = true,
};
/// 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 {
pub fn runCommandInDir(allocator: std.mem.Allocator, argv: []const []const u8, dir: Dir, options: RunOptions) !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,
});
const output_behaviour: std.process.Child.StdIo = switch (options.output) {
.stream => .Inherit,
.capture => .Pipe,
};
var child = std.process.Child.init(argv, allocator);
child.stdin_behavior = .Ignore;
child.stdout_behavior = output_behaviour;
child.stderr_behavior = output_behaviour;
if (options.output == .stream) {
child.stdout = std.io.getStdOut();
child.stderr = std.io.getStdErr();
}
child.cwd = cwd_path;
var stdout = try std.ArrayListUnmanaged(u8).initCapacity(allocator, 0);
var stderr = try std.ArrayListUnmanaged(u8).initCapacity(allocator, 0);
errdefer {
stdout.deinit(allocator);
stderr.deinit(allocator);
}
try child.spawn();
switch (options.output) {
.capture => try collectOutput(child, allocator, &stdout, &stderr),
.stream => {},
}
if (!options.wait) return;
const result = std.process.Child.RunResult{
.term = try child.wait(),
.stdout = try stdout.toOwnedSlice(allocator),
.stderr = try stderr.toOwnedSlice(allocator),
};
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
const command = try std.mem.join(allocator, " ", argv);
defer allocator.free(command);
std.debug.print("[exec] {s}", .{command});
if (result.term.Exited != 0) {
printFailure();
std.debug.print(
\\
\\Error running command: {s}
\\
\\[stdout]:
\\
\\{s}
\\
\\[stderr]:
\\
\\{s}
\\
, .{ command, result.stdout, result.stderr });
if (result.stdout.len > 0) {
std.debug.print("\n[stdout]:\n{s}\n", .{result.stdout});
}
if (result.stderr.len > 0) {
std.debug.print("\n[stderr]:\n{s}\n", .{result.stderr});
}
return error.JetzigCommandError;
} else {
printSuccess();
const message = try std.mem.join(allocator, " ", argv);
defer allocator.free(message);
printSuccess(message);
}
}
fn collectOutput(
child: std.process.Child,
allocator: std.mem.Allocator,
stdout: *std.ArrayListUnmanaged(u8),
stderr: *std.ArrayListUnmanaged(u8),
) !void {
const max_output_bytes = 50 * 1024;
std.debug.assert(child.stdout_behavior == .Pipe);
std.debug.assert(child.stderr_behavior == .Pipe);
var poller = std.io.poll(allocator, enum { stdout, stderr }, .{
.stdout = child.stdout.?,
.stderr = child.stderr.?,
});
defer poller.deinit();
std.debug.print("(working) .", .{});
while (try poller.poll()) {
if (poller.fifo(.stdout).count > max_output_bytes)
return error.StdoutStreamTooLong;
if (poller.fifo(.stderr).count > max_output_bytes)
return error.StderrStreamTooLong;
std.debug.print(".", .{});
}
std.debug.print(" (done)\n", .{});
try writeFifoDataToArrayList(allocator, stdout, poller.fifo(.stdout));
try writeFifoDataToArrayList(allocator, stderr, poller.fifo(.stderr));
}
// Borrowed from `std.process.Child.writeFifoDataToArrayList` - non-public but needed in our
// modified `collectOutput`
fn writeFifoDataToArrayList(allocator: std.mem.Allocator, list: *std.ArrayListUnmanaged(u8), fifo: *std.io.PollFifo) !void {
if (fifo.head != 0) fifo.realign();
if (list.capacity == 0) {
list.* = .{
.items = fifo.buf[0..fifo.count],
.capacity = fifo.buf.len,
};
fifo.* = std.io.PollFifo.init(fifo.allocator);
} else {
try list.appendSlice(allocator, fifo.buf[0..fifo.count]);
}
}
/// Generate a full GitHub URL for passing to `zig fetch`.
pub fn githubUrl(allocator: std.mem.Allocator) ![]const u8 {
var client = std.http.Client{ .allocator = allocator };
@ -290,8 +360,6 @@ pub fn environmentBuildOption(environment: cli.Environment) []const u8 {
};
}
pub fn unicodePrint(comptime fmt: []const u8, args: anytype) !void {
if (builtin.os.tag == .windows) {
// Windows-specific code
@ -320,6 +388,6 @@ const UTF8ConsoleOutput = struct {
}
fn deinit(self: UTF8ConsoleOutput) void {
_ = std.os.windows.kernel32.SetConsoleOutputCP(self.original);
_ = std.os.windows.kernel32.SetConsoleOutputCP(self.original);
}
};
};

1
demo/.gitignore vendored
View File

@ -4,3 +4,4 @@ static/
src/app/views/**/.*.zig
.DS_Store
log/
src/routes.zig

44
demo/Makefile Normal file
View File

@ -0,0 +1,44 @@
# Makefile
#
# Use this Makefile to set up a local Docker PostgreSQL database and run tests, or launch a local
# development database.
#
## Tests
#
# Set up test database and run application tests:
#
# ```
# make test
# ```
#
## Development
#
# Set up development database and launch the demo Jetzig app:
#
# ```
# make dev
# ```
#
# TODO: Move all of this into `build.zig`
test_database=jetzig_demo_test
dev_database=jetzig_demo_dev
port=14173
export JETQUERY_HOSTNAME=localhost
export JETQUERY_USERNAME=postgres
export JETQUERY_PASSWORD=postgres
export JETQUERY_POOL_SIZE=2
.PHONY: test
test: env=JETQUERY_DATABASE=${test_database} JETQUERY_PORT=${port}
test:
docker compose up --detach --wait --renew-anon-volumes --remove-orphans --force-recreate
${env} zig build -Denvironment=testing jetzig:database:setup
${env} zig build -Denvironment=testing jetzig:test
.PHONY: dev
dev: env=JETQUERY_DATABASE=${dev_database} JETQUERY_PORT=${port}
dev:
docker compose up --detach --wait --renew-anon-volumes --remove-orphans
${env} zig build -Denvironment=testing jetzig:database:setup
${env} jetzig server

View File

@ -1,7 +1,8 @@
.{
.name = "jetzig-demo",
.name = .jetzig_demo,
.version = "0.0.0",
.minimum_zig_version = "0.12.0",
.fingerprint = 0x3877c19710a92a5c,
.dependencies = .{
.jetzig = .{
.path = "../",

7
demo/compose.yml Normal file
View File

@ -0,0 +1,7 @@
services:
postgres:
image: postgres:17
ports:
- 14173:5432
environment:
POSTGRES_PASSWORD: 'postgres'

View File

@ -1,48 +1,16 @@
pub const database = .{
// Null adapter fails when a database call is invoked.
.development = .{
.adapter = .null,
.adapter = .postgresql,
.username = "postgres",
.password = "postgres",
.hostname = "localhost",
.database = "jetzig_demo_dev",
.port = 14173, // See `compose.yml`
},
// This configuration is used for CI
// in GitHub
.testing = .{
.adapter = .null,
.adapter = .postgresql,
.database = "jetzig_demo_test",
},
.production = .{
.adapter = .null,
},
// PostgreSQL adapter configuration.
//
// All options except `adapter` can be configured using environment variables:
//
// * JETQUERY_HOSTNAME
// * JETQUERY_PORT
// * JETQUERY_USERNAME
// * JETQUERY_PASSWORD
// * JETQUERY_DATABASE
//
// .testing = .{
// .adapter = .postgresql,
// .hostname = "localhost",
// .port = 5432,
// .username = "postgres",
// .password = "password",
// .database = "myapp_testing",
// },
//
// .development = .{
// .adapter = .postgresql,
// .hostname = "localhost",
// .port = 5432,
// .username = "postgres",
// .password = "password",
// .database = "myapp_development",
// },
//
// .production = .{
// .adapter = .postgresql,
// .hostname = "localhost",
// .port = 5432,
// .username = "postgres",
// .password = "password",
// .database = "myapp_production",
// },
};

View File

@ -0,0 +1,48 @@
pub const database = .{
// Null adapter fails when a database call is invoked.
.development = .{
.adapter = .null,
},
.testing = .{
.adapter = .null,
},
.production = .{
.adapter = .null,
},
// PostgreSQL adapter configuration.
//
// All options except `adapter` can be configured using environment variables:
//
// * JETQUERY_HOSTNAME
// * JETQUERY_PORT
// * JETQUERY_USERNAME
// * JETQUERY_PASSWORD
// * JETQUERY_DATABASE
//
// .testing = .{
// .adapter = .postgresql,
// .hostname = "localhost",
// .port = 5432,
// .username = "postgres",
// .password = "password",
// .database = "myapp_testing",
// },
//
// .development = .{
// .adapter = .postgresql,
// .hostname = "localhost",
// .port = 5432,
// .username = "postgres",
// .password = "password",
// .database = "myapp_development",
// },
//
// .production = .{
// .adapter = .postgresql,
// .hostname = "localhost",
// .port = 5432,
// .username = "postgres",
// .password = "password",
// .database = "myapp_production",
// },
};

View File

@ -0,0 +1,14 @@
const jetquery = @import("jetzig").jetquery;
pub const User = jetquery.Model(
@This(),
"users",
struct {
id: i32,
email: []const u8,
password_hash: []const u8,
created_at: jetquery.DateTime,
updated_at: jetquery.DateTime,
},
.{},
);

View File

@ -1,9 +1,9 @@
const jetquery = @import("jetquery");
pub fn up(repo: *jetquery.Repo) !void {
pub fn up(repo: anytype) !void {
_ = repo;
}
pub fn down(repo: *jetquery.Repo) !void {
pub fn down(repo: anytype) !void {
_ = repo;
}

View File

@ -0,0 +1,20 @@
const std = @import("std");
const jetquery = @import("jetquery");
const t = jetquery.schema.table;
pub fn up(repo: anytype) !void {
try repo.createTable(
"users",
&.{
t.primaryKey("id", .{}),
t.column("email", .string, .{ .unique = true, .index = true }),
t.column("password_hash", .string, .{}),
t.timestamps(.{}),
},
.{},
);
}
pub fn down(repo: anytype) !void {
try repo.dropTable("users", .{});
}

View File

@ -12,8 +12,8 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig
env,
.{
.subject = "Hello!!!",
.from = "bob@jetzig.dev",
.to = &.{"bob@jetzig.dev"},
.from = .{ .email = "bob@jetzig.dev" },
.to = &.{.{ .email = "bob@jetzig.dev" }},
.html = "<div>Hello!</div>",
.text = "Hello!",
},

View File

@ -3,7 +3,7 @@ const jetzig = @import("jetzig");
// Default values for this mailer.
pub const defaults: jetzig.mail.DefaultMailParams = .{
.from = "no-reply@example.com",
.from = .{ .email = "no-reply@example.com" },
.subject = "Default subject",
};

View File

@ -7,4 +7,4 @@
<input type="submit" value="Submit Spam" />
</form>
<div>Try clearing `_jetzig_session` cookie before clicking "Submit Spam"</div>
<div>Try clearing `_jetzig-session` cookie before clicking "Submit Spam"</div>

View File

@ -0,0 +1,3 @@
<div>
<span>Content goes here</span>
</div>

View File

@ -0,0 +1,61 @@
const std = @import("std");
const jetzig = @import("jetzig");
const auth = @import("jetzig").auth;
pub fn index(request: *jetzig.Request) !jetzig.View {
return request.render(.ok);
}
pub fn post(request: *jetzig.Request) !jetzig.View {
const Login = struct {
email: []const u8,
password: []const u8,
};
const params = try request.expectParams(Login) orelse {
return request.fail(.forbidden);
};
// Lookup the user by email
const query = jetzig.database.Query(.User).findBy(
.{ .email = params.email },
);
const user = try request.repo.execute(query) orelse {
return request.fail(.forbidden);
};
// Check that the password matches
if (try auth.verifyPassword(
request.allocator,
user.password_hash,
params.password,
)) {
try auth.signIn(request, user.id);
return request.redirect("/", .found);
}
return request.fail(.forbidden);
}
test "post" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
const hashed_pass = try auth.hashPassword(std.testing.allocator, "test");
defer std.testing.allocator.free(hashed_pass);
try jetzig.database.Query(.User).deleteAll().execute(app.repo);
try app.repo.insert(.User, .{
.id = 1,
.email = "test@test.com",
.password_hash = hashed_pass,
});
const response = try app.request(.POST, "/login", .{
.json = .{
.email = "test@test.com",
.password = "test",
},
});
try response.expectStatus(.found);
}

View File

@ -0,0 +1,7 @@
<form method="post" id="login">
<input type="email" name="email" placeholder="name@example.com">
<label for="email">Email address</label>
<input type="password" name="password" placeholder="Password">
<label for="password">Password</label>
<button type="submit" form="login">Sign in</button>
</form>

View File

@ -10,7 +10,7 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
// * `src/app/mailers/welcome/html.zmpl`
// * `src/app/mailers/welcome/text.zmpl`
// All mailer templates have access to the same template data as a view template.
const mail = request.mail("welcome", .{ .to = &.{"hello@jetzig.dev"} });
const mail = request.mail("welcome", .{ .to = &.{.{ .email = "hello@jetzig.dev" }} });
// Deliver the email asynchronously via a built-in mail Job. Use `.now` to send the email
// synchronously (i.e. before the request has returned).

View File

@ -5,8 +5,8 @@ const importedFunction = @import("../lib/example.zig").exampleFunction;
pub const layout = "application";
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
var root = try data.object();
pub fn index(request: *jetzig.Request) !jetzig.View {
var root = try request.data(.object);
try root.put("message", "Welcome to Jetzig!");
try root.put("custom_number", customFunction(100, 200, 300));
try root.put("imported_number", importedFunction(100, 200, 300));

View File

@ -4,7 +4,7 @@
<!-- Renders `src/app/views/root/_quotes.zmpl`: -->
<div>
@partial root/quotes(message: .message)
@partial root/quotes(message: $.message)
</div>
<div>

View File

@ -87,7 +87,7 @@ pub const jetzig_options = struct {
.path = "/",
},
.production => .{
.same_site = true,
.same_site = .lax,
.secure = true,
.http_only = true,
.path = "/",

View File

@ -376,7 +376,7 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
path,
@intCast(stat.size),
null,
@alignOf(u8),
.of(u8),
0,
);
defer self.allocator.free(source);
@ -390,7 +390,7 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
for (self.ast.nodes.items(.tag), 0..) |tag, index| {
switch (tag) {
.fn_proto_multi, .fn_proto_one, .fn_proto_simple => |function_tag| {
var function = try self.parseFunction(function_tag, index, path, source);
var function = try self.parseFunction(function_tag, @enumFromInt(index), path, source);
if (function) |*capture| {
if (capture.args.len == 0) {
std.debug.print(
@ -414,7 +414,7 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
}
},
.simple_var_decl => {
const decl = self.ast.simpleVarDecl(asNodeIndex(index));
const decl = self.ast.simpleVarDecl(@enumFromInt(index));
if (self.isStaticParamsDecl(decl)) {
self.data.reset();
const params = try self.data.root(.object);
@ -452,10 +452,11 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
// Parse the `pub const static_params` definition and into a `jetzig.data.Value`.
fn parseStaticParamsDecl(self: *Routes, decl: std.zig.Ast.full.VarDecl, params: *jetzig.data.Value) !void {
const init_node = self.ast.nodes.items(.tag)[decl.ast.init_node];
switch (init_node) {
const init_node = decl.ast.init_node.unwrap() orelse return;
switch (self.ast.nodeTag(init_node)) {
.struct_init_dot_two, .struct_init_dot_two_comma => {
try self.parseStruct(decl.ast.init_node, params);
try self.parseStruct(init_node, params);
},
else => return,
}
@ -488,14 +489,14 @@ fn parseArray(self: *Routes, node: std.zig.Ast.Node.Index, params: *jetzig.data.
const array = maybe_array.?;
const main_token = self.ast.nodes.items(.main_token)[node];
const main_token = self.ast.nodeMainToken(node);
const field_name = self.ast.tokenSlice(main_token - 3);
const params_array = try self.data.array();
try params.put(field_name, params_array);
for (array.ast.elements) |element| {
const elem = self.ast.nodes.items(.tag)[element];
const elem = self.ast.nodeTag(element);
switch (elem) {
.struct_init_dot, .struct_init_dot_two, .struct_init_dot_two_comma => {
const route_params = try self.data.object();
@ -508,19 +509,20 @@ fn parseArray(self: *Routes, node: std.zig.Ast.Node.Index, params: *jetzig.data.
try self.parseField(element, route_params);
},
.string_literal => {
const string_token = self.ast.nodes.items(.main_token)[element];
const string_token = self.ast.nodeMainToken(element);
const string_value = self.ast.tokenSlice(string_token);
// Strip quotes: `"foo"` -> `foo`
try params_array.append(string_value[1 .. string_value.len - 1]);
},
.number_literal => {
const number_token = self.ast.nodes.items(.main_token)[element];
const number_token = self.ast.nodeMainToken(element);
const number_value = self.ast.tokenSlice(number_token);
try params_array.append(try parseNumber(number_value, self.data));
},
inline else => {
const tag = self.ast.nodes.items(.tag)[element];
@setEvalBranchQuota(10_000);
const tag = self.ast.nodeTag(element);
std.debug.print("Unexpected token: {}\n", .{tag});
return error.JetzigStaticParamsParseError;
},
@ -530,22 +532,21 @@ fn parseArray(self: *Routes, node: std.zig.Ast.Node.Index, params: *jetzig.data.
// Parse the value of a param field (recursively when field is a struct/array)
fn parseField(self: *Routes, node: std.zig.Ast.Node.Index, params: *jetzig.data.Value) anyerror!void {
const tag = self.ast.nodes.items(.tag)[node];
switch (tag) {
switch (self.ast.nodeTag(node)) {
// Route params, e.g. `.index = .{ ... }`
.array_init_dot, .array_init_dot_two, .array_init_dot_comma, .array_init_dot_two_comma => {
try self.parseArray(node, params);
},
.struct_init_dot, .struct_init_dot_two, .struct_init_dot_two_comma => {
const nested_params = try self.data.object();
const main_token = self.ast.nodes.items(.main_token)[node];
const main_token = self.ast.nodeMainToken(node);
const field_name = self.ast.tokenSlice(main_token - 3);
try params.put(field_name, nested_params);
try self.parseStruct(node, nested_params);
},
// Individual param in a params struct, e.g. `.foo = "bar"`
.string_literal => {
const main_token = self.ast.nodes.items(.main_token)[node];
const main_token = self.ast.nodeMainToken(node);
const field_name = self.ast.tokenSlice(main_token - 2);
const field_value = self.ast.tokenSlice(main_token);
@ -556,13 +557,13 @@ fn parseField(self: *Routes, node: std.zig.Ast.Node.Index, params: *jetzig.data.
);
},
.number_literal => {
const main_token = self.ast.nodes.items(.main_token)[node];
const main_token = self.ast.nodeMainToken(node);
const field_name = self.ast.tokenSlice(main_token - 2);
const field_value = self.ast.tokenSlice(main_token);
try params.put(field_name, try parseNumber(field_value, self.data));
},
else => {
else => |tag| {
std.debug.print("Unexpected token: {}\n", .{tag});
return error.JetzigStaticParamsParseError;
},
@ -593,16 +594,16 @@ fn isStaticParamsDecl(self: *Routes, decl: std.zig.Ast.full.VarDecl) bool {
fn parseFunction(
self: *Routes,
function_type: std.zig.Ast.Node.Tag,
index: usize,
index: std.zig.Ast.Node.Index,
path: []const u8,
source: []const u8,
) !?Function {
var buf: [1]std.zig.Ast.Node.Index = undefined;
const fn_proto = switch (function_type) {
.fn_proto_multi => self.ast.fnProtoMulti(@as(u32, @intCast(index))),
.fn_proto_one => self.ast.fnProtoOne(&buf, @as(u32, @intCast(index))),
.fn_proto_simple => self.ast.fnProtoSimple(&buf, @as(u32, @intCast(index))),
.fn_proto_multi => self.ast.fnProtoMulti(index),
.fn_proto_one => self.ast.fnProtoOne(&buf, index),
.fn_proto_simple => self.ast.fnProtoSimple(&buf, index),
else => unreachable,
};
if (fn_proto.name_token) |token| {
@ -619,7 +620,7 @@ fn parseFunction(
while (it.next()) |arg| {
if (arg.name_token) |arg_token| {
const arg_name = self.ast.tokenSlice(arg_token);
const node = self.ast.nodes.get(arg.type_expr);
const node = self.ast.nodes.get(@intFromEnum(arg.type_expr.?));
const type_name = try self.parseTypeExpr(node);
try args.append(.{ .name = arg_name, .type_name = type_name });
}
@ -665,10 +666,6 @@ fn parseTypeExpr(self: *Routes, node: std.zig.Ast.Node) ![]const u8 {
return error.JetzigAstParserError;
}
fn asNodeIndex(index: usize) std.zig.Ast.Node.Index {
return @as(std.zig.Ast.Node.Index, @intCast(index));
}
fn isActionFunctionName(name: []const u8) bool {
inline for (@typeInfo(jetzig.views.Route.Action).@"enum".fields) |field| {
if (std.mem.eql(u8, field.name, name)) return true;

View File

@ -33,7 +33,7 @@ pub fn main() !void {
const Repo = jetzig.jetquery.Repo(jetzig.database.adapter, Schema);
var repo = try Repo.loadConfig(
allocator,
std.enums.nameCast(jetzig.jetquery.Environment, jetzig.environment),
@field(jetzig.jetquery.Environment, @tagName(jetzig.environment)),
.{ .env = try jetzig.database.repoEnv(env), .context = .cli },
);
defer repo.deinit();
@ -60,7 +60,7 @@ pub fn main() !void {
const email = args[2];
try repo.insert(std.enums.nameCast(std.meta.DeclEnum(Schema), model), .{
try repo.insert(@field(std.meta.DeclEnum(Schema), model), .{
.email = email,
.password_hash = try hashPassword(allocator, password),
});

View File

@ -15,7 +15,7 @@ const production_drop_failure_message = "To drop a production database, " ++
const environment = jetzig.build_options.environment;
const config = @field(jetquery.config.database, @tagName(environment));
const Action = enum { migrate, rollback, create, drop, reflect };
const Action = enum { migrate, rollback, create, drop, reflect, setup, update };
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
@ -46,6 +46,8 @@ pub fn main() !void {
.{ "create", .create },
.{ "drop", .drop },
.{ "reflect", .reflect },
.{ "setup", .setup },
.{ "update", .update },
});
const action = map.get(args[1]) orelse return error.JetzigUnrecognizedArgument;
@ -77,6 +79,25 @@ pub fn main() !void {
defer repo.deinit();
try repo.createDatabase(database, .{});
},
.setup => {
{
var repo = try migrationsRepo(.create, allocator, repo_env);
defer repo.deinit();
try repo.createDatabase(database, .{});
}
{
var repo = try migrationsRepo(.update, allocator, repo_env);
defer repo.deinit();
try Migrate(config.adapter).init(&repo).migrate();
try reflectSchema(allocator, repo_env);
}
},
.update => {
var repo = try migrationsRepo(action, allocator, repo_env);
defer repo.deinit();
try Migrate(config.adapter).init(&repo).migrate();
try reflectSchema(allocator, repo_env);
},
.drop => {
if (environment == .production) {
const confirm = std.process.getEnvVarOwned(allocator, confirm_drop_env) catch |err| {
@ -103,33 +124,7 @@ pub fn main() !void {
}
},
.reflect => {
var cwd = try jetzig.util.detectJetzigProjectDir();
defer cwd.close();
const Repo = jetquery.Repo(config.adapter, Schema);
var repo = try Repo.loadConfig(
allocator,
std.enums.nameCast(jetquery.Environment, environment),
.{ .context = .migration, .env = repo_env },
);
const reflect = @import("jetquery_reflect").Reflect(config.adapter, Schema).init(
allocator,
&repo,
.{
.import_jetquery =
\\@import("jetzig").jetquery
,
},
);
const schema = try reflect.generateSchema();
const project_dir = try jetzig.util.detectJetzigProjectDir();
const project_dir_realpath = try project_dir.realpathAlloc(allocator, ".");
const path = try std.fs.path.join(
allocator,
&.{ project_dir_realpath, "src", "app", "database", "Schema.zig" },
);
try jetzig.util.createFile(path, schema);
std.log.info("Database schema written to `{s}`.", .{path});
try reflectSchema(allocator, repo_env);
},
}
}
@ -138,15 +133,46 @@ const MigrationsRepo = jetquery.Repo(config.adapter, MigrateSchema);
fn migrationsRepo(action: Action, allocator: std.mem.Allocator, repo_env: anytype) !MigrationsRepo {
return try MigrationsRepo.loadConfig(
allocator,
std.enums.nameCast(jetquery.Environment, environment),
@field(jetquery.Environment, @tagName(environment)),
.{
.admin = switch (action) {
.migrate, .rollback => false,
.migrate, .rollback, .update => false,
.create, .drop => true,
.reflect => unreachable, // We use a separate repo for schema reflection.
.setup => unreachable, // Setup uses `create` and then `update`
},
.context = .migration,
.env = repo_env,
},
);
}
fn reflectSchema(allocator: std.mem.Allocator, repo_env: anytype) !void {
var cwd = try jetzig.util.detectJetzigProjectDir();
defer cwd.close();
const Repo = jetquery.Repo(config.adapter, Schema);
var repo = try Repo.loadConfig(
allocator,
@field(jetquery.Environment, @tagName(environment)),
.{ .context = .migration, .env = repo_env },
);
const reflect = @import("jetquery_reflect").Reflect(config.adapter, Schema).init(
allocator,
&repo,
.{
.import_jetquery =
\\@import("jetzig").jetquery
,
},
);
const schema = try reflect.generateSchema();
const project_dir = try jetzig.util.detectJetzigProjectDir();
const project_dir_realpath = try project_dir.realpathAlloc(allocator, ".");
const path = try std.fs.path.join(
allocator,
&.{ project_dir_realpath, "src", "app", "database", "Schema.zig" },
);
try jetzig.util.createFile(path, schema);
std.log.info("Database schema written to `{s}`.", .{path});
}

View File

@ -33,7 +33,10 @@ pub const Date = jetcommon.types.Date;
pub const authenticity_token_name = config.get([]const u8, "authenticity_token_name");
pub const build_options = @import("build_options");
pub const environment = std.enums.nameCast(Environment.EnvironmentName, build_options.environment);
pub const environment = @field(
Environment.EnvironmentName,
@tagName(build_options.environment),
);
/// The primary interface for a Jetzig application. Create an `App` in your application's
/// `src/main.zig` and call `start` to launch the application.

View File

@ -24,8 +24,7 @@ const AppOptions = struct {
};
/// Starts an application. `routes` should be `@import("routes").routes`, a generated file
/// automatically created at build time. `templates` should be
/// `@import("src/app/views/zmpl.manifest.zig").templates`, created by Zmpl at compile time.
/// automatically created at build time.
pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
defer self.env.deinit();
@ -35,7 +34,7 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
defer mime_map.deinit();
try mime_map.build();
const routes = try createRoutes(self.allocator, &routes_module.routes);
const routes = try createRoutes(self.allocator, if (@hasDecl(routes_module, "routes")) &routes_module.routes else &.{});
defer {
for (routes) |var_route| {
var_route.deinitParams();
@ -63,7 +62,7 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
var log_thread = try std.Thread.spawn(
.{ .allocator = self.allocator },
jetzig.loggers.LogQueue.Reader.publish,
.{ &self.env.log_queue.reader, .{} },
.{ &self.env.log_queue.reader, jetzig.loggers.LogQueue.Reader.PublishOptions{} },
);
defer log_thread.join();
@ -87,8 +86,8 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
self.env,
routes,
self.custom_routes.items,
&routes_module.jobs,
&routes_module.mailers,
if (@hasDecl(routes_module, "jobs")) &routes_module.jobs else &.{},
if (@hasDecl(routes_module, "jobs")) &routes_module.mailers else &.{},
&mime_map,
&store,
&job_queue,
@ -107,8 +106,8 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
.vars = self.env.vars,
.environment = self.env.environment,
.routes = routes,
.jobs = &routes_module.jobs,
.mailers = &routes_module.mailers,
.jobs = if (@hasDecl(routes_module, "jobs")) &routes_module.jobs else &.{},
.mailers = if (@hasDecl(routes_module, "jobs")) &routes_module.mailers else &.{},
.store = &store,
.cache = &cache,
.repo = &repo,
@ -132,7 +131,7 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
return;
},
else => {
try server.logger.ERROR("Encountered error: {}\nExiting.\n", .{err});
try server.logger.ERROR("Encountered error at server launch: {}\nExiting.\n", .{err});
std.process.exit(1);
},
}

View File

@ -196,15 +196,15 @@ pub fn init(parent_allocator: std.mem.Allocator, env_options: EnvironmentOptions
const env_file = std.fs.cwd().openFile(options.options.@"env-file", .{}) catch |err|
switch (err) {
error.FileNotFound => null,
else => return err,
};
error.FileNotFound => null,
else => return err,
};
const vars = try Vars.init(allocator, env_file);
var launch_logger = LaunchLogger{
.stdout = stdout,
.stderr = stderr,
.stdout = stdout.file,
.stderr = stderr.file,
.silent = env_options.silent,
};
@ -213,8 +213,8 @@ pub fn init(parent_allocator: std.mem.Allocator, env_options: EnvironmentOptions
.development_logger = jetzig.loggers.DevelopmentLogger.init(
allocator,
resolveLogLevel(options.options.@"log-level", jetzig.environment),
stdout,
stderr,
stdout.file,
stderr.file,
),
},
.production => jetzig.loggers.Logger{
@ -242,7 +242,7 @@ pub fn init(parent_allocator: std.mem.Allocator, env_options: EnvironmentOptions
}
const secret_len = jetzig.http.Session.Cipher.key_length;
const secret_value = try getSecret(allocator, launch_logger, secret_len, jetzig.environment);
const secret_value = try getSecret(allocator, launch_logger, jetzig.environment);
const secret = if (secret_value.len > secret_len) secret_value[0..secret_len] else secret_value;
if (secret.len != secret_len) {
@ -304,29 +304,31 @@ pub fn deinit(self: Environment) void {
self.parent_allocator.destroy(self.arena);
}
fn getLogFile(stream: enum { stdout, stderr }, options: Options) !std.fs.File {
fn getLogFile(stream: enum { stdout, stderr }, options: Options) !jetzig.loggers.LogFile {
const path = switch (stream) {
.stdout => options.log,
.stderr => options.@"log-error",
};
if (std.mem.eql(u8, path, "-")) return switch (stream) {
.stdout => std.io.getStdOut(),
.stdout => .{ .file = std.io.getStdOut(), .sync = false },
.stderr => if (std.mem.eql(u8, options.log, "-"))
std.io.getStdErr()
.{ .file = std.io.getStdErr(), .sync = false }
else
try std.fs.createFileAbsolute(options.log, .{ .truncate = false }),
.{
.file = try std.fs.createFileAbsolute(options.log, .{ .truncate = false }),
.sync = true,
},
};
const file = try std.fs.createFileAbsolute(path, .{ .truncate = false });
try file.seekFromEnd(0);
return file;
return .{ .file = file, .sync = true };
}
fn getSecret(
allocator: std.mem.Allocator,
logger: LaunchLogger,
comptime len: u10,
environment: EnvironmentName,
) ![]const u8 {
const env_var = "JETZIG_SECRET";
@ -348,10 +350,11 @@ fn getSecret(
std.process.exit(1);
}
const secret = try jetzig.util.generateSecret(allocator, len);
const secret = "jetzig-development-cookie-secret";
try logger.log(
.WARN,
"Running in {s} mode, using auto-generated cookie encryption key: {s}",
"Running in {s} mode, using default development cookie encryption key: `{s}`",
.{ @tagName(environment), secret },
);
try logger.log(

View File

@ -14,7 +14,7 @@ pub fn getUserId(comptime id_type: IdType, request: *jetzig.Request) !?switch (i
} {
const session = try request.session();
return session.getT(std.enums.nameCast(jetzig.data.ValueType, id_type), "_jetzig_user_id");
return session.getT(@field(jetzig.data.ValueType, @tagName(id_type)), "_jetzig_user_id");
}
pub fn signIn(request: *jetzig.Request, user_id: anytype) !void {
@ -42,13 +42,16 @@ pub fn verifyPassword(
}
pub fn hashPassword(allocator: std.mem.Allocator, password: []const u8) ![]const u8 {
const buf = try allocator.alloc(u8, 128);
return try std.crypto.pwhash.argon2.strHash(
var buf: [128]u8 = undefined;
const hash = try std.crypto.pwhash.argon2.strHash(
password,
.{
.allocator = allocator,
.params = .{ .t = 3, .m = 32, .p = 4 },
},
buf,
&buf,
);
const result = try allocator.alloc(u8, hash.len);
@memcpy(result, hash);
return result;
}

View File

@ -18,7 +18,10 @@ pub const mail = @import("mail.zig");
pub const kv = @import("kv.zig");
pub const db = @import("database.zig");
pub const Environment = @import("Environment.zig");
pub const environment = std.enums.nameCast(Environment.EnvironmentName, build_options.environment);
pub const environment = @field(
Environment.EnvironmentName,
@tagName(build_options.environment),
);
pub const build_options = @import("build_options");
const root = @import("root");

View File

@ -2,9 +2,9 @@ const std = @import("std");
const jetzig = @import("../jetzig.zig");
pub const adapter = std.enums.nameCast(
pub const adapter = @field(
jetzig.jetquery.adapters.Name,
@field(jetzig.jetquery.config.database, @tagName(jetzig.environment)).adapter,
@tagName(@field(jetzig.jetquery.config.database, @tagName(jetzig.environment)).adapter),
);
pub const Schema = jetzig.config.get(type, "Schema");
@ -25,7 +25,7 @@ pub fn repo(allocator: std.mem.Allocator, app: anytype) !Repo {
return try Repo.loadConfig(
allocator,
std.enums.nameCast(jetzig.jetquery.Environment, jetzig.environment),
@field(jetzig.jetquery.Environment, @tagName(jetzig.environment)),
.{
.eventCallback = Callback.callbackFn,
.lazy_connect = switch (jetzig.environment) {

View File

@ -98,7 +98,7 @@ const SourceLine = struct { content: []const u8, line: usize };
pub fn readLineFromFile(allocator: std.mem.Allocator, path: []const u8, line: usize) !SourceLine {
const file = try std.fs.openFileAbsolute(path, .{});
var buf: [std.mem.page_size]u8 = undefined;
var buf: [std.heap.pageSize()]u8 = undefined;
var count: usize = 1;
var cursor: usize = 0;

View File

@ -141,6 +141,7 @@ pub fn init(
.HEAD => Method.HEAD,
.PUT => Method.PUT,
.OPTIONS => Method.OPTIONS,
.CONNECT, .OTHER => return error.JetzigUnsupportedHttpMethod,
};
const response_data = try allocator.create(jetzig.data.Data);
@ -496,12 +497,14 @@ pub fn cookies(self: *Request) !*jetzig.http.Cookies {
/// `jetzig.http.Session`.
pub fn session(self: *Request) !*jetzig.http.Session {
if (self._session) |capture| return capture;
const cookie_name = self.server.env.vars.get("JETZIG_SESSION_COOKIE") orelse
jetzig.http.Session.default_cookie_name;
const local_session = try self.allocator.create(jetzig.http.Session);
local_session.* = jetzig.http.Session.init(
self.allocator,
try self.cookies(),
self.server.env.secret,
.{ .cookie_name = cookie_name },
);
local_session.parse() catch |err| {
switch (err) {
@ -580,25 +583,18 @@ const RequestMail = struct {
_ = options;
var mail_job = try self.request.job("__jetzig_mail");
try mail_job.params.put("mailer_name", mail_job.data.string(self.name));
const from = if (self.mail_params.from) |from| mail_job.data.string(from) else null;
try mail_job.params.put("from", from);
try mail_job.params.put("mailer_name", self.name);
try mail_job.params.put("from", self.mail_params.get(.from));
var to_array = try mail_job.data.array();
if (self.mail_params.to) |capture| {
for (capture) |to| try to_array.append(mail_job.data.string(to));
if (self.mail_params.get(.to)) |to| {
for (to) |each| try to_array.append(.{ .email = each.email, .name = each.name });
}
try mail_job.params.put("to", to_array);
const subject = if (self.mail_params.subject) |subject| mail_job.data.string(subject) else null;
try mail_job.params.put("subject", subject);
const html = if (self.mail_params.html) |html| mail_job.data.string(html) else null;
try mail_job.params.put("html", html);
const text = if (self.mail_params.text) |text| mail_job.data.string(text) else null;
try mail_job.params.put("text", text);
try mail_job.params.put("subject", self.mail_params.get(.subject));
try mail_job.params.put("html", self.mail_params.get(.html));
try mail_job.params.put("text", self.mail_params.get(.text));
if (self.request.response_data.value) |value| try mail_job.params.put(
"params",

View File

@ -110,6 +110,8 @@ pub fn listen(self: *Server) !void {
self.initialized = true;
try jetzig.http.middleware.afterLaunch(self);
return try httpz_server.listen();
}
@ -151,7 +153,23 @@ pub fn processNextRequest(
try request.process();
var middleware_data = try jetzig.http.middleware.afterRequest(&request);
if (try maybeMiddlewareRender(&request, &response)) {
try self.logger.logRequest(&request);
return;
}
try self.renderResponse(&request);
try request.response.headers.append("Content-Type", response.content_type);
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
try request.respond();
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
jetzig.http.middleware.deinit(&middleware_data, &request);
try self.logger.logRequest(&request);
}
fn maybeMiddlewareRender(request: *jetzig.http.Request, response: *const jetzig.http.Response) !bool {
if (request.middleware_rendered) |_| {
// Request processing ends when a middleware renders or redirects.
if (request.redirect_state) |state| {
@ -162,17 +180,8 @@ pub fn processNextRequest(
}
try request.response.headers.append("Content-Type", response.content_type);
try request.respond();
} else {
try self.renderResponse(&request);
try request.response.headers.append("Content-Type", response.content_type);
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
try request.respond();
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
jetzig.http.middleware.deinit(&middleware_data, &request);
}
try self.logger.logRequest(&request);
return true;
} else return false;
}
fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {

View File

@ -2,12 +2,12 @@ const std = @import("std");
const jetzig = @import("../../jetzig.zig");
pub const cookie_name = "_jetzig-session";
pub const Cipher = std.crypto.aead.chacha_poly.XChaCha20Poly1305;
allocator: std.mem.Allocator,
encryption_key: []const u8,
cookies: *jetzig.http.Cookies,
cookie_name: []const u8,
initialized: bool = false,
data: jetzig.data.Data,
@ -15,22 +15,30 @@ state: enum { parsed, pending } = .pending,
const Self = @This();
pub const default_cookie_name = "_jetzig-session";
pub const Options = struct {
cookie_name: []const u8 = default_cookie_name,
};
pub fn init(
allocator: std.mem.Allocator,
cookies: *jetzig.http.Cookies,
encryption_key: []const u8,
options: Options,
) Self {
return .{
.allocator = allocator,
.data = jetzig.data.Data.init(allocator),
.cookies = cookies,
.cookie_name = options.cookie_name,
.encryption_key = encryption_key,
};
}
/// Parse session cookie.
pub fn parse(self: *Self) !void {
if (self.cookies.get(cookie_name)) |cookie| {
if (self.cookies.get(self.cookie_name)) |cookie| {
try self.parseSessionCookie(cookie.value);
} else {
try self.reset();
@ -111,7 +119,7 @@ fn save(self: *Self) !void {
defer self.allocator.free(encrypted);
const encoded = try jetzig.util.base64Encode(self.allocator, encrypted);
defer self.allocator.free(encoded);
try self.cookies.put(.{ .name = cookie_name, .value = encoded });
try self.cookies.put(.{ .name = self.cookie_name, .value = encoded });
}
fn parseSessionCookie(self: *Self, cookie_value: []const u8) !void {
@ -180,7 +188,7 @@ test "put and get session key/value" {
try cookies.parse();
const secret: [Cipher.key_length]u8 = [_]u8{0x69} ** Cipher.key_length;
var session = Self.init(allocator, &cookies, &secret);
var session = Self.init(allocator, &cookies, &secret, .{});
defer session.deinit();
var data = jetzig.data.Data.init(allocator);
@ -199,7 +207,7 @@ test "remove session key/value" {
try cookies.parse();
const secret: [Cipher.key_length]u8 = [_]u8{0x69} ** Cipher.key_length;
var session = Self.init(allocator, &cookies, &secret);
var session = Self.init(allocator, &cookies, &secret, .{});
defer session.deinit();
var data = jetzig.data.Data.init(allocator);
@ -224,7 +232,7 @@ test "get value from parsed/decrypted cookie" {
try cookies.parse();
const secret: [Cipher.key_length]u8 = [_]u8{0x69} ** Cipher.key_length;
var session = Self.init(allocator, &cookies, &secret);
var session = Self.init(allocator, &cookies, &secret, .{});
defer session.deinit();
try session.parse();
@ -233,17 +241,32 @@ test "get value from parsed/decrypted cookie" {
}
test "invalid cookie value - too short" {
const allocator = std.testing.allocator;
var cookies = jetzig.http.Cookies.init(allocator, "_jetzig-session=abc");
defer cookies.deinit();
try cookies.parse();
const secret: [Cipher.key_length]u8 = [_]u8{0x69} ** Cipher.key_length;
var session = Self.init(allocator, &cookies, &secret, .{});
defer session.deinit();
try std.testing.expectError(error.JetzigInvalidSessionCookie, session.parse());
}
test "custom session cookie name" {
const allocator = std.testing.allocator;
var cookies = jetzig.http.Cookies.init(
allocator,
"_jetzig-session=abc",
"custom-cookie-name=fPCFwZHvPDT-XCVcsQUSspDLchS3tRuJDqPpB2v3127VXpRP_bPcPLgpHK6RiVkfcP1bMtU",
);
defer cookies.deinit();
try cookies.parse();
const secret: [Cipher.key_length]u8 = [_]u8{0x69} ** Cipher.key_length;
var session = Self.init(allocator, &cookies, &secret);
var session = Self.init(allocator, &cookies, &secret, .{ .cookie_name = "custom-cookie-name" });
defer session.deinit();
try std.testing.expectError(error.JetzigInvalidSessionCookie, session.parse());
try session.parse();
var value = (session.get("foo")).?;
try std.testing.expectEqualStrings("bar", try value.toString());
}

View File

@ -48,6 +48,14 @@ pub fn Type(comptime name: MiddlewareEnum()) type {
}
}
pub fn afterLaunch(server: *jetzig.http.Server) !void {
inline for (middlewares) |middleware| {
if (comptime @hasDecl(middleware, "afterLaunch")) {
try middleware.afterLaunch(server);
}
}
}
pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
var middleware_data = MiddlewareData.init(0) catch unreachable;

View File

@ -42,8 +42,8 @@ pub fn expectParams(request: *jetzig.http.Request, T: type) !?T {
} else if (@typeInfo(field.type) == .optional) {
// if no matching param found and params struct provides a default value, use it,
// otherwise set value to null
@field(t, field.name) = if (field.default_value) |default_value|
@as(*field.type, @ptrCast(@alignCast(@constCast(default_value)))).*
@field(t, field.name) = if (field.default_value_ptr) |default_value_ptr|
@as(*field.type, @ptrCast(@alignCast(@constCast(default_value_ptr)))).*
else
null;
statuses[index] = .blank;

View File

@ -41,9 +41,9 @@ fn jetKVOptions(options: KVOptions) jetzig.jetkv.Options {
.port = options.valkey_options.port,
.connect_timeout = options.valkey_options.connect_timeout * std.time.ms_per_s,
.read_timeout = options.valkey_options.read_timeout * std.time.ms_per_s,
.connect = std.enums.nameCast(
.connect = @field(
jetzig.jetkv.ValkeyBackendOptions.ConnectMode,
options.valkey_options.connect,
@tagName(options.valkey_options.connect),
),
.buffer_size = options.valkey_options.buffer_size,
.pool_size = options.valkey_options.pool_size,

View File

@ -12,6 +12,11 @@ pub const NullLogger = @import("loggers/NullLogger.zig");
pub const LogQueue = @import("loggers/LogQueue.zig");
pub const LogFile = struct {
file: std.fs.File,
sync: bool = false,
};
pub const LogLevel = enum(u4) { TRACE, DEBUG, INFO, WARN, ERROR, FATAL };
pub const LogFormat = enum { development, production, json, null };

View File

@ -6,11 +6,15 @@ const jetzig = @import("../../jetzig.zig");
const buffer_size = jetzig.config.get(usize, "log_message_buffer_len");
const max_pool_len = jetzig.config.get(usize, "max_log_pool_len");
const List = std.DoublyLinkedList(Event);
const List = std.DoublyLinkedList;
const ListNode = struct {
event: Event,
node: std.DoublyLinkedList.Node = .{},
};
const Buffer = [buffer_size]u8;
allocator: std.mem.Allocator,
node_allocator: std.heap.MemoryPool(List.Node),
node_allocator: std.heap.MemoryPool(ListNode),
buffer_allocator: std.heap.MemoryPool(Buffer),
list: List,
read_write_mutex: std.Thread.Mutex,
@ -18,7 +22,7 @@ condition: std.Thread.Condition,
condition_mutex: std.Thread.Mutex,
writer: Writer = undefined,
reader: Reader = undefined,
node_pool: std.ArrayList(*List.Node),
node_pool: std.ArrayList(*ListNode),
buffer_pool: std.ArrayList(*Buffer),
position: usize,
stdout_is_tty: bool = undefined,
@ -42,13 +46,13 @@ const Event = struct {
pub fn init(allocator: std.mem.Allocator) LogQueue {
return .{
.allocator = allocator,
.node_allocator = initPool(allocator, List.Node),
.node_allocator = initPool(allocator, ListNode),
.buffer_allocator = initPool(allocator, Buffer),
.list = List{},
.condition = std.Thread.Condition{},
.condition_mutex = std.Thread.Mutex{},
.read_write_mutex = std.Thread.Mutex{},
.node_pool = std.ArrayList(*List.Node).init(allocator),
.node_pool = std.ArrayList(*ListNode).init(allocator),
.buffer_pool = std.ArrayList(*Buffer).init(allocator),
.position = 0,
};
@ -66,7 +70,11 @@ pub fn deinit(self: *LogQueue) void {
}
/// Set the stdout and stderr outputs. Must be called before `print`.
pub fn setFiles(self: *LogQueue, stdout_file: std.fs.File, stderr_file: std.fs.File) !void {
pub fn setFiles(
self: *LogQueue,
stdout_file: jetzig.loggers.LogFile,
stderr_file: jetzig.loggers.LogFile,
) !void {
self.writer = Writer{
.queue = self,
.mutex = std.Thread.Mutex{},
@ -76,11 +84,11 @@ pub fn setFiles(self: *LogQueue, stdout_file: std.fs.File, stderr_file: std.fs.F
.stderr_file = stderr_file,
.queue = self,
};
self.stdout_is_tty = stdout_file.isTty();
self.stderr_is_tty = stderr_file.isTty();
self.stdout_is_tty = stdout_file.file.isTty();
self.stderr_is_tty = stderr_file.file.isTty();
self.stdout_colorize = std.io.tty.detectConfig(stdout_file) != .no_color;
self.stderr_colorize = std.io.tty.detectConfig(stderr_file) != .no_color;
self.stdout_colorize = std.io.tty.detectConfig(stdout_file.file) != .no_color;
self.stderr_colorize = std.io.tty.detectConfig(stderr_file.file) != .no_color;
self.state = .ready;
}
@ -143,17 +151,21 @@ pub const Writer = struct {
/// Reader for `LogQueue`. Reads log events from the queue and writes them to the designated
/// target (stdout or stderr).
pub const Reader = struct {
stdout_file: std.fs.File,
stderr_file: std.fs.File,
stdout_file: jetzig.loggers.LogFile,
stderr_file: jetzig.loggers.LogFile,
queue: *LogQueue,
pub const PublishOptions = struct {
oneshot: bool = false,
};
/// Publish log events from the queue. Invoke from a dedicated thread. Sleeps when log queue
/// is empty, wakes up when a new event is published.
pub fn publish(self: *Reader, options: struct { oneshot: bool = false }) !void {
pub fn publish(self: *Reader, options: PublishOptions) !void {
std.debug.assert(self.queue.state == .ready);
const stdout_writer = self.stdout_file.writer();
const stderr_writer = self.stderr_file.writer();
const stdout_writer = self.stdout_file.file.writer();
const stderr_writer = self.stderr_file.file.writer();
while (true) {
self.queue.condition_mutex.lock();
@ -173,13 +185,13 @@ pub const Reader = struct {
.stdout => {
stdout_written = true;
if (builtin.os.tag == .windows) {
file = self.stdout_file;
file = self.stdout_file.file;
}
},
.stderr => {
stderr_written = true;
if (builtin.os.tag == .windows) {
file = self.stderr_file;
file = self.stderr_file.file;
}
},
}
@ -212,8 +224,8 @@ pub const Reader = struct {
}
}
if (stdout_written and !self.queue.stdout_is_tty) try self.stdout_file.sync();
if (stderr_written and !self.queue.stderr_is_tty) try self.stderr_file.sync();
if (stdout_written and self.stdout_file.sync) try self.stdout_file.file.sync();
if (stderr_written and self.stderr_file.sync) try self.stderr_file.file.sync();
if (options.oneshot) break;
}
@ -233,8 +245,8 @@ fn append(self: *LogQueue, event: Event) !void {
self.position += 1;
node.* = .{ .data = event };
self.list.append(node);
node.* = .{ .event = event };
self.list.append(&node.node);
self.condition.signal();
}
@ -245,16 +257,17 @@ fn popFirst(self: *LogQueue) !?Event {
defer self.read_write_mutex.unlock();
if (self.list.popFirst()) |node| {
const value = node.data;
const list_node: *ListNode = @fieldParentPtr("node", node);
const value = list_node.event;
self.position -= 1;
if (self.position < self.node_pool.items.len) {
self.node_pool.items[self.position] = node;
self.node_pool.items[self.position] = list_node;
} else {
if (self.node_pool.items.len >= max_pool_len) {
self.node_allocator.destroy(node);
self.node_allocator.destroy(list_node);
self.position += 1;
} else {
try self.node_pool.append(node);
try self.node_pool.append(list_node);
}
}
return value;
@ -280,7 +293,7 @@ test "print to stdout and stderr" {
const stderr = try tmp_dir.dir.createFile("stderr.log", .{ .read = true });
defer stderr.close();
try log_queue.setFiles(stdout, stderr);
try log_queue.setFiles(.{ .file = stdout }, .{ .file = stderr });
try log_queue.print("foo {s}\n", .{"bar"}, .stdout);
try log_queue.print("baz {s}\n", .{"qux"}, .stderr);
try log_queue.print("quux {s}\n", .{"corge"}, .stdout);
@ -324,7 +337,7 @@ test "long messages" {
const stderr = try tmp_dir.dir.createFile("stderr.log", .{ .read = true });
defer stderr.close();
try log_queue.setFiles(stdout, stderr);
try log_queue.setFiles(.{ .file = stdout }, .{ .file = stderr });
try log_queue.print("foo" ** buffer_size, .{}, .stdout);
try log_queue.reader.publish(.{ .oneshot = true });

View File

@ -3,6 +3,7 @@ const std = @import("std");
pub const Mail = @import("mail/Mail.zig");
pub const SMTPConfig = @import("mail/SMTPConfig.zig");
pub const MailParams = @import("mail/MailParams.zig");
pub const Address = MailParams.Address;
pub const DefaultMailParams = MailParams.DefaultMailParams;
pub const components = @import("mail/components.zig");
pub const Job = @import("mail/Job.zig");

View File

@ -46,14 +46,15 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig
if (env.environment == .development and !jetzig.config.get(bool, "force_development_email_delivery")) {
try env.logger.INFO(
"Skipping mail delivery in development environment:\n{s}",
.{try mail.generateData()},
\\Skipping mail delivery in development environment:
\\To: {?s}
\\{s}
,
.{ mail.params.get(.to), try mail.generateData() },
);
} else {
try mail.deliver();
try env.logger.INFO("Delivered mail to: {s}", .{
try std.mem.join(allocator, ", ", mail.params.to.?),
});
try env.logger.INFO("Delivered mail to: {s}", .{mail.params.to.?});
}
}
@ -69,19 +70,32 @@ 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) ?jetzig.mail.Address {
return if (from) |capture| switch (capture.*) {
.null => null,
.string => |string| string.value,
.string => |string| .{ .email = string.value },
.object => |object| .{
.email = object.getT(.string, "email") orelse return null,
.name = object.getT(.string, "name") orelse return null,
},
else => unreachable,
} else null;
}
fn resolveTo(allocator: std.mem.Allocator, params: *const jetzig.data.Value) !?[]const []const u8 {
var to = std.ArrayList([]const u8).init(allocator);
fn resolveTo(allocator: std.mem.Allocator, params: *const jetzig.data.Value) !?[]const jetzig.mail.Address {
var to = std.ArrayList(jetzig.mail.Address).init(allocator);
if (params.get("to")) |capture| {
for (capture.items(.array)) |recipient| {
try to.append(recipient.string.value);
const maybe_address: ?jetzig.mail.Address = switch (recipient.*) {
.null => null,
.string => |string| .{ .email = string.value },
.object => |object| .{
.email = object.getT(.string, "email") orelse return error.JetzigMissingEmailField,
.name = object.getT(.string, "name"),
},
else => unreachable,
};
if (maybe_address) |address| try to.append(address);
}
}
return if (to.items.len > 0) try to.toOwnedSlice() else null;

View File

@ -30,9 +30,16 @@ pub fn deliver(self: Mail) !void {
const data = try self.generateData();
defer self.allocator.free(data);
const to = try self.allocator.alloc(smtp.Message.Address, self.params.to.?.len);
defer self.allocator.free(to);
for (self.params.to.?, 0..) |address, index| {
to[index] = .{ .address = address.email, .name = address.name };
}
try smtp.send(.{
.from = self.params.from.?,
.to = self.params.to.?,
.from = .{ .address = self.params.from.?.email, .name = self.params.from.?.name },
.to = to,
.data = data,
}, try self.config.toSMTP(self.allocator, self.env));
}
@ -147,8 +154,8 @@ test "HTML part only" {
.config = .{},
.boundary = 123456789,
.params = .{
.from = "user@example.com",
.to = &.{"user@example.com"},
.from = .{ .name = "Bob", .email = "user@example.com" },
.to = &.{.{ .name = "Alice", .email = "user@example.com" }},
.subject = "Test subject",
.html = "<div>Hello</div>",
},
@ -158,7 +165,7 @@ test "HTML part only" {
defer std.testing.allocator.free(actual);
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
\\From: user@example.com
\\From: Bob <user@example.com>
\\Subject: Test subject
\\MIME-Version: 1.0
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
@ -183,8 +190,8 @@ test "text part only" {
.config = .{},
.boundary = 123456789,
.params = .{
.from = "user@example.com",
.to = &.{"user@example.com"},
.from = .{ .name = "Bob", .email = "user@example.com" },
.to = &.{.{ .name = "Alice", .email = "user@example.com" }},
.subject = "Test subject",
.text = "Hello",
},
@ -194,7 +201,7 @@ test "text part only" {
defer std.testing.allocator.free(actual);
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
\\From: user@example.com
\\From: Bob <user@example.com>
\\Subject: Test subject
\\MIME-Version: 1.0
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
@ -219,8 +226,8 @@ test "HTML and text parts" {
.config = .{},
.boundary = 123456789,
.params = .{
.from = "user@example.com",
.to = &.{"user@example.com"},
.from = .{ .name = "Bob", .email = "user@example.com" },
.to = &.{.{ .name = "Alice", .email = "user@example.com" }},
.subject = "Test subject",
.html = "<div>Hello</div>",
.text = "Hello",
@ -231,7 +238,7 @@ test "HTML and text parts" {
defer std.testing.allocator.free(actual);
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
\\From: user@example.com
\\From: Bob <user@example.com>
\\Subject: Test subject
\\MIME-Version: 1.0
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
@ -255,6 +262,41 @@ test "HTML and text parts" {
try std.testing.expectEqualStrings(expected, actual);
}
test "default email address name" {
const mail = Mail{
.allocator = std.testing.allocator,
.env = undefined,
.config = .{},
.boundary = 123456789,
.params = .{
.from = .{ .email = "user@example.com" },
.to = &.{.{ .email = "user@example.com" }},
.subject = "Test subject",
.text = "Hello",
},
};
const actual = try generateData(mail);
defer std.testing.allocator.free(actual);
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
\\From: user@example.com <user@example.com>
\\Subject: Test subject
\\MIME-Version: 1.0
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
\\--=_alternative_123456789
\\Content-Type: text/plain; charset="UTF-8"
\\Content-Transfer-Encoding: quoted-printable
\\
\\Hello
\\
\\.
\\
, "\n", "\r\n");
defer std.testing.allocator.free(expected);
try std.testing.expectEqualStrings(expected, actual);
}
test "long content encoding" {
const mail = Mail{
.allocator = std.testing.allocator,
@ -262,8 +304,8 @@ test "long content encoding" {
.config = .{},
.boundary = 123456789,
.params = .{
.from = "user@example.com",
.to = &.{"user@example.com"},
.from = .{ .name = "Bob", .email = "user@example.com" },
.to = &.{.{ .name = "Alice", .email = "user@example.com" }},
.subject = "Test subject",
.html = "<html><body><div style=\"background-color: black; color: #ff00ff;\">Hellooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo!!!</div></body></html>",
.text = "Hello",
@ -274,7 +316,7 @@ test "long content encoding" {
defer std.testing.allocator.free(actual);
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
\\From: user@example.com
\\From: Bob <user@example.com>
\\Subject: Test subject
\\MIME-Version: 1.0
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
@ -312,8 +354,8 @@ test "non-latin alphabet encoding" {
.config = .{},
.boundary = 123456789,
.params = .{
.from = "user@example.com",
.to = &.{"user@example.com"},
.from = .{ .name = "Bob", .email = "user@example.com" },
.to = &.{.{ .name = "Alice", .email = "user@example.com" }},
.subject = "Test subject",
.html = "<html><body><div>你爱学习 Zig 吗?</div></body></html>",
@ -325,7 +367,7 @@ test "non-latin alphabet encoding" {
defer std.testing.allocator.free(actual);
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
\\From: user@example.com
\\From: Bob <user@example.com>
\\Subject: Test subject
\\MIME-Version: 1.0
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"

View File

@ -1,22 +1,31 @@
subject: ?[]const u8 = null,
from: ?[]const u8 = null,
to: ?[]const []const u8 = null,
cc: ?[]const []const u8 = null,
bcc: ?[]const []const u8 = null, // TODO
from: ?Address = null,
to: ?[]const Address = null,
cc: ?[]const Address = null,
bcc: ?[]const Address = null, // TODO
html: ?[]const u8 = null,
text: ?[]const u8 = null,
defaults: ?DefaultMailParams = null,
pub const DefaultMailParams = struct {
subject: ?[]const u8 = null,
from: ?[]const u8 = null,
to: ?[]const []const u8 = null,
cc: ?[]const []const u8 = null,
bcc: ?[]const []const u8 = null, // TODO
from: ?Address = null,
to: ?[]const Address = null,
cc: ?[]const Address = null,
bcc: ?[]const Address = null, // TODO
html: ?[]const u8 = null,
text: ?[]const u8 = null,
};
pub const Address = struct {
name: ?[]const u8 = null,
email: []const u8,
pub fn format(address: Address, _: anytype, _: anytype, writer: anytype) !void {
try writer.print("{s} <{s}>", .{ address.name orelse address.email, address.email });
}
};
const MailParams = @This();
pub fn get(
@ -24,10 +33,10 @@ pub fn get(
comptime field: enum { subject, from, to, cc, bcc, html, text },
) ?switch (field) {
.subject => []const u8,
.from => []const u8,
.to => []const []const u8,
.cc => []const []const u8,
.bcc => []const []const u8,
.from => Address,
.to => []const Address,
.cc => []const Address,
.bcc => []const Address,
.html => []const u8,
.text => []const u8,
} {

View File

@ -12,7 +12,7 @@ const TokenParams = @Type(.{
.name = jetzig.authenticity_token_name ++ "",
.type = []const u8,
.is_comptime = false,
.default_value = null,
.default_value_ptr = null,
.alignment = @alignOf([]const u8),
}},
},

View File

@ -28,17 +28,17 @@ pub fn beforeResponse(request: *jetzig.http.Request, response: *jetzig.http.Resp
const compressed = switch (encoding) {
.gzip => jetzig.util.gzip(request.allocator, response.content, .{}) catch |err|
return request.server.logger.logError(err),
return request.server.logger.logError(@errorReturnTrace(), err),
.deflate => jetzig.util.deflate(request.allocator, response.content, .{}) catch |err|
return request.server.logger.logError(err),
return request.server.logger.logError(@errorReturnTrace(), err),
};
response.headers.append("Content-Encoding", @tagName(encoding)) catch |err|
return request.server.logger.logError(err);
return request.server.logger.logError(@errorReturnTrace(), err);
// Make caching work
response.headers.append("Vary", "Accept-Encoding") catch |err|
return request.server.logger.logError(err);
return request.server.logger.logError(@errorReturnTrace(), err);
response.content = compressed;
}
@ -48,9 +48,9 @@ fn detectEncoding(request: *const jetzig.http.Request) ?Encoding {
while (headers_it.next()) |header| {
var it = std.mem.tokenizeScalar(u8, header.value, ',');
while (it.next()) |param| {
inline for (@typeInfo(Encoding).Enum.fields) |field| {
inline for (@typeInfo(Encoding).@"enum".fields) |field| {
if (std.mem.eql(u8, field.name, jetzig.util.strip(param))) {
return std.enums.nameCast(Encoding, field.name);
return @field(Encoding, field.name);
}
}
}

View File

@ -8,10 +8,10 @@ const HtmxMiddleware = @This();
/// request doesn't come via htmx and, when the request does come from htmx, only return the
/// content rendered directly by the view function.
pub fn afterRequest(request: *jetzig.http.Request) !void {
if (request.headers.get("HX-Target")) |target| {
if (request.headers.get("HX-Request")) |_| {
try request.server.logger.DEBUG(
"[middleware-htmx] htmx request detected, disabling layout. (#{s})",
.{target},
"[middleware-htmx] HX-Request header, disabling layout.",
.{},
);
request.setLayout(null);
}
@ -19,17 +19,21 @@ pub fn afterRequest(request: *jetzig.http.Request) !void {
/// If a redirect was issued during request processing, reset any response data, set response
/// status to `200 OK` and replace the `Location` header with a `HX-Redirect` header.
/// Add Vary response header to prevent caching the page without layout for requests not coming
/// from htmx.
pub fn beforeResponse(request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
switch (response.status_code) {
.moved_permanently, .found => {},
else => return,
}
if (request.headers.get("HX-Request") == null) return;
if (response.headers.get("Location")) |location| {
response.status_code = .ok;
request.response_data.reset();
try response.headers.append("HX-Redirect", location);
switch (response.status_code) {
.moved_permanently, .found => {
if (response.headers.get("Location")) |location| {
response.status_code = .ok;
request.response_data.reset();
try response.headers.append("HX-Redirect", location);
}
},
else => {
try response.headers.append("Vary", "HX-Request");
},
}
}

View File

@ -55,7 +55,7 @@ pub fn init(allocator: std.mem.Allocator, routes_module: type) !App {
try cookies.parse();
const session = try alloc.create(jetzig.http.Session);
session.* = jetzig.http.Session.init(alloc, cookies, jetzig.testing.secret);
session.* = jetzig.http.Session.init(alloc, cookies, jetzig.testing.secret, .{});
app.* = App{
.arena = arena,
@ -78,6 +78,7 @@ pub fn init(allocator: std.mem.Allocator, routes_module: type) !App {
/// Free allocated resources for test app.
pub fn deinit(self: *App) void {
self.repo.deinit();
self.arena.deinit();
self.allocator.destroy(self.arena);
if (self.logger.test_logger.file) |file| file.close();
@ -236,7 +237,7 @@ pub fn initSession(self: *App) !void {
const allocator = self.arena.allocator();
var local_session = try allocator.create(jetzig.http.Session);
local_session.* = jetzig.http.Session.init(allocator, self.cookies, jetzig.testing.secret);
local_session.* = jetzig.http.Session.init(allocator, self.cookies, jetzig.testing.secret, .{});
try local_session.parse();
self.session = local_session;
@ -346,9 +347,12 @@ fn stubbedRequest(
.route_data = null,
.middlewares = undefined,
.address = undefined,
.method = std.enums.nameCast(httpz.Method, @tagName(method)),
.method = @field(httpz.Method, @tagName(method)),
.protocol = .HTTP11,
.params = undefined,
.conn = undefined,
.method_string = undefined,
.unread_body = undefined,
.headers = request_headers,
.body_buffer = if (options.getBody()) |capture|
.{ .data = @constCast(capture), .type = .static }