Compare commits

..

8 Commits

Author SHA1 Message Date
Bob Farrell
72d6eebab8 WIP 2025-02-08 16:16:58 +00:00
Bob Farrell
960840895e WIP 2025-02-08 15:17:30 +00:00
Bob Farrell
136aed3d54 WIP 2025-02-08 15:16:40 +00:00
Bob Farrell
36f9d32ae6 WIP 2025-02-08 15:16:18 +00:00
Bob Farrell
a18ba77faf WIP 2025-02-08 15:16:18 +00:00
Bob Farrell
33358fa5fa WIP 2025-02-08 15:16:18 +00:00
Bob Farrell
342924b8c0 WIP 2025-02-08 15:16:16 +00:00
Bob Farrell
7b43e87399 WIP 2025-02-08 15:15:39 +00:00
46 changed files with 351 additions and 643 deletions

View File

@ -18,17 +18,18 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: 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 # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Setup Zig - 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 version
- run: zig env - run: zig env
@ -42,17 +43,7 @@ jobs:
- name: Run App Tests - name: Run App Tests
run: | run: |
cd demo cd demo
zig build -Denvironment=testing jetzig:database:create
zig build -Denvironment=testing jetzig:database:migrate
zig build -Denvironment=testing jetzig:test 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 - name: Build artifacts
if: ${{ matrix.os == 'ubuntu-latest' }} if: ${{ matrix.os == 'ubuntu-latest' }}

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/) 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. _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). Join us on Discord ! [https://discord.gg/eufqssz7X6](https://discord.gg/eufqssz7X6).

View File

@ -5,9 +5,6 @@ pub const GenerateMimeTypes = @import("src/GenerateMimeTypes.zig");
const zmpl_build = @import("zmpl"); const zmpl_build = @import("zmpl");
const Environment = enum { development, testing, production }; 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 { pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
@ -39,7 +36,7 @@ pub fn build(b: *std.Build) !void {
.{ .{
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
.use_llvm = b.option(bool, "use_llvm", "Use LLVM") orelse use_llvm_default, .use_llvm = b.option(bool, "use_llvm", "Use LLVM") orelse false,
.zmpl_templates_paths = templates_paths, .zmpl_templates_paths = templates_paths,
.zmpl_auto_build = false, .zmpl_auto_build = false,
.zmpl_markdown_fragments = try generateMarkdownFragments(b), .zmpl_markdown_fragments = try generateMarkdownFragments(b),
@ -67,15 +64,18 @@ pub fn build(b: *std.Build) !void {
const jetcommon_dep = b.dependency("jetcommon", .{ .target = target, .optimize = optimize }); const jetcommon_dep = b.dependency("jetcommon", .{ .target = target, .optimize = optimize });
const zmd_dep = b.dependency("zmd", .{ .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 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 // 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 // If we would do it the other way around, we would have to do
// b.dependency("jetzig",.{}).builder.dependency("zmpl",.{}).module("zmpl"); // b.dependency("jetzig",.{}).builder.dependency("zmpl",.{}).module("zmpl");
b.modules.put("zmpl", zmpl_dep.module("zmpl")) catch @panic("Out of memory"); 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("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("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("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"); 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", .{ const smtp_client_dep = b.dependency("smtp_client", .{
.target = target, .target = target,
@ -138,7 +138,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
const target = exe.root_module.resolved_target orelse @panic("Unable to detect compile target."); const target = exe.root_module.resolved_target orelse @panic("Unable to detect compile target.");
const optimize = exe.root_module.optimize orelse .Debug; const optimize = exe.root_module.optimize orelse .Debug;
exe.use_llvm = exe.use_llvm orelse use_llvm_default; exe.use_llvm = exe.use_llvm orelse (optimize != .Debug);
if (optimize != .Debug) exe.linkLibC(); if (optimize != .Debug) exe.linkLibC();
@ -180,6 +180,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
const jetzig_module = jetzig_dep.module("jetzig"); const jetzig_module = jetzig_dep.module("jetzig");
const zmpl_module = jetzig_dep.module("zmpl"); const zmpl_module = jetzig_dep.module("zmpl");
const zmd_module = jetzig_dep.module("zmd"); const zmd_module = jetzig_dep.module("zmd");
const pg_module = jetzig_dep.module("pg");
const jetquery_module = jetzig_dep.module("jetquery"); const jetquery_module = jetzig_dep.module("jetquery");
const jetcommon_module = jetzig_dep.module("jetcommon"); const jetcommon_module = jetzig_dep.module("jetcommon");
const jetquery_migrate_module = jetzig_dep.module("jetquery_migrate"); const jetquery_migrate_module = jetzig_dep.module("jetquery_migrate");
@ -194,6 +195,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
exe.root_module.addImport("jetzig", jetzig_module); exe.root_module.addImport("jetzig", jetzig_module);
exe.root_module.addImport("zmpl", zmpl_module); exe.root_module.addImport("zmpl", zmpl_module);
exe.root_module.addImport("zmd", zmd_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 (b.option(bool, "jetzig_runner", "Used internally by `jetzig server` command.")) |jetzig_runner| {
if (jetzig_runner) { if (jetzig_runner) {
@ -204,24 +206,24 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
} }
const root_path = if (b.build_root.path) |build_root_path| const root_path = if (b.build_root.path) |build_root_path|
try std.fs.path.join(b.allocator, &.{ build_root_path, "." }) try std.fs.path.join(b.allocator, &.{ build_root_path, "src" })
else else
try std.fs.cwd().realpathAlloc(b.allocator, "."); try std.fs.cwd().realpathAlloc(b.allocator, "src");
const templates_path: []const u8 = try std.fs.path.join( const templates_path: []const u8 = try std.fs.path.join(
b.allocator, b.allocator,
&[_][]const u8{ root_path, "src", "app" }, &[_][]const u8{ root_path, "app" },
); );
const views_path: []const u8 = try std.fs.path.join( const views_path: []const u8 = try std.fs.path.join(
b.allocator, b.allocator,
&[_][]const u8{ root_path, "src", "app", "views" }, &[_][]const u8{ root_path, "app", "views" },
); );
const jobs_path = try std.fs.path.join( const jobs_path = try std.fs.path.join(
b.allocator, b.allocator,
&[_][]const u8{ root_path, "src", "app", "jobs" }, &[_][]const u8{ root_path, "app", "jobs" },
); );
const mailers_path = try std.fs.path.join( const mailers_path = try std.fs.path.join(
b.allocator, b.allocator,
&[_][]const u8{ root_path, "src", "app", "mailers" }, &[_][]const u8{ root_path, "app", "mailers" },
); );
const exe_routes_file = b.addExecutable(.{ const exe_routes_file = b.addExecutable(.{
@ -238,7 +240,11 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
exe_routes_file.root_module.addImport("zmpl", zmpl_module); exe_routes_file.root_module.addImport("zmpl", zmpl_module);
const run_routes_file_cmd = b.addRunArtifact(exe_routes_file); const run_routes_file_cmd = b.addRunArtifact(exe_routes_file);
run_routes_file_cmd.has_side_effects = true;
const source_files = b.addUpdateSourceFiles();
const routes_file_path = run_routes_file_cmd.addOutputFileArg("routes.zig"); const routes_file_path = run_routes_file_cmd.addOutputFileArg("routes.zig");
source_files.addCopyFileToSource(routes_file_path, "src/routes.zig");
run_routes_file_cmd.addArgs(&.{ run_routes_file_cmd.addArgs(&.{
root_path, root_path,
b.pathFromRoot("src"), b.pathFromRoot("src"),
@ -248,9 +254,10 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
mailers_path, mailers_path,
}); });
const routes_module = b.createModule(.{ .root_source_file = routes_file_path }); const routes_module = b.createModule(.{ .root_source_file = b.path("src/routes.zig") });
routes_module.addImport("jetzig", jetzig_module); routes_module.addImport("jetzig", jetzig_module);
exe.root_module.addImport("routes", routes_module); exe.root_module.addImport("routes", routes_module);
exe.step.dependOn(&run_routes_file_cmd.step);
const exe_static_routes = b.addExecutable(.{ const exe_static_routes = b.addExecutable(.{
.name = "static", .name = "static",
@ -322,6 +329,12 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
run_tests_file_cmd.addFileArg(.{ .src_path = .{ .owner = b, .sub_path = sub_path } }); run_tests_file_cmd.addFileArg(.{ .src_path = .{ .owner = b, .sub_path = sub_path } });
} }
const jetzig_steps = jetzig_dep.builder.top_level_steps;
const jetzig_compile_step = jetzig_steps.get("compile").?;
const compile_step = b.step("compile", "Compile Zmpl templates");
compile_step.dependOn(&jetzig_compile_step.step);
jetzig_compile_step.step.dependOn(&source_files.step);
const exe_unit_tests = b.addTest(.{ const exe_unit_tests = b.addTest(.{
.root_source_file = tests_file_path, .root_source_file = tests_file_path,
.target = target, .target = target,
@ -408,9 +421,6 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
registerDatabaseSteps(b, exe_database); 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("jetzig", jetzig_module);
exe_routes.root_module.addImport("routes", routes_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);
@ -425,8 +435,6 @@ fn registerDatabaseSteps(b: *std.Build, exe_database: *std.Build.Step.Compile) v
.{ "create", "Create a database for your Jetzig app." }, .{ "create", "Create a database for your Jetzig app." },
.{ "drop", "Drop your Jetzig app's database." }, .{ "drop", "Drop your Jetzig app's database." },
.{ "reflect", "Read your app's database and generate a JetQuery schema." }, .{ "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| { inline for (commands) |command| {
@ -447,7 +455,7 @@ fn generateMarkdownFragments(b: *std.Build) ![]const u8 {
} }
}; };
const stat = try file.stat(); 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| { if (try getMarkdownFragmentsSource(b.allocator, source)) |markdown_fragments_source| {
return try std.fmt.allocPrint(b.allocator, return try std.fmt.allocPrint(b.allocator,
\\const std = @import("std"); \\const std = @import("std");
@ -468,10 +476,10 @@ fn getMarkdownFragmentsSource(allocator: std.mem.Allocator, source: [:0]const u8
for (ast.nodes.items(.tag), 0..) |tag, index| { for (ast.nodes.items(.tag), 0..) |tag, index| {
switch (tag) { switch (tag) {
.simple_var_decl => { .simple_var_decl => {
const decl = ast.simpleVarDecl(@enumFromInt(index)); const decl = ast.simpleVarDecl(@intCast(index));
const identifier = ast.tokenSlice(decl.ast.mut_token + 1); const identifier = ast.tokenSlice(decl.ast.mut_token + 1);
if (std.mem.eql(u8, identifier, "markdown_fragments")) { if (std.mem.eql(u8, identifier, "markdown_fragments")) {
return ast.getNodeSource(@enumFromInt(index)); return ast.getNodeSource(@intCast(index));
} }
}, },
else => continue, else => continue,

View File

@ -1,40 +1,48 @@
.{ .{
.name = .jetzig, .name = "jetzig",
.version = "0.0.0", .version = "0.0.0",
.fingerprint = 0x93ad8bfa2d209022,
.minimum_zig_version = "0.15.0-dev.355+206bd1ced",
.dependencies = .{ .dependencies = .{
.jetcommon = .{
.url = "https://github.com/jetzig-framework/jetcommon/archive/fb4edc13759d87bfcd9b1f5fcefdf93f8c9c62dd.tar.gz",
.hash = "jetcommon-0.1.0-jPY_DS1HAAAP8xp5HSWB_ZY7m9JEYUmm8adQFrse0lwB",
},
.zmd = .{ .zmd = .{
.url = "https://github.com/jetzig-framework/zmd/archive/d6c8aa9a9cde99674ccb096d8f94ed09cba8dab.tar.gz", .url = "https://github.com/jetzig-framework/zmd/archive/d6c8aa9a9cde99674ccb096d8f94ed09cba8dab.tar.gz",
.hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163", .hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163",
}, },
.smtp_client = .{ .jetkv = .{
.url = "https://github.com/karlseguin/smtp_client.zig/archive/5163c66cc42cdd93176a6b1cad45f3db3a291a6a.tar.gz", .url = "https://github.com/jetzig-framework/jetkv/archive/9d754e552e7569239a900ed9e0f313a0554ed2d3.tar.gz",
.hash = "smtp_client-0.0.1-AAAAAIJkAQCngHtRYVUMsMuncmicSHK_7ugwWibDzQ4S", .hash = "122013f8596bc615990fd7771c833cab4d2959ecac8d05c4f6c973aa46624e43afea",
}, },
.args = .{ .args = .{
.url = "https://github.com/bobf/zig-args/archive/88cbade9a517a4014824f8f53f3c48c8a0b2ffe1.tar.gz", .url = "https://github.com/ikskuh/zig-args/archive/968258dc1b1230493d8f1677097c832a3d7e0bd8.tar.gz",
.hash = "zig_args-0.0.0-jqtN6P_NAAC97fGpk9hS2K681jkiqPsWP6w3ucb_ctGH", .hash = "1220bdedf1a993d852d8aebcd63922a8fb163fac37b9c6ff72d187b2847a4a3a4248",
},
.jetkv = .{
.url = "https://github.com/jetzig-framework/jetkv/archive/5a94e3bac0a6e291efc9d6534beb2d311671ff17.tar.gz",
.hash = "jetkv-0.0.0-zCv0fmCGAgCyYqwHjk0P5KrYVRew1MJAtbtAcIO-WPpT",
}, },
.zmpl = .{ .zmpl = .{
.url = "https://github.com/jetzig-framework/zmpl/archive/c57fc9b83027e8c1459d9625c3509f59f0fb89f3.tar.gz", .url = "https://github.com/jetzig-framework/zmpl/archive/a249c7a34328d16a6e52f24d8c551f29b4f05ec0.tar.gz",
.hash = "zmpl-0.0.1-SYFGBgdqAwDeA6xm4KAhpKoNrWs5CMQK6x447zhWclCs", .hash = "1220454ad7214ff46f6893f95854e29721bdd66a2ec590ff0a214ce35d57131a9d28",
},
.httpz = .{
.url = "https://github.com/karlseguin/http.zig/archive/37d7cb9819b804ade5f4b974b82f8dd0622225ed.tar.gz",
.hash = "httpz-0.0.0-PNVzrEK4BgBpHQGA2m0RPqPGEjnTdDXHodBwzjYDrmps",
}, },
.jetquery = .{ .jetquery = .{
.url = "https://github.com/jetzig-framework/jetquery/archive/e1f969f2e3e0e1ad9cc30d56fde9739aa692fdc3.tar.gz", .path = "../jetquery",
.hash = "jetquery-0.0.0-TNf3zo2ABgBgcsIAvJ1Ud2B2zDzrBy9GQ31kKmTYZ7Ya", },
.jetcommon = .{
.url = "https://github.com/jetzig-framework/jetcommon/archive/5be57d534b3d469f5570cd4b373b8d61032b1b8b.tar.gz",
.hash = "122079c6ceb28fa93163c2f95e2f175bb8f93f3075fa34af63045671ab7dd824e756",
},
.pg = .{
.path = "../pg.zig",
},
.httpz = .{
.url = "https://github.com/karlseguin/http.zig/archive/a691d731047e9a5a79d71ac594cb8f5fad1d0705.tar.gz",
.hash = "122072c92285c8c44055eb45058b834d1e7ecd46a5704d58a207103c39fb5922b8f5",
},
.smtp_client = .{
.url = "https://github.com/karlseguin/smtp_client.zig/archive/5163c66cc42cdd93176a6b1cad45f3db3a291a6a.tar.gz",
.hash = "1220a7807b5161550cb0cba772689c4872bfeee8305a26c3cd0e12a8ccde1d546910",
},
.smtp_client = .{
.url = "https://github.com/karlseguin/smtp_client.zig/archive/5163c66cc42cdd93176a6b1cad45f3db3a291a6a.tar.gz",
.hash = "1220a7807b5161550cb0cba772689c4872bfeee8305a26c3cd0e12a8ccde1d546910",
},
.jetquery = .{
.url = "https://github.com/jetzig-framework/jetquery/archive/d4010cfd9ced2e7deb0f3a6cc64e2d32b8db95ba.tar.gz",
.hash = "122069eeb0d43931e49f93419bdb5930ac3a6bc35d1e977738fe872ecaac8ff32aec",
}, },
}, },

View File

@ -1,17 +1,15 @@
.{ .{
.name = .jetzig_cli, .name = "jetzig-cli",
.fingerprint = 0x73894a3e0616c96a,
.version = "0.0.0", .version = "0.0.0",
.minimum_zig_version = "0.12.0", .minimum_zig_version = "0.12.0",
.dependencies = .{ .dependencies = .{
.args = .{ .args = .{
.url = "https://github.com/bobf/zig-args/archive/88cbade9a517a4014824f8f53f3c48c8a0b2ffe1.tar.gz", .url = "https://github.com/ikskuh/zig-args/archive/968258dc1b1230493d8f1677097c832a3d7e0bd8.tar.gz",
.hash = "zig_args-0.0.0-jqtN6P_NAAC97fGpk9hS2K681jkiqPsWP6w3ucb_ctGH", .hash = "1220bdedf1a993d852d8aebcd63922a8fb163fac37b9c6ff72d187b2847a4a3a4248",
}, },
.jetquery = .{ .jetquery = .{
.url = "https://github.com/jetzig-framework/jetquery/archive/e1f969f2e3e0e1ad9cc30d56fde9739aa692fdc3.tar.gz", .path = "../../jetquery",
.hash = "jetquery-0.0.0-TNf3zo2ABgBgcsIAvJ1Ud2B2zDzrBy9GQ31kKmTYZ7Ya",
}, },
}, },
.paths = .{ .paths = .{

View File

@ -155,7 +155,7 @@ pub fn run(
.windows => {}, .windows => {},
else => std.debug.print("Bundle `bundle.tar.gz` generated successfully.", .{}), 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 { fn locateMarkdownFiles(allocator: std.mem.Allocator, dir: std.fs.Dir, views_path: []const u8, paths: *std.ArrayList([]const u8)) !void {

View File

@ -106,7 +106,7 @@ pub fn run(
try copySourceFile( try copySourceFile(
allocator, allocator,
install_dir, install_dir,
"demo/config/database_template.zig", "demo/config/database.zig",
"config/database.zig", "config/database.zig",
null, null,
); );
@ -261,7 +261,7 @@ fn copySourceFile(
util.printFailure(); util.printFailure();
return err; return err;
}; };
util.printSuccess(null); util.printSuccess();
} }
// Read a file from Jetzig source code. // Read a file from Jetzig source code.

View File

@ -4,12 +4,13 @@ const args = @import("args");
const util = @import("../util.zig"); const util = @import("../util.zig");
pub const watch_changes_pause_duration = 1 * 1000 * 1000 * 1000; pub const watch_changes_pause_duration = 100 * std.time.ns_per_ms;
/// Command line options for the `server` command. /// Command line options for the `server` command.
pub const Options = struct { pub const Options = struct {
reload: bool = true, reload: bool = true,
debug: bool = true, debug: bool = true,
incremental: bool = false,
pub const meta = .{ pub const meta = .{
.full_text = .full_text =
@ -19,10 +20,12 @@ pub const Options = struct {
\\ \\
\\ jetzig server \\ jetzig server
\\ jetzig server --reload=false --debug=false \\ jetzig server --reload=false --debug=false
\\ jetzig server --incremental=true
, ,
.option_docs = .{ .option_docs = .{
.reload = "Enable or disable automatic reload on update (default: true)", .reload = "Enable or disable automatic reload on update (default: true)",
.debug = "Enable or disable the development debug console (default: true)", .debug = "Enable or disable the development debug console (default: true)",
.incremental = "Enable or disable the incremental compilation (default: false) [experimental]",
}, },
}; };
}; };
@ -51,8 +54,6 @@ pub fn run(
const realpath = try std.fs.realpathAlloc(allocator, "."); const realpath = try std.fs.realpathAlloc(allocator, ".");
defer allocator.free(realpath); defer allocator.free(realpath);
var mtime = try totalMtime(allocator, cwd, "src");
std.debug.print( std.debug.print(
"Launching development server. [reload:{s}]\n", "Launching development server. [reload:{s}]\n",
.{ .{
@ -66,26 +67,51 @@ pub fn run(
try argv.appendSlice(&.{ try argv.appendSlice(&.{
"zig", "zig",
"build", "build",
"--watch",
});
if (options.incremental) try argv.append("-fincremental");
try argv.appendSlice(&.{
util.environmentBuildOption(main_options.options.environment), util.environmentBuildOption(main_options.options.environment),
"-Djetzig_runner=true", "-Djetzig_runner=true",
}); });
if (options.debug) try argv.append("-Ddebug_console=true"); if (options.debug) try argv.append("-Ddebug_console=true");
try argv.appendSlice(&.{ try argv.appendSlice(&.{
"install", "install",
"--color", "--color",
"on", "on",
}); });
while (true) { _ = cwd.createFile("src/routes.zig", .{}) catch {};
util.runCommandInDir(allocator, argv.items, .{ .path = realpath }, .{}) catch { var exe_path = try util.locateExecutable(allocator, cwd, .{});
std.debug.print("Build failed, waiting for file change...\n", .{}); var mtime = if (exe_path) |path| blk: {
try awaitFileChange(allocator, cwd, &mtime); const stat = try std.fs.cwd().statFile(path);
std.debug.print("Changes detected, restarting server...\n", .{}); break :blk stat.mtime;
continue; } else 0;
};
util.runCommandInDir(
allocator,
argv.items,
.{ .path = realpath },
.{ .output = .stream, .wait = false },
) catch {
std.debug.print("Build failed, waiting for file change...\n", .{});
std.process.exit(1);
};
var zmpl_thread = try std.Thread.spawn(
.{ .allocator = allocator },
zmplThread,
.{ allocator, cwd },
);
defer zmpl_thread.join();
while (true) {
exe_path = try util.locateExecutable(allocator, cwd, .{});
const exe_path = try util.locateExecutable(allocator, cwd, .{});
if (exe_path == null) { if (exe_path == null) {
std.debug.print("Unable to locate compiled executable. Exiting.\n", .{}); std.debug.print("Unable to locate compiled executable. Exiting.\n", .{});
std.process.exit(1); std.process.exit(1);
@ -112,21 +138,36 @@ pub fn run(
std.process.exit(term.Exited); std.process.exit(term.Exited);
} }
// HACK: This currenly doesn't restart the server when it exits, maybe that try awaitFileChange(exe_path.?, &mtime);
// could be implemented in the future.
try awaitFileChange(allocator, cwd, &mtime);
std.debug.print("Changes detected, restarting server...\n", .{}); std.debug.print("Changes detected, restarting server...\n", .{});
_ = try process.kill(); _ = try process.kill();
} }
} }
fn awaitFileChange(allocator: std.mem.Allocator, cwd: std.fs.Dir, mtime: *i128) !void { fn awaitFileChange(path: []const u8, mtime: *i128) !void {
while (true) { while (true) {
std.time.sleep(watch_changes_pause_duration); std.time.sleep(watch_changes_pause_duration);
const new_mtime = try totalMtime(allocator, cwd, "src"); const stat = try std.fs.cwd().statFile(path);
if (new_mtime > mtime.*) { if (stat.mtime > mtime.*) {
mtime.* = new_mtime; mtime.* = stat.mtime;
return;
}
}
}
fn zmplThread(allocator: std.mem.Allocator, cwd: std.fs.Dir) void {
while (true) {
awaitDirChange(allocator, cwd, "src/app/views") catch break;
util.runCommand(allocator, &.{ "zig", "build", "compile" }) catch break;
}
}
fn awaitDirChange(allocator: std.mem.Allocator, dir: std.fs.Dir, sub_path: []const u8) !void {
const mtime = try totalMtime(allocator, dir, sub_path);
while (true) {
std.time.sleep(watch_changes_pause_duration);
const new_mtime = try totalMtime(allocator, dir, sub_path);
if (new_mtime > mtime) {
return; return;
} }
} }
@ -143,12 +184,12 @@ fn totalMtime(allocator: std.mem.Allocator, cwd: std.fs.Dir, sub_path: []const u
while (try walker.next()) |entry| { while (try walker.next()) |entry| {
if (entry.kind != .file) continue; if (entry.kind != .file) continue;
const extension = std.fs.path.extension(entry.path); // const extension = std.fs.path.extension(entry.path);
if (std.mem.eql(u8, extension, ".zig") or std.mem.eql(u8, extension, ".zmpl")) { // if (std.mem.eql(u8, extension, ".zig") or std.mem.eql(u8, extension, ".zmpl")) {
const stat = try dir.statFile(entry.path); const stat = try dir.statFile(entry.path);
sum += stat.mtime; sum += stat.mtime;
} // }
} }
return sum; return sum;

View File

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

View File

@ -23,8 +23,8 @@ const icons = .{
}; };
/// Print a success confirmation. /// Print a success confirmation.
pub fn printSuccess(message: ?[]const u8) void { pub fn printSuccess() void {
std.debug.print(" " ++ icons.check ++ " {s}\n", .{message orelse ""}); std.debug.print(" " ++ icons.check ++ "\n", .{});
} }
/// Print a failure confirmation. /// Print a failure confirmation.
@ -198,7 +198,7 @@ pub fn runCommandInDir(allocator: std.mem.Allocator, argv: []const []const u8, d
try child.spawn(); try child.spawn();
switch (options.output) { switch (options.output) {
.capture => try collectOutput(child, allocator, &stdout, &stderr), .capture => try child.collectOutput(allocator, &stdout, &stderr, 50 * 1024),
.stream => {}, .stream => {},
} }
@ -222,57 +222,10 @@ pub fn runCommandInDir(allocator: std.mem.Allocator, argv: []const []const u8, d
} }
return error.JetzigCommandError; return error.JetzigCommandError;
} else { } else {
const message = try std.mem.join(allocator, " ", argv); printSuccess();
defer allocator.free(message);
printSuccess(message);
} }
} }
fn collectOutput(
child: std.process.Child,
allocator: std.mem.Allocator,
stdout: *std.ArrayListUnmanaged(u8),
stderr: *std.ArrayListUnmanaged(u8),
) !void {
const max_output_bytes = 50 * 1024;
std.debug.assert(child.stdout_behavior == .Pipe);
std.debug.assert(child.stderr_behavior == .Pipe);
var poller = std.io.poll(allocator, enum { stdout, stderr }, .{
.stdout = child.stdout.?,
.stderr = child.stderr.?,
});
defer poller.deinit();
std.debug.print("(working) .", .{});
while (try poller.poll()) {
if (poller.fifo(.stdout).count > max_output_bytes)
return error.StdoutStreamTooLong;
if (poller.fifo(.stderr).count > max_output_bytes)
return error.StderrStreamTooLong;
std.debug.print(".", .{});
}
std.debug.print(" (done)\n", .{});
try writeFifoDataToArrayList(allocator, stdout, poller.fifo(.stdout));
try writeFifoDataToArrayList(allocator, stderr, poller.fifo(.stderr));
}
// Borrowed from `std.process.Child.writeFifoDataToArrayList` - non-public but needed in our
// modified `collectOutput`
fn writeFifoDataToArrayList(allocator: std.mem.Allocator, list: *std.ArrayListUnmanaged(u8), fifo: *std.io.PollFifo) !void {
if (fifo.head != 0) fifo.realign();
if (list.capacity == 0) {
list.* = .{
.items = fifo.buf[0..fifo.count],
.capacity = fifo.buf.len,
};
fifo.* = std.io.PollFifo.init(fifo.allocator);
} else {
try list.appendSlice(allocator, fifo.buf[0..fifo.count]);
}
}
/// Generate a full GitHub URL for passing to `zig fetch`. /// Generate a full GitHub URL for passing to `zig fetch`.
pub fn githubUrl(allocator: std.mem.Allocator) ![]const u8 { pub fn githubUrl(allocator: std.mem.Allocator) ![]const u8 {
var client = std.http.Client{ .allocator = allocator }; var client = std.http.Client{ .allocator = allocator };

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", .version = "0.0.0",
.minimum_zig_version = "0.12.0", .minimum_zig_version = "0.12.0",
.fingerprint = 0x3877c19710a92a5c,
.dependencies = .{ .dependencies = .{
.jetzig = .{ .jetzig = .{
.path = "../", .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 = .{ pub const database = .{
// Null adapter fails when a database call is invoked.
.development = .{ .development = .{
.adapter = .postgresql, .adapter = .null,
.username = "postgres",
.password = "postgres",
.hostname = "localhost",
.database = "jetzig_demo_dev",
.port = 14173, // See `compose.yml`
}, },
// This configuration is used for CI
// in GitHub
.testing = .{ .testing = .{
.adapter = .postgresql, .adapter = .null,
.database = "jetzig_demo_test",
}, },
.production = .{
.adapter = .null,
},
// PostgreSQL adapter configuration.
//
// All options except `adapter` can be configured using environment variables:
//
// * JETQUERY_HOSTNAME
// * JETQUERY_PORT
// * JETQUERY_USERNAME
// * JETQUERY_PASSWORD
// * JETQUERY_DATABASE
//
// .testing = .{
// .adapter = .postgresql,
// .hostname = "localhost",
// .port = 5432,
// .username = "postgres",
// .password = "password",
// .database = "myapp_testing",
// },
//
// .development = .{
// .adapter = .postgresql,
// .hostname = "localhost",
// .port = 5432,
// .username = "postgres",
// .password = "password",
// .database = "myapp_development",
// },
//
// .production = .{
// .adapter = .postgresql,
// .hostname = "localhost",
// .port = 5432,
// .username = "postgres",
// .password = "password",
// .database = "myapp_production",
// },
}; };

View File

@ -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"); const jetquery = @import("jetquery");
pub fn up(repo: anytype) !void { pub fn up(repo: *jetquery.Repo) !void {
_ = repo; _ = repo;
} }
pub fn down(repo: anytype) !void { pub fn down(repo: *jetquery.Repo) !void {
_ = repo; _ = 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

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

View File

@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/prism.css" /> <link rel="stylesheet" href="/prism.css" />
<meta http-equiv="refresh" content="1" />
</head> </head>
<body> <body>

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

@ -5,13 +5,14 @@ const importedFunction = @import("../lib/example.zig").exampleFunction;
pub const layout = "application"; pub const layout = "application";
pub fn index(request: *jetzig.Request) !jetzig.View { pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
var root = try request.data(.object); var root = try data.object();
try root.put("message", "Welcome to Jetzig!"); try root.put("message", "Welcome to Jetzig!");
try root.put("custom_number", customFunction(100, 200, 300)); try root.put("custom_number", customFunction(100, 200, 300));
try root.put("imported_number", importedFunction(100, 200, 300)); try root.put("imported_number", importedFunction(100, 200, 300));
try request.response.headers.append("x-example-header", "example header value"); try request.response.headers.append("x-example-header", "example header value");
try request.server.logger.INFO("data", .{});
return request.render(.ok); return request.render(.ok);
} }

View File

@ -1,5 +1,4 @@
<div class="text-center pt-10 m-auto"> <div class="text-center pt-10 m-auto">
<div><img class="p-3 mx-auto" src="/jetzig.png" /></div> <div><img class="p-3 mx-auto" src="/jetzig.png" /></div>
<!-- Renders `src/app/views/root/_quotes.zmpl`: --> <!-- Renders `src/app/views/root/_quotes.zmpl`: -->

View File

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

View File

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

View File

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

View File

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

View File

@ -62,7 +62,7 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
var log_thread = try std.Thread.spawn( var log_thread = try std.Thread.spawn(
.{ .allocator = self.allocator }, .{ .allocator = self.allocator },
jetzig.loggers.LogQueue.Reader.publish, jetzig.loggers.LogQueue.Reader.publish,
.{ &self.env.log_queue.reader, jetzig.loggers.LogQueue.Reader.PublishOptions{} }, .{ &self.env.log_queue.reader, .{} },
); );
defer log_thread.join(); defer log_thread.join();

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| const env_file = std.fs.cwd().openFile(options.options.@"env-file", .{}) catch |err|
switch (err) { switch (err) {
error.FileNotFound => null, error.FileNotFound => null,
else => return err, else => return err,
}; };
const vars = try Vars.init(allocator, env_file); const vars = try Vars.init(allocator, env_file);
var launch_logger = LaunchLogger{ var launch_logger = LaunchLogger{
.stdout = stdout.file, .stdout = stdout,
.stderr = stderr.file, .stderr = stderr,
.silent = env_options.silent, .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( .development_logger = jetzig.loggers.DevelopmentLogger.init(
allocator, allocator,
resolveLogLevel(options.options.@"log-level", jetzig.environment), resolveLogLevel(options.options.@"log-level", jetzig.environment),
stdout.file, stdout,
stderr.file, stderr,
), ),
}, },
.production => jetzig.loggers.Logger{ .production => jetzig.loggers.Logger{
@ -304,26 +304,23 @@ pub fn deinit(self: Environment) void {
self.parent_allocator.destroy(self.arena); 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) { const path = switch (stream) {
.stdout => options.log, .stdout => options.log,
.stderr => options.@"log-error", .stderr => options.@"log-error",
}; };
if (std.mem.eql(u8, path, "-")) return switch (stream) { 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, "-")) .stderr => if (std.mem.eql(u8, options.log, "-"))
.{ .file = std.io.getStdErr(), .sync = false } std.io.getStdErr()
else else
.{ try std.fs.createFileAbsolute(options.log, .{ .truncate = false }),
.file = try std.fs.createFileAbsolute(options.log, .{ .truncate = false }),
.sync = true,
},
}; };
const file = try std.fs.createFileAbsolute(path, .{ .truncate = false }); const file = try std.fs.createFileAbsolute(path, .{ .truncate = false });
try file.seekFromEnd(0); try file.seekFromEnd(0);
return .{ .file = file, .sync = true }; return file;
} }
fn getSecret( fn getSecret(

View File

@ -14,7 +14,7 @@ pub fn getUserId(comptime id_type: IdType, request: *jetzig.Request) !?switch (i
} { } {
const session = try request.session(); 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 { 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 { pub fn hashPassword(allocator: std.mem.Allocator, password: []const u8) ![]const u8 {
var buf: [128]u8 = undefined; const buf = try allocator.alloc(u8, 128);
const hash = try std.crypto.pwhash.argon2.strHash( return try std.crypto.pwhash.argon2.strHash(
password, password,
.{ .{
.allocator = allocator, .allocator = allocator,
.params = .{ .t = 3, .m = 32, .p = 4 }, .params = .{ .t = 3, .m = 32, .p = 4 },
}, },
&buf, buf,
); );
const result = try allocator.alloc(u8, hash.len);
@memcpy(result, hash);
return result;
} }

View File

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

View File

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

View File

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

View File

@ -497,14 +497,12 @@ pub fn cookies(self: *Request) !*jetzig.http.Cookies {
/// `jetzig.http.Session`. /// `jetzig.http.Session`.
pub fn session(self: *Request) !*jetzig.http.Session { pub fn session(self: *Request) !*jetzig.http.Session {
if (self._session) |capture| return capture; 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); const local_session = try self.allocator.create(jetzig.http.Session);
local_session.* = jetzig.http.Session.init( local_session.* = jetzig.http.Session.init(
self.allocator, self.allocator,
try self.cookies(), try self.cookies(),
self.server.env.secret, self.server.env.secret,
.{ .cookie_name = cookie_name },
); );
local_session.parse() catch |err| { local_session.parse() catch |err| {
switch (err) { switch (err) {
@ -584,17 +582,17 @@ const RequestMail = struct {
var mail_job = try self.request.job("__jetzig_mail"); var mail_job = try self.request.job("__jetzig_mail");
try mail_job.params.put("mailer_name", self.name); 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("from", self.mail_params.from);
var to_array = try mail_job.data.array(); var to_array = try mail_job.data.array();
if (self.mail_params.get(.to)) |to| { if (self.mail_params.to) |to| {
for (to) |each| try to_array.append(.{ .email = each.email, .name = each.name }); for (to) |each| try to_array.append(each);
} }
try mail_job.params.put("to", to_array); try mail_job.params.put("to", to_array);
try mail_job.params.put("subject", self.mail_params.get(.subject)); try mail_job.params.put("subject", self.mail_params.subject);
try mail_job.params.put("html", self.mail_params.get(.html)); try mail_job.params.put("html", self.mail_params.html);
try mail_job.params.put("text", self.mail_params.get(.text)); try mail_job.params.put("text", self.mail_params.text);
if (self.request.response_data.value) |value| try mail_job.params.put( if (self.request.response_data.value) |value| try mail_job.params.put(
"params", "params",

View File

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

View File

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

View File

@ -12,11 +12,6 @@ pub const NullLogger = @import("loggers/NullLogger.zig");
pub const LogQueue = @import("loggers/LogQueue.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 LogLevel = enum(u4) { TRACE, DEBUG, INFO, WARN, ERROR, FATAL };
pub const LogFormat = enum { development, production, json, null }; pub const LogFormat = enum { development, production, json, null };

View File

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

View File

@ -46,11 +46,8 @@ 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")) { if (env.environment == .development and !jetzig.config.get(bool, "force_development_email_delivery")) {
try env.logger.INFO( try env.logger.INFO(
\\Skipping mail delivery in development environment: "Skipping mail delivery in development environment:\n{s}",
\\To: {?s} .{try mail.generateData()},
\\{s}
,
.{ mail.params.get(.to), try mail.generateData() },
); );
} else { } else {
try mail.deliver(); try mail.deliver();
@ -90,8 +87,8 @@ fn resolveTo(allocator: std.mem.Allocator, params: *const jetzig.data.Value) !?[
.null => null, .null => null,
.string => |string| .{ .email = string.value }, .string => |string| .{ .email = string.value },
.object => |object| .{ .object => |object| .{
.email = object.getT(.string, "email") orelse return error.JetzigMissingEmailField, .email = object.getT(.string, "email") orelse return null,
.name = object.getT(.string, "name"), .name = object.getT(.string, "name") orelse return null,
}, },
else => unreachable, else => unreachable,
}; };

View File

@ -165,7 +165,7 @@ test "HTML part only" {
defer std.testing.allocator.free(actual); defer std.testing.allocator.free(actual);
const expected = try std.mem.replaceOwned(u8, std.testing.allocator, const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
\\From: Bob <user@example.com> \\From: <Bob> user@example.com
\\Subject: Test subject \\Subject: Test subject
\\MIME-Version: 1.0 \\MIME-Version: 1.0
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789" \\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
@ -201,7 +201,7 @@ test "text part only" {
defer std.testing.allocator.free(actual); defer std.testing.allocator.free(actual);
const expected = try std.mem.replaceOwned(u8, std.testing.allocator, const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
\\From: Bob <user@example.com> \\From: <Bob> user@example.com
\\Subject: Test subject \\Subject: Test subject
\\MIME-Version: 1.0 \\MIME-Version: 1.0
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789" \\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
@ -238,7 +238,7 @@ test "HTML and text parts" {
defer std.testing.allocator.free(actual); defer std.testing.allocator.free(actual);
const expected = try std.mem.replaceOwned(u8, std.testing.allocator, const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
\\From: Bob <user@example.com> \\From: <Bob> user@example.com
\\Subject: Test subject \\Subject: Test subject
\\MIME-Version: 1.0 \\MIME-Version: 1.0
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789" \\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
@ -262,41 +262,6 @@ test "HTML and text parts" {
try std.testing.expectEqualStrings(expected, actual); 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" { test "long content encoding" {
const mail = Mail{ const mail = Mail{
.allocator = std.testing.allocator, .allocator = std.testing.allocator,
@ -316,7 +281,7 @@ test "long content encoding" {
defer std.testing.allocator.free(actual); defer std.testing.allocator.free(actual);
const expected = try std.mem.replaceOwned(u8, std.testing.allocator, const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
\\From: Bob <user@example.com> \\From: <Bob> user@example.com
\\Subject: Test subject \\Subject: Test subject
\\MIME-Version: 1.0 \\MIME-Version: 1.0
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789" \\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
@ -367,7 +332,7 @@ test "non-latin alphabet encoding" {
defer std.testing.allocator.free(actual); defer std.testing.allocator.free(actual);
const expected = try std.mem.replaceOwned(u8, std.testing.allocator, const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
\\From: Bob <user@example.com> \\From: <Bob> user@example.com
\\Subject: Test subject \\Subject: Test subject
\\MIME-Version: 1.0 \\MIME-Version: 1.0
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789" \\Content-Type: multipart/alternative; boundary="=_alternative_123456789"

View File

@ -22,7 +22,7 @@ pub const Address = struct {
email: []const u8, email: []const u8,
pub fn format(address: Address, _: anytype, _: anytype, writer: anytype) !void { pub fn format(address: Address, _: anytype, _: anytype, writer: anytype) !void {
try writer.print("{s} <{s}>", .{ address.name orelse address.email, address.email }); try writer.print("<{?s}> {s}", .{ address.name, address.email });
} }
}; };

View File

@ -28,17 +28,17 @@ pub fn beforeResponse(request: *jetzig.http.Request, response: *jetzig.http.Resp
const compressed = switch (encoding) { const compressed = switch (encoding) {
.gzip => jetzig.util.gzip(request.allocator, response.content, .{}) catch |err| .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| .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| 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 // Make caching work
response.headers.append("Vary", "Accept-Encoding") catch |err| response.headers.append("Vary", "Accept-Encoding") catch |err|
return request.server.logger.logError(@errorReturnTrace(), err); return request.server.logger.logError(err);
response.content = compressed; response.content = compressed;
} }
@ -48,9 +48,9 @@ fn detectEncoding(request: *const jetzig.http.Request) ?Encoding {
while (headers_it.next()) |header| { while (headers_it.next()) |header| {
var it = std.mem.tokenizeScalar(u8, header.value, ','); var it = std.mem.tokenizeScalar(u8, header.value, ',');
while (it.next()) |param| { 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))) { 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 /// request doesn't come via htmx and, when the request does come from htmx, only return the
/// content rendered directly by the view function. /// content rendered directly by the view function.
pub fn afterRequest(request: *jetzig.http.Request) !void { 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( try request.server.logger.DEBUG(
"[middleware-htmx] HX-Request header, disabling layout.", "[middleware-htmx] htmx request detected, disabling layout. (#{s})",
.{}, .{target},
); );
request.setLayout(null); request.setLayout(null);
} }
@ -19,21 +19,17 @@ pub fn afterRequest(request: *jetzig.http.Request) !void {
/// If a redirect was issued during request processing, reset any response data, set response /// 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. /// 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 { pub fn beforeResponse(request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
switch (response.status_code) {
.moved_permanently, .found => {},
else => return,
}
if (request.headers.get("HX-Request") == null) return; if (request.headers.get("HX-Request") == null) return;
switch (response.status_code) { if (response.headers.get("Location")) |location| {
.moved_permanently, .found => { response.status_code = .ok;
if (response.headers.get("Location")) |location| { request.response_data.reset();
response.status_code = .ok; try response.headers.append("HX-Redirect", location);
request.response_data.reset();
try response.headers.append("HX-Redirect", location);
}
},
else => {
try response.headers.append("Vary", "HX-Request");
},
} }
} }

View File

@ -55,7 +55,7 @@ pub fn init(allocator: std.mem.Allocator, routes_module: type) !App {
try cookies.parse(); try cookies.parse();
const session = try alloc.create(jetzig.http.Session); 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{ app.* = App{
.arena = arena, .arena = arena,
@ -78,7 +78,6 @@ pub fn init(allocator: std.mem.Allocator, routes_module: type) !App {
/// Free allocated resources for test app. /// Free allocated resources for test app.
pub fn deinit(self: *App) void { pub fn deinit(self: *App) void {
self.repo.deinit();
self.arena.deinit(); self.arena.deinit();
self.allocator.destroy(self.arena); self.allocator.destroy(self.arena);
if (self.logger.test_logger.file) |file| file.close(); if (self.logger.test_logger.file) |file| file.close();
@ -237,7 +236,7 @@ pub fn initSession(self: *App) !void {
const allocator = self.arena.allocator(); const allocator = self.arena.allocator();
var local_session = try allocator.create(jetzig.http.Session); 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(); try local_session.parse();
self.session = local_session; self.session = local_session;
@ -347,7 +346,7 @@ fn stubbedRequest(
.route_data = null, .route_data = null,
.middlewares = undefined, .middlewares = undefined,
.address = undefined, .address = undefined,
.method = @field(httpz.Method, @tagName(method)), .method = std.enums.nameCast(httpz.Method, @tagName(method)),
.protocol = .HTTP11, .protocol = .HTTP11,
.params = undefined, .params = undefined,
.conn = undefined, .conn = undefined,