Compare commits

..

No commits in common. "main" and "anti-csrf" have entirely different histories.

75 changed files with 702 additions and 1969 deletions

View File

@ -15,20 +15,23 @@ jobs:
build:
strategy:
matrix:
#Deactivated windows for I don't know why it fails
#os: [ubuntu-latest, macos-latest, windows-latest]
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
uses: mlugg/setup-zig@main
# 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
- run: zig version
- run: zig env
@ -42,17 +45,7 @@ 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

@ -1,21 +0,0 @@
The MIT License (Expat)
Copyright (c) Zig contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -6,8 +6,6 @@ _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).
@ -22,7 +20,6 @@ If you are interested in _Jetzig_ you will probably find these tools interesting
* [ZTS](https://github.com/zigster64/zts)
* [Zine](https://github.com/kristoff-it/zine)
* [Zinc](https://github.com/zon-dev/zinc/)
* [zUI](https://github.com/thienpow/zui)
## Checklist
@ -60,5 +57,4 @@ If you are interested in _Jetzig_ you will probably find these tools interesting
* [Zackary Housend](https://github.com/z1fire)
* [Andreas Stührk](https://github.com/Trundle)
* [Karl Seguin](https://github.com/karlseguin)
* [Bob Farrell](https://github.com/bobf)

View File

@ -5,9 +5,6 @@ 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(.{});
@ -39,7 +36,6 @@ 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),
@ -50,11 +46,6 @@ 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 });
@ -67,15 +58,18 @@ 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,
@ -116,7 +110,6 @@ pub fn build(b: *std.Build) !void {
const test_build_options = b.addOptions();
test_build_options.addOption(Environment, "environment", .testing);
test_build_options.addOption(bool, "build_static", true);
test_build_options.addOption(bool, "debug_console", false);
const run_main_tests = b.addRunArtifact(main_tests);
main_tests.root_module.addOptions("build_options", test_build_options);
@ -135,11 +128,9 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
return error.ZmplVersionNotSupported;
}
const target = exe.root_module.resolved_target orelse @panic("Unable to detect compile target.");
const target = b.host;
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(
@ -147,27 +138,18 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
"environment",
"Jetzig server environment.",
) orelse .development;
const build_static = b.option(
bool,
"build_static",
"Pre-render static routes. [default: false in development, true in testing/production]",
) orelse (environment != .development);
const debug_console = b.option(
bool,
"debug_console",
"Render a debug console on error. Default: false.",
) orelse false;
if (debug_console == true and environment != .development) {
std.debug.print("Environment must be `development` when `debug_console` is enabled.", .{});
return error.JetzigBuildError;
}
const jetzig_dep = b.dependency(
"jetzig",
.{ .optimize = optimize, .target = target },
.{
.optimize = optimize,
.target = target,
},
);
const jetquery_dep = jetzig_dep.builder.dependency("jetquery", .{
@ -180,6 +162,7 @@ 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");
@ -188,12 +171,12 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
const build_options = b.addOptions();
build_options.addOption(Environment, "environment", environment);
build_options.addOption(bool, "build_static", build_static);
build_options.addOption(bool, "debug_console", debug_console);
jetzig_module.addOptions("build_options", build_options);
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) {
@ -203,10 +186,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
}
}
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 root_path = b.build_root.path orelse 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" },
@ -229,7 +209,6 @@ 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);
@ -257,7 +236,6 @@ 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") });
@ -326,7 +304,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 = .{ .mode = .simple, .path = jetzig_dep.path("src/test_runner.zig") },
.test_runner = 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);
@ -366,7 +344,6 @@ 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");
@ -375,7 +352,6 @@ 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);
@ -397,7 +373,6 @@ 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);
@ -408,12 +383,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);
exe_routes.root_module.addImport("app", &exe.root_module);
const run_routes_cmd = b.addRunArtifact(exe_routes);
routes_step.dependOn(&run_routes_cmd.step);
}
@ -425,8 +397,6 @@ 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| {
@ -447,7 +417,7 @@ fn generateMarkdownFragments(b: *std.Build) ![]const u8 {
}
};
const stat = try file.stat();
const source = try file.readToEndAllocOptions(b.allocator, @intCast(stat.size), null, .of(u8), 0);
const source = try file.readToEndAllocOptions(b.allocator, @intCast(stat.size), null, @alignOf(u8), 0);
if (try getMarkdownFragmentsSource(b.allocator, source)) |markdown_fragments_source| {
return try std.fmt.allocPrint(b.allocator,
\\const std = @import("std");
@ -468,10 +438,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(@enumFromInt(index));
const decl = ast.simpleVarDecl(@intCast(index));
const identifier = ast.tokenSlice(decl.ast.mut_token + 1);
if (std.mem.eql(u8, identifier, "markdown_fragments")) {
return ast.getNodeSource(@enumFromInt(index));
return ast.getNodeSource(@intCast(index));
}
},
else => continue,

View File

@ -1,40 +1,43 @@
.{
.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",
},
.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/bobf/zig-args/archive/88cbade9a517a4014824f8f53f3c48c8a0b2ffe1.tar.gz",
.hash = "zig_args-0.0.0-jqtN6P_NAAC97fGpk9hS2K681jkiqPsWP6w3ucb_ctGH",
.zmpl = .{
.url = "https://github.com/jetzig-framework/zmpl/archive/af75c8b842c3957eb97b4fc4bc49c7b2243968fa.tar.gz",
.hash = "1220ecac93d295dafd2f034a86f0979f6108d40e5ea1a39e3a2b9977c35147cac684",
},
.jetkv = .{
.url = "https://github.com/jetzig-framework/jetkv/archive/5a94e3bac0a6e291efc9d6534beb2d311671ff17.tar.gz",
.hash = "jetkv-0.0.0-zCv0fmCGAgCyYqwHjk0P5KrYVRew1MJAtbtAcIO-WPpT",
},
.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/37d7cb9819b804ade5f4b974b82f8dd0622225ed.tar.gz",
.hash = "httpz-0.0.0-PNVzrEK4BgBpHQGA2m0RPqPGEjnTdDXHodBwzjYDrmps",
.url = "https://github.com/jetzig-framework/jetkv/archive/2b1130a48979ea2871c8cf6ca89c38b1e7062839.tar.gz",
.hash = "12201d75d73aad5e1c996de4d5ae87a00e58479c8d469bc2eeb5fdeeac8857bc09af",
},
.jetquery = .{
.url = "https://github.com/jetzig-framework/jetquery/archive/e1f969f2e3e0e1ad9cc30d56fde9739aa692fdc3.tar.gz",
.hash = "jetquery-0.0.0-TNf3zo2ABgBgcsIAvJ1Ud2B2zDzrBy9GQ31kKmTYZ7Ya",
.url = "https://github.com/jetzig-framework/jetquery/archive/a31db467c4af1c97bc7c806e1cc1a81a39162954.tar.gz",
.hash = "12203af0466ccc3a9ab57fcdf57c92c57989fa7e827d81bc98d0a5787d65402c73c3",
},
.jetcommon = .{
.url = "https://github.com/jetzig-framework/jetcommon/archive/86f24cfdf2aaa0e8ada4539a6edef882708ced2b.tar.gz",
.hash = "12200439fc28aa7fa08f0e8fea100f6724c34c9dbfaaae4feec482c80e5ac08ea4f6",
},
.args = .{
.url = "https://github.com/ikskuh/zig-args/archive/0abdd6947a70e6d8cc83b66228cea614aa856206.tar.gz",
.hash = "1220411a8c46d95bbf3b6e2059854bcb3c5159d428814099df5294232b9980517e9c",
},
.pg = .{
.url = "https://github.com/karlseguin/pg.zig/archive/9226a0a256cd000eee2f9652c8c03af8dcbed79b.tar.gz",
.hash = "12202b30ebf018ca398f665e51cae6d000fdfe2d08d5ec369e3110f762c548b154a4",
},
.smtp_client = .{
// Pending https://github.com/karlseguin/smtp_client.zig/pull/9 before using mainline
.url = "https://github.com/bobf/smtp_client.zig/archive/945554f22f025ba8a1efd01e56bda427bb4e22ca.tar.gz",
.hash = "1220de146446d0cae4396e346cb8283dd5e086491f8577ddbd5e03ad0928111d8bc6",
},
.httpz = .{
.url = "https://github.com/karlseguin/http.zig/archive/da9e944de0be6e5c67ca711dd238ce82d81558b4.tar.gz",
.hash = "12201df692f62d526fdf94e6000cf8de2142edf27484887e2e8f1ec5db4c9b808e5c",
},
},

View File

@ -1,17 +1,16 @@
.{
.name = .jetzig_cli,
.fingerprint = 0x73894a3e0616c96a,
.name = "jetzig-cli",
.version = "0.0.0",
.minimum_zig_version = "0.12.0",
.dependencies = .{
.args = .{
.url = "https://github.com/bobf/zig-args/archive/88cbade9a517a4014824f8f53f3c48c8a0b2ffe1.tar.gz",
.hash = "zig_args-0.0.0-jqtN6P_NAAC97fGpk9hS2K681jkiqPsWP6w3ucb_ctGH",
.url = "https://github.com/ikskuh/zig-args/archive/0abdd6947a70e6d8cc83b66228cea614aa856206.tar.gz",
.hash = "1220411a8c46d95bbf3b6e2059854bcb3c5159d428814099df5294232b9980517e9c",
},
.jetquery = .{
.url = "https://github.com/jetzig-framework/jetquery/archive/e1f969f2e3e0e1ad9cc30d56fde9739aa692fdc3.tar.gz",
.hash = "jetquery-0.0.0-TNf3zo2ABgBgcsIAvJ1Ud2B2zDzrBy9GQ31kKmTYZ7Ya",
.url = "https://github.com/jetzig-framework/jetquery/archive/a31db467c4af1c97bc7c806e1cc1a81a39162954.tar.gz",
.hash = "12203af0466ccc3a9ab57fcdf57c92c57989fa7e827d81bc98d0a5787d65402c73c3",
},
},
.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(null);
util.printSuccess();
}
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

@ -39,11 +39,6 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he
};
try mailer_file.writeAll(
\\const std = @import("std");
\\const jetzig = @import("jetzig");
\\
\\
\\
\\// The `deliver` function is invoked every time this mailer is used to send an email.
\\// Use this function to set default mail params (e.g. a default `from` address or
\\// `subject`) before the mail is delivered.
@ -56,16 +51,21 @@ 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 various information about the environment. See `jetzig.jobs.JobEnv`.
\\// * env: Provides the following fields:
\\// - logger: Logger attached to the same stream as the Jetzig server.
\\// - environment: Enum of `{ production, development }`.
\\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

@ -43,7 +43,7 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he
const action_args = if (args.len > 1)
args[1..]
else
&[_][]const u8{ "index", "get", "new", "edit", "post", "put", "patch", "delete" };
&[_][]const u8{ "index", "get", "new", "post", "put", "patch", "delete" };
var actions = std.ArrayList(Action).init(allocator);
defer actions.deinit();
@ -92,7 +92,7 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he
std.debug.print("Generated view: {s}\n", .{realpath});
}
const Method = enum { index, get, new, edit, post, put, patch, delete };
const Method = enum { index, get, new, post, put, patch, delete };
const Action = struct {
method: Method,
static: bool,
@ -126,15 +126,15 @@ fn writeAction(allocator: std.mem.Allocator, writer: anytype, action: Action) !v
@tagName(action.method),
switch (action.method) {
.index, .post, .new => "",
.get, .edit, .put, .patch, .delete => "id: []const u8, ",
.get, .put, .patch, .delete => "id: []const u8, ",
},
if (action.static) "StaticRequest" else "Request",
switch (action.method) {
.index, .post, .new => "",
.get, .edit, .put, .patch, .delete => "_ = id;\n ",
.get, .put, .patch, .delete => "_ = id;\n ",
},
switch (action.method) {
.index, .get, .edit, .new => ".ok",
.index, .get, .new => ".ok",
.post => ".created",
.put, .patch, .delete => ".ok",
},
@ -164,18 +164,17 @@ fn writeTest(allocator: std.mem.Allocator, writer: anytype, name: []const u8, ac
.{
@tagName(action.method),
switch (action.method) {
.index, .get, .edit, .new => "GET",
.index, .get, .new => "GET",
.put, .patch, .delete, .post => action_upper,
},
name,
switch (action.method) {
.index, .post => "",
.edit => "/example-id/edit",
.new => "/new",
.get, .put, .patch, .delete => "/example-id",
},
switch (action.method) {
.index, .get, .new, .edit => ".ok",
.index, .get, .new => ".ok",
.post => ".created",
.put, .patch, .delete => ".ok",
},
@ -209,7 +208,7 @@ fn writeStaticParams(allocator: std.mem.Allocator, actions: []Action, writer: an
defer allocator.free(output);
try writer.writeAll(output);
},
.get, .put, .patch, .delete, .edit => {
.get, .put, .patch, .delete => {
const output = try std.fmt.allocPrint(
allocator,
\\ .{s} = .{{

View File

@ -3,6 +3,7 @@ const args = @import("args");
const util = @import("../util.zig");
const cli = @import("../cli.zig");
const init_data = @import("init_data").init_data;
/// Command line options for the `init` command.
@ -106,7 +107,7 @@ pub fn run(
try copySourceFile(
allocator,
install_dir,
"demo/config/database_template.zig",
"demo/config/database.zig",
"config/database.zig",
null,
);
@ -202,14 +203,13 @@ pub fn run(
github_url,
},
.{ .dir = install_dir },
.{},
);
// TODO: Use arg or interactive prompt to do Git setup in net project, default to no.
// const git_setup = false;
// if (git_setup) try gitSetup(allocator, install_dir);
try util.unicodePrint(
std.debug.print(
\\
\\Setup complete! ✈️ 🦎
\\
@ -217,7 +217,7 @@ pub fn run(
\\
\\ $ cd {s}
\\
\\ $ zig build run or jetzig server
\\ $ zig build run
\\
\\And then browse to http://localhost:8080/
\\
@ -261,7 +261,7 @@ fn copySourceFile(
util.printFailure();
return err;
};
util.printSuccess(null);
util.printSuccess();
}
// Read a file from Jetzig source code.
@ -334,7 +334,6 @@ fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void {
".",
},
.{ .path = install_dir },
.{},
);
try util.runCommandInDir(
@ -345,7 +344,6 @@ fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void {
".",
},
.{ .path = install_dir },
.{},
);
try util.runCommandInDir(
@ -357,6 +355,5 @@ fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void {
"Initialize Jetzig project",
},
.{ .path = install_dir },
.{},
);
}

View File

@ -9,20 +9,22 @@ pub const watch_changes_pause_duration = 1 * 1000 * 1000 * 1000;
/// Command line options for the `server` command.
pub const Options = struct {
reload: bool = true,
debug: bool = true,
pub const meta = .{
.full_text =
\\Launches a development server.
\\
\\The development server reloads when files in `src/` are updated.
\\
\\To disable this behaviour, pass `--reload=false`
\\
\\Example:
\\
\\ jetzig server
\\ jetzig server --reload=false --debug=false
\\ jetzig server --reload=false
,
.option_docs = .{
.reload = "Enable or disable automatic reload on update (default: true)",
.debug = "Enable or disable the development debug console (default: true)",
},
};
};
@ -60,25 +62,20 @@ pub fn run(
},
);
var argv = std.ArrayList([]const u8).init(allocator);
defer argv.deinit();
try argv.appendSlice(&.{
"zig",
"build",
util.environmentBuildOption(main_options.options.environment),
"-Djetzig_runner=true",
});
if (options.debug) try argv.append("-Ddebug_console=true");
try argv.appendSlice(&.{
"install",
"--color",
"on",
});
while (true) {
util.runCommandInDir(allocator, argv.items, .{ .path = realpath }, .{}) catch {
util.runCommandInDir(
allocator,
&.{
"zig",
"build",
util.environmentBuildOption(main_options.options.environment),
"-Djetzig_runner=true",
"install",
"--color",
"on",
},
.{ .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", .{});
@ -91,9 +88,10 @@ pub fn run(
std.process.exit(1);
}
const argv = &[_][]const u8{exe_path.?};
defer allocator.free(exe_path.?);
var process = std.process.Child.init(&.{exe_path.?}, allocator);
var process = std.process.Child.init(argv, allocator);
process.stdin_behavior = .Inherit;
process.stdout_behavior = .Inherit;
process.stderr_behavior = .Inherit;

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_template.zig",
"demo/config/database.zig",
".gitignore",
};

View File

@ -23,8 +23,8 @@ const icons = .{
};
/// Print a success confirmation.
pub fn printSuccess(message: ?[]const u8) void {
std.debug.print(" " ++ icons.check ++ " {s}\n", .{message orelse ""});
pub fn printSuccess() void {
std.debug.print(" " ++ icons.check ++ "\n", .{});
}
/// 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,118 +161,48 @@ 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, options: RunOptions) !void {
pub fn runCommandInDir(allocator: std.mem.Allocator, argv: []const []const u8, dir: Dir) !void {
const cwd_path = switch (dir) {
.path => |capture| capture,
.dir => |capture| try capture.realpathAlloc(allocator, "."),
};
defer if (dir == .dir) allocator.free(cwd_path);
const 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),
};
const result = try std.process.Child.run(.{
.allocator = allocator,
.argv = argv,
.cwd = cwd_path,
});
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();
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});
}
std.debug.print(
\\
\\Error running command: {s}
\\
\\[stdout]:
\\
\\{s}
\\
\\[stderr]:
\\
\\{s}
\\
, .{ command, result.stdout, result.stderr });
return error.JetzigCommandError;
} else {
const message = try std.mem.join(allocator, " ", argv);
defer allocator.free(message);
printSuccess(message);
printSuccess();
}
}
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 };
@ -359,35 +289,3 @@ pub fn environmentBuildOption(environment: cli.Environment) []const u8 {
inline else => |tag| "-Denvironment=" ++ @tagName(tag),
};
}
pub fn unicodePrint(comptime fmt: []const u8, args: anytype) !void {
if (builtin.os.tag == .windows) {
// Windows-specific code
const cp_out = try UTF8ConsoleOutput.init();
defer cp_out.deinit();
std.debug.print(comptime fmt, args);
} else {
// Non-Windows platforms just print normally
std.debug.print(fmt, args);
}
}
const UTF8ConsoleOutput = struct {
original: c_uint,
fn init() !UTF8ConsoleOutput {
const original = std.os.windows.kernel32.GetConsoleOutputCP();
if (original == 0) {
return error.FailedToGetConsoleOutputCP;
}
const result = std.os.windows.kernel32.SetConsoleOutputCP(65001); // UTF-8 code page
if (result == 0) {
return error.FailedToSetConsoleOutputCP;
}
return .{ .original = original };
}
fn deinit(self: UTF8ConsoleOutput) void {
_ = std.os.windows.kernel32.SetConsoleOutputCP(self.original);
}
};

1
demo/.gitignore vendored
View File

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

View File

@ -1,44 +0,0 @@
# 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,8 +1,7 @@
.{
.name = .jetzig_demo,
.name = "jetzig-demo",
.version = "0.0.0",
.minimum_zig_version = "0.12.0",
.fingerprint = 0x3877c19710a92a5c,
.dependencies = .{
.jetzig = .{
.path = "../",

View File

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

View File

@ -1,16 +1,48 @@
pub const database = .{
// Null adapter fails when a database call is invoked.
.development = .{
.adapter = .postgresql,
.username = "postgres",
.password = "postgres",
.hostname = "localhost",
.database = "jetzig_demo_dev",
.port = 14173, // See `compose.yml`
.adapter = .null,
},
// This configuration is used for CI
// in GitHub
.testing = .{
.adapter = .postgresql,
.database = "jetzig_demo_test",
.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

@ -1,48 +0,0 @@
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

@ -1,14 +0,0 @@
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: anytype) !void {
pub fn up(repo: *jetquery.Repo) !void {
_ = repo;
}
pub fn down(repo: anytype) !void {
pub fn down(repo: *jetquery.Repo) !void {
_ = repo;
}

View File

@ -1,20 +0,0 @@
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 = .{ .email = "bob@jetzig.dev" },
.to = &.{.{ .email = "bob@jetzig.dev" }},
.from = "bob@jetzig.dev",
.to = &.{"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 = .{ .email = "no-reply@example.com" },
.from = "no-reply@example.com",
.subject = "Default subject",
};

View File

@ -1,12 +1,7 @@
const std = @import("std");
const jetzig = @import("jetzig");
// Anti-CSRF middleware can be included in the view's `actions` declaration to apply CSRF
// protection just to this specific view, or it can be added to your application's global
// middleware stack defined in `jetzig_options` in `src/main.zig`.
//
// Use `{{context.authenticityToken()}}` or `{{context.authenticityFormField()}}` in a Zmpl
// template to generate a token, store it in the user's session, and inject it into the page.
pub const layout = "application";
pub const actions = .{
.before = .{jetzig.middleware.AntiCsrfMiddleware},

View File

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

View File

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

View File

@ -1,61 +0,0 @@
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

@ -1,7 +0,0 @@
<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 = &.{.{ .email = "hello@jetzig.dev" }} });
const mail = request.mail("welcome", .{ .to = &.{"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

@ -1,15 +0,0 @@
const std = @import("std");
const jetzig = @import("jetzig");
pub fn index(request: *jetzig.Request) !jetzig.View {
return request.renderTemplate("basic/index", .ok);
}
test "index" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
const response = try app.request(.GET, "/render_template", .{});
try response.expectStatus(.ok);
try response.expectBodyContains("Hello");
}

View File

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

View File

@ -5,8 +5,8 @@ const importedFunction = @import("../lib/example.zig").exampleFunction;
pub const layout = "application";
pub fn index(request: *jetzig.Request) !jetzig.View {
var root = try request.data(.object);
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
var root = try 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));
@ -16,12 +16,6 @@ pub fn index(request: *jetzig.Request) !jetzig.View {
return request.render(.ok);
}
pub fn edit(id: []const u8, request: *jetzig.Request) !jetzig.View {
var root = try request.data(.object);
try root.put("id", id);
return request.render(.ok);
}
fn customFunction(a: i32, b: i32, c: i32) i32 {
return a + b + c;
}

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

@ -15,11 +15,6 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
return request.render(.ok);
}
pub fn edit(id: []const u8, request: *jetzig.Request) !jetzig.View {
try request.server.logger.INFO("id: {s}", .{id});
return request.render(.ok);
}
pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
_ = data;
const params = try request.params();

View File

@ -87,61 +87,49 @@ pub const jetzig_options = struct {
.path = "/",
},
.production => .{
.same_site = .lax,
.same_site = true,
.secure = true,
.http_only = true,
.path = "/",
},
};
/// Key-value store options.
/// Available backends:
/// * memory: Simple, in-memory hashmap-backed store.
/// * file: Rudimentary file-backed store.
/// * valkey: Valkey-backed store with connection pool.
///
/// When using `.file` or `.valkey` backend, you must also set `.file_options` or
/// `.valkey_options` respectively.
///
/// ## File backend:
// .backend = .file,
// .file_options = .{
// .path = "/path/to/jetkv-store.db",
// .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096),
// },
//
// ## Valkey backend
// .backend = .valkey,
// .valkey_options = .{
// .host = "localhost",
// .port = 6379,
// .timeout = 1000, // in milliseconds, i.e. 1 second.
// .connect = .lazy, // Connect on first use, or `auto` to connect on server startup.
// .buffer_size = 8192,
// .pool_size = 8,
// },
/// Available configuration options for `store`, `job_queue`, and `cache` are identical.
///
/// For production deployment, the `valkey` backend is recommended for all use cases.
///
/// The general-purpose key-value store is exposed as `request.store` in views and is also
/// available in as `env.store` in all jobs/mailers.
pub const store: jetzig.kv.Store.Options = .{
/// Key-value store options. Set backend to `.file` to use a file-based store.
/// When using `.file` backend, you must also set `.file_options`.
/// The key-value store is exposed as `request.store` in views and is also available in as
/// `env.store` in all jobs/mailers.
pub const store: jetzig.kv.Store.KVOptions = .{
.backend = .memory,
// .backend = .file,
// .file_options = .{
// .path = "/path/to/jetkv-store.db",
// .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096),
// },
};
/// Job queue options. Identical to `store` options, but allows using different
/// backends (e.g. `.memory` for key-value store, `.file` for jobs queue.
/// The job queue is managed internally by Jetzig.
pub const job_queue: jetzig.kv.Store.Options = .{
pub const job_queue: jetzig.kv.Store.KVOptions = .{
.backend = .memory,
// .backend = .file,
// .file_options = .{
// .path = "/path/to/jetkv-queue.db",
// .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096),
// },
};
/// Cache options. Identical to `store` options, but allows using different
/// backends (e.g. `.memory` for key-value store, `.file` for cache.
pub const cache: jetzig.kv.Store.Options = .{
pub const cache: jetzig.kv.Store.KVOptions = .{
.backend = .memory,
// .backend = .file,
// .file_options = .{
// .path = "/path/to/jetkv-cache.db",
// .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096),
// },
};
/// SMTP configuration for Jetzig Mail. It is recommended to use a local SMTP relay,

View File

@ -54,20 +54,10 @@ const Function = struct {
defer self.routes.allocator.free(relative_path);
const path = relative_path[0 .. relative_path.len - std.fs.path.extension(relative_path).len];
const is_root = std.mem.eql(u8, path, "root");
const is_new = std.mem.eql(u8, self.name, "new");
const is_edit = std.mem.eql(u8, self.name, "edit");
if (is_root) {
if (is_edit) return try self.routes.allocator.dupe(u8, "/edit");
if (is_new) return try self.routes.allocator.dupe(u8, "/new");
return try self.routes.allocator.dupe(u8, "/");
}
if (std.mem.eql(u8, path, "root")) return try self.routes.allocator.dupe(u8, "/");
const maybe_new = if (is_new) ("/new") else "";
// jetzig.http.Path.actionPath translates `/foo/bar/1/edit` to `/foo/bar/edit`
const maybe_edit = if (is_edit) ("/edit") else "";
return try std.mem.concat(self.routes.allocator, u8, &[_][]const u8{ "/", path, maybe_new, maybe_edit });
const maybe_new = if (std.mem.eql(u8, self.name, "new")) "/new" else "";
return try std.mem.concat(self.routes.allocator, u8, &[_][]const u8{ "/", path, maybe_new });
}
pub fn lessThanFn(context: void, lhs: Function, rhs: Function) bool {
@ -315,7 +305,6 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
.{ "index", false },
.{ "post", false },
.{ "new", false },
.{ "edit", true },
.{ "get", true },
.{ "edit", true },
.{ "put", true },
@ -376,7 +365,7 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
path,
@intCast(stat.size),
null,
.of(u8),
@alignOf(u8),
0,
);
defer self.allocator.free(source);
@ -390,7 +379,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, @enumFromInt(index), path, source);
var function = try self.parseFunction(function_tag, index, path, source);
if (function) |*capture| {
if (capture.args.len == 0) {
std.debug.print(
@ -414,7 +403,7 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
}
},
.simple_var_decl => {
const decl = self.ast.simpleVarDecl(@enumFromInt(index));
const decl = self.ast.simpleVarDecl(asNodeIndex(index));
if (self.isStaticParamsDecl(decl)) {
self.data.reset();
const params = try self.data.root(.object);
@ -452,11 +441,10 @@ 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 = decl.ast.init_node.unwrap() orelse return;
switch (self.ast.nodeTag(init_node)) {
const init_node = self.ast.nodes.items(.tag)[decl.ast.init_node];
switch (init_node) {
.struct_init_dot_two, .struct_init_dot_two_comma => {
try self.parseStruct(init_node, params);
try self.parseStruct(decl.ast.init_node, params);
},
else => return,
}
@ -489,14 +477,14 @@ fn parseArray(self: *Routes, node: std.zig.Ast.Node.Index, params: *jetzig.data.
const array = maybe_array.?;
const main_token = self.ast.nodeMainToken(node);
const main_token = self.ast.nodes.items(.main_token)[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.nodeTag(element);
const elem = self.ast.nodes.items(.tag)[element];
switch (elem) {
.struct_init_dot, .struct_init_dot_two, .struct_init_dot_two_comma => {
const route_params = try self.data.object();
@ -509,20 +497,19 @@ 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.nodeMainToken(element);
const string_token = self.ast.nodes.items(.main_token)[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.nodeMainToken(element);
const number_token = self.ast.nodes.items(.main_token)[element];
const number_value = self.ast.tokenSlice(number_token);
try params_array.append(try parseNumber(number_value, self.data));
},
inline else => {
@setEvalBranchQuota(10_000);
const tag = self.ast.nodeTag(element);
const tag = self.ast.nodes.items(.tag)[element];
std.debug.print("Unexpected token: {}\n", .{tag});
return error.JetzigStaticParamsParseError;
},
@ -532,21 +519,22 @@ 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 {
switch (self.ast.nodeTag(node)) {
const tag = self.ast.nodes.items(.tag)[node];
switch (tag) {
// 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.nodeMainToken(node);
const main_token = self.ast.nodes.items(.main_token)[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.nodeMainToken(node);
const main_token = self.ast.nodes.items(.main_token)[node];
const field_name = self.ast.tokenSlice(main_token - 2);
const field_value = self.ast.tokenSlice(main_token);
@ -557,13 +545,13 @@ fn parseField(self: *Routes, node: std.zig.Ast.Node.Index, params: *jetzig.data.
);
},
.number_literal => {
const main_token = self.ast.nodeMainToken(node);
const main_token = self.ast.nodes.items(.main_token)[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 => |tag| {
else => {
std.debug.print("Unexpected token: {}\n", .{tag});
return error.JetzigStaticParamsParseError;
},
@ -594,16 +582,16 @@ fn isStaticParamsDecl(self: *Routes, decl: std.zig.Ast.full.VarDecl) bool {
fn parseFunction(
self: *Routes,
function_type: std.zig.Ast.Node.Tag,
index: std.zig.Ast.Node.Index,
index: usize,
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(index),
.fn_proto_one => self.ast.fnProtoOne(&buf, index),
.fn_proto_simple => self.ast.fnProtoSimple(&buf, index),
.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))),
else => unreachable,
};
if (fn_proto.name_token) |token| {
@ -620,7 +608,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(@intFromEnum(arg.type_expr.?));
const node = self.ast.nodes.get(arg.type_expr);
const type_name = try self.parseTypeExpr(node);
try args.append(.{ .name = arg_name, .type_name = type_name });
}
@ -666,6 +654,10 @@ 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

@ -1,121 +0,0 @@
/* reset https://www.joshwcomeau.com/css/custom-css-reset/ */
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
p {
text-wrap: pretty;
}
h1, h2, h3, h4, h5, h6 {
text-wrap: balance;
}
#root, #__next {
isolation: isolate;
}
/* styles */
body {
background-color: #222;
}
h1 {
font-family: monospace;
color: #e55;
padding: 1rem;
font-size: 1.6rem;
}
h2 {
font-family: monospace;
color: #a0a0a0;
padding: 1rem;
font-size: 1.6rem;
}
.stack-trace {
/* background-color: #e555; */
padding: 1rem;
font-family: monospace;
}
.stack-trace .stack-source-line .file-name {
color: #90bfd7;
display: block;
padding: 0.4rem;
font-weight: bold;
}
.stack-trace .stack-source-line {
background-color: #333;
padding: 1rem;
margin: 0;
border-bottom: 1px solid #ffa3;
}
.stack-trace .stack-source-line:last-child {
border-bottom: none;
}
.stack-trace .stack-source-line .line-content {
margin: 0;
padding: 0;
}
.stack-trace .stack-source-line .line-content.surrounding {
color: #ffa;
}
.stack-trace .stack-source-line .line-content.target {
color: #faa;
background-color: #e552;
}
.stack-trace .stack-source-line .line-content .line-number {
display: inline;
}
pre {
display: inline;
margin-right: 1rem;
}
.response-data {
color: #fff;
background-color: #333;
padding: 1rem;
margin: 1rem;
}
/* PrismJS 1.29.0
https://prismjs.com/download.html#themes=prism-tomorrow&languages=json+zig */
code[class*=language-],pre[class*=language-]{color:#ccc;font-family:monospace;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]:not(pre)>code[class*=language-]{white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
.stack-trace .stack-source-line .line-number {
color: #ffa !important;
}

File diff suppressed because one or more lines are too long

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,
@field(jetzig.jetquery.Environment, @tagName(jetzig.environment)),
std.enums.nameCast(jetzig.jetquery.Environment, 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(@field(std.meta.DeclEnum(Schema), model), .{
try repo.insert(std.enums.nameCast(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, setup, update };
const Action = enum { migrate, rollback, create, drop, reflect };
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
@ -46,8 +46,6 @@ pub fn main() !void {
.{ "create", .create },
.{ "drop", .drop },
.{ "reflect", .reflect },
.{ "setup", .setup },
.{ "update", .update },
});
const action = map.get(args[1]) orelse return error.JetzigUnrecognizedArgument;
@ -79,25 +77,6 @@ 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| {
@ -124,7 +103,33 @@ pub fn main() !void {
}
},
.reflect => {
try reflectSchema(allocator, repo_env);
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});
},
}
}
@ -133,46 +138,15 @@ const MigrationsRepo = jetquery.Repo(config.adapter, MigrateSchema);
fn migrationsRepo(action: Action, allocator: std.mem.Allocator, repo_env: anytype) !MigrationsRepo {
return try MigrationsRepo.loadConfig(
allocator,
@field(jetquery.Environment, @tagName(environment)),
std.enums.nameCast(jetquery.Environment, environment),
.{
.admin = switch (action) {
.migrate, .rollback, .update => false,
.migrate, .rollback => 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

@ -24,10 +24,9 @@ pub fn main() !void {
.get => jetzig.colors.cyan("{s: <7}"),
.index => jetzig.colors.blue("{s: <7}"),
.new => jetzig.colors.green("{s: <7}"),
.edit => jetzig.colors.bold(.yellow, "{s: <7}"),
.post => jetzig.colors.yellow("{s: <7}"),
.put => jetzig.colors.magenta("{s: <7}"),
.patch => jetzig.colors.bold(.magenta, "{s: <7}"),
.patch => jetzig.colors.bright_magenta("{s: <7}"),
.delete => jetzig.colors.red("{s: <7}"),
.custom => unreachable,
};
@ -37,7 +36,6 @@ pub fn main() !void {
route.uri_path ++ switch (route.action) {
.index, .post => "",
.new => "/new",
.edit => "/:id/edit",
.get, .put, .patch, .delete => "/:id",
.custom => "",
},
@ -56,7 +54,7 @@ pub fn main() !void {
for (jetzig_app.custom_routes.items) |route| {
log(
" " ++ jetzig.colors.bold(.white, "{s: <7}") ++ " " ++ padded_path ++ " {s}:{s}",
" " ++ jetzig.colors.bold(jetzig.colors.white("{s: <7}")) ++ " " ++ padded_path ++ " {s}:{s}",
.{ route.name, route.uri_path, route.view_name, route.name },
);
}

View File

@ -23,7 +23,6 @@ pub const testing = @import("jetzig/testing.zig");
pub const config = @import("jetzig/config.zig");
pub const auth = @import("jetzig/auth.zig");
pub const callbacks = @import("jetzig/callbacks.zig");
pub const debug = @import("jetzig/debug.zig");
pub const TemplateContext = @import("jetzig/TemplateContext.zig");
pub const DateTime = jetcommon.types.DateTime;
@ -33,10 +32,7 @@ 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 = @field(
Environment.EnvironmentName,
@tagName(build_options.environment),
);
pub const environment = std.enums.nameCast(Environment.EnvironmentName, 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,7 +24,8 @@ const AppOptions = struct {
};
/// Starts an application. `routes` should be `@import("routes").routes`, a generated file
/// automatically created at build time.
/// automatically created at build time. `templates` should be
/// `@import("src/app/views/zmpl.manifest.zig").templates`, created by Zmpl at compile time.
pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
defer self.env.deinit();
@ -34,7 +35,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, if (@hasDecl(routes_module, "routes")) &routes_module.routes else &.{});
const routes = try createRoutes(self.allocator, &routes_module.routes);
defer {
for (routes) |var_route| {
var_route.deinitParams();
@ -47,13 +48,22 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
self.allocator.free(custom_route.template);
};
var store = try jetzig.kv.Store.GeneralStore.init(self.allocator, self.env.logger, .general);
var store = try jetzig.kv.Store.init(
self.allocator,
jetzig.config.get(jetzig.kv.Store.KVOptions, "store"),
);
defer store.deinit();
var job_queue = try jetzig.kv.Store.JobQueueStore.init(self.allocator, self.env.logger, .jobs);
var job_queue = try jetzig.kv.Store.init(
self.allocator,
jetzig.config.get(jetzig.kv.Store.KVOptions, "job_queue"),
);
defer job_queue.deinit();
var cache = try jetzig.kv.Store.CacheStore.init(self.allocator, self.env.logger, .cache);
var cache = try jetzig.kv.Store.init(
self.allocator,
jetzig.config.get(jetzig.kv.Store.KVOptions, "cache"),
);
defer cache.deinit();
var repo = try jetzig.database.repo(self.allocator, self);
@ -62,7 +72,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, jetzig.loggers.LogQueue.Reader.PublishOptions{} },
.{ &self.env.log_queue.reader, .{} },
);
defer log_thread.join();
@ -86,8 +96,8 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
self.env,
routes,
self.custom_routes.items,
if (@hasDecl(routes_module, "jobs")) &routes_module.jobs else &.{},
if (@hasDecl(routes_module, "jobs")) &routes_module.mailers else &.{},
&routes_module.jobs,
&routes_module.mailers,
&mime_map,
&store,
&job_queue,
@ -106,11 +116,10 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
.vars = self.env.vars,
.environment = self.env.environment,
.routes = routes,
.jobs = if (@hasDecl(routes_module, "jobs")) &routes_module.jobs else &.{},
.mailers = if (@hasDecl(routes_module, "jobs")) &routes_module.mailers else &.{},
.jobs = &routes_module.jobs,
.mailers = &routes_module.mailers,
.store = &store,
.cache = &cache,
.repo = &repo,
.mutex = &mutex,
},
);
@ -131,7 +140,7 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
return;
},
else => {
try server.logger.ERROR("Encountered error at server launch: {}\nExiting.\n", .{err});
try server.logger.ERROR("Encountered error: {}\nExiting.\n", .{err});
std.process.exit(1);
},
}
@ -202,7 +211,7 @@ inline fn viewType(path: []const u8) enum { with_id, without_id, with_args } {
pub fn createRoutes(
allocator: std.mem.Allocator,
comptime_routes: []const jetzig.views.Route,
) ![]const *const jetzig.views.Route {
) ![]*jetzig.views.Route {
var routes = std.ArrayList(*jetzig.views.Route).init(allocator);
for (comptime_routes) |const_route| {

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.file,
.stderr = stderr.file,
.stdout = stdout,
.stderr = stderr,
.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.file,
stderr.file,
stdout,
stderr,
),
},
.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, jetzig.environment);
const secret_value = try getSecret(allocator, launch_logger, secret_len, jetzig.environment);
const secret = if (secret_value.len > secret_len) secret_value[0..secret_len] else secret_value;
if (secret.len != secret_len) {
@ -304,31 +304,29 @@ pub fn deinit(self: Environment) void {
self.parent_allocator.destroy(self.arena);
}
fn getLogFile(stream: enum { stdout, stderr }, options: Options) !jetzig.loggers.LogFile {
fn getLogFile(stream: enum { stdout, stderr }, options: Options) !std.fs.File {
const path = switch (stream) {
.stdout => options.log,
.stderr => options.@"log-error",
};
if (std.mem.eql(u8, path, "-")) return switch (stream) {
.stdout => .{ .file = std.io.getStdOut(), .sync = false },
.stdout => std.io.getStdOut(),
.stderr => if (std.mem.eql(u8, options.log, "-"))
.{ .file = std.io.getStdErr(), .sync = false }
std.io.getStdErr()
else
.{
.file = try std.fs.createFileAbsolute(options.log, .{ .truncate = false }),
.sync = true,
},
try std.fs.createFileAbsolute(options.log, .{ .truncate = false }),
};
const file = try std.fs.createFileAbsolute(path, .{ .truncate = false });
try file.seekFromEnd(0);
return .{ .file = file, .sync = true };
return file;
}
fn getSecret(
allocator: std.mem.Allocator,
logger: LaunchLogger,
comptime len: u10,
environment: EnvironmentName,
) ![]const u8 {
const env_var = "JETZIG_SECRET";
@ -350,11 +348,10 @@ fn getSecret(
std.process.exit(1);
}
const secret = "jetzig-development-cookie-secret";
const secret = try jetzig.util.generateSecret(allocator, len);
try logger.log(
.WARN,
"Running in {s} mode, using default development cookie encryption key: `{s}`",
"Running in {s} mode, using auto-generated 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(@field(jetzig.data.ValueType, @tagName(id_type)), "_jetzig_user_id");
return session.getT(std.enums.nameCast(jetzig.data.ValueType, id_type), "_jetzig_user_id");
}
pub fn signIn(request: *jetzig.Request, user_id: anytype) !void {
@ -42,16 +42,13 @@ pub fn verifyPassword(
}
pub fn hashPassword(allocator: std.mem.Allocator, password: []const u8) ![]const u8 {
var buf: [128]u8 = undefined;
const hash = try std.crypto.pwhash.argon2.strHash(
const buf = try allocator.alloc(u8, 128);
return 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

@ -5,25 +5,6 @@ const builtin = @import("builtin");
const types = @import("types.zig");
const jetzig = @import("../jetzig.zig");
pub const Color = enum {
black,
red,
green,
yellow,
blue,
magenta,
cyan,
white,
bright_black,
bright_red,
bright_green,
bright_yellow,
bright_blue,
bright_magenta,
bright_cyan,
bright_white,
};
// Must be consistent with `std.io.tty.Color` for Windows compatibility.
pub const codes = .{
.escape = "\x1b[",
@ -72,7 +53,12 @@ const ansi_colors = .{
.{ "2", .dim },
.{ "0", .reset },
};
pub const codes_map = std.StaticStringMap(std.io.tty.Color).initComptime(ansi_colors);
pub const codes_map = if (@hasDecl(std, "ComptimeStringMap"))
std.ComptimeStringMap(std.io.tty.Color, ansi_colors)
else if (@hasDecl(std, "StaticStringMap"))
std.StaticStringMap(std.io.tty.Color).initComptime(ansi_colors)
else
unreachable;
// Map basic ANSI color codes to Windows TextAttribute colors
// used by std.os.windows.SetConsoleTextAttribute()
@ -97,7 +83,12 @@ const windows_colors = .{
.{ "2", 7 },
.{ "0", 7 },
};
pub const windows_map = std.StaticStringMap(u16).initComptime(windows_colors);
pub const windows_map = if (@hasDecl(std, "ComptimeStringMap"))
std.ComptimeStringMap(u16, windows_colors)
else if (@hasDecl(std, "StaticStringMap"))
std.StaticStringMap(u16).initComptime(windows_colors)
else
unreachable;
/// Colorize a log message. Note that we force `.escape_codes` when we are a TTY even on Windows.
/// `jetzig.loggers.LogQueue` parses the ANSI codes and uses `std.io.tty.Config.setColor` to
@ -131,8 +122,8 @@ fn runtimeWrap(allocator: std.mem.Allocator, attribute: []const u8, message: []c
);
}
pub fn bold(comptime color: Color, comptime message: []const u8) []const u8 {
return codes.escape ++ @field(codes, @tagName(color)) ++ codes.escape ++ codes.bold ++ message ++ codes.escape ++ codes.reset;
pub fn bold(comptime message: []const u8) []const u8 {
return codes.escape ++ codes.bold ++ message ++ codes.escape ++ codes.reset;
}
pub fn black(comptime message: []const u8) []const u8 {

View File

@ -18,10 +18,7 @@ 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 = @field(
Environment.EnvironmentName,
@tagName(build_options.environment),
);
pub const environment = std.enums.nameCast(Environment.EnvironmentName, build_options.environment);
pub const build_options = @import("build_options");
const root = @import("root");
@ -110,38 +107,38 @@ pub const Schema: type = struct {};
/// When using `.file` backend, you must also set `.file_options`.
/// The key-value store is exposed as `request.store` in views and is also available in as
/// `env.store` in all jobs/mailers.
pub const store: kv.Store.Options = .{
pub const store: kv.Store.KVOptions = .{
.backend = .memory,
// .backend = .file,
// .file_options = .{
// .path = "/path/to/jetkv-store.db",
// .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.FileBackend.addressSpace(4096),
// .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096),
// },
};
/// Job queue options. Identical to `store` options, but allows using different
/// backends (e.g. `.memory` for key-value store, `.file` for jobs queue.
/// The job queue is managed internally by Jetzig.
pub const job_queue: kv.Store.Options = .{
pub const job_queue: kv.Store.KVOptions = .{
.backend = .memory,
// .backend = .file,
// .file_options = .{
// .path = "/path/to/jetkv-queue.db",
// .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.JetKV.addressSpace(4096),
// .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096),
// },
};
/// Cache. Identical to `store` options, but allows using different
/// backends (e.g. `.memory` for key-value store, `.file` for cache.
pub const cache: kv.Store.Options = .{
pub const cache: kv.Store.KVOptions = .{
.backend = .memory,
// .backend = .file,
// .file_options = .{
// .path = "/path/to/jetkv-cache.db",
// .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.JetKV.addressSpace(4096),
// .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096),
// },
};
@ -163,7 +160,7 @@ pub const cookies: http.Cookies.CookieOptions = switch (environment) {
.production => .{
.secure = true,
.http_only = true,
.same_site = .lax,
.same_site = true,
.path = "/",
},
};

View File

@ -2,9 +2,9 @@ const std = @import("std");
const jetzig = @import("../jetzig.zig");
pub const adapter = @field(
pub const adapter = std.enums.nameCast(
jetzig.jetquery.adapters.Name,
@tagName(@field(jetzig.jetquery.config.database, @tagName(jetzig.environment)).adapter),
@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,
@field(jetzig.jetquery.Environment, @tagName(jetzig.environment)),
std.enums.nameCast(jetzig.jetquery.Environment, jetzig.environment),
.{
.eventCallback = Callback.callbackFn,
.lazy_connect = switch (jetzig.environment) {

View File

@ -1,218 +0,0 @@
const std = @import("std");
const builtin = @import("builtin");
pub const ErrorInfo = struct {
stack_trace: ?*std.builtin.StackTrace = null,
err: ?anyerror = null,
};
pub fn sourceLocations(
allocator: std.mem.Allocator,
debug_info: *std.debug.SelfInfo,
stack_trace: *std.builtin.StackTrace,
) ![]const std.debug.SourceLocation {
var source_locations = std.ArrayList(std.debug.SourceLocation).init(allocator);
if (builtin.strip_debug_info) return error.MissingDebugInfo;
var frame_index: usize = 0;
var frames_left: usize = @min(stack_trace.index, stack_trace.instruction_addresses.len);
while (frames_left != 0) : ({
frames_left -= 1;
frame_index = (frame_index + 1) % stack_trace.instruction_addresses.len;
}) {
const return_address = stack_trace.instruction_addresses[frame_index];
const address = return_address - 1;
const module = try debug_info.getModuleForAddress(address);
const symbol_info = try module.getSymbolAtAddress(debug_info.allocator, address);
if (symbol_info.source_location) |source_location| {
try source_locations.append(source_location);
}
}
return try source_locations.toOwnedSlice();
}
pub const HtmlStackTrace = struct {
allocator: std.mem.Allocator,
stack_trace: *std.builtin.StackTrace,
pub fn format(self: HtmlStackTrace, _: anytype, _: anytype, writer: anytype) !void {
const debug_info = try std.debug.getSelfDebugInfo();
const source_locations = try sourceLocations(
self.allocator,
debug_info,
self.stack_trace,
);
for (source_locations) |source_location| {
defer debug_info.allocator.free(source_location.file_name);
try writer.print(
\\<div class='stack-source-line'>
\\ <span class='file-name'>{s}:{d}</span>
\\
,
.{
source_location.file_name,
source_location.line,
},
);
const surrounding_previous = try surroundingLinesFromFile(
self.allocator,
.previous,
3,
source_location.file_name,
source_location.line,
);
const surrounding_next = try surroundingLinesFromFile(
self.allocator,
.next,
3,
source_location.file_name,
source_location.line,
);
const target_source_line = try readLineFromFile(
self.allocator,
source_location.file_name,
source_location.line,
);
for (surrounding_previous) |source_line| {
try writer.print(surrounding_line_template, .{ source_line.line, source_line.content });
}
try writer.print(target_line_template, .{ target_source_line.line, target_source_line.content });
for (surrounding_next) |source_line| {
try writer.print(surrounding_line_template, .{ source_line.line, source_line.content });
}
try writer.print(
\\</div>
\\
, .{});
}
}
};
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.heap.pageSize()]u8 = undefined;
var count: usize = 1;
var cursor: usize = 0;
seek: {
while (true) {
const bytes_read = try file.readAll(buf[0..]);
for (buf[0..bytes_read]) |char| {
if (char == '\n') count += 1;
cursor += 1;
if (count == line) {
cursor += 1;
break :seek;
}
}
if (bytes_read < buf.len) return error.EndOfFile;
}
}
var size: usize = 0;
try file.seekTo(cursor);
read: {
const bytes_read = try file.readAll(buf[0..]);
if (std.mem.indexOf(u8, buf[0..bytes_read], "\n")) |index| {
size += index;
break :read;
} else if (bytes_read < buf.len) {
size += bytes_read;
break :read;
} else {
while (true) {
const more_bytes_read = try file.readAll(buf[0..]);
if (std.mem.indexOf(u8, buf[0..more_bytes_read], "\n")) |index| {
size += index;
break :read;
} else if (more_bytes_read < buf.len) {
size += more_bytes_read;
break :read;
} else {
size += more_bytes_read;
}
}
}
}
const line_content = try allocator.alloc(u8, size);
try file.seekTo(cursor);
const bytes_read = try file.readAll(line_content[0..]);
std.debug.assert(bytes_read == size);
return .{ .content = line_content[0..], .line = line };
}
fn surroundingLinesFromFile(
allocator: std.mem.Allocator,
context: enum { previous, next },
desired_count: usize,
path: []const u8,
target_line: usize,
) ![]SourceLine {
// This isn't very efficient but we only use it in debug mode so not a huge deal.
const start = switch (context) {
.previous => if (target_line > desired_count)
target_line - desired_count
else
target_line,
.next => target_line + 2,
};
var lines = std.ArrayList(SourceLine).init(allocator);
switch (context) {
.previous => {
for (start..target_line) |line| {
try lines.append(try readLineFromFile(allocator, path, line));
}
},
.next => {
for (0..desired_count, start..) |_, line| {
try lines.append(try readLineFromFile(allocator, path, line));
}
},
}
return try lines.toOwnedSlice();
}
pub const console_template =
\\<!DOCTYPE html>
\\<html>
\\ <head>
\\ <style>
\\{3s}
\\ </style>
\\ </head>
\\ <body>
\\ <h1>Encountered Error: {0s}</h1>
\\ <div class="stack-trace">
\\ {1}
\\ </div>
\\ <h2>Response Data</h2>
\\ <div class="response-data">
\\ <pre><code class="language-json">{2s}</code></pre>
\\ </div>
\\ <script src="/_jetzig_debug.js"></script>
\\ </body>
\\</html>
;
const surrounding_line_template =
\\<div class='line-content surrounding'><pre class="line-number">{d: >4}</pre><pre><code class="language-zig">{s}</code></pre></div>
\\
;
const target_line_template =
\\<div class='line-content target'><pre class="line-number">{d: >4}</pre><pre><code class="language-zig">{s}</code></pre></div>
\\
;

View File

@ -16,12 +16,11 @@ file_path: []const u8,
resource_id: []const u8,
extension: ?[]const u8,
query: ?[]const u8,
method: ?jetzig.Request.Method,
const Path = @This();
const Self = @This();
/// Initialize a new HTTP Path.
pub fn init(path: []const u8) Path {
pub fn init(path: []const u8) Self {
const base_path = getBasePath(path);
return .{
@ -32,19 +31,18 @@ pub fn init(path: []const u8) Path {
.resource_id = getResourceId(base_path),
.extension = getExtension(path),
.query = getQuery(path),
.method = getMethod(path),
};
}
/// No-op - no allocations currently performed.
pub fn deinit(self: *Path) void {
pub fn deinit(self: *Self) void {
_ = self;
}
/// For a given route with a possible `:id` placeholder, return the matching URL segment for that
/// placeholder. e.g. route with path `/foo/:id/bar` and request path `/foo/1234/bar` returns
/// `"1234"`.
pub fn resourceId(self: Path, route: jetzig.views.Route) []const u8 {
pub fn resourceId(self: Self, route: jetzig.views.Route) []const u8 {
var route_uri_path_it = std.mem.splitScalar(u8, route.uri_path, '/');
var base_path_it = std.mem.splitScalar(u8, self.base_path, '/');
@ -56,7 +54,7 @@ pub fn resourceId(self: Path, route: jetzig.views.Route) []const u8 {
return self.resource_id;
}
pub fn resourceArgs(self: Path, route: jetzig.views.Route, allocator: std.mem.Allocator) ![]const []const u8 {
pub fn resourceArgs(self: Self, route: jetzig.views.Route, allocator: std.mem.Allocator) ![]const []const u8 {
var args = std.ArrayList([]const u8).init(allocator);
var route_uri_path_it = std.mem.splitScalar(u8, route.uri_path, '/');
var path_it = std.mem.splitScalar(u8, self.base_path, '/');
@ -84,41 +82,21 @@ pub fn resourceArgs(self: Path, route: jetzig.views.Route, allocator: std.mem.Al
// * `"/foo/bar/baz"`
// * `"/foo/bar/baz.html"`
// * `"/foo/bar/baz.html?qux=quux&corge=grault"`
// * `"/foo/bar/baz/_PATCH"`
fn getBasePath(path: []const u8) []const u8 {
const base = if (std.mem.indexOfScalar(u8, path, '?')) |query_index| blk: {
if (std.mem.indexOfScalar(u8, path, '?')) |query_index| {
if (std.mem.lastIndexOfScalar(u8, path[0..query_index], '.')) |extension_index| {
break :blk path[0..extension_index];
return path[0..extension_index];
} else {
break :blk path[0..query_index];
return path[0..query_index];
}
} else if (std.mem.lastIndexOfScalar(u8, path, '.')) |extension_index| blk: {
break :blk if (isRootPath(path[0..extension_index]))
} else if (std.mem.lastIndexOfScalar(u8, path, '.')) |extension_index| {
return if (isRootPath(path[0..extension_index]))
path[0..extension_index]
else
std.mem.trimRight(u8, path[0..extension_index], "/");
} else blk: {
break :blk if (isRootPath(path)) path else std.mem.trimRight(u8, path, "/");
};
if (std.mem.lastIndexOfScalar(u8, base, '/')) |last_index| {
if (std.mem.startsWith(u8, base[last_index..], "/_")) {
return base[0..last_index];
} else {
return base;
}
} else return base;
}
fn getMethod(path: []const u8) ?jetzig.Request.Method {
var it = std.mem.splitBackwardsScalar(u8, path, '/');
const last_segment = it.next() orelse return null;
inline for (comptime std.enums.values(jetzig.Request.Method)) |method| {
if (std.mem.startsWith(u8, last_segment, "_" ++ @tagName(method))) {
return method;
}
} else {
return if (isRootPath(path)) path else std.mem.trimRight(u8, path, "/");
}
return null;
}
// Extract `"/foo/bar"` from:
@ -153,9 +131,6 @@ fn getFilePath(path: []const u8) []const u8 {
// * `"/baz"`
fn getResourceId(base_path: []const u8) []const u8 {
var it = std.mem.splitBackwardsScalar(u8, base_path, '/');
if (std.mem.endsWith(u8, base_path, "/edit")) _ = it.next();
while (it.next()) |segment| return segment;
return base_path;
}
@ -189,228 +164,169 @@ fn getQuery(path: []const u8) ?[]const u8 {
}
}
// Extract `/foo/bar/edit` from `/foo/bar/1/edit`
// Extract `/foo/bar` from `/foo/bar/1`
pub fn actionPath(self: Path, buf: *[2048]u8) []const u8 {
if (self.path.len > 2048) return self.path; // Should never happen but we don't want to panic or overflow.
if (std.mem.endsWith(u8, self.path, "/edit")) {
var it = std.mem.tokenizeScalar(u8, self.path, '/');
var cursor: usize = 0;
const count = std.mem.count(u8, self.path, "/");
var index: usize = 0;
buf[0] = '/';
cursor += 1;
while (it.next()) |segment| : (index += 1) {
if (index + 2 == count) continue; // Skip ID - we special-case this in `resourceId`
@memcpy(buf[cursor .. cursor + segment.len], segment);
cursor += segment.len;
if (index + 1 < count) {
@memcpy(buf[cursor .. cursor + 1], "/");
cursor += 1;
}
}
return buf[0..cursor];
} else return self.path;
}
inline fn isRootPath(path: []const u8) bool {
return std.mem.eql(u8, path, "/");
}
test ".base_path (with extension, with query)" {
const path = Path.init("/foo/bar/baz.html?qux=quux&corge=grault");
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault");
try std.testing.expectEqualStrings("/foo/bar/baz", path.base_path);
}
test ".base_path (with extension, without query)" {
const path = Path.init("/foo/bar/baz.html");
const path = Self.init("/foo/bar/baz.html");
try std.testing.expectEqualStrings("/foo/bar/baz", path.base_path);
}
test ".base_path (without extension, without query)" {
const path = Path.init("/foo/bar/baz");
const path = Self.init("/foo/bar/baz");
try std.testing.expectEqualStrings("/foo/bar/baz", path.base_path);
}
test ".base_path (with trailing slash)" {
const path = Path.init("/foo/bar/");
const path = Self.init("/foo/bar/");
try std.testing.expectEqualStrings("/foo/bar", path.base_path);
}
test ".base_path (root path)" {
const path = Path.init("/");
const path = Self.init("/");
try std.testing.expectEqualStrings("/", path.base_path);
}
test ".base_path (root path with extension)" {
const path = Path.init("/.json");
const path = Self.init("/.json");
try std.testing.expectEqualStrings("/", path.base_path);
try std.testing.expectEqualStrings(".json", path.extension.?);
}
test ".directory (with extension, with query)" {
const path = Path.init("/foo/bar/baz.html?qux=quux&corge=grault");
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault");
try std.testing.expectEqualStrings("/foo/bar", path.directory);
}
test ".directory (with extension, without query)" {
const path = Path.init("/foo/bar/baz.html");
const path = Self.init("/foo/bar/baz.html");
try std.testing.expectEqualStrings("/foo/bar", path.directory);
}
test ".directory (without extension, without query)" {
const path = Path.init("/foo/bar/baz");
const path = Self.init("/foo/bar/baz");
try std.testing.expectEqualStrings("/foo/bar", path.directory);
}
test ".directory (without extension, without query, root path)" {
const path = Path.init("/");
const path = Self.init("/");
try std.testing.expectEqualStrings("/", path.directory);
}
test ".resource_id (with extension, with query)" {
const path = Path.init("/foo/bar/baz.html?qux=quux&corge=grault");
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault");
try std.testing.expectEqualStrings("baz", path.resource_id);
}
test ".resource_id (with extension, without query)" {
const path = Path.init("/foo/bar/baz.html");
const path = Self.init("/foo/bar/baz.html");
try std.testing.expectEqualStrings("baz", path.resource_id);
}
test ".resource_id (without extension, without query)" {
const path = Path.init("/foo/bar/baz");
const path = Self.init("/foo/bar/baz");
try std.testing.expectEqualStrings("baz", path.resource_id);
}
test ".resource_id (without extension, without query, without base path)" {
const path = Path.init("/baz");
const path = Self.init("/baz");
try std.testing.expectEqualStrings("baz", path.resource_id);
}
test ".resource_id (with trailing slash)" {
const path = Path.init("/foo/bar/");
const path = Self.init("/foo/bar/");
try std.testing.expectEqualStrings("bar", path.resource_id);
}
test ".extension (with query)" {
const path = Path.init("/foo/bar/baz.html?qux=quux&corge=grault");
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault");
try std.testing.expectEqualStrings(".html", path.extension.?);
}
test ".extension (without query)" {
const path = Path.init("/foo/bar/baz.html");
const path = Self.init("/foo/bar/baz.html");
try std.testing.expectEqualStrings(".html", path.extension.?);
}
test ".extension (without extension)" {
const path = Path.init("/foo/bar/baz");
const path = Self.init("/foo/bar/baz");
try std.testing.expect(path.extension == null);
}
test ".query (with extension, with query)" {
const path = Path.init("/foo/bar/baz.html?qux=quux&corge=grault");
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault");
try std.testing.expectEqualStrings(path.query.?, "qux=quux&corge=grault");
}
test ".query (without extension, with query)" {
const path = Path.init("/foo/bar/baz?qux=quux&corge=grault");
const path = Self.init("/foo/bar/baz?qux=quux&corge=grault");
try std.testing.expectEqualStrings(path.query.?, "qux=quux&corge=grault");
}
test ".query (with extension, without query)" {
const path = Path.init("/foo/bar/baz.json");
const path = Self.init("/foo/bar/baz.json");
try std.testing.expect(path.query == null);
}
test ".query (without extension, without query)" {
const path = Path.init("/foo/bar/baz");
const path = Self.init("/foo/bar/baz");
try std.testing.expect(path.query == null);
}
test ".query (with empty query)" {
const path = Path.init("/foo/bar/baz?");
const path = Self.init("/foo/bar/baz?");
try std.testing.expect(path.query == null);
}
test ".file_path (with extension, with query)" {
const path = Path.init("/foo/bar/baz.json?qux=quux&corge=grault");
const path = Self.init("/foo/bar/baz.json?qux=quux&corge=grault");
try std.testing.expectEqualStrings("/foo/bar/baz.json", path.file_path);
}
test ".file_path (with extension, without query)" {
const path = Path.init("/foo/bar/baz.json");
const path = Self.init("/foo/bar/baz.json");
try std.testing.expectEqualStrings("/foo/bar/baz.json", path.file_path);
}
test ".file_path (without extension, without query)" {
const path = Path.init("/foo/bar/baz");
const path = Self.init("/foo/bar/baz");
try std.testing.expectEqualStrings("/foo/bar/baz", path.file_path);
}
test ".file_path (without extension, with query)" {
const path = Path.init("/foo/bar/baz?qux=quux&corge=grault");
const path = Self.init("/foo/bar/baz?qux=quux&corge=grault");
try std.testing.expectEqualStrings("/foo/bar/baz", path.file_path);
}
test ".resource_id (/foo/bar/123/edit)" {
const path = Path.init("/foo/bar/123/edit");
try std.testing.expectEqualStrings("123", path.resource_id);
}
test ".actionPath (/foo/bar/123/edit)" {
var buf: [2048]u8 = undefined;
const path = Path.init("/foo/bar/123/edit").actionPath(&buf);
try std.testing.expectEqualStrings("/foo/bar/edit", path);
}
test ".actionPath (/foo/bar)" {
var buf: [2048]u8 = undefined;
const path = Path.init("/foo/bar").actionPath(&buf);
try std.testing.expectEqualStrings("/foo/bar", path);
}
test ".base_path (/foo/bar/1/_PATCH" {
const path = Path.init("/foo/bar/1/_PATCH");
try std.testing.expectEqualStrings("/foo/bar/1", path.base_path);
try std.testing.expectEqualStrings("1", path.resource_id);
}
test ".method (/foo/bar/1/_PATCH" {
const path = Path.init("/foo/bar/1/_PATCH");
try std.testing.expect(path.method.? == .PATCH);
}

View File

@ -61,7 +61,7 @@ pub fn parse(self: *Query) !void {
else => return error.JetzigQueryParseError,
}
} else {
var array = try jetzig.zmpl.Data.createArray(self.data.allocator);
var array = try jetzig.zmpl.Data.createArray(self.data.allocator());
try array.append(self.dataValue(item.value));
try params.put(key, array);
}
@ -72,7 +72,7 @@ pub fn parse(self: *Query) !void {
else => return error.JetzigQueryParseError,
}
} else {
var object = try jetzig.zmpl.Data.createObject(self.data.allocator);
var object = try jetzig.zmpl.Data.createObject(self.data.allocator());
try object.put(mapping.field, self.dataValue(item.value));
try params.put(mapping.key, object);
}
@ -109,10 +109,10 @@ fn mappingParam(input: []const u8) ?struct { key: []const u8, field: []const u8
fn dataValue(self: Query, value: ?[]const u8) *jetzig.data.Data.Value {
if (value) |item_value| {
const duped = self.data.allocator.dupe(u8, item_value) catch @panic("OOM");
const duped = self.data.allocator().dupe(u8, item_value) catch @panic("OOM");
return self.data.string(uriDecode(duped));
} else {
return jetzig.zmpl.Data._null(self.data.allocator);
return jetzig.zmpl.Data._null(self.data.allocator());
}
}

View File

@ -11,16 +11,6 @@ pub const Method = enum { DELETE, GET, PATCH, POST, HEAD, PUT, CONNECT, OPTIONS,
pub const Modifier = enum { edit, new };
pub const Format = enum { HTML, JSON, UNKNOWN };
pub const Protocol = enum { http, https };
pub const RequestState = enum {
initial, // No processing has taken place
processed, // Request headers have been processed
after_request, // Initial middleware processing
rendered, // Rendered by middleware or view
redirected, // Redirected by middleware or view
failed, // Failed by middleware or view
before_response, // Post middleware processing
finalized, // Response generated
};
allocator: std.mem.Allocator,
path: jetzig.http.Path,
@ -40,85 +30,82 @@ parsed_multipart: ?*jetzig.data.Data = null,
_cookies: ?*jetzig.http.Cookies = null,
_session: ?*jetzig.http.Session = null,
body: []const u8 = undefined,
state: RequestState = .initial,
state: enum { initial, processed } = .initial,
response_started: bool = false,
dynamic_assigned_template: ?[]const u8 = null,
layout: ?[]const u8 = null,
layout_disabled: bool = false,
// TODO: Squash rendered/redirected/failed into
// `state: enum { initial, rendered, redirected, failed }`
rendered: bool = false,
redirected: bool = false,
failed: bool = false,
redirect_state: ?RedirectState = null,
middleware_rendered: ?struct { name: []const u8, action: []const u8 } = null,
middleware_rendered_during_response: bool = false,
middleware_data: jetzig.http.middleware.MiddlewareData = undefined,
rendered_multiple: bool = false,
rendered_view: ?jetzig.views.View = null,
start_time: i128,
store: RequestStore(jetzig.kv.Store.GeneralStore),
cache: RequestStore(jetzig.kv.Store.CacheStore),
store: RequestStore,
cache: RequestStore,
repo: *jetzig.database.Repo,
global: *jetzig.Global,
/// Wrapper for KV store that uses the request's arena allocator for fetching values.
pub fn RequestStore(T: type) type {
return struct {
allocator: std.mem.Allocator,
store: *T,
pub const RequestStore = struct {
allocator: std.mem.Allocator,
store: *jetzig.kv.Store,
const Self = @This();
/// Put a String or into the key-value store.
pub fn get(self: RequestStore, key: []const u8) !?*jetzig.data.Value {
return try self.store.get(try self.data(), key);
}
/// Get a Value from the store.
pub fn get(self: Self, key: []const u8) !?*jetzig.data.Value {
return try self.store.get(try self.data(), key);
}
/// Get a String from the store.
pub fn put(self: RequestStore, key: []const u8, value: anytype) !void {
const alloc = (try self.data()).allocator();
try self.store.put(key, try jetzig.Data.zmplValue(value, alloc));
}
/// Store a Value in the key-value store.
pub fn put(self: Self, key: []const u8, value: anytype) !void {
const alloc = (try self.data()).allocator;
try self.store.put(key, try jetzig.Data.zmplValue(value, alloc));
}
/// Remove a String to from the key-value store and return it if found.
pub fn fetchRemove(self: RequestStore, key: []const u8) !?*jetzig.data.Value {
return try self.store.fetchRemove(try self.data(), key);
}
/// Store a Value in the key-value store with an expiration time in seconds.
pub fn putExpire(self: Self, key: []const u8, value: anytype, expiration: i32) !void {
const alloc = (try self.data()).allocator;
try self.store.putExpire(key, try jetzig.Data.zmplValue(value, alloc), expiration);
}
/// Remove a String to from the key-value store.
pub fn remove(self: RequestStore, key: []const u8) !void {
try self.store.remove(key);
}
/// Remove a String to from the key-value store and return it if found.
pub fn fetchRemove(self: Self, key: []const u8) !?*jetzig.data.Value {
return try self.store.fetchRemove(try self.data(), key);
}
/// Append a Value to the end of an Array in the key-value store.
pub fn append(self: RequestStore, key: []const u8, value: anytype) !void {
const alloc = (try self.data()).allocator();
try self.store.append(key, try jetzig.Data.zmplValue(value, alloc));
}
/// Remove a String to from the key-value store.
pub fn remove(self: Self, key: []const u8) !void {
try self.store.remove(key);
}
/// Prepend a Value to the start of an Array in the key-value store.
pub fn prepend(self: RequestStore, key: []const u8, value: anytype) !void {
const alloc = (try self.data()).allocator();
try self.store.prepend(key, try jetzig.Data.zmplValue(value, alloc));
}
/// Append a Value to the end of an Array in the key-value store.
pub fn append(self: Self, key: []const u8, value: anytype) !void {
const alloc = (try self.data()).allocator;
try self.store.append(key, try jetzig.Data.zmplValue(value, alloc));
}
/// Pop a String from an Array in the key-value store.
pub fn pop(self: RequestStore, key: []const u8) !?*jetzig.data.Value {
return try self.store.pop(try self.data(), key);
}
/// Prepend a Value to the start of an Array in the key-value store.
pub fn prepend(self: Self, key: []const u8, value: anytype) !void {
const alloc = (try self.data()).allocator;
try self.store.prepend(key, try jetzig.Data.zmplValue(value, alloc));
}
/// Left-pop a String from an Array in the key-value store.
pub fn popFirst(self: RequestStore, key: []const u8) !?*jetzig.data.Value {
return try self.store.popFirst(try self.data(), key);
}
/// Pop a String from an Array in the key-value store.
pub fn pop(self: Self, key: []const u8) !?*jetzig.data.Value {
return try self.store.pop(try self.data(), key);
}
/// Left-pop a String from an Array in the key-value store.
pub fn popFirst(self: Self, key: []const u8) !?*jetzig.data.Value {
return try self.store.popFirst(try self.data(), key);
}
fn data(self: Self) !*jetzig.data.Data {
const arena_data = try self.allocator.create(jetzig.data.Data);
arena_data.* = jetzig.data.Data.init(self.allocator);
return arena_data;
}
};
}
fn data(self: RequestStore) !*jetzig.data.Data {
const arena_data = try self.allocator.create(jetzig.data.Data);
arena_data.* = jetzig.data.Data.init(self.allocator);
return arena_data;
}
};
pub fn init(
allocator: std.mem.Allocator,
@ -129,11 +116,7 @@ pub fn init(
response: *jetzig.http.Response,
repo: *jetzig.database.Repo,
) !Request {
const path = jetzig.http.Path.init(httpz_request.url.raw);
// We can fake the HTTP method by appending `/_PATCH` (e.g.) to the end of the URL.
// This allows using PATCH, PUT, DELETE from HTML forms.
const method = path.method orelse switch (httpz_request.method) {
const method = switch (httpz_request.method) {
.DELETE => Method.DELETE,
.GET => Method.GET,
.PATCH => Method.PATCH,
@ -141,7 +124,6 @@ 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);
@ -149,7 +131,7 @@ pub fn init(
return .{
.allocator = allocator,
.path = path,
.path = jetzig.http.Path.init(httpz_request.url.raw),
.method = method,
.headers = jetzig.http.Headers.init(allocator, httpz_request.headers),
.server = server,
@ -195,7 +177,6 @@ pub fn respond(self: *Request) !void {
const status = jetzig.http.status_codes.get(self.response.status_code);
self.httpz_response.status = try status.getCodeInt();
self.httpz_response.body = self.response.content;
self.state = .finalized;
}
/// Set the root value for response data.
@ -210,9 +191,10 @@ pub fn data(self: Request, comptime root: @TypeOf(.enum_literal)) !*jetzig.Data.
/// Render a response. This function can only be called once per request (repeat calls will
/// trigger an error).
pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
if (self.isRendered()) self.rendered_multiple = true;
if (self.rendered or self.failed) self.rendered_multiple = true;
self.state = .rendered;
self.rendered = true;
if (self.response_started) self.middleware_rendered_during_response = true;
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
return self.rendered_view.?;
}
@ -220,20 +202,15 @@ pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode)
/// Render an error. This function can only be called once per request (repeat calls will
/// trigger an error).
pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
if (self.isRendered()) self.rendered_multiple = true;
if (self.rendered or self.redirected) self.rendered_multiple = true;
self.state = .failed;
self.rendered = true;
self.failed = true;
if (self.response_started) self.middleware_rendered_during_response = true;
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
return self.rendered_view.?;
}
pub inline fn isRendered(self: *const Request) bool {
return switch (self.state) {
.initial, .processed, .after_request, .before_response => false,
.rendered, .redirected, .failed, .finalized => true,
};
}
/// Issue a redirect to a new location.
/// ```zig
/// return request.redirect("https://www.example.com/", .moved_permanently);
@ -247,9 +224,11 @@ pub fn redirect(
location: []const u8,
redirect_status: enum { moved_permanently, found },
) jetzig.views.View {
if (self.isRendered()) self.rendered_multiple = true;
if (self.rendered or self.failed) self.rendered_multiple = true;
self.state = .redirected;
self.rendered = true;
self.redirected = true;
if (self.response_started) self.middleware_rendered_during_response = true;
const status_code = switch (redirect_status) {
.moved_permanently => jetzig.http.status_codes.StatusCode.moved_permanently,
@ -497,14 +476,12 @@ 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) {
@ -583,18 +560,25 @@ const RequestMail = struct {
_ = options;
var mail_job = try self.request.job("__jetzig_mail");
try mail_job.params.put("mailer_name", self.name);
try mail_job.params.put("from", self.mail_params.get(.from));
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);
var to_array = try mail_job.data.array();
if (self.mail_params.get(.to)) |to| {
for (to) |each| try to_array.append(.{ .email = each.email, .name = each.name });
if (self.mail_params.to) |capture| {
for (capture) |to| try to_array.append(mail_job.data.string(to));
}
try mail_job.params.put("to", to_array);
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));
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);
if (self.request.response_data.value) |value| try mail_job.params.put(
"params",
@ -616,7 +600,6 @@ const RequestMail = struct {
.store = self.request.server.store,
.cache = self.request.server.cache,
.mutex = undefined,
.repo = self.request.repo,
},
),
}
@ -704,19 +687,6 @@ pub fn setTemplate(self: *Request, name: []const u8) void {
self.dynamic_assigned_template = name;
}
/// Set a custom template and render the response.
/// ```zig
/// return request.renderTemplate("blogs/comments/get", .ok);
/// ```
pub fn renderTemplate(
self: *Request,
name: []const u8,
status_code: jetzig.http.StatusCode,
) jetzig.views.View {
self.dynamic_assigned_template = name;
return self.render(status_code);
}
pub fn joinPath(self: *const Request, args: anytype) ![]const u8 {
const fields = std.meta.fields(@TypeOf(args));
var buf: [fields.len][]const u8 = undefined;
@ -775,7 +745,6 @@ pub fn match(self: *Request, route: jetzig.views.Route) !bool {
.index => self.isMatch(.exact, route),
.get => self.isMatch(.resource_id, route),
.new => self.isMatch(.exact, route),
.edit => self.isMatch(.exact, route),
else => false,
},
.POST => switch (route.action) {
@ -808,18 +777,8 @@ fn isMatch(
.resource_id => self.path.directory,
};
if (route.action == .get) {
// Special case for `/foobar/1/new` -> render `new()` - prevent matching `get`
if (std.mem.eql(u8, self.path.resource_id, "new")) return false;
// Special case for `/foobar/1/edit` -> render `edit()` - prevent matching `get`
if (std.mem.eql(u8, self.path.resource_id, "edit")) return false;
}
if (route.action == .edit and std.mem.endsWith(u8, self.path.path, "/edit")) {
var buf: [2048]u8 = undefined;
const action_path = self.path.actionPath(&buf);
if (std.mem.eql(u8, action_path, route.uri_path)) return true;
}
// Special case for `/foobar/1/new` -> render `new()`
if (route.action == .get and std.mem.eql(u8, self.path.resource_id, "new")) return false;
return std.mem.eql(u8, path, route.uri_path);
}

View File

@ -9,33 +9,32 @@ const httpz = @import("httpz");
allocator: std.mem.Allocator,
logger: jetzig.loggers.Logger,
env: jetzig.Environment,
routes: []const *const jetzig.views.Route,
custom_routes: []const jetzig.views.Route,
routes: []*jetzig.views.Route,
custom_routes: []jetzig.views.Route,
job_definitions: []const jetzig.JobDefinition,
mailer_definitions: []const jetzig.MailerDefinition,
mime_map: *jetzig.http.mime.MimeMap,
initialized: bool = false,
store: *jetzig.kv.Store.GeneralStore,
job_queue: *jetzig.kv.Store.JobQueueStore,
cache: *jetzig.kv.Store.CacheStore,
store: *jetzig.kv.Store,
job_queue: *jetzig.kv.Store,
cache: *jetzig.kv.Store,
repo: *jetzig.database.Repo,
global: *anyopaque,
decoded_static_route_params: []const *jetzig.data.Value = &.{},
debug_mutex: std.Thread.Mutex = .{},
decoded_static_route_params: []*jetzig.data.Value = &.{},
const Server = @This();
pub fn init(
allocator: std.mem.Allocator,
env: jetzig.Environment,
routes: []const *const jetzig.views.Route,
custom_routes: []const jetzig.views.Route,
routes: []*jetzig.views.Route,
custom_routes: []jetzig.views.Route,
job_definitions: []const jetzig.JobDefinition,
mailer_definitions: []const jetzig.MailerDefinition,
mime_map: *jetzig.http.mime.MimeMap,
store: *jetzig.kv.Store.GeneralStore,
job_queue: *jetzig.kv.Store.JobQueueStore,
cache: *jetzig.kv.Store.CacheStore,
store: *jetzig.kv.Store,
job_queue: *jetzig.kv.Store,
cache: *jetzig.kv.Store,
repo: *jetzig.database.Repo,
global: *anyopaque,
) Server {
@ -74,44 +73,36 @@ const Dispatcher = struct {
pub fn listen(self: *Server) !void {
try self.decodeStaticParams();
const worker_count = jetzig.config.get(u16, "worker_count");
const thread_count: u16 = jetzig.config.get(?u16, "thread_count") orelse @intCast(try std.Thread.getCpuCount());
var httpz_server = try httpz.Server(Dispatcher).init(
self.allocator,
.{
.port = self.env.port,
.address = self.env.bind,
.thread_pool = .{
.count = thread_count,
.count = jetzig.config.get(?u16, "thread_count") orelse @intCast(try std.Thread.getCpuCount()),
.buffer_size = jetzig.config.get(usize, "buffer_size"),
},
.workers = .{
.count = worker_count,
.count = jetzig.config.get(u16, "worker_count"),
.max_conn = jetzig.config.get(u16, "max_connections"),
.retain_allocated_bytes = jetzig.config.get(usize, "arena_size"),
},
.request = .{
.max_multiform_count = jetzig.config.get(usize, "max_multipart_form_fields"),
.max_body_size = jetzig.config.get(usize, "max_bytes_request_body"),
},
},
Dispatcher{ .server = self },
);
defer httpz_server.deinit();
try self.logger.INFO("Listening on http://{s}:{d} [{s}] [workers:{d} threads:{d}]", .{
try self.logger.INFO("Listening on http://{s}:{} [{s}]", .{
self.env.bind,
self.env.port,
@tagName(self.env.environment),
worker_count,
thread_count,
});
self.initialized = true;
try jetzig.http.middleware.afterLaunch(self);
return try httpz_server.listen();
}
@ -120,11 +111,7 @@ pub fn errorHandlerFn(self: *Server, request: *httpz.Request, response: *httpz.R
self.logger.ERROR("Encountered error: {s} {s}", .{ @errorName(err), request.url.raw }) catch {};
const stack = @errorReturnTrace();
if (stack) |capture| {
self.debug_mutex.lock();
defer self.debug_mutex.unlock();
self.logStackTrace(capture, request.arena) catch {};
}
if (stack) |capture| self.logStackTrace(capture, request.arena) catch {};
response.body = "500 Internal Server Error";
}
@ -136,9 +123,6 @@ pub fn processNextRequest(
) !void {
const start_time = std.time.nanoTimestamp();
var repo = try self.repo.bindConnect(.{ .allocator = httpz_response.arena });
defer repo.release();
var response = try jetzig.http.Response.init(httpz_response.arena, httpz_response);
var request = try jetzig.http.Request.init(
httpz_response.arena,
@ -147,31 +131,14 @@ pub fn processNextRequest(
httpz_request,
httpz_response,
&response,
&repo,
self.repo,
);
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.middleware_rendered) |_| { // Request processing ends when a middleware renders or redirects.
if (request.redirect_state) |state| {
try request.renderRedirect(state);
} else if (request.rendered_view) |rendered| {
@ -180,8 +147,17 @@ fn maybeMiddlewareRender(request: *jetzig.http.Request, response: *const jetzig.
}
try request.response.headers.append("Content-Type", response.content_type);
try request.respond();
return true;
} else return false;
} 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);
}
fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
@ -221,12 +197,9 @@ fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
for (route.before_callbacks) |callback| {
try callback(request, route);
if (request.rendered_view) |view| {
if (request.state == .failed) {
request.setResponse(
try self.renderError(request, view.status_code, .{}),
.{},
);
} else if (request.state == .rendered) {
if (request.failed) {
request.setResponse(try self.renderError(request, view.status_code), .{});
} else if (request.rendered) {
// TODO: Allow callbacks to set content
}
return;
@ -286,9 +259,7 @@ fn renderHTML(
return request.setResponse(rendered_error, .{});
};
return if (request.state == .redirected or
request.state == .failed or
request.dynamic_assigned_template != null)
return if (request.redirected or request.failed or request.dynamic_assigned_template != null)
request.setResponse(rendered, .{})
else
request.setResponse(try self.renderNotFound(request), .{});
@ -356,12 +327,12 @@ fn renderView(
return try self.renderInternalServerError(request, @errorReturnTrace(), err);
};
if (request.state == .failed) {
if (request.failed) {
const view: jetzig.views.View = request.rendered_view orelse .{
.data = request.response_data,
.status_code = .internal_server_error,
};
return try self.renderError(request, view.status_code, .{});
return try self.renderError(request, view.status_code);
}
const template: ?zmpl.Template = if (request.dynamic_assigned_template) |request_template|
@ -372,7 +343,7 @@ fn renderView(
if (request.rendered_multiple) return error.JetzigMultipleRenderError;
if (request.rendered_view) |rendered_view| {
if (request.state == .redirected) return .{ .view = rendered_view, .content = "" };
if (request.redirected) return .{ .view = rendered_view, .content = "" };
if (template) |capture| {
return .{
@ -381,21 +352,12 @@ fn renderView(
};
} else {
return switch (request.requestFormat()) {
.HTML, .UNKNOWN => blk: {
try self.logger.DEBUG(
"Missing template for route `{s}.{s}`. Expected: `src/app/views/{s}.zmpl`.",
.{ route.view_name, @tagName(route.action), route.template },
);
if (comptime jetzig.build_options.debug_console) {
return error.ZmplTemplateNotFound;
}
break :blk try self.renderNotFound(request);
},
.HTML, .UNKNOWN => try self.renderNotFound(request),
.JSON => .{ .view = rendered_view, .content = "" },
};
}
} else {
if (request.state == .processed) {
if (!request.redirected) {
try self.logger.WARN("`request.render` was not invoked. Rendering empty content.", .{});
}
request.response_data.reset();
@ -499,42 +461,32 @@ fn renderInternalServerError(
stack_trace: ?*std.builtin.StackTrace,
err: anyerror,
) !RenderedView {
request.response_data.reset();
try self.logger.logError(stack_trace, err);
const status = jetzig.http.StatusCode.internal_server_error;
return try self.renderError(request, status, .{ .stack_trace = stack_trace, .err = err });
const status = .internal_server_error;
return try self.renderError(request, status);
}
fn renderNotFound(self: *Server, request: *jetzig.http.Request) !RenderedView {
request.response_data.reset();
const status: jetzig.http.StatusCode = .not_found;
return try self.renderError(request, status, .{});
return try self.renderError(request, status);
}
fn renderBadRequest(self: *Server, request: *jetzig.http.Request) !RenderedView {
request.response_data.reset();
const status: jetzig.http.StatusCode = .bad_request;
return try self.renderError(request, status, .{});
return try self.renderError(request, status);
}
fn renderError(
self: Server,
request: *jetzig.http.Request,
status_code: jetzig.http.StatusCode,
error_info: jetzig.debug.ErrorInfo,
) !RenderedView {
if (comptime jetzig.build_options.debug_console) {
return try self.renderDebugConsole(request, status_code, error_info);
} else return try self.renderGeneralError(request, status_code);
}
fn renderGeneralError(
self: Server,
request: *jetzig.http.Request,
status_code: jetzig.http.StatusCode,
) !RenderedView {
if (try self.renderErrorView(request, status_code)) |view| return view;
if (try renderStaticErrorPage(request, status_code)) |view| return view;
@ -542,42 +494,6 @@ fn renderGeneralError(
return try renderDefaultError(request, status_code);
}
fn renderDebugConsole(
self: Server,
request: *jetzig.http.Request,
status_code: jetzig.http.StatusCode,
error_info: jetzig.debug.ErrorInfo,
) !RenderedView {
if (comptime jetzig.build_options.debug_console) {
var buf = std.ArrayList(u8).init(request.allocator);
const writer = buf.writer();
if (error_info.stack_trace) |stack_trace| {
const debug_content = jetzig.debug.HtmlStackTrace{
.allocator = request.allocator,
.stack_trace = stack_trace,
};
const error_name = if (error_info.err) |err| @errorName(err) else "[UnknownError]";
try writer.print(
jetzig.debug.console_template,
.{
error_name,
debug_content,
try request.response_data.toJsonOptions(.{ .pretty = true }),
@embedFile("../../assets/debug.css"),
},
);
} else return try self.renderGeneralError(request, status_code);
const content = try buf.toOwnedSlice();
return .{
.view = .{ .data = request.response_data, .status_code = status_code },
.content = if (content.len == 0) "" else content,
};
} else unreachable;
}
fn renderErrorView(
self: Server,
request: *jetzig.http.Request,
@ -725,13 +641,6 @@ const StaticResource = struct {
};
fn matchStaticResource(self: *Server, request: *jetzig.http.Request) !?StaticResource {
if (comptime jetzig.build_options.debug_console) {
if (std.mem.eql(u8, request.path.path, "/_jetzig_debug.js")) return .{
.content = @embedFile("../../assets/debug.js"),
.mime_type = "text/javascript",
};
}
// TODO: Map public and static routes at launch to avoid accessing the file system when
// matching any route - currently every request causes file system traversal.
const public_resource = try self.matchPublicContent(request);
@ -809,7 +718,7 @@ fn matchStaticContent(self: *Server, request: *jetzig.http.Request) !?[]const u8
self.decoded_static_route_params[index].get("params"),
route,
request,
params.*,
params,
)) return switch (request_format) {
.HTML, .UNKNOWN => static_output.output.html,
.JSON => static_output.output.json,
@ -845,7 +754,7 @@ fn matchStaticOutput(
maybe_expected_params: ?*jetzig.data.Value,
route: jetzig.views.Route,
request: *const jetzig.http.Request,
params: jetzig.data.Value,
params: *jetzig.data.Value,
) bool {
return if (maybe_expected_params) |expected_params| blk: {
const params_match = expected_params.count() == 0 or expected_params.eql(params);

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,30 +15,22 @@ 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(self.cookie_name)) |cookie| {
if (self.cookies.get(cookie_name)) |cookie| {
try self.parseSessionCookie(cookie.value);
} else {
try self.reset();
@ -119,7 +111,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 = self.cookie_name, .value = encoded });
try self.cookies.put(.{ .name = cookie_name, .value = encoded });
}
fn parseSessionCookie(self: *Self, cookie_value: []const u8) !void {
@ -188,7 +180,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);
@ -207,7 +199,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);
@ -232,7 +224,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();
@ -241,32 +233,17 @@ 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,
"custom-cookie-name=fPCFwZHvPDT-XCVcsQUSspDLchS3tRuJDqPpB2v3127VXpRP_bPcPLgpHK6RiVkfcP1bMtU",
"_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, .{ .cookie_name = "custom-cookie-name" });
var session = Self.init(allocator, &cookies, &secret);
defer session.deinit();
try session.parse();
var value = (session.get("foo")).?;
try std.testing.expectEqualStrings("bar", try value.toString());
try std.testing.expectError(error.JetzigInvalidSessionCookie, session.parse());
}

View File

@ -48,14 +48,6 @@ 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;
@ -68,11 +60,8 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
}
}
request.state = .after_request;
inline for (middlewares, 0..) |middleware, index| {
if (comptime !@hasDecl(middleware, "afterRequest")) continue;
if (comptime @hasDecl(middleware, "init")) {
const data = middleware_data.get(index).?;
try @call(
@ -83,8 +72,7 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
} else {
try @call(.always_inline, middleware.afterRequest, .{request});
}
if (request.state != .after_request) {
if (request.rendered or request.redirected) {
request.middleware_rendered = .{ .name = @typeName(middleware), .action = "afterRequest" };
break;
}
@ -98,11 +86,11 @@ pub fn beforeResponse(
middleware_data: *MiddlewareData,
request: *jetzig.http.Request,
) !void {
request.state = .before_response;
request.response_started = true;
inline for (middlewares, 0..) |middleware, index| {
if (comptime !@hasDecl(middleware, "beforeResponse")) continue;
if (request.state == .before_response) {
if (!request.middleware_rendered_during_response) {
if (comptime @hasDecl(middleware, "init")) {
const data = middleware_data.get(index).?;
try @call(
@ -111,19 +99,11 @@ pub fn beforeResponse(
.{ @as(*middleware, @ptrCast(@alignCast(data))), request, request.response },
);
} else {
try @call(
.always_inline,
middleware.beforeResponse,
.{ request, request.response },
);
try @call(.always_inline, middleware.beforeResponse, .{ request, request.response });
}
}
if (request.state != .before_response) {
request.middleware_rendered = .{
.name = @typeName(middleware),
.action = "beforeResponse",
};
if (request.middleware_rendered_during_response) {
request.middleware_rendered = .{ .name = @typeName(middleware), .action = "beforeResponse" };
break;
}
}

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_ptr) |default_value_ptr|
@as(*field.type, @ptrCast(@alignCast(@constCast(default_value_ptr)))).*
@field(t, field.name) = if (field.default_value) |default_value|
@as(*field.type, @ptrCast(@alignCast(@constCast(default_value)))).*
else
null;
statuses[index] = .blank;

View File

@ -16,26 +16,24 @@ pub const JobEnv = struct {
/// Environment configured at server launch
vars: jetzig.Environment.Vars,
/// All routes detected by Jetzig on startup
routes: []const *const jetzig.Route,
routes: []*const jetzig.Route,
/// All mailers detected by Jetzig on startup
mailers: []const jetzig.MailerDefinition,
/// All jobs detected by Jetzig on startup
jobs: []const jetzig.JobDefinition,
/// Global key-value store
store: *jetzig.kv.Store.GeneralStore,
store: *jetzig.kv.Store,
/// Global cache
cache: *jetzig.kv.Store.CacheStore,
/// Database repo
repo: *jetzig.database.Repo,
cache: *jetzig.kv.Store,
/// Global mutex - use with caution if it is necessary to guarantee thread safety/consistency
/// between concurrent job workers
mutex: *std.Thread.Mutex,
};
allocator: std.mem.Allocator,
store: *jetzig.kv.Store.GeneralStore,
job_queue: *jetzig.kv.Store.JobQueueStore,
cache: *jetzig.kv.Store.CacheStore,
store: *jetzig.kv.Store,
job_queue: *jetzig.kv.Store,
cache: *jetzig.kv.Store,
logger: jetzig.loggers.Logger,
name: []const u8,
definition: ?JobDefinition,
@ -47,9 +45,9 @@ const Job = @This();
/// Initialize a new Job
pub fn init(
allocator: std.mem.Allocator,
store: *jetzig.kv.Store.GeneralStore,
job_queue: *jetzig.kv.Store.JobQueueStore,
cache: *jetzig.kv.Store.CacheStore,
store: *jetzig.kv.Store,
job_queue: *jetzig.kv.Store,
cache: *jetzig.kv.Store,
logger: jetzig.loggers.Logger,
jobs: []const JobDefinition,
name: []const u8,

View File

@ -5,7 +5,7 @@ const jetzig = @import("../../jetzig.zig");
const Pool = @This();
allocator: std.mem.Allocator,
job_queue: *jetzig.kv.Store.JobQueueStore,
job_queue: *jetzig.kv.Store,
job_env: jetzig.jobs.JobEnv,
pool: std.Thread.Pool = undefined,
workers: std.ArrayList(*jetzig.jobs.Worker),
@ -13,7 +13,7 @@ workers: std.ArrayList(*jetzig.jobs.Worker),
/// Initialize a new worker thread pool.
pub fn init(
allocator: std.mem.Allocator,
job_queue: *jetzig.kv.Store.JobQueueStore,
job_queue: *jetzig.kv.Store,
job_env: jetzig.jobs.JobEnv,
) Pool {
return .{

View File

@ -6,14 +6,14 @@ const Worker = @This();
allocator: std.mem.Allocator,
job_env: jetzig.jobs.JobEnv,
id: usize,
job_queue: *jetzig.kv.Store.JobQueueStore,
job_queue: *jetzig.kv.Store,
interval: usize,
pub fn init(
allocator: std.mem.Allocator,
job_env: jetzig.jobs.JobEnv,
id: usize,
job_queue: *jetzig.kv.Store.JobQueueStore,
job_queue: *jetzig.kv.Store,
interval: usize,
) Worker {
return .{

View File

@ -1,36 +1,3 @@
const std = @import("std");
const config = @import("config.zig");
pub const Store = struct {
/// Configuration for JetKV. Encompasses all backends:
/// * valkey
/// * memory
/// * file
///
/// The Valkey backend is recommended for production deployment. `memory` and `file` can be
/// used in local development for convenience. All backends have a unified interface, i.e.
/// they can be swapped out without any code changes.
pub const Options = @import("kv/Store.zig").KVOptions;
// For backward compatibility - `jetzig.kv.Options` is preferred.
pub const KVOptions = Options;
/// General-purpose store. Use for storing data with no expiry.
pub const GeneralStore = @import("kv/Store.zig").Store(config.get(Store.Options, "store"));
/// Store ephemeral data.
pub const CacheStore = @import("kv/Store.zig").Store(config.get(Store.Options, "cache"));
/// Background job storage.
pub const JobQueueStore = @import("kv/Store.zig").Store(config.get(Store.Options, "job_queue"));
/// Generic store type. Create a custom store by passing `Options`, e.g.:
/// ```zig
/// var store = Generic(.{ .backend = .memory }).init(allocator, logger, .custom);
/// ```
pub const Generic = @import("kv/Store.zig").Store;
/// Role a given store fills. Used in log outputs.
pub const Role = @import("kv/Store.zig").Role;
};
pub const Store = @import("kv/Store.zig");

View File

@ -1,156 +1,87 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
const Store = @This();
store: jetzig.jetkv.JetKV,
options: KVOptions,
pub const KVOptions = struct {
backend: enum { memory, file, valkey } = .memory,
backend: enum { memory, file } = .memory,
file_options: struct {
path: ?[]const u8 = null,
address_space_size: u32 = jetzig.jetkv.FileBackend.addressSpace(4096),
address_space_size: u32 = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096),
truncate: bool = false,
} = .{},
valkey_options: struct {
host: []const u8 = "localhost",
port: u16 = 6379,
connect_timeout: u64 = 1000, // (ms)
read_timeout: u64 = 1000, // (ms)
connect: enum { auto, manual, lazy } = .lazy,
buffer_size: u32 = 8192,
pool_size: u16 = 8,
} = .{},
};
const ValueType = enum { string, array };
fn jetKVOptions(options: KVOptions) jetzig.jetkv.Options {
return switch (options.backend) {
.file => .{
.backend = .file,
.file_backend_options = .{
.path = options.file_options.path,
.address_space_size = options.file_options.address_space_size,
.truncate = options.file_options.truncate,
/// Initialize a new memory or file store.
pub fn init(allocator: std.mem.Allocator, options: KVOptions) !Store {
const store = try jetzig.jetkv.JetKV.init(
allocator,
switch (options.backend) {
.file => .{
.backend = .file,
.file_backend_options = .{
.path = options.file_options.path,
.address_space_size = options.file_options.address_space_size,
.truncate = options.file_options.truncate,
},
},
.memory => .{
.backend = .memory,
},
},
.memory => .{
.backend = .memory,
},
.valkey => .{
.backend = .valkey,
.valkey_backend_options = .{
.host = options.valkey_options.host,
.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 = @field(
jetzig.jetkv.ValkeyBackendOptions.ConnectMode,
@tagName(options.valkey_options.connect),
),
.buffer_size = options.valkey_options.buffer_size,
.pool_size = options.valkey_options.pool_size,
},
},
};
);
return .{ .store = store, .options = options };
}
/// Role a given store fills. Used in log outputs.
pub const Role = enum { jobs, cache, general, custom };
/// Free allocated resources/close database file.
pub fn deinit(self: *Store) void {
self.store.deinit();
}
pub fn Store(comptime options: KVOptions) type {
return struct {
const Self = @This();
/// Put a or into the key-value store.
pub fn put(self: *Store, key: []const u8, value: *jetzig.data.Value) !void {
try self.store.put(key, try value.toJson());
}
store: jetzig.jetkv.JetKV(jetKVOptions(options)),
logger: jetzig.loggers.Logger,
options: KVOptions,
role: Role,
/// Get a Value from the store.
pub fn get(self: *Store, data: *jetzig.data.Data, key: []const u8) !?*jetzig.data.Value {
return try parseValue(data, try self.store.get(data.allocator(), key));
}
/// Initialize a new memory or file store.
pub fn init(allocator: std.mem.Allocator, logger: jetzig.loggers.Logger, role: Role) !Self {
const store = try jetzig.jetkv.JetKV(jetKVOptions(options)).init(allocator);
/// Remove a Value to from the key-value store and return it if found.
pub fn fetchRemove(self: *Store, data: *jetzig.data.Data, key: []const u8) !?*jetzig.data.Value {
return try parseValue(data, try self.store.fetchRemove(data.allocator(), key));
}
return .{ .store = store, .role = role, .logger = logger, .options = options };
}
/// Remove a Value to from the key-value store.
pub fn remove(self: *Store, key: []const u8) !void {
try self.store.remove(key);
}
/// Free allocated resources/close database file.
pub fn deinit(self: *Self) void {
self.store.deinit();
}
/// Append a Value to the end of an Array in the key-value store.
pub fn append(self: *Store, key: []const u8, value: *const jetzig.data.Value) !void {
try self.store.append(key, try value.toJson());
}
/// Put a or into the key-value store.
pub fn put(self: *Self, key: []const u8, value: *jetzig.data.Value) !void {
try self.store.put(key, try value.toJson());
if (self.role == .cache) {
try self.logger.DEBUG(
"[cache:{s}:store] {s}",
.{ @tagName(self.store.backend), key },
);
}
}
/// Prepend a Value to the start of an Array in the key-value store.
pub fn prepend(self: *Store, key: []const u8, value: *const jetzig.data.Value) !void {
try self.store.prepend(key, try value.toJson());
}
/// Put a or into the key-value store with an expiration in seconds.
pub fn putExpire(self: *Self, key: []const u8, value: *jetzig.data.Value, expiration: i32) !void {
try self.store.putExpire(key, try value.toJson(), expiration);
if (self.role == .cache) {
try self.logger.DEBUG(
"[cache:{s}:store:expire:{d}s] {s}",
.{ @tagName(self.store.backend), expiration, key },
);
}
}
/// Pop a Value from an Array in the key-value store.
pub fn pop(self: *Store, data: *jetzig.data.Data, key: []const u8) !?*jetzig.data.Value {
return try parseValue(data, try self.store.pop(data.allocator(), key));
}
/// Get a Value from the store.
pub fn get(self: *Self, data: *jetzig.data.Data, key: []const u8) !?*jetzig.data.Value {
const start = std.time.nanoTimestamp();
const json = try self.store.get(data.allocator, key);
const value = try parseValue(data, json);
const end = std.time.nanoTimestamp();
if (self.role == .cache) {
if (value == null) {
try self.logger.DEBUG("[cache:miss] {s}", .{key});
} else {
try self.logger.DEBUG(
"[cache:{s}:hit:{}] {s}",
.{
@tagName(self.store.backend),
std.fmt.fmtDuration(@intCast(end - start)),
key,
},
);
}
}
return value;
}
/// Remove a Value to from the key-value store and return it if found.
pub fn fetchRemove(self: *Self, data: *jetzig.data.Data, key: []const u8) !?*jetzig.data.Value {
return try parseValue(data, try self.store.fetchRemove(data.allocator, key));
}
/// Remove a Value to from the key-value store.
pub fn remove(self: *Self, key: []const u8) !void {
try self.store.remove(key);
}
/// Append a Value to the end of an Array in the key-value store.
pub fn append(self: *Self, key: []const u8, value: *const jetzig.data.Value) !void {
try self.store.append(key, try value.toJson());
}
/// Prepend a Value to the start of an Array in the key-value store.
pub fn prepend(self: *Self, key: []const u8, value: *const jetzig.data.Value) !void {
try self.store.prepend(key, try value.toJson());
}
/// Pop a Value from an Array in the key-value store.
pub fn pop(self: *Self, data: *jetzig.data.Data, key: []const u8) !?*jetzig.data.Value {
return try parseValue(data, try self.store.pop(data.allocator, key));
}
/// Left-pop a Value from an Array in the key-value store.
pub fn popFirst(self: *Self, data: *jetzig.data.Data, key: []const u8) !?*jetzig.data.Value {
return try parseValue(data, try self.store.popFirst(data.allocator, key));
}
};
/// Left-pop a Value from an Array in the key-value store.
pub fn popFirst(self: *Store, data: *jetzig.data.Data, key: []const u8) !?*jetzig.data.Value {
return try parseValue(data, try self.store.popFirst(data.allocator(), key));
}
fn parseValue(data: *jetzig.data.Data, maybe_json: ?[]const u8) !?*jetzig.data.Value {

View File

@ -12,11 +12,6 @@ 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

@ -107,7 +107,7 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request)
if (request.middleware_rendered) |middleware| middleware.action else "",
if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.white ++ ":" else "",
if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.bright_cyan else "",
if (request.middleware_rendered) |_| @tagName(request.state) else "",
if (request.middleware_rendered) |_| if (request.redirected) "redirect" else "render" else "",
if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.reset else "",
if (request.middleware_rendered) |_| "]" else "",
request.path.path,
@ -166,16 +166,9 @@ const sql_tokens = .{
"VALUES",
};
const sql_statement_map = std.StaticStringMap(jetzig.colors.Color).initComptime(.{
.{ "SELECT", .blue },
.{ "DELETE", .red },
.{ "UPDATE", .yellow },
.{ "INSERT", .green },
});
fn printSql(self: *const DevelopmentLogger, sql: []const u8) !void {
const string_color = jetzig.colors.codes.escape ++ jetzig.colors.codes.green;
const identifier_color = jetzig.colors.codes.escape ++ jetzig.colors.codes.white;
const identifier_color = jetzig.colors.codes.escape ++ jetzig.colors.codes.yellow;
const reset_color = jetzig.colors.codes.escape ++ jetzig.colors.codes.reset;
var buf: [4096]u8 = undefined;
var stream = std.io.fixedBufferStream(&buf);
@ -228,16 +221,9 @@ fn printSql(self: *const DevelopmentLogger, sql: []const u8) !void {
try writer.print("{c}", .{sql[index]});
index += 1;
} else {
@setEvalBranchQuota(2000);
inline for (sql_tokens) |token| {
if (std.mem.startsWith(u8, sql[index..], token)) {
const formatted_token = if (sql_statement_map.get(token)) |color|
switch (color) {
inline else => |tag| jetzig.colors.bold(tag, token),
}
else
jetzig.colors.cyan(token);
try writer.print("{s}", .{formatted_token});
try writer.print(jetzig.colors.cyan(token), .{});
index += token.len;
break;
}

View File

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

View File

@ -86,7 +86,7 @@ pub fn logRequest(self: ProductionLogger, request: *const jetzig.http.Request) !
if (request.middleware_rendered) |_| ":" else "",
if (request.middleware_rendered) |middleware| middleware.action else "",
if (request.middleware_rendered) |_| ":" else "",
if (request.middleware_rendered) |_| @tagName(request.state) else "",
if (request.middleware_rendered) |_| if (request.redirected) "redirect" else "render" else "",
if (request.middleware_rendered) |_| "]" else "",
request.path.path,
}, .stdout);

View File

@ -3,7 +3,6 @@ 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,15 +46,14 @@ 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:
\\To: {?s}
\\{s}
,
.{ mail.params.get(.to), try mail.generateData() },
"Skipping mail delivery in development environment:\n{s}",
.{try mail.generateData()},
);
} else {
try mail.deliver();
try env.logger.INFO("Delivered mail to: {s}", .{mail.params.to.?});
try env.logger.INFO("Delivered mail to: {s}", .{
try std.mem.join(allocator, ", ", mail.params.to.?),
});
}
}
@ -70,32 +69,19 @@ fn resolveSubject(subject: ?*const jetzig.data.Value) ?[]const u8 {
}
}
fn resolveFrom(from: ?*const jetzig.data.Value) ?jetzig.mail.Address {
fn resolveFrom(from: ?*const jetzig.data.Value) ?[]const u8 {
return if (from) |capture| switch (capture.*) {
.null => null,
.string => |string| .{ .email = string.value },
.object => |object| .{
.email = object.getT(.string, "email") orelse return null,
.name = object.getT(.string, "name") orelse return null,
},
.string => |string| string.value,
else => unreachable,
} else null;
}
fn resolveTo(allocator: std.mem.Allocator, params: *const jetzig.data.Value) !?[]const jetzig.mail.Address {
var to = std.ArrayList(jetzig.mail.Address).init(allocator);
fn resolveTo(allocator: std.mem.Allocator, params: *const jetzig.data.Value) !?[]const []const u8 {
var to = std.ArrayList([]const u8).init(allocator);
if (params.get("to")) |capture| {
for (capture.items(.array)) |recipient| {
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);
try to.append(recipient.string.value);
}
}
return if (to.items.len > 0) try to.toOwnedSlice() else null;
@ -144,7 +130,7 @@ fn defaultHtml(
data.value = if (params.get("params")) |capture|
capture
else
try jetzig.zmpl.Data.createObject(data.allocator);
try jetzig.zmpl.Data.createObject(data.allocator());
try data.addConst("jetzig_view", data.string(""));
try data.addConst("jetzig_action", data.string(""));
return if (jetzig.zmpl.findPrefixed("mailers", mailer.html_template)) |template|
@ -162,7 +148,7 @@ fn defaultText(
data.value = if (params.get("params")) |capture|
capture
else
try jetzig.zmpl.Data.createObject(data.allocator);
try jetzig.zmpl.Data.createObject(data.allocator());
try data.addConst("jetzig_view", data.string(""));
try data.addConst("jetzig_action", data.string(""));
return if (jetzig.zmpl.findPrefixed("mailers", mailer.text_template)) |template|

View File

@ -30,16 +30,9 @@ 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 = .{ .address = self.params.from.?.email, .name = self.params.from.?.name },
.to = to,
.from = self.params.from.?,
.to = self.params.to.?,
.data = data,
}, try self.config.toSMTP(self.allocator, self.env));
}
@ -154,8 +147,8 @@ test "HTML part only" {
.config = .{},
.boundary = 123456789,
.params = .{
.from = .{ .name = "Bob", .email = "user@example.com" },
.to = &.{.{ .name = "Alice", .email = "user@example.com" }},
.from = "user@example.com",
.to = &.{"user@example.com"},
.subject = "Test subject",
.html = "<div>Hello</div>",
},
@ -165,7 +158,7 @@ test "HTML part only" {
defer std.testing.allocator.free(actual);
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
\\From: Bob <user@example.com>
\\From: user@example.com
\\Subject: Test subject
\\MIME-Version: 1.0
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
@ -190,8 +183,8 @@ test "text part only" {
.config = .{},
.boundary = 123456789,
.params = .{
.from = .{ .name = "Bob", .email = "user@example.com" },
.to = &.{.{ .name = "Alice", .email = "user@example.com" }},
.from = "user@example.com",
.to = &.{"user@example.com"},
.subject = "Test subject",
.text = "Hello",
},
@ -201,7 +194,7 @@ test "text part only" {
defer std.testing.allocator.free(actual);
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
\\From: Bob <user@example.com>
\\From: user@example.com
\\Subject: Test subject
\\MIME-Version: 1.0
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
@ -226,8 +219,8 @@ test "HTML and text parts" {
.config = .{},
.boundary = 123456789,
.params = .{
.from = .{ .name = "Bob", .email = "user@example.com" },
.to = &.{.{ .name = "Alice", .email = "user@example.com" }},
.from = "user@example.com",
.to = &.{"user@example.com"},
.subject = "Test subject",
.html = "<div>Hello</div>",
.text = "Hello",
@ -238,7 +231,7 @@ test "HTML and text parts" {
defer std.testing.allocator.free(actual);
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
\\From: Bob <user@example.com>
\\From: user@example.com
\\Subject: Test subject
\\MIME-Version: 1.0
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
@ -262,41 +255,6 @@ 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,
@ -304,8 +262,8 @@ test "long content encoding" {
.config = .{},
.boundary = 123456789,
.params = .{
.from = .{ .name = "Bob", .email = "user@example.com" },
.to = &.{.{ .name = "Alice", .email = "user@example.com" }},
.from = "user@example.com",
.to = &.{"user@example.com"},
.subject = "Test subject",
.html = "<html><body><div style=\"background-color: black; color: #ff00ff;\">Hellooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo!!!</div></body></html>",
.text = "Hello",
@ -316,7 +274,7 @@ test "long content encoding" {
defer std.testing.allocator.free(actual);
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
\\From: Bob <user@example.com>
\\From: user@example.com
\\Subject: Test subject
\\MIME-Version: 1.0
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
@ -354,8 +312,8 @@ test "non-latin alphabet encoding" {
.config = .{},
.boundary = 123456789,
.params = .{
.from = .{ .name = "Bob", .email = "user@example.com" },
.to = &.{.{ .name = "Alice", .email = "user@example.com" }},
.from = "user@example.com",
.to = &.{"user@example.com"},
.subject = "Test subject",
.html = "<html><body><div>你爱学习 Zig 吗?</div></body></html>",
@ -367,7 +325,7 @@ test "non-latin alphabet encoding" {
defer std.testing.allocator.free(actual);
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
\\From: Bob <user@example.com>
\\From: user@example.com
\\Subject: Test subject
\\MIME-Version: 1.0
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"

View File

@ -1,31 +1,22 @@
subject: ?[]const u8 = null,
from: ?Address = null,
to: ?[]const Address = null,
cc: ?[]const Address = null,
bcc: ?[]const Address = null, // TODO
from: ?[]const u8 = null,
to: ?[]const []const u8 = null,
cc: ?[]const []const u8 = null,
bcc: ?[]const []const u8 = null, // TODO
html: ?[]const u8 = null,
text: ?[]const u8 = null,
defaults: ?DefaultMailParams = null,
pub const DefaultMailParams = struct {
subject: ?[]const u8 = null,
from: ?Address = null,
to: ?[]const Address = null,
cc: ?[]const Address = null,
bcc: ?[]const Address = null, // TODO
from: ?[]const u8 = null,
to: ?[]const []const u8 = null,
cc: ?[]const []const u8 = null,
bcc: ?[]const []const u8 = 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(
@ -33,10 +24,10 @@ pub fn get(
comptime field: enum { subject, from, to, cc, bcc, html, text },
) ?switch (field) {
.subject => []const u8,
.from => Address,
.to => []const Address,
.cc => []const Address,
.bcc => []const Address,
.from => []const u8,
.to => []const []const u8,
.cc => []const []const u8,
.bcc => []const []const u8,
.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_ptr = null,
.default_value = 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(@errorReturnTrace(), err),
return request.server.logger.logError(err),
.deflate => jetzig.util.deflate(request.allocator, response.content, .{}) catch |err|
return request.server.logger.logError(@errorReturnTrace(), err),
return request.server.logger.logError(err),
};
response.headers.append("Content-Encoding", @tagName(encoding)) catch |err|
return request.server.logger.logError(@errorReturnTrace(), err);
return request.server.logger.logError(err);
// Make caching work
response.headers.append("Vary", "Accept-Encoding") catch |err|
return request.server.logger.logError(@errorReturnTrace(), err);
return request.server.logger.logError(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 @field(Encoding, field.name);
return std.enums.nameCast(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-Request")) |_| {
if (request.headers.get("HX-Target")) |target| {
try request.server.logger.DEBUG(
"[middleware-htmx] HX-Request header, disabling layout.",
.{},
"[middleware-htmx] htmx request detected, disabling layout. (#{s})",
.{target},
);
request.setLayout(null);
}
@ -19,21 +19,13 @@ 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 {
if (response.status_code != .moved_permanently and response.status_code != .found) return;
if (request.headers.get("HX-Request") == null) return;
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");
},
if (response.headers.get("Location")) |location| {
response.status_code = .ok;
request.response_data.reset();
try response.headers.append("HX-Redirect", location);
}
}

View File

@ -4,14 +4,13 @@ const jetzig = @import("../../jetzig.zig");
const httpz = @import("httpz");
const App = @This();
const MemoryStore = jetzig.kv.Store.Generic(.{ .backend = .memory });
allocator: std.mem.Allocator,
routes: []const jetzig.views.Route,
arena: *std.heap.ArenaAllocator,
store: *MemoryStore,
cache: *MemoryStore,
job_queue: *MemoryStore,
store: *jetzig.kv.Store,
cache: *jetzig.kv.Store,
job_queue: *jetzig.kv.Store,
multipart_boundary: ?[]const u8 = null,
logger: jetzig.loggers.Logger,
server: Server,
@ -55,15 +54,15 @@ 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,
.allocator = allocator,
.routes = &routes_module.routes,
.store = try createStore(arena.allocator(), logger, .general),
.cache = try createStore(arena.allocator(), logger, .cache),
.job_queue = try createStore(arena.allocator(), logger, .jobs),
.store = try createStore(arena.allocator()),
.cache = try createStore(arena.allocator()),
.job_queue = try createStore(arena.allocator()),
.logger = logger,
.server = .{ .logger = logger },
.repo = repo,
@ -78,7 +77,6 @@ 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();
@ -237,7 +235,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;
@ -347,12 +345,9 @@ fn stubbedRequest(
.route_data = null,
.middlewares = undefined,
.address = undefined,
.method = @field(httpz.Method, @tagName(method)),
.method = std.enums.nameCast(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 }
@ -396,17 +391,9 @@ fn multiFormKeyValue(allocator: std.mem.Allocator, max: usize) !*httpz.key_value
return key_value;
}
fn createStore(
allocator: std.mem.Allocator,
logger: jetzig.loggers.Logger,
role: jetzig.kv.Store.Role,
) !*MemoryStore {
const store = try allocator.create(MemoryStore);
store.* = try MemoryStore.init(
allocator,
logger,
role,
);
fn createStore(allocator: std.mem.Allocator) !*jetzig.kv.Store {
const store = try allocator.create(jetzig.kv.Store);
store.* = try jetzig.kv.Store.init(allocator, .{});
return store;
}

View File

@ -5,7 +5,7 @@ const view_types = @import("view_types.zig");
const Route = @This();
pub const Action = enum { index, get, new, edit, post, put, patch, delete, custom };
pub const Action = enum { index, get, new, post, put, patch, delete, custom };
pub const View = union(enum) {
with_id: view_types.ViewWithId,
@ -32,7 +32,6 @@ pub const Formats = struct {
index: ?[]const ResponseFormat = null,
get: ?[]const ResponseFormat = null,
new: ?[]const ResponseFormat = null,
edit: ?[]const ResponseFormat = null,
post: ?[]const ResponseFormat = null,
put: ?[]const ResponseFormat = null,
patch: ?[]const ResponseFormat = null,
@ -115,7 +114,6 @@ pub fn validateFormat(self: Route, request: *const jetzig.http.Request) bool {
.index => formats.index orelse return true,
.get => formats.get orelse return true,
.new => formats.new orelse return true,
.edit => formats.edit orelse return true,
.post => formats.post orelse return true,
.put => formats.put orelse return true,
.patch => formats.patch orelse return true,