forked from jetzig-mirror/jetzig
Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1feb18fb74 | ||
|
|
86d82026ab | ||
|
|
01d0862043 | ||
|
|
95d3f10bc9 | ||
|
|
dee5701b4a | ||
|
|
b7c3c0045a | ||
|
|
6d9fa8bff4 | ||
|
|
4bd9d8b990 | ||
|
|
8171ab5b5d | ||
|
|
13907aad7c | ||
|
|
aae9fd182b | ||
|
|
4c568f2606 | ||
|
|
94cf122847 | ||
|
|
b32d24ad80 | ||
|
|
3fd42f2ebb | ||
|
|
ace82985ef | ||
|
|
e96a3d36b6 | ||
|
|
dc982cf125 | ||
|
|
6d522c07c5 | ||
|
|
c26d563898 | ||
|
|
2c52792217 | ||
|
|
327b0390de | ||
|
|
2cb1b4e959 | ||
|
|
a46e944499 | ||
|
|
79574ed861 | ||
|
|
00ed3067dd | ||
|
|
aae890998f | ||
|
|
21bf237437 | ||
|
|
b81c6dfe91 | ||
|
|
9e4e8529c5 | ||
|
|
359b4fc2eb | ||
|
|
70a8b0f698 | ||
|
|
f94db09551 | ||
|
|
46a311174f | ||
|
|
48e7139e26 | ||
|
|
c6ce935ce1 | ||
|
|
22688ccaa3 | ||
|
|
f3788567bb | ||
|
|
58f7e0e76a | ||
|
|
b070d281e3 | ||
|
|
9012ec3aaf | ||
|
|
182ceee17f | ||
|
|
034ecddfa0 | ||
|
|
19e17bc447 | ||
|
|
d8a992ce38 | ||
|
|
b8af924f8e | ||
|
|
29612e463f | ||
|
|
23d0102ae8 | ||
|
|
544b33e279 | ||
|
|
d3157e4247 | ||
|
|
501c6184a5 | ||
|
|
d6226bd4ef | ||
|
|
7eea1e3f0a | ||
|
|
3418698220 | ||
|
|
9c4b6198cd | ||
|
|
06674db51f | ||
|
|
099a2a5349 | ||
|
|
c0d6fb162b | ||
|
|
a16e1c8716 | ||
|
|
1dfe58fb64 | ||
|
|
719bd300a7 | ||
|
|
8a4f91f26a | ||
|
|
0a4690e001 | ||
|
|
da2978ed04 | ||
|
|
fb969d9b81 | ||
|
|
492c1833b2 | ||
|
|
87c7adb50f | ||
|
|
ae356c0ebf | ||
|
|
ecd66db3dc | ||
|
|
b71e738ec0 | ||
|
|
1ccaa45d34 | ||
|
|
492ac4a999 | ||
|
|
a5430ff380 | ||
|
|
1b5c72b75f | ||
|
|
6223f256d8 | ||
|
|
a99999af52 | ||
|
|
b5ae4ec181 | ||
|
|
5ebb60d759 | ||
|
|
350f9512e6 | ||
|
|
413fd30aee | ||
|
|
76f14a89a1 | ||
|
|
475ed26952 | ||
|
|
35adaf3009 | ||
|
|
31f8a4a93c | ||
|
|
be8e517a06 | ||
|
|
6cabafc63f | ||
|
|
63cd638149 | ||
|
|
f9d8ef3776 | ||
|
|
2f7687a0e5 | ||
|
|
7b03850593 | ||
|
|
9cf6705c88 | ||
|
|
22badde8c0 | ||
|
|
e8ce232700 | ||
|
|
50bcda1cbe | ||
|
|
ac600326a9 | ||
|
|
6075708bf4 | ||
|
|
54480a7b39 | ||
|
|
e27fc06d8c | ||
|
|
6dbeed7109 | ||
|
|
c91eda51d3 | ||
|
|
a3e3cb6dfe | ||
|
|
1fa6db77df | ||
|
|
b4b7251021 | ||
|
|
8ffbbbbf6a | ||
|
|
4e1a26a5f9 | ||
|
|
6bd0273e2d | ||
|
|
937efd9121 | ||
|
|
f44a6b33c5 | ||
|
|
835885a947 | ||
|
|
773eb1dc6e | ||
|
|
9684181c64 | ||
|
|
1565ae3b73 | ||
|
|
8095bbcb76 | ||
|
|
036aec1682 | ||
|
|
130a7c81a8 | ||
|
|
b1becfd6ec | ||
|
|
7f71d1d6da |
23
.github/workflows/CI.yml
vendored
23
.github/workflows/CI.yml
vendored
@ -15,23 +15,20 @@ 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
|
||||
# You may pin to the exact commit or the version.
|
||||
# uses: goto-bus-stop/setup-zig@41ae19e72e21b9a1380e86ff9f058db709fc8fc6
|
||||
uses: goto-bus-stop/setup-zig@v2
|
||||
with:
|
||||
version: master
|
||||
cache: true # Let's see how this behaves
|
||||
uses: mlugg/setup-zig@main
|
||||
|
||||
- run: zig version
|
||||
- run: zig env
|
||||
@ -45,7 +42,17 @@ jobs:
|
||||
- name: Run App Tests
|
||||
run: |
|
||||
cd demo
|
||||
zig build -Denvironment=testing jetzig:database:create
|
||||
zig build -Denvironment=testing jetzig:database:migrate
|
||||
zig build -Denvironment=testing jetzig:test
|
||||
env:
|
||||
JETQUERY_HOSTNAME: 'localhost'
|
||||
JETQUERY_USERNAME: 'postgres'
|
||||
JETQUERY_PASSWORD: 'postgres'
|
||||
JETQUERY_DATABASE: 'jetzig_demo_test'
|
||||
# Assume a small amount of connections are allowed
|
||||
# into postgres
|
||||
JETQUERY_POOL_SIZE: 2
|
||||
|
||||
- name: Build artifacts
|
||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
|
||||
21
LICENSE-Zig
Normal file
21
LICENSE-Zig
Normal file
@ -0,0 +1,21 @@
|
||||
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.
|
||||
@ -6,6 +6,8 @@ _Jetzig_ is a web framework written in 100% pure [Zig](https://ziglang.org) :liz
|
||||
|
||||
Official website: [jetzig.dev](https://www.jetzig.dev/)
|
||||
|
||||
Please note that _Jetzig_'s `main` branch aims to be compatible with the latest [Zig nightly master build](https://ziglang.org/download/) and older versions of _Zig_ are not supported.
|
||||
|
||||
_Jetzig_ aims to provide a rich set of user-friendly tools for building modern web applications quickly. See the checklist below.
|
||||
|
||||
Join us on Discord ! [https://discord.gg/eufqssz7X6](https://discord.gg/eufqssz7X6).
|
||||
@ -20,6 +22,7 @@ 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
|
||||
|
||||
@ -57,4 +60,5 @@ 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)
|
||||
|
||||
62
build.zig
62
build.zig
@ -5,6 +5,9 @@ pub const GenerateMimeTypes = @import("src/GenerateMimeTypes.zig");
|
||||
|
||||
const zmpl_build = @import("zmpl");
|
||||
const Environment = enum { development, testing, production };
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const use_llvm_default = builtin.os.tag != .linux;
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
@ -36,6 +39,7 @@ pub fn build(b: *std.Build) !void {
|
||||
.{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.use_llvm = b.option(bool, "use_llvm", "Use LLVM") orelse use_llvm_default,
|
||||
.zmpl_templates_paths = templates_paths,
|
||||
.zmpl_auto_build = false,
|
||||
.zmpl_markdown_fragments = try generateMarkdownFragments(b),
|
||||
@ -46,6 +50,11 @@ pub fn build(b: *std.Build) !void {
|
||||
},
|
||||
);
|
||||
|
||||
const zmpl_steps = zmpl_dep.builder.top_level_steps;
|
||||
const zmpl_compile_step = zmpl_steps.get("compile").?;
|
||||
const compile_step = b.step("compile", "Compile Zmpl templates");
|
||||
compile_step.dependOn(&zmpl_compile_step.step);
|
||||
|
||||
const zmpl_module = zmpl_dep.module("zmpl");
|
||||
|
||||
const jetkv_dep = b.dependency("jetkv", .{ .target = target, .optimize = optimize });
|
||||
@ -58,18 +67,15 @@ pub fn build(b: *std.Build) !void {
|
||||
const jetcommon_dep = b.dependency("jetcommon", .{ .target = target, .optimize = optimize });
|
||||
const zmd_dep = b.dependency("zmd", .{ .target = target, .optimize = optimize });
|
||||
const httpz_dep = b.dependency("httpz", .{ .target = target, .optimize = optimize });
|
||||
const pg_dep = b.dependency("pg", .{ .target = target, .optimize = optimize });
|
||||
|
||||
// This is the way to make it look nice in the zig build script
|
||||
// If we would do it the other way around, we would have to do
|
||||
// b.dependency("jetzig",.{}).builder.dependency("zmpl",.{}).module("zmpl");
|
||||
b.modules.put("zmpl", zmpl_dep.module("zmpl")) catch @panic("Out of memory");
|
||||
b.modules.put("zmd", zmd_dep.module("zmd")) catch @panic("Out of memory");
|
||||
b.modules.put("pg", pg_dep.module("pg")) catch @panic("Out of memory");
|
||||
b.modules.put("jetquery", jetquery_dep.module("jetquery")) catch @panic("Out of memory");
|
||||
b.modules.put("jetcommon", jetcommon_dep.module("jetcommon")) catch @panic("Out of memory");
|
||||
b.modules.put("jetquery_migrate", jetquery_dep.module("jetquery_migrate")) catch @panic("Out of memory");
|
||||
jetquery_dep.module("jetquery").addImport("pg", pg_dep.module("pg"));
|
||||
|
||||
const smtp_client_dep = b.dependency("smtp_client", .{
|
||||
.target = target,
|
||||
@ -110,6 +116,7 @@ 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);
|
||||
|
||||
@ -128,9 +135,11 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
return error.ZmplVersionNotSupported;
|
||||
}
|
||||
|
||||
const target = b.host;
|
||||
const target = exe.root_module.resolved_target orelse @panic("Unable to detect compile target.");
|
||||
const optimize = exe.root_module.optimize orelse .Debug;
|
||||
|
||||
exe.use_llvm = exe.use_llvm orelse use_llvm_default;
|
||||
|
||||
if (optimize != .Debug) exe.linkLibC();
|
||||
|
||||
const environment = b.option(
|
||||
@ -138,18 +147,27 @@ 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", .{
|
||||
@ -162,7 +180,6 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
const jetzig_module = jetzig_dep.module("jetzig");
|
||||
const zmpl_module = jetzig_dep.module("zmpl");
|
||||
const zmd_module = jetzig_dep.module("zmd");
|
||||
const pg_module = jetzig_dep.module("pg");
|
||||
const jetquery_module = jetzig_dep.module("jetquery");
|
||||
const jetcommon_module = jetzig_dep.module("jetcommon");
|
||||
const jetquery_migrate_module = jetzig_dep.module("jetquery_migrate");
|
||||
@ -171,12 +188,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) {
|
||||
@ -186,7 +203,10 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
}
|
||||
}
|
||||
|
||||
const root_path = b.build_root.path orelse try std.fs.cwd().realpathAlloc(b.allocator, ".");
|
||||
const root_path = if (b.build_root.path) |build_root_path|
|
||||
try std.fs.path.join(b.allocator, &.{ build_root_path, "." })
|
||||
else
|
||||
try std.fs.cwd().realpathAlloc(b.allocator, ".");
|
||||
const templates_path: []const u8 = try std.fs.path.join(
|
||||
b.allocator,
|
||||
&[_][]const u8{ root_path, "src", "app" },
|
||||
@ -209,6 +229,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
.root_source_file = jetzig_dep.path("src/routes_file.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.use_llvm = exe.use_llvm,
|
||||
});
|
||||
|
||||
exe_routes_file.root_module.addImport("jetzig", jetzig_module);
|
||||
@ -236,6 +257,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
.root_source_file = jetzig_dep.path("src/compile_static_routes.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.use_llvm = exe.use_llvm,
|
||||
});
|
||||
|
||||
const main_module = b.createModule(.{ .root_source_file = b.path("src/main.zig") });
|
||||
@ -304,7 +326,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
.root_source_file = tests_file_path,
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.test_runner = jetzig_dep.path("src/test_runner.zig"),
|
||||
.test_runner = .{ .mode = .simple, .path = jetzig_dep.path("src/test_runner.zig") },
|
||||
});
|
||||
exe_unit_tests.root_module.addImport("jetzig", jetzig_module);
|
||||
exe_unit_tests.root_module.addImport("static", static_module);
|
||||
@ -344,6 +366,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
.root_source_file = jetzig_dep.path("src/commands/routes.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.use_llvm = exe.use_llvm,
|
||||
});
|
||||
|
||||
const auth_user_create_step = b.step("jetzig:auth:user:create", "List all routes in your app");
|
||||
@ -352,6 +375,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
.root_source_file = jetzig_dep.path("src/commands/auth.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.use_llvm = exe.use_llvm,
|
||||
});
|
||||
exe_auth.root_module.addImport("jetquery", jetquery_module);
|
||||
exe_auth.root_module.addImport("jetzig", jetzig_module);
|
||||
@ -373,6 +397,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
.root_source_file = jetzig_dep.path("src/commands/database.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.use_llvm = exe.use_llvm,
|
||||
});
|
||||
exe_database.root_module.addImport("jetquery", jetquery_module);
|
||||
exe_database.root_module.addImport("jetzig", jetzig_module);
|
||||
@ -383,9 +408,12 @@ 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);
|
||||
}
|
||||
@ -397,6 +425,8 @@ fn registerDatabaseSteps(b: *std.Build, exe_database: *std.Build.Step.Compile) v
|
||||
.{ "create", "Create a database for your Jetzig app." },
|
||||
.{ "drop", "Drop your Jetzig app's database." },
|
||||
.{ "reflect", "Read your app's database and generate a JetQuery schema." },
|
||||
.{ "setup", "Create the database, run migrations, and generate schema." },
|
||||
.{ "update", "Run migrations and generate schema." },
|
||||
};
|
||||
|
||||
inline for (commands) |command| {
|
||||
@ -417,7 +447,7 @@ fn generateMarkdownFragments(b: *std.Build) ![]const u8 {
|
||||
}
|
||||
};
|
||||
const stat = try file.stat();
|
||||
const source = try file.readToEndAllocOptions(b.allocator, @intCast(stat.size), null, @alignOf(u8), 0);
|
||||
const source = try file.readToEndAllocOptions(b.allocator, @intCast(stat.size), null, .of(u8), 0);
|
||||
if (try getMarkdownFragmentsSource(b.allocator, source)) |markdown_fragments_source| {
|
||||
return try std.fmt.allocPrint(b.allocator,
|
||||
\\const std = @import("std");
|
||||
@ -438,10 +468,10 @@ fn getMarkdownFragmentsSource(allocator: std.mem.Allocator, source: [:0]const u8
|
||||
for (ast.nodes.items(.tag), 0..) |tag, index| {
|
||||
switch (tag) {
|
||||
.simple_var_decl => {
|
||||
const decl = ast.simpleVarDecl(@intCast(index));
|
||||
const decl = ast.simpleVarDecl(@enumFromInt(index));
|
||||
const identifier = ast.tokenSlice(decl.ast.mut_token + 1);
|
||||
if (std.mem.eql(u8, identifier, "markdown_fragments")) {
|
||||
return ast.getNodeSource(@intCast(index));
|
||||
return ast.getNodeSource(@enumFromInt(index));
|
||||
}
|
||||
},
|
||||
else => continue,
|
||||
|
||||
@ -1,43 +1,40 @@
|
||||
.{
|
||||
.name = "jetzig",
|
||||
.name = .jetzig,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0x93ad8bfa2d209022,
|
||||
.minimum_zig_version = "0.15.0-dev.355+206bd1ced",
|
||||
.dependencies = .{
|
||||
.jetcommon = .{
|
||||
.url = "https://github.com/jetzig-framework/jetcommon/archive/fb4edc13759d87bfcd9b1f5fcefdf93f8c9c62dd.tar.gz",
|
||||
.hash = "jetcommon-0.1.0-jPY_DS1HAAAP8xp5HSWB_ZY7m9JEYUmm8adQFrse0lwB",
|
||||
},
|
||||
.zmd = .{
|
||||
.url = "https://github.com/jetzig-framework/zmd/archive/d6c8aa9a9cde99674ccb096d8f94ed09cba8dab.tar.gz",
|
||||
.hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163",
|
||||
},
|
||||
.zmpl = .{
|
||||
.url = "https://github.com/jetzig-framework/zmpl/archive/af75c8b842c3957eb97b4fc4bc49c7b2243968fa.tar.gz",
|
||||
.hash = "1220ecac93d295dafd2f034a86f0979f6108d40e5ea1a39e3a2b9977c35147cac684",
|
||||
},
|
||||
.jetkv = .{
|
||||
.url = "https://github.com/jetzig-framework/jetkv/archive/2b1130a48979ea2871c8cf6ca89c38b1e7062839.tar.gz",
|
||||
.hash = "12201d75d73aad5e1c996de4d5ae87a00e58479c8d469bc2eeb5fdeeac8857bc09af",
|
||||
},
|
||||
.jetquery = .{
|
||||
.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",
|
||||
.smtp_client = .{
|
||||
.url = "https://github.com/karlseguin/smtp_client.zig/archive/5163c66cc42cdd93176a6b1cad45f3db3a291a6a.tar.gz",
|
||||
.hash = "smtp_client-0.0.1-AAAAAIJkAQCngHtRYVUMsMuncmicSHK_7ugwWibDzQ4S",
|
||||
},
|
||||
.args = .{
|
||||
.url = "https://github.com/ikskuh/zig-args/archive/0abdd6947a70e6d8cc83b66228cea614aa856206.tar.gz",
|
||||
.hash = "1220411a8c46d95bbf3b6e2059854bcb3c5159d428814099df5294232b9980517e9c",
|
||||
.url = "https://github.com/bobf/zig-args/archive/88cbade9a517a4014824f8f53f3c48c8a0b2ffe1.tar.gz",
|
||||
.hash = "zig_args-0.0.0-jqtN6P_NAAC97fGpk9hS2K681jkiqPsWP6w3ucb_ctGH",
|
||||
},
|
||||
.pg = .{
|
||||
.url = "https://github.com/karlseguin/pg.zig/archive/9226a0a256cd000eee2f9652c8c03af8dcbed79b.tar.gz",
|
||||
.hash = "12202b30ebf018ca398f665e51cae6d000fdfe2d08d5ec369e3110f762c548b154a4",
|
||||
.jetkv = .{
|
||||
.url = "https://github.com/jetzig-framework/jetkv/archive/5a94e3bac0a6e291efc9d6534beb2d311671ff17.tar.gz",
|
||||
.hash = "jetkv-0.0.0-zCv0fmCGAgCyYqwHjk0P5KrYVRew1MJAtbtAcIO-WPpT",
|
||||
},
|
||||
.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",
|
||||
.zmpl = .{
|
||||
.url = "https://github.com/jetzig-framework/zmpl/archive/c57fc9b83027e8c1459d9625c3509f59f0fb89f3.tar.gz",
|
||||
.hash = "zmpl-0.0.1-SYFGBgdqAwDeA6xm4KAhpKoNrWs5CMQK6x447zhWclCs",
|
||||
},
|
||||
.httpz = .{
|
||||
.url = "https://github.com/karlseguin/http.zig/archive/da9e944de0be6e5c67ca711dd238ce82d81558b4.tar.gz",
|
||||
.hash = "12201df692f62d526fdf94e6000cf8de2142edf27484887e2e8f1ec5db4c9b808e5c",
|
||||
.url = "https://github.com/karlseguin/http.zig/archive/37d7cb9819b804ade5f4b974b82f8dd0622225ed.tar.gz",
|
||||
.hash = "httpz-0.0.0-PNVzrEK4BgBpHQGA2m0RPqPGEjnTdDXHodBwzjYDrmps",
|
||||
},
|
||||
.jetquery = .{
|
||||
.url = "https://github.com/jetzig-framework/jetquery/archive/e1f969f2e3e0e1ad9cc30d56fde9739aa692fdc3.tar.gz",
|
||||
.hash = "jetquery-0.0.0-TNf3zo2ABgBgcsIAvJ1Ud2B2zDzrBy9GQ31kKmTYZ7Ya",
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@ -1,16 +1,17 @@
|
||||
.{
|
||||
.name = "jetzig-cli",
|
||||
.name = .jetzig_cli,
|
||||
.fingerprint = 0x73894a3e0616c96a,
|
||||
.version = "0.0.0",
|
||||
.minimum_zig_version = "0.12.0",
|
||||
|
||||
.dependencies = .{
|
||||
.args = .{
|
||||
.url = "https://github.com/ikskuh/zig-args/archive/0abdd6947a70e6d8cc83b66228cea614aa856206.tar.gz",
|
||||
.hash = "1220411a8c46d95bbf3b6e2059854bcb3c5159d428814099df5294232b9980517e9c",
|
||||
.url = "https://github.com/bobf/zig-args/archive/88cbade9a517a4014824f8f53f3c48c8a0b2ffe1.tar.gz",
|
||||
.hash = "zig_args-0.0.0-jqtN6P_NAAC97fGpk9hS2K681jkiqPsWP6w3ucb_ctGH",
|
||||
},
|
||||
.jetquery = .{
|
||||
.url = "https://github.com/jetzig-framework/jetquery/archive/a31db467c4af1c97bc7c806e1cc1a81a39162954.tar.gz",
|
||||
.hash = "12203af0466ccc3a9ab57fcdf57c92c57989fa7e827d81bc98d0a5787d65402c73c3",
|
||||
.url = "https://github.com/jetzig-framework/jetquery/archive/e1f969f2e3e0e1ad9cc30d56fde9739aa692fdc3.tar.gz",
|
||||
.hash = "jetquery-0.0.0-TNf3zo2ABgBgcsIAvJ1Ud2B2zDzrBy9GQ31kKmTYZ7Ya",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
|
||||
@ -149,13 +149,13 @@ pub fn run(
|
||||
const tmpdir_real_path = try tmpdir.realpathAlloc(allocator, ".");
|
||||
defer allocator.free(tmpdir_real_path);
|
||||
|
||||
try util.runCommandInDir(allocator, tar_argv.items, .{ .path = tmpdir_real_path });
|
||||
try util.runCommandInDir(allocator, tar_argv.items, .{ .path = tmpdir_real_path }, .{});
|
||||
|
||||
switch (builtin.os.tag) {
|
||||
.windows => {},
|
||||
else => std.debug.print("Bundle `bundle.tar.gz` generated successfully.", .{}),
|
||||
}
|
||||
util.printSuccess();
|
||||
util.printSuccess(null);
|
||||
}
|
||||
|
||||
fn locateMarkdownFiles(allocator: std.mem.Allocator, dir: std.fs.Dir, views_path: []const u8, paths: *std.ArrayList([]const u8)) !void {
|
||||
@ -215,7 +215,7 @@ fn zig_build_install(allocator: std.mem.Allocator, path: []const u8, options: Op
|
||||
defer project_dir.close();
|
||||
project_dir.makePath(".bundle") catch {};
|
||||
|
||||
try util.runCommandInDir(allocator, install_argv.items, .{ .path = path });
|
||||
try util.runCommandInDir(allocator, install_argv.items, .{ .path = path }, .{});
|
||||
|
||||
const install_bin_path = try std.fs.path.join(allocator, &[_][]const u8{ ".bundle", "bin" });
|
||||
defer allocator.free(install_bin_path);
|
||||
|
||||
@ -39,6 +39,11 @@ 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.
|
||||
@ -51,21 +56,16 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he
|
||||
\\// * allocator: Arena allocator for use during the mail delivery process.
|
||||
\\// * mail: Mail parameters (from, to, subject, etc.). Inspect or override any values
|
||||
\\// assigned when the mail was created.
|
||||
\\// * data: Provides `data.string()` etc. for generating Jetzig Values.
|
||||
\\// * params: Template data for `text.zmpl` and `html.zmpl`. Inherits all response data
|
||||
\\// assigned in a view function and can be modified for email-specific content.
|
||||
\\// * env: Provides the following fields:
|
||||
\\// - logger: Logger attached to the same stream as the Jetzig server.
|
||||
\\// - environment: Enum of `{ production, development }`.
|
||||
\\// * env: Provides various information about the environment. See `jetzig.jobs.JobEnv`.
|
||||
\\pub fn deliver(
|
||||
\\ allocator: std.mem.Allocator,
|
||||
\\ mail: *jetzig.mail.MailParams,
|
||||
\\ data: *jetzig.data.Data,
|
||||
\\ params: *jetzig.data.Value,
|
||||
\\ env: jetzig.jobs.JobEnv,
|
||||
\\) !void {
|
||||
\\ _ = allocator;
|
||||
\\ _ = data;
|
||||
\\ _ = params;
|
||||
\\ try env.logger.INFO("Delivering email with subject: '{?s}'", .{mail.get(.subject)});
|
||||
\\}
|
||||
|
||||
@ -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", "post", "put", "patch", "delete" };
|
||||
&[_][]const u8{ "index", "get", "new", "edit", "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, post, put, patch, delete };
|
||||
const Method = enum { index, get, new, edit, 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, .put, .patch, .delete => "id: []const u8, ",
|
||||
.get, .edit, .put, .patch, .delete => "id: []const u8, ",
|
||||
},
|
||||
if (action.static) "StaticRequest" else "Request",
|
||||
switch (action.method) {
|
||||
.index, .post, .new => "",
|
||||
.get, .put, .patch, .delete => "_ = id;\n ",
|
||||
.get, .edit, .put, .patch, .delete => "_ = id;\n ",
|
||||
},
|
||||
switch (action.method) {
|
||||
.index, .get, .new => ".ok",
|
||||
.index, .get, .edit, .new => ".ok",
|
||||
.post => ".created",
|
||||
.put, .patch, .delete => ".ok",
|
||||
},
|
||||
@ -164,17 +164,18 @@ fn writeTest(allocator: std.mem.Allocator, writer: anytype, name: []const u8, ac
|
||||
.{
|
||||
@tagName(action.method),
|
||||
switch (action.method) {
|
||||
.index, .get, .new => "GET",
|
||||
.index, .get, .edit, .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 => ".ok",
|
||||
.index, .get, .new, .edit => ".ok",
|
||||
.post => ".created",
|
||||
.put, .patch, .delete => ".ok",
|
||||
},
|
||||
@ -208,7 +209,7 @@ fn writeStaticParams(allocator: std.mem.Allocator, actions: []Action, writer: an
|
||||
defer allocator.free(output);
|
||||
try writer.writeAll(output);
|
||||
},
|
||||
.get, .put, .patch, .delete => {
|
||||
.get, .put, .patch, .delete, .edit => {
|
||||
const output = try std.fmt.allocPrint(
|
||||
allocator,
|
||||
\\ .{s} = .{{
|
||||
|
||||
@ -3,7 +3,6 @@ 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.
|
||||
@ -107,7 +106,7 @@ pub fn run(
|
||||
try copySourceFile(
|
||||
allocator,
|
||||
install_dir,
|
||||
"demo/config/database.zig",
|
||||
"demo/config/database_template.zig",
|
||||
"config/database.zig",
|
||||
null,
|
||||
);
|
||||
@ -203,13 +202,14 @@ 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);
|
||||
|
||||
std.debug.print(
|
||||
try util.unicodePrint(
|
||||
\\
|
||||
\\Setup complete! ✈️ 🦎
|
||||
\\
|
||||
@ -217,7 +217,7 @@ pub fn run(
|
||||
\\
|
||||
\\ $ cd {s}
|
||||
\\
|
||||
\\ $ zig build run
|
||||
\\ $ zig build run or jetzig server
|
||||
\\
|
||||
\\And then browse to http://localhost:8080/
|
||||
\\
|
||||
@ -261,7 +261,7 @@ fn copySourceFile(
|
||||
util.printFailure();
|
||||
return err;
|
||||
};
|
||||
util.printSuccess();
|
||||
util.printSuccess(null);
|
||||
}
|
||||
|
||||
// Read a file from Jetzig source code.
|
||||
@ -334,6 +334,7 @@ fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void {
|
||||
".",
|
||||
},
|
||||
.{ .path = install_dir },
|
||||
.{},
|
||||
);
|
||||
|
||||
try util.runCommandInDir(
|
||||
@ -344,6 +345,7 @@ fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void {
|
||||
".",
|
||||
},
|
||||
.{ .path = install_dir },
|
||||
.{},
|
||||
);
|
||||
|
||||
try util.runCommandInDir(
|
||||
@ -355,5 +357,6 @@ fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void {
|
||||
"Initialize Jetzig project",
|
||||
},
|
||||
.{ .path = install_dir },
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,22 +9,20 @@ 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
|
||||
\\ jetzig server --reload=false --debug=false
|
||||
,
|
||||
.option_docs = .{
|
||||
.reload = "Enable or disable automatic reload on update (default: true)",
|
||||
.debug = "Enable or disable the development debug console (default: true)",
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -62,20 +60,25 @@ 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,
|
||||
&.{
|
||||
"zig",
|
||||
"build",
|
||||
util.environmentBuildOption(main_options.options.environment),
|
||||
"-Djetzig_runner=true",
|
||||
"install",
|
||||
"--color",
|
||||
"on",
|
||||
},
|
||||
.{ .path = realpath },
|
||||
) catch {
|
||||
util.runCommandInDir(allocator, argv.items, .{ .path = realpath }, .{}) catch {
|
||||
std.debug.print("Build failed, waiting for file change...\n", .{});
|
||||
try awaitFileChange(allocator, cwd, &mtime);
|
||||
std.debug.print("Changes detected, restarting server...\n", .{});
|
||||
@ -88,10 +91,9 @@ pub fn run(
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
const argv = &[_][]const u8{exe_path.?};
|
||||
defer allocator.free(exe_path.?);
|
||||
|
||||
var process = std.process.Child.init(argv, allocator);
|
||||
var process = std.process.Child.init(&.{exe_path.?}, allocator);
|
||||
process.stdin_behavior = .Inherit;
|
||||
process.stdout_behavior = .Inherit;
|
||||
process.stderr_behavior = .Inherit;
|
||||
|
||||
@ -30,7 +30,7 @@ pub fn initDataModule(build: *std.Build) !*std.Build.Module {
|
||||
"demo/public/zmpl.png",
|
||||
"demo/public/favicon.ico",
|
||||
"demo/public/styles.css",
|
||||
"demo/config/database.zig",
|
||||
"demo/config/database_template.zig",
|
||||
".gitignore",
|
||||
};
|
||||
|
||||
|
||||
158
cli/util.zig
158
cli/util.zig
@ -23,8 +23,8 @@ const icons = .{
|
||||
};
|
||||
|
||||
/// Print a success confirmation.
|
||||
pub fn printSuccess() void {
|
||||
std.debug.print(" " ++ icons.check ++ "\n", .{});
|
||||
pub fn printSuccess(message: ?[]const u8) void {
|
||||
std.debug.print(" " ++ icons.check ++ " {s}\n", .{message orelse ""});
|
||||
}
|
||||
|
||||
/// Print a failure confirmation.
|
||||
@ -153,7 +153,7 @@ pub fn runCommandStreaming(allocator: std.mem.Allocator, install_path: []const u
|
||||
pub fn runCommand(allocator: std.mem.Allocator, argv: []const []const u8) !void {
|
||||
var dir = try detectJetzigProjectDir();
|
||||
defer dir.close();
|
||||
try runCommandInDir(allocator, argv, .{ .dir = dir });
|
||||
try runCommandInDir(allocator, argv, .{ .dir = dir }, .{});
|
||||
}
|
||||
|
||||
const Dir = union(enum) {
|
||||
@ -161,48 +161,118 @@ const Dir = union(enum) {
|
||||
dir: std.fs.Dir,
|
||||
};
|
||||
|
||||
pub const RunOptions = struct {
|
||||
output: enum { stream, capture } = .capture,
|
||||
wait: bool = true,
|
||||
};
|
||||
|
||||
/// Runs a command as a child process in the given directory and verifies successful exit code.
|
||||
pub fn runCommandInDir(allocator: std.mem.Allocator, argv: []const []const u8, dir: Dir) !void {
|
||||
pub fn runCommandInDir(allocator: std.mem.Allocator, argv: []const []const u8, dir: Dir, options: RunOptions) !void {
|
||||
const cwd_path = switch (dir) {
|
||||
.path => |capture| capture,
|
||||
.dir => |capture| try capture.realpathAlloc(allocator, "."),
|
||||
};
|
||||
defer if (dir == .dir) allocator.free(cwd_path);
|
||||
|
||||
const result = try std.process.Child.run(.{
|
||||
.allocator = allocator,
|
||||
.argv = argv,
|
||||
.cwd = cwd_path,
|
||||
});
|
||||
const output_behaviour: std.process.Child.StdIo = switch (options.output) {
|
||||
.stream => .Inherit,
|
||||
.capture => .Pipe,
|
||||
};
|
||||
|
||||
var child = std.process.Child.init(argv, allocator);
|
||||
child.stdin_behavior = .Ignore;
|
||||
child.stdout_behavior = output_behaviour;
|
||||
child.stderr_behavior = output_behaviour;
|
||||
if (options.output == .stream) {
|
||||
child.stdout = std.io.getStdOut();
|
||||
child.stderr = std.io.getStdErr();
|
||||
}
|
||||
child.cwd = cwd_path;
|
||||
|
||||
var stdout = try std.ArrayListUnmanaged(u8).initCapacity(allocator, 0);
|
||||
var stderr = try std.ArrayListUnmanaged(u8).initCapacity(allocator, 0);
|
||||
errdefer {
|
||||
stdout.deinit(allocator);
|
||||
stderr.deinit(allocator);
|
||||
}
|
||||
|
||||
try child.spawn();
|
||||
switch (options.output) {
|
||||
.capture => try collectOutput(child, allocator, &stdout, &stderr),
|
||||
.stream => {},
|
||||
}
|
||||
|
||||
if (!options.wait) return;
|
||||
|
||||
const result = std.process.Child.RunResult{
|
||||
.term = try child.wait(),
|
||||
.stdout = try stdout.toOwnedSlice(allocator),
|
||||
.stderr = try stderr.toOwnedSlice(allocator),
|
||||
};
|
||||
defer allocator.free(result.stdout);
|
||||
defer allocator.free(result.stderr);
|
||||
|
||||
const command = try std.mem.join(allocator, " ", argv);
|
||||
defer allocator.free(command);
|
||||
|
||||
std.debug.print("[exec] {s}", .{command});
|
||||
|
||||
if (result.term.Exited != 0) {
|
||||
printFailure();
|
||||
std.debug.print(
|
||||
\\
|
||||
\\Error running command: {s}
|
||||
\\
|
||||
\\[stdout]:
|
||||
\\
|
||||
\\{s}
|
||||
\\
|
||||
\\[stderr]:
|
||||
\\
|
||||
\\{s}
|
||||
\\
|
||||
, .{ command, result.stdout, result.stderr });
|
||||
if (result.stdout.len > 0) {
|
||||
std.debug.print("\n[stdout]:\n{s}\n", .{result.stdout});
|
||||
}
|
||||
if (result.stderr.len > 0) {
|
||||
std.debug.print("\n[stderr]:\n{s}\n", .{result.stderr});
|
||||
}
|
||||
return error.JetzigCommandError;
|
||||
} else {
|
||||
printSuccess();
|
||||
const message = try std.mem.join(allocator, " ", argv);
|
||||
defer allocator.free(message);
|
||||
printSuccess(message);
|
||||
}
|
||||
}
|
||||
|
||||
fn collectOutput(
|
||||
child: std.process.Child,
|
||||
allocator: std.mem.Allocator,
|
||||
stdout: *std.ArrayListUnmanaged(u8),
|
||||
stderr: *std.ArrayListUnmanaged(u8),
|
||||
) !void {
|
||||
const max_output_bytes = 50 * 1024;
|
||||
std.debug.assert(child.stdout_behavior == .Pipe);
|
||||
std.debug.assert(child.stderr_behavior == .Pipe);
|
||||
var poller = std.io.poll(allocator, enum { stdout, stderr }, .{
|
||||
.stdout = child.stdout.?,
|
||||
.stderr = child.stderr.?,
|
||||
});
|
||||
defer poller.deinit();
|
||||
|
||||
std.debug.print("(working) .", .{});
|
||||
|
||||
while (try poller.poll()) {
|
||||
if (poller.fifo(.stdout).count > max_output_bytes)
|
||||
return error.StdoutStreamTooLong;
|
||||
if (poller.fifo(.stderr).count > max_output_bytes)
|
||||
return error.StderrStreamTooLong;
|
||||
std.debug.print(".", .{});
|
||||
}
|
||||
|
||||
std.debug.print(" (done)\n", .{});
|
||||
|
||||
try writeFifoDataToArrayList(allocator, stdout, poller.fifo(.stdout));
|
||||
try writeFifoDataToArrayList(allocator, stderr, poller.fifo(.stderr));
|
||||
}
|
||||
|
||||
// Borrowed from `std.process.Child.writeFifoDataToArrayList` - non-public but needed in our
|
||||
// modified `collectOutput`
|
||||
fn writeFifoDataToArrayList(allocator: std.mem.Allocator, list: *std.ArrayListUnmanaged(u8), fifo: *std.io.PollFifo) !void {
|
||||
if (fifo.head != 0) fifo.realign();
|
||||
if (list.capacity == 0) {
|
||||
list.* = .{
|
||||
.items = fifo.buf[0..fifo.count],
|
||||
.capacity = fifo.buf.len,
|
||||
};
|
||||
fifo.* = std.io.PollFifo.init(fifo.allocator);
|
||||
} else {
|
||||
try list.appendSlice(allocator, fifo.buf[0..fifo.count]);
|
||||
}
|
||||
}
|
||||
/// Generate a full GitHub URL for passing to `zig fetch`.
|
||||
pub fn githubUrl(allocator: std.mem.Allocator) ![]const u8 {
|
||||
var client = std.http.Client{ .allocator = allocator };
|
||||
@ -289,3 +359,35 @@ 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
1
demo/.gitignore
vendored
@ -4,3 +4,4 @@ static/
|
||||
src/app/views/**/.*.zig
|
||||
.DS_Store
|
||||
log/
|
||||
src/routes.zig
|
||||
|
||||
44
demo/Makefile
Normal file
44
demo/Makefile
Normal file
@ -0,0 +1,44 @@
|
||||
# Makefile
|
||||
#
|
||||
# Use this Makefile to set up a local Docker PostgreSQL database and run tests, or launch a local
|
||||
# development database.
|
||||
#
|
||||
## Tests
|
||||
#
|
||||
# Set up test database and run application tests:
|
||||
#
|
||||
# ```
|
||||
# make test
|
||||
# ```
|
||||
#
|
||||
## Development
|
||||
#
|
||||
# Set up development database and launch the demo Jetzig app:
|
||||
#
|
||||
# ```
|
||||
# make dev
|
||||
# ```
|
||||
#
|
||||
# TODO: Move all of this into `build.zig`
|
||||
test_database=jetzig_demo_test
|
||||
dev_database=jetzig_demo_dev
|
||||
port=14173
|
||||
|
||||
export JETQUERY_HOSTNAME=localhost
|
||||
export JETQUERY_USERNAME=postgres
|
||||
export JETQUERY_PASSWORD=postgres
|
||||
export JETQUERY_POOL_SIZE=2
|
||||
|
||||
.PHONY: test
|
||||
test: env=JETQUERY_DATABASE=${test_database} JETQUERY_PORT=${port}
|
||||
test:
|
||||
docker compose up --detach --wait --renew-anon-volumes --remove-orphans --force-recreate
|
||||
${env} zig build -Denvironment=testing jetzig:database:setup
|
||||
${env} zig build -Denvironment=testing jetzig:test
|
||||
|
||||
.PHONY: dev
|
||||
dev: env=JETQUERY_DATABASE=${dev_database} JETQUERY_PORT=${port}
|
||||
dev:
|
||||
docker compose up --detach --wait --renew-anon-volumes --remove-orphans
|
||||
${env} zig build -Denvironment=testing jetzig:database:setup
|
||||
${env} jetzig server
|
||||
@ -1,7 +1,8 @@
|
||||
.{
|
||||
.name = "jetzig-demo",
|
||||
.name = .jetzig_demo,
|
||||
.version = "0.0.0",
|
||||
.minimum_zig_version = "0.12.0",
|
||||
.fingerprint = 0x3877c19710a92a5c,
|
||||
.dependencies = .{
|
||||
.jetzig = .{
|
||||
.path = "../",
|
||||
|
||||
7
demo/compose.yml
Normal file
7
demo/compose.yml
Normal file
@ -0,0 +1,7 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17
|
||||
ports:
|
||||
- 14173:5432
|
||||
environment:
|
||||
POSTGRES_PASSWORD: 'postgres'
|
||||
@ -1,48 +1,16 @@
|
||||
pub const database = .{
|
||||
// Null adapter fails when a database call is invoked.
|
||||
.development = .{
|
||||
.adapter = .null,
|
||||
.adapter = .postgresql,
|
||||
.username = "postgres",
|
||||
.password = "postgres",
|
||||
.hostname = "localhost",
|
||||
.database = "jetzig_demo_dev",
|
||||
.port = 14173, // See `compose.yml`
|
||||
},
|
||||
// This configuration is used for CI
|
||||
// in GitHub
|
||||
.testing = .{
|
||||
.adapter = .null,
|
||||
.adapter = .postgresql,
|
||||
.database = "jetzig_demo_test",
|
||||
},
|
||||
.production = .{
|
||||
.adapter = .null,
|
||||
},
|
||||
// PostgreSQL adapter configuration.
|
||||
//
|
||||
// All options except `adapter` can be configured using environment variables:
|
||||
//
|
||||
// * JETQUERY_HOSTNAME
|
||||
// * JETQUERY_PORT
|
||||
// * JETQUERY_USERNAME
|
||||
// * JETQUERY_PASSWORD
|
||||
// * JETQUERY_DATABASE
|
||||
//
|
||||
// .testing = .{
|
||||
// .adapter = .postgresql,
|
||||
// .hostname = "localhost",
|
||||
// .port = 5432,
|
||||
// .username = "postgres",
|
||||
// .password = "password",
|
||||
// .database = "myapp_testing",
|
||||
// },
|
||||
//
|
||||
// .development = .{
|
||||
// .adapter = .postgresql,
|
||||
// .hostname = "localhost",
|
||||
// .port = 5432,
|
||||
// .username = "postgres",
|
||||
// .password = "password",
|
||||
// .database = "myapp_development",
|
||||
// },
|
||||
//
|
||||
// .production = .{
|
||||
// .adapter = .postgresql,
|
||||
// .hostname = "localhost",
|
||||
// .port = 5432,
|
||||
// .username = "postgres",
|
||||
// .password = "password",
|
||||
// .database = "myapp_production",
|
||||
// },
|
||||
};
|
||||
|
||||
48
demo/config/database_template.zig
Normal file
48
demo/config/database_template.zig
Normal file
@ -0,0 +1,48 @@
|
||||
pub const database = .{
|
||||
// Null adapter fails when a database call is invoked.
|
||||
.development = .{
|
||||
.adapter = .null,
|
||||
},
|
||||
.testing = .{
|
||||
.adapter = .null,
|
||||
},
|
||||
.production = .{
|
||||
.adapter = .null,
|
||||
},
|
||||
// PostgreSQL adapter configuration.
|
||||
//
|
||||
// All options except `adapter` can be configured using environment variables:
|
||||
//
|
||||
// * JETQUERY_HOSTNAME
|
||||
// * JETQUERY_PORT
|
||||
// * JETQUERY_USERNAME
|
||||
// * JETQUERY_PASSWORD
|
||||
// * JETQUERY_DATABASE
|
||||
//
|
||||
// .testing = .{
|
||||
// .adapter = .postgresql,
|
||||
// .hostname = "localhost",
|
||||
// .port = 5432,
|
||||
// .username = "postgres",
|
||||
// .password = "password",
|
||||
// .database = "myapp_testing",
|
||||
// },
|
||||
//
|
||||
// .development = .{
|
||||
// .adapter = .postgresql,
|
||||
// .hostname = "localhost",
|
||||
// .port = 5432,
|
||||
// .username = "postgres",
|
||||
// .password = "password",
|
||||
// .database = "myapp_development",
|
||||
// },
|
||||
//
|
||||
// .production = .{
|
||||
// .adapter = .postgresql,
|
||||
// .hostname = "localhost",
|
||||
// .port = 5432,
|
||||
// .username = "postgres",
|
||||
// .password = "password",
|
||||
// .database = "myapp_production",
|
||||
// },
|
||||
};
|
||||
14
demo/src/app/database/Schema.zig
Normal file
14
demo/src/app/database/Schema.zig
Normal file
@ -0,0 +1,14 @@
|
||||
const jetquery = @import("jetzig").jetquery;
|
||||
|
||||
pub const User = jetquery.Model(
|
||||
@This(),
|
||||
"users",
|
||||
struct {
|
||||
id: i32,
|
||||
email: []const u8,
|
||||
password_hash: []const u8,
|
||||
created_at: jetquery.DateTime,
|
||||
updated_at: jetquery.DateTime,
|
||||
},
|
||||
.{},
|
||||
);
|
||||
@ -1,9 +1,9 @@
|
||||
const jetquery = @import("jetquery");
|
||||
|
||||
pub fn up(repo: *jetquery.Repo) !void {
|
||||
pub fn up(repo: anytype) !void {
|
||||
_ = repo;
|
||||
}
|
||||
|
||||
pub fn down(repo: *jetquery.Repo) !void {
|
||||
pub fn down(repo: anytype) !void {
|
||||
_ = repo;
|
||||
}
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
const std = @import("std");
|
||||
const jetquery = @import("jetquery");
|
||||
const t = jetquery.schema.table;
|
||||
|
||||
pub fn up(repo: anytype) !void {
|
||||
try repo.createTable(
|
||||
"users",
|
||||
&.{
|
||||
t.primaryKey("id", .{}),
|
||||
t.column("email", .string, .{ .unique = true, .index = true }),
|
||||
t.column("password_hash", .string, .{}),
|
||||
t.timestamps(.{}),
|
||||
},
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn down(repo: anytype) !void {
|
||||
try repo.dropTable("users", .{});
|
||||
}
|
||||
@ -12,8 +12,8 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig
|
||||
env,
|
||||
.{
|
||||
.subject = "Hello!!!",
|
||||
.from = "bob@jetzig.dev",
|
||||
.to = &.{"bob@jetzig.dev"},
|
||||
.from = .{ .email = "bob@jetzig.dev" },
|
||||
.to = &.{.{ .email = "bob@jetzig.dev" }},
|
||||
.html = "<div>Hello!</div>",
|
||||
.text = "Hello!",
|
||||
},
|
||||
|
||||
@ -3,7 +3,7 @@ const jetzig = @import("jetzig");
|
||||
|
||||
// Default values for this mailer.
|
||||
pub const defaults: jetzig.mail.DefaultMailParams = .{
|
||||
.from = "no-reply@example.com",
|
||||
.from = .{ .email = "no-reply@example.com" },
|
||||
.subject = "Default subject",
|
||||
};
|
||||
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
pub const layout = "application";
|
||||
// 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 actions = .{
|
||||
.before = .{jetzig.middleware.AntiCsrfMiddleware},
|
||||
|
||||
@ -6,3 +6,5 @@
|
||||
|
||||
<input type="submit" value="Submit Spam" />
|
||||
</form>
|
||||
|
||||
<div>Try clearing `_jetzig-session` cookie before clicking "Submit Spam"</div>
|
||||
|
||||
3
demo/src/app/views/channels/index.zmpl
Normal file
3
demo/src/app/views/channels/index.zmpl
Normal file
@ -0,0 +1,3 @@
|
||||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
61
demo/src/app/views/login.zig
Normal file
61
demo/src/app/views/login.zig
Normal file
@ -0,0 +1,61 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
const auth = @import("jetzig").auth;
|
||||
|
||||
pub fn index(request: *jetzig.Request) !jetzig.View {
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
pub fn post(request: *jetzig.Request) !jetzig.View {
|
||||
const Login = struct {
|
||||
email: []const u8,
|
||||
password: []const u8,
|
||||
};
|
||||
|
||||
const params = try request.expectParams(Login) orelse {
|
||||
return request.fail(.forbidden);
|
||||
};
|
||||
|
||||
// Lookup the user by email
|
||||
const query = jetzig.database.Query(.User).findBy(
|
||||
.{ .email = params.email },
|
||||
);
|
||||
|
||||
const user = try request.repo.execute(query) orelse {
|
||||
return request.fail(.forbidden);
|
||||
};
|
||||
|
||||
// Check that the password matches
|
||||
if (try auth.verifyPassword(
|
||||
request.allocator,
|
||||
user.password_hash,
|
||||
params.password,
|
||||
)) {
|
||||
try auth.signIn(request, user.id);
|
||||
return request.redirect("/", .found);
|
||||
}
|
||||
return request.fail(.forbidden);
|
||||
}
|
||||
|
||||
test "post" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const hashed_pass = try auth.hashPassword(std.testing.allocator, "test");
|
||||
defer std.testing.allocator.free(hashed_pass);
|
||||
|
||||
try jetzig.database.Query(.User).deleteAll().execute(app.repo);
|
||||
try app.repo.insert(.User, .{
|
||||
.id = 1,
|
||||
.email = "test@test.com",
|
||||
.password_hash = hashed_pass,
|
||||
});
|
||||
|
||||
const response = try app.request(.POST, "/login", .{
|
||||
.json = .{
|
||||
.email = "test@test.com",
|
||||
.password = "test",
|
||||
},
|
||||
});
|
||||
try response.expectStatus(.found);
|
||||
}
|
||||
7
demo/src/app/views/login/index.zmpl
Normal file
7
demo/src/app/views/login/index.zmpl
Normal file
@ -0,0 +1,7 @@
|
||||
<form method="post" id="login">
|
||||
<input type="email" name="email" placeholder="name@example.com">
|
||||
<label for="email">Email address</label>
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
<label for="password">Password</label>
|
||||
<button type="submit" form="login">Sign in</button>
|
||||
</form>
|
||||
@ -10,7 +10,7 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
// * `src/app/mailers/welcome/html.zmpl`
|
||||
// * `src/app/mailers/welcome/text.zmpl`
|
||||
// All mailer templates have access to the same template data as a view template.
|
||||
const mail = request.mail("welcome", .{ .to = &.{"hello@jetzig.dev"} });
|
||||
const mail = request.mail("welcome", .{ .to = &.{.{ .email = "hello@jetzig.dev" }} });
|
||||
|
||||
// Deliver the email asynchronously via a built-in mail Job. Use `.now` to send the email
|
||||
// synchronously (i.e. before the request has returned).
|
||||
|
||||
15
demo/src/app/views/render_template.zig
Normal file
15
demo/src/app/views/render_template.zig
Normal file
@ -0,0 +1,15 @@
|
||||
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");
|
||||
}
|
||||
3
demo/src/app/views/render_template/index.zmpl
Normal file
3
demo/src/app/views/render_template/index.zmpl
Normal file
@ -0,0 +1,3 @@
|
||||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
@ -5,8 +5,8 @@ const importedFunction = @import("../lib/example.zig").exampleFunction;
|
||||
|
||||
pub const layout = "application";
|
||||
|
||||
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
var root = try data.object();
|
||||
pub fn index(request: *jetzig.Request) !jetzig.View {
|
||||
var root = try request.data(.object);
|
||||
try root.put("message", "Welcome to Jetzig!");
|
||||
try root.put("custom_number", customFunction(100, 200, 300));
|
||||
try root.put("imported_number", importedFunction(100, 200, 300));
|
||||
@ -16,6 +16,12 @@ 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 {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
<!-- Renders `src/app/views/root/_quotes.zmpl`: -->
|
||||
<div>
|
||||
@partial root/quotes(message: .message)
|
||||
@partial root/quotes(message: $.message)
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@ -15,6 +15,11 @@ 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();
|
||||
|
||||
@ -87,49 +87,61 @@ pub const jetzig_options = struct {
|
||||
.path = "/",
|
||||
},
|
||||
.production => .{
|
||||
.same_site = true,
|
||||
.same_site = .lax,
|
||||
.secure = true,
|
||||
.http_only = true,
|
||||
.path = "/",
|
||||
},
|
||||
};
|
||||
|
||||
/// 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 = .{
|
||||
/// 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 = .{
|
||||
.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.KVOptions = .{
|
||||
pub const job_queue: jetzig.kv.Store.Options = .{
|
||||
.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.KVOptions = .{
|
||||
pub const cache: jetzig.kv.Store.Options = .{
|
||||
.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,
|
||||
|
||||
@ -54,10 +54,20 @@ 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];
|
||||
if (std.mem.eql(u8, path, "root")) return try self.routes.allocator.dupe(u8, "/");
|
||||
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, "/");
|
||||
}
|
||||
|
||||
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 });
|
||||
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 });
|
||||
}
|
||||
|
||||
pub fn lessThanFn(context: void, lhs: Function, rhs: Function) bool {
|
||||
@ -305,6 +315,7 @@ 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 },
|
||||
@ -365,7 +376,7 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
|
||||
path,
|
||||
@intCast(stat.size),
|
||||
null,
|
||||
@alignOf(u8),
|
||||
.of(u8),
|
||||
0,
|
||||
);
|
||||
defer self.allocator.free(source);
|
||||
@ -379,7 +390,7 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
|
||||
for (self.ast.nodes.items(.tag), 0..) |tag, index| {
|
||||
switch (tag) {
|
||||
.fn_proto_multi, .fn_proto_one, .fn_proto_simple => |function_tag| {
|
||||
var function = try self.parseFunction(function_tag, index, path, source);
|
||||
var function = try self.parseFunction(function_tag, @enumFromInt(index), path, source);
|
||||
if (function) |*capture| {
|
||||
if (capture.args.len == 0) {
|
||||
std.debug.print(
|
||||
@ -403,7 +414,7 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
|
||||
}
|
||||
},
|
||||
.simple_var_decl => {
|
||||
const decl = self.ast.simpleVarDecl(asNodeIndex(index));
|
||||
const decl = self.ast.simpleVarDecl(@enumFromInt(index));
|
||||
if (self.isStaticParamsDecl(decl)) {
|
||||
self.data.reset();
|
||||
const params = try self.data.root(.object);
|
||||
@ -441,10 +452,11 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
|
||||
|
||||
// Parse the `pub const static_params` definition and into a `jetzig.data.Value`.
|
||||
fn parseStaticParamsDecl(self: *Routes, decl: std.zig.Ast.full.VarDecl, params: *jetzig.data.Value) !void {
|
||||
const init_node = self.ast.nodes.items(.tag)[decl.ast.init_node];
|
||||
switch (init_node) {
|
||||
const init_node = decl.ast.init_node.unwrap() orelse return;
|
||||
|
||||
switch (self.ast.nodeTag(init_node)) {
|
||||
.struct_init_dot_two, .struct_init_dot_two_comma => {
|
||||
try self.parseStruct(decl.ast.init_node, params);
|
||||
try self.parseStruct(init_node, params);
|
||||
},
|
||||
else => return,
|
||||
}
|
||||
@ -477,14 +489,14 @@ fn parseArray(self: *Routes, node: std.zig.Ast.Node.Index, params: *jetzig.data.
|
||||
|
||||
const array = maybe_array.?;
|
||||
|
||||
const main_token = self.ast.nodes.items(.main_token)[node];
|
||||
const main_token = self.ast.nodeMainToken(node);
|
||||
const field_name = self.ast.tokenSlice(main_token - 3);
|
||||
|
||||
const params_array = try self.data.array();
|
||||
try params.put(field_name, params_array);
|
||||
|
||||
for (array.ast.elements) |element| {
|
||||
const elem = self.ast.nodes.items(.tag)[element];
|
||||
const elem = self.ast.nodeTag(element);
|
||||
switch (elem) {
|
||||
.struct_init_dot, .struct_init_dot_two, .struct_init_dot_two_comma => {
|
||||
const route_params = try self.data.object();
|
||||
@ -497,19 +509,20 @@ fn parseArray(self: *Routes, node: std.zig.Ast.Node.Index, params: *jetzig.data.
|
||||
try self.parseField(element, route_params);
|
||||
},
|
||||
.string_literal => {
|
||||
const string_token = self.ast.nodes.items(.main_token)[element];
|
||||
const string_token = self.ast.nodeMainToken(element);
|
||||
const string_value = self.ast.tokenSlice(string_token);
|
||||
|
||||
// Strip quotes: `"foo"` -> `foo`
|
||||
try params_array.append(string_value[1 .. string_value.len - 1]);
|
||||
},
|
||||
.number_literal => {
|
||||
const number_token = self.ast.nodes.items(.main_token)[element];
|
||||
const number_token = self.ast.nodeMainToken(element);
|
||||
const number_value = self.ast.tokenSlice(number_token);
|
||||
try params_array.append(try parseNumber(number_value, self.data));
|
||||
},
|
||||
inline else => {
|
||||
const tag = self.ast.nodes.items(.tag)[element];
|
||||
@setEvalBranchQuota(10_000);
|
||||
const tag = self.ast.nodeTag(element);
|
||||
std.debug.print("Unexpected token: {}\n", .{tag});
|
||||
return error.JetzigStaticParamsParseError;
|
||||
},
|
||||
@ -519,22 +532,21 @@ fn parseArray(self: *Routes, node: std.zig.Ast.Node.Index, params: *jetzig.data.
|
||||
|
||||
// Parse the value of a param field (recursively when field is a struct/array)
|
||||
fn parseField(self: *Routes, node: std.zig.Ast.Node.Index, params: *jetzig.data.Value) anyerror!void {
|
||||
const tag = self.ast.nodes.items(.tag)[node];
|
||||
switch (tag) {
|
||||
switch (self.ast.nodeTag(node)) {
|
||||
// Route params, e.g. `.index = .{ ... }`
|
||||
.array_init_dot, .array_init_dot_two, .array_init_dot_comma, .array_init_dot_two_comma => {
|
||||
try self.parseArray(node, params);
|
||||
},
|
||||
.struct_init_dot, .struct_init_dot_two, .struct_init_dot_two_comma => {
|
||||
const nested_params = try self.data.object();
|
||||
const main_token = self.ast.nodes.items(.main_token)[node];
|
||||
const main_token = self.ast.nodeMainToken(node);
|
||||
const field_name = self.ast.tokenSlice(main_token - 3);
|
||||
try params.put(field_name, nested_params);
|
||||
try self.parseStruct(node, nested_params);
|
||||
},
|
||||
// Individual param in a params struct, e.g. `.foo = "bar"`
|
||||
.string_literal => {
|
||||
const main_token = self.ast.nodes.items(.main_token)[node];
|
||||
const main_token = self.ast.nodeMainToken(node);
|
||||
const field_name = self.ast.tokenSlice(main_token - 2);
|
||||
const field_value = self.ast.tokenSlice(main_token);
|
||||
|
||||
@ -545,13 +557,13 @@ fn parseField(self: *Routes, node: std.zig.Ast.Node.Index, params: *jetzig.data.
|
||||
);
|
||||
},
|
||||
.number_literal => {
|
||||
const main_token = self.ast.nodes.items(.main_token)[node];
|
||||
const main_token = self.ast.nodeMainToken(node);
|
||||
const field_name = self.ast.tokenSlice(main_token - 2);
|
||||
const field_value = self.ast.tokenSlice(main_token);
|
||||
|
||||
try params.put(field_name, try parseNumber(field_value, self.data));
|
||||
},
|
||||
else => {
|
||||
else => |tag| {
|
||||
std.debug.print("Unexpected token: {}\n", .{tag});
|
||||
return error.JetzigStaticParamsParseError;
|
||||
},
|
||||
@ -582,16 +594,16 @@ fn isStaticParamsDecl(self: *Routes, decl: std.zig.Ast.full.VarDecl) bool {
|
||||
fn parseFunction(
|
||||
self: *Routes,
|
||||
function_type: std.zig.Ast.Node.Tag,
|
||||
index: usize,
|
||||
index: std.zig.Ast.Node.Index,
|
||||
path: []const u8,
|
||||
source: []const u8,
|
||||
) !?Function {
|
||||
var buf: [1]std.zig.Ast.Node.Index = undefined;
|
||||
|
||||
const fn_proto = switch (function_type) {
|
||||
.fn_proto_multi => self.ast.fnProtoMulti(@as(u32, @intCast(index))),
|
||||
.fn_proto_one => self.ast.fnProtoOne(&buf, @as(u32, @intCast(index))),
|
||||
.fn_proto_simple => self.ast.fnProtoSimple(&buf, @as(u32, @intCast(index))),
|
||||
.fn_proto_multi => self.ast.fnProtoMulti(index),
|
||||
.fn_proto_one => self.ast.fnProtoOne(&buf, index),
|
||||
.fn_proto_simple => self.ast.fnProtoSimple(&buf, index),
|
||||
else => unreachable,
|
||||
};
|
||||
if (fn_proto.name_token) |token| {
|
||||
@ -608,7 +620,7 @@ fn parseFunction(
|
||||
while (it.next()) |arg| {
|
||||
if (arg.name_token) |arg_token| {
|
||||
const arg_name = self.ast.tokenSlice(arg_token);
|
||||
const node = self.ast.nodes.get(arg.type_expr);
|
||||
const node = self.ast.nodes.get(@intFromEnum(arg.type_expr.?));
|
||||
const type_name = try self.parseTypeExpr(node);
|
||||
try args.append(.{ .name = arg_name, .type_name = type_name });
|
||||
}
|
||||
@ -654,10 +666,6 @@ fn parseTypeExpr(self: *Routes, node: std.zig.Ast.Node) ![]const u8 {
|
||||
return error.JetzigAstParserError;
|
||||
}
|
||||
|
||||
fn asNodeIndex(index: usize) std.zig.Ast.Node.Index {
|
||||
return @as(std.zig.Ast.Node.Index, @intCast(index));
|
||||
}
|
||||
|
||||
fn isActionFunctionName(name: []const u8) bool {
|
||||
inline for (@typeInfo(jetzig.views.Route.Action).@"enum".fields) |field| {
|
||||
if (std.mem.eql(u8, field.name, name)) return true;
|
||||
|
||||
121
src/assets/debug.css
Normal file
121
src/assets/debug.css
Normal file
@ -0,0 +1,121 @@
|
||||
/* 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;
|
||||
}
|
||||
5
src/assets/debug.js
Normal file
5
src/assets/debug.js
Normal file
File diff suppressed because one or more lines are too long
@ -33,7 +33,7 @@ pub fn main() !void {
|
||||
const Repo = jetzig.jetquery.Repo(jetzig.database.adapter, Schema);
|
||||
var repo = try Repo.loadConfig(
|
||||
allocator,
|
||||
std.enums.nameCast(jetzig.jetquery.Environment, jetzig.environment),
|
||||
@field(jetzig.jetquery.Environment, @tagName(jetzig.environment)),
|
||||
.{ .env = try jetzig.database.repoEnv(env), .context = .cli },
|
||||
);
|
||||
defer repo.deinit();
|
||||
@ -60,7 +60,7 @@ pub fn main() !void {
|
||||
|
||||
const email = args[2];
|
||||
|
||||
try repo.insert(std.enums.nameCast(std.meta.DeclEnum(Schema), model), .{
|
||||
try repo.insert(@field(std.meta.DeclEnum(Schema), model), .{
|
||||
.email = email,
|
||||
.password_hash = try hashPassword(allocator, password),
|
||||
});
|
||||
|
||||
@ -15,7 +15,7 @@ const production_drop_failure_message = "To drop a production database, " ++
|
||||
|
||||
const environment = jetzig.build_options.environment;
|
||||
const config = @field(jetquery.config.database, @tagName(environment));
|
||||
const Action = enum { migrate, rollback, create, drop, reflect };
|
||||
const Action = enum { migrate, rollback, create, drop, reflect, setup, update };
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
@ -46,6 +46,8 @@ pub fn main() !void {
|
||||
.{ "create", .create },
|
||||
.{ "drop", .drop },
|
||||
.{ "reflect", .reflect },
|
||||
.{ "setup", .setup },
|
||||
.{ "update", .update },
|
||||
});
|
||||
const action = map.get(args[1]) orelse return error.JetzigUnrecognizedArgument;
|
||||
|
||||
@ -77,6 +79,25 @@ pub fn main() !void {
|
||||
defer repo.deinit();
|
||||
try repo.createDatabase(database, .{});
|
||||
},
|
||||
.setup => {
|
||||
{
|
||||
var repo = try migrationsRepo(.create, allocator, repo_env);
|
||||
defer repo.deinit();
|
||||
try repo.createDatabase(database, .{});
|
||||
}
|
||||
{
|
||||
var repo = try migrationsRepo(.update, allocator, repo_env);
|
||||
defer repo.deinit();
|
||||
try Migrate(config.adapter).init(&repo).migrate();
|
||||
try reflectSchema(allocator, repo_env);
|
||||
}
|
||||
},
|
||||
.update => {
|
||||
var repo = try migrationsRepo(action, allocator, repo_env);
|
||||
defer repo.deinit();
|
||||
try Migrate(config.adapter).init(&repo).migrate();
|
||||
try reflectSchema(allocator, repo_env);
|
||||
},
|
||||
.drop => {
|
||||
if (environment == .production) {
|
||||
const confirm = std.process.getEnvVarOwned(allocator, confirm_drop_env) catch |err| {
|
||||
@ -103,33 +124,7 @@ pub fn main() !void {
|
||||
}
|
||||
},
|
||||
.reflect => {
|
||||
var cwd = try jetzig.util.detectJetzigProjectDir();
|
||||
defer cwd.close();
|
||||
|
||||
const Repo = jetquery.Repo(config.adapter, Schema);
|
||||
var repo = try Repo.loadConfig(
|
||||
allocator,
|
||||
std.enums.nameCast(jetquery.Environment, environment),
|
||||
.{ .context = .migration, .env = repo_env },
|
||||
);
|
||||
const reflect = @import("jetquery_reflect").Reflect(config.adapter, Schema).init(
|
||||
allocator,
|
||||
&repo,
|
||||
.{
|
||||
.import_jetquery =
|
||||
\\@import("jetzig").jetquery
|
||||
,
|
||||
},
|
||||
);
|
||||
const schema = try reflect.generateSchema();
|
||||
const project_dir = try jetzig.util.detectJetzigProjectDir();
|
||||
const project_dir_realpath = try project_dir.realpathAlloc(allocator, ".");
|
||||
const path = try std.fs.path.join(
|
||||
allocator,
|
||||
&.{ project_dir_realpath, "src", "app", "database", "Schema.zig" },
|
||||
);
|
||||
try jetzig.util.createFile(path, schema);
|
||||
std.log.info("Database schema written to `{s}`.", .{path});
|
||||
try reflectSchema(allocator, repo_env);
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -138,15 +133,46 @@ const MigrationsRepo = jetquery.Repo(config.adapter, MigrateSchema);
|
||||
fn migrationsRepo(action: Action, allocator: std.mem.Allocator, repo_env: anytype) !MigrationsRepo {
|
||||
return try MigrationsRepo.loadConfig(
|
||||
allocator,
|
||||
std.enums.nameCast(jetquery.Environment, environment),
|
||||
@field(jetquery.Environment, @tagName(environment)),
|
||||
.{
|
||||
.admin = switch (action) {
|
||||
.migrate, .rollback => false,
|
||||
.migrate, .rollback, .update => false,
|
||||
.create, .drop => true,
|
||||
.reflect => unreachable, // We use a separate repo for schema reflection.
|
||||
.setup => unreachable, // Setup uses `create` and then `update`
|
||||
},
|
||||
.context = .migration,
|
||||
.env = repo_env,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn reflectSchema(allocator: std.mem.Allocator, repo_env: anytype) !void {
|
||||
var cwd = try jetzig.util.detectJetzigProjectDir();
|
||||
defer cwd.close();
|
||||
|
||||
const Repo = jetquery.Repo(config.adapter, Schema);
|
||||
var repo = try Repo.loadConfig(
|
||||
allocator,
|
||||
@field(jetquery.Environment, @tagName(environment)),
|
||||
.{ .context = .migration, .env = repo_env },
|
||||
);
|
||||
const reflect = @import("jetquery_reflect").Reflect(config.adapter, Schema).init(
|
||||
allocator,
|
||||
&repo,
|
||||
.{
|
||||
.import_jetquery =
|
||||
\\@import("jetzig").jetquery
|
||||
,
|
||||
},
|
||||
);
|
||||
const schema = try reflect.generateSchema();
|
||||
const project_dir = try jetzig.util.detectJetzigProjectDir();
|
||||
const project_dir_realpath = try project_dir.realpathAlloc(allocator, ".");
|
||||
const path = try std.fs.path.join(
|
||||
allocator,
|
||||
&.{ project_dir_realpath, "src", "app", "database", "Schema.zig" },
|
||||
);
|
||||
try jetzig.util.createFile(path, schema);
|
||||
std.log.info("Database schema written to `{s}`.", .{path});
|
||||
}
|
||||
|
||||
@ -24,9 +24,10 @@ 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.bright_magenta("{s: <7}"),
|
||||
.patch => jetzig.colors.bold(.magenta, "{s: <7}"),
|
||||
.delete => jetzig.colors.red("{s: <7}"),
|
||||
.custom => unreachable,
|
||||
};
|
||||
@ -36,6 +37,7 @@ pub fn main() !void {
|
||||
route.uri_path ++ switch (route.action) {
|
||||
.index, .post => "",
|
||||
.new => "/new",
|
||||
.edit => "/:id/edit",
|
||||
.get, .put, .patch, .delete => "/:id",
|
||||
.custom => "",
|
||||
},
|
||||
@ -54,7 +56,7 @@ pub fn main() !void {
|
||||
|
||||
for (jetzig_app.custom_routes.items) |route| {
|
||||
log(
|
||||
" " ++ jetzig.colors.bold(jetzig.colors.white("{s: <7}")) ++ " " ++ padded_path ++ " {s}:{s}",
|
||||
" " ++ jetzig.colors.bold(.white, "{s: <7}") ++ " " ++ padded_path ++ " {s}:{s}",
|
||||
.{ route.name, route.uri_path, route.view_name, route.name },
|
||||
);
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ 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;
|
||||
@ -32,7 +33,10 @@ pub const Date = jetcommon.types.Date;
|
||||
pub const authenticity_token_name = config.get([]const u8, "authenticity_token_name");
|
||||
|
||||
pub const build_options = @import("build_options");
|
||||
pub const environment = std.enums.nameCast(Environment.EnvironmentName, build_options.environment);
|
||||
pub const environment = @field(
|
||||
Environment.EnvironmentName,
|
||||
@tagName(build_options.environment),
|
||||
);
|
||||
|
||||
/// The primary interface for a Jetzig application. Create an `App` in your application's
|
||||
/// `src/main.zig` and call `start` to launch the application.
|
||||
|
||||
@ -24,8 +24,7 @@ const AppOptions = struct {
|
||||
};
|
||||
|
||||
/// Starts an application. `routes` should be `@import("routes").routes`, a generated file
|
||||
/// automatically created at build time. `templates` should be
|
||||
/// `@import("src/app/views/zmpl.manifest.zig").templates`, created by Zmpl at compile time.
|
||||
/// automatically created at build time.
|
||||
pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
|
||||
defer self.env.deinit();
|
||||
|
||||
@ -35,7 +34,7 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
|
||||
defer mime_map.deinit();
|
||||
try mime_map.build();
|
||||
|
||||
const routes = try createRoutes(self.allocator, &routes_module.routes);
|
||||
const routes = try createRoutes(self.allocator, if (@hasDecl(routes_module, "routes")) &routes_module.routes else &.{});
|
||||
defer {
|
||||
for (routes) |var_route| {
|
||||
var_route.deinitParams();
|
||||
@ -48,22 +47,13 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
|
||||
self.allocator.free(custom_route.template);
|
||||
};
|
||||
|
||||
var store = try jetzig.kv.Store.init(
|
||||
self.allocator,
|
||||
jetzig.config.get(jetzig.kv.Store.KVOptions, "store"),
|
||||
);
|
||||
var store = try jetzig.kv.Store.GeneralStore.init(self.allocator, self.env.logger, .general);
|
||||
defer store.deinit();
|
||||
|
||||
var job_queue = try jetzig.kv.Store.init(
|
||||
self.allocator,
|
||||
jetzig.config.get(jetzig.kv.Store.KVOptions, "job_queue"),
|
||||
);
|
||||
var job_queue = try jetzig.kv.Store.JobQueueStore.init(self.allocator, self.env.logger, .jobs);
|
||||
defer job_queue.deinit();
|
||||
|
||||
var cache = try jetzig.kv.Store.init(
|
||||
self.allocator,
|
||||
jetzig.config.get(jetzig.kv.Store.KVOptions, "cache"),
|
||||
);
|
||||
var cache = try jetzig.kv.Store.CacheStore.init(self.allocator, self.env.logger, .cache);
|
||||
defer cache.deinit();
|
||||
|
||||
var repo = try jetzig.database.repo(self.allocator, self);
|
||||
@ -72,7 +62,7 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
|
||||
var log_thread = try std.Thread.spawn(
|
||||
.{ .allocator = self.allocator },
|
||||
jetzig.loggers.LogQueue.Reader.publish,
|
||||
.{ &self.env.log_queue.reader, .{} },
|
||||
.{ &self.env.log_queue.reader, jetzig.loggers.LogQueue.Reader.PublishOptions{} },
|
||||
);
|
||||
defer log_thread.join();
|
||||
|
||||
@ -96,8 +86,8 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
|
||||
self.env,
|
||||
routes,
|
||||
self.custom_routes.items,
|
||||
&routes_module.jobs,
|
||||
&routes_module.mailers,
|
||||
if (@hasDecl(routes_module, "jobs")) &routes_module.jobs else &.{},
|
||||
if (@hasDecl(routes_module, "jobs")) &routes_module.mailers else &.{},
|
||||
&mime_map,
|
||||
&store,
|
||||
&job_queue,
|
||||
@ -116,10 +106,11 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
|
||||
.vars = self.env.vars,
|
||||
.environment = self.env.environment,
|
||||
.routes = routes,
|
||||
.jobs = &routes_module.jobs,
|
||||
.mailers = &routes_module.mailers,
|
||||
.jobs = if (@hasDecl(routes_module, "jobs")) &routes_module.jobs else &.{},
|
||||
.mailers = if (@hasDecl(routes_module, "jobs")) &routes_module.mailers else &.{},
|
||||
.store = &store,
|
||||
.cache = &cache,
|
||||
.repo = &repo,
|
||||
.mutex = &mutex,
|
||||
},
|
||||
);
|
||||
@ -140,7 +131,7 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
|
||||
return;
|
||||
},
|
||||
else => {
|
||||
try server.logger.ERROR("Encountered error: {}\nExiting.\n", .{err});
|
||||
try server.logger.ERROR("Encountered error at server launch: {}\nExiting.\n", .{err});
|
||||
std.process.exit(1);
|
||||
},
|
||||
}
|
||||
@ -211,7 +202,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,
|
||||
) ![]*jetzig.views.Route {
|
||||
) ![]const *const jetzig.views.Route {
|
||||
var routes = std.ArrayList(*jetzig.views.Route).init(allocator);
|
||||
|
||||
for (comptime_routes) |const_route| {
|
||||
|
||||
@ -196,15 +196,15 @@ pub fn init(parent_allocator: std.mem.Allocator, env_options: EnvironmentOptions
|
||||
|
||||
const env_file = std.fs.cwd().openFile(options.options.@"env-file", .{}) catch |err|
|
||||
switch (err) {
|
||||
error.FileNotFound => null,
|
||||
else => return err,
|
||||
};
|
||||
error.FileNotFound => null,
|
||||
else => return err,
|
||||
};
|
||||
|
||||
const vars = try Vars.init(allocator, env_file);
|
||||
|
||||
var launch_logger = LaunchLogger{
|
||||
.stdout = stdout,
|
||||
.stderr = stderr,
|
||||
.stdout = stdout.file,
|
||||
.stderr = stderr.file,
|
||||
.silent = env_options.silent,
|
||||
};
|
||||
|
||||
@ -213,8 +213,8 @@ pub fn init(parent_allocator: std.mem.Allocator, env_options: EnvironmentOptions
|
||||
.development_logger = jetzig.loggers.DevelopmentLogger.init(
|
||||
allocator,
|
||||
resolveLogLevel(options.options.@"log-level", jetzig.environment),
|
||||
stdout,
|
||||
stderr,
|
||||
stdout.file,
|
||||
stderr.file,
|
||||
),
|
||||
},
|
||||
.production => jetzig.loggers.Logger{
|
||||
@ -242,7 +242,7 @@ pub fn init(parent_allocator: std.mem.Allocator, env_options: EnvironmentOptions
|
||||
}
|
||||
|
||||
const secret_len = jetzig.http.Session.Cipher.key_length;
|
||||
const secret_value = try getSecret(allocator, launch_logger, secret_len, jetzig.environment);
|
||||
const secret_value = try getSecret(allocator, launch_logger, jetzig.environment);
|
||||
const secret = if (secret_value.len > secret_len) secret_value[0..secret_len] else secret_value;
|
||||
|
||||
if (secret.len != secret_len) {
|
||||
@ -304,29 +304,31 @@ pub fn deinit(self: Environment) void {
|
||||
self.parent_allocator.destroy(self.arena);
|
||||
}
|
||||
|
||||
fn getLogFile(stream: enum { stdout, stderr }, options: Options) !std.fs.File {
|
||||
fn getLogFile(stream: enum { stdout, stderr }, options: Options) !jetzig.loggers.LogFile {
|
||||
const path = switch (stream) {
|
||||
.stdout => options.log,
|
||||
.stderr => options.@"log-error",
|
||||
};
|
||||
|
||||
if (std.mem.eql(u8, path, "-")) return switch (stream) {
|
||||
.stdout => std.io.getStdOut(),
|
||||
.stdout => .{ .file = std.io.getStdOut(), .sync = false },
|
||||
.stderr => if (std.mem.eql(u8, options.log, "-"))
|
||||
std.io.getStdErr()
|
||||
.{ .file = std.io.getStdErr(), .sync = false }
|
||||
else
|
||||
try std.fs.createFileAbsolute(options.log, .{ .truncate = false }),
|
||||
.{
|
||||
.file = try std.fs.createFileAbsolute(options.log, .{ .truncate = false }),
|
||||
.sync = true,
|
||||
},
|
||||
};
|
||||
|
||||
const file = try std.fs.createFileAbsolute(path, .{ .truncate = false });
|
||||
try file.seekFromEnd(0);
|
||||
return file;
|
||||
return .{ .file = file, .sync = true };
|
||||
}
|
||||
|
||||
fn getSecret(
|
||||
allocator: std.mem.Allocator,
|
||||
logger: LaunchLogger,
|
||||
comptime len: u10,
|
||||
environment: EnvironmentName,
|
||||
) ![]const u8 {
|
||||
const env_var = "JETZIG_SECRET";
|
||||
@ -348,10 +350,11 @@ fn getSecret(
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
const secret = try jetzig.util.generateSecret(allocator, len);
|
||||
const secret = "jetzig-development-cookie-secret";
|
||||
|
||||
try logger.log(
|
||||
.WARN,
|
||||
"Running in {s} mode, using auto-generated cookie encryption key: {s}",
|
||||
"Running in {s} mode, using default development cookie encryption key: `{s}`",
|
||||
.{ @tagName(environment), secret },
|
||||
);
|
||||
try logger.log(
|
||||
|
||||
@ -14,7 +14,7 @@ pub fn getUserId(comptime id_type: IdType, request: *jetzig.Request) !?switch (i
|
||||
} {
|
||||
const session = try request.session();
|
||||
|
||||
return session.getT(std.enums.nameCast(jetzig.data.ValueType, id_type), "_jetzig_user_id");
|
||||
return session.getT(@field(jetzig.data.ValueType, @tagName(id_type)), "_jetzig_user_id");
|
||||
}
|
||||
|
||||
pub fn signIn(request: *jetzig.Request, user_id: anytype) !void {
|
||||
@ -42,13 +42,16 @@ pub fn verifyPassword(
|
||||
}
|
||||
|
||||
pub fn hashPassword(allocator: std.mem.Allocator, password: []const u8) ![]const u8 {
|
||||
const buf = try allocator.alloc(u8, 128);
|
||||
return try std.crypto.pwhash.argon2.strHash(
|
||||
var buf: [128]u8 = undefined;
|
||||
const hash = try std.crypto.pwhash.argon2.strHash(
|
||||
password,
|
||||
.{
|
||||
.allocator = allocator,
|
||||
.params = .{ .t = 3, .m = 32, .p = 4 },
|
||||
},
|
||||
buf,
|
||||
&buf,
|
||||
);
|
||||
const result = try allocator.alloc(u8, hash.len);
|
||||
@memcpy(result, hash);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -5,6 +5,25 @@ 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[",
|
||||
@ -53,12 +72,7 @@ const ansi_colors = .{
|
||||
.{ "2", .dim },
|
||||
.{ "0", .reset },
|
||||
};
|
||||
pub const codes_map = if (@hasDecl(std, "ComptimeStringMap"))
|
||||
std.ComptimeStringMap(std.io.tty.Color, ansi_colors)
|
||||
else if (@hasDecl(std, "StaticStringMap"))
|
||||
std.StaticStringMap(std.io.tty.Color).initComptime(ansi_colors)
|
||||
else
|
||||
unreachable;
|
||||
pub const codes_map = std.StaticStringMap(std.io.tty.Color).initComptime(ansi_colors);
|
||||
|
||||
// Map basic ANSI color codes to Windows TextAttribute colors
|
||||
// used by std.os.windows.SetConsoleTextAttribute()
|
||||
@ -83,12 +97,7 @@ const windows_colors = .{
|
||||
.{ "2", 7 },
|
||||
.{ "0", 7 },
|
||||
};
|
||||
pub const windows_map = if (@hasDecl(std, "ComptimeStringMap"))
|
||||
std.ComptimeStringMap(u16, windows_colors)
|
||||
else if (@hasDecl(std, "StaticStringMap"))
|
||||
std.StaticStringMap(u16).initComptime(windows_colors)
|
||||
else
|
||||
unreachable;
|
||||
pub const windows_map = std.StaticStringMap(u16).initComptime(windows_colors);
|
||||
|
||||
/// 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
|
||||
@ -122,8 +131,8 @@ fn runtimeWrap(allocator: std.mem.Allocator, attribute: []const u8, message: []c
|
||||
);
|
||||
}
|
||||
|
||||
pub fn bold(comptime message: []const u8) []const u8 {
|
||||
return codes.escape ++ codes.bold ++ message ++ codes.escape ++ codes.reset;
|
||||
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 black(comptime message: []const u8) []const u8 {
|
||||
|
||||
@ -18,7 +18,10 @@ pub const mail = @import("mail.zig");
|
||||
pub const kv = @import("kv.zig");
|
||||
pub const db = @import("database.zig");
|
||||
pub const Environment = @import("Environment.zig");
|
||||
pub const environment = std.enums.nameCast(Environment.EnvironmentName, build_options.environment);
|
||||
pub const environment = @field(
|
||||
Environment.EnvironmentName,
|
||||
@tagName(build_options.environment),
|
||||
);
|
||||
pub const build_options = @import("build_options");
|
||||
|
||||
const root = @import("root");
|
||||
@ -107,38 +110,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.KVOptions = .{
|
||||
pub const store: kv.Store.Options = .{
|
||||
.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),
|
||||
// .address_space_size = jetzig.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.KVOptions = .{
|
||||
pub const job_queue: kv.Store.Options = .{
|
||||
.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),
|
||||
// .address_space_size = jetzig.jetkv.JetKV.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.KVOptions = .{
|
||||
pub const cache: kv.Store.Options = .{
|
||||
.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),
|
||||
// .address_space_size = jetzig.jetkv.JetKV.addressSpace(4096),
|
||||
// },
|
||||
};
|
||||
|
||||
@ -160,7 +163,7 @@ pub const cookies: http.Cookies.CookieOptions = switch (environment) {
|
||||
.production => .{
|
||||
.secure = true,
|
||||
.http_only = true,
|
||||
.same_site = true,
|
||||
.same_site = .lax,
|
||||
.path = "/",
|
||||
},
|
||||
};
|
||||
|
||||
@ -2,9 +2,9 @@ const std = @import("std");
|
||||
|
||||
const jetzig = @import("../jetzig.zig");
|
||||
|
||||
pub const adapter = std.enums.nameCast(
|
||||
pub const adapter = @field(
|
||||
jetzig.jetquery.adapters.Name,
|
||||
@field(jetzig.jetquery.config.database, @tagName(jetzig.environment)).adapter,
|
||||
@tagName(@field(jetzig.jetquery.config.database, @tagName(jetzig.environment)).adapter),
|
||||
);
|
||||
|
||||
pub const Schema = jetzig.config.get(type, "Schema");
|
||||
@ -25,7 +25,7 @@ pub fn repo(allocator: std.mem.Allocator, app: anytype) !Repo {
|
||||
|
||||
return try Repo.loadConfig(
|
||||
allocator,
|
||||
std.enums.nameCast(jetzig.jetquery.Environment, jetzig.environment),
|
||||
@field(jetzig.jetquery.Environment, @tagName(jetzig.environment)),
|
||||
.{
|
||||
.eventCallback = Callback.callbackFn,
|
||||
.lazy_connect = switch (jetzig.environment) {
|
||||
|
||||
218
src/jetzig/debug.zig
Normal file
218
src/jetzig/debug.zig
Normal file
@ -0,0 +1,218 @@
|
||||
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>
|
||||
\\
|
||||
;
|
||||
@ -16,11 +16,12 @@ file_path: []const u8,
|
||||
resource_id: []const u8,
|
||||
extension: ?[]const u8,
|
||||
query: ?[]const u8,
|
||||
method: ?jetzig.Request.Method,
|
||||
|
||||
const Self = @This();
|
||||
const Path = @This();
|
||||
|
||||
/// Initialize a new HTTP Path.
|
||||
pub fn init(path: []const u8) Self {
|
||||
pub fn init(path: []const u8) Path {
|
||||
const base_path = getBasePath(path);
|
||||
|
||||
return .{
|
||||
@ -31,18 +32,19 @@ pub fn init(path: []const u8) Self {
|
||||
.resource_id = getResourceId(base_path),
|
||||
.extension = getExtension(path),
|
||||
.query = getQuery(path),
|
||||
.method = getMethod(path),
|
||||
};
|
||||
}
|
||||
|
||||
/// No-op - no allocations currently performed.
|
||||
pub fn deinit(self: *Self) void {
|
||||
pub fn deinit(self: *Path) 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: Self, route: jetzig.views.Route) []const u8 {
|
||||
pub fn resourceId(self: Path, 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, '/');
|
||||
|
||||
@ -54,7 +56,7 @@ pub fn resourceId(self: Self, route: jetzig.views.Route) []const u8 {
|
||||
return self.resource_id;
|
||||
}
|
||||
|
||||
pub fn resourceArgs(self: Self, route: jetzig.views.Route, allocator: std.mem.Allocator) ![]const []const u8 {
|
||||
pub fn resourceArgs(self: Path, 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, '/');
|
||||
@ -82,21 +84,41 @@ pub fn resourceArgs(self: Self, 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 {
|
||||
if (std.mem.indexOfScalar(u8, path, '?')) |query_index| {
|
||||
const base = if (std.mem.indexOfScalar(u8, path, '?')) |query_index| blk: {
|
||||
if (std.mem.lastIndexOfScalar(u8, path[0..query_index], '.')) |extension_index| {
|
||||
return path[0..extension_index];
|
||||
break :blk path[0..extension_index];
|
||||
} else {
|
||||
return path[0..query_index];
|
||||
break :blk path[0..query_index];
|
||||
}
|
||||
} else if (std.mem.lastIndexOfScalar(u8, path, '.')) |extension_index| {
|
||||
return if (isRootPath(path[0..extension_index]))
|
||||
} else if (std.mem.lastIndexOfScalar(u8, path, '.')) |extension_index| blk: {
|
||||
break :blk if (isRootPath(path[0..extension_index]))
|
||||
path[0..extension_index]
|
||||
else
|
||||
std.mem.trimRight(u8, path[0..extension_index], "/");
|
||||
} else {
|
||||
return if (isRootPath(path)) path else std.mem.trimRight(u8, path, "/");
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract `"/foo/bar"` from:
|
||||
@ -131,6 +153,9 @@ 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;
|
||||
}
|
||||
@ -164,169 +189,228 @@ 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 = Self.init("/foo/bar/baz.html?qux=quux&corge=grault");
|
||||
const path = Path.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 = Self.init("/foo/bar/baz.html");
|
||||
const path = Path.init("/foo/bar/baz.html");
|
||||
|
||||
try std.testing.expectEqualStrings("/foo/bar/baz", path.base_path);
|
||||
}
|
||||
|
||||
test ".base_path (without extension, without query)" {
|
||||
const path = Self.init("/foo/bar/baz");
|
||||
const path = Path.init("/foo/bar/baz");
|
||||
|
||||
try std.testing.expectEqualStrings("/foo/bar/baz", path.base_path);
|
||||
}
|
||||
|
||||
test ".base_path (with trailing slash)" {
|
||||
const path = Self.init("/foo/bar/");
|
||||
const path = Path.init("/foo/bar/");
|
||||
|
||||
try std.testing.expectEqualStrings("/foo/bar", path.base_path);
|
||||
}
|
||||
|
||||
test ".base_path (root path)" {
|
||||
const path = Self.init("/");
|
||||
const path = Path.init("/");
|
||||
|
||||
try std.testing.expectEqualStrings("/", path.base_path);
|
||||
}
|
||||
|
||||
test ".base_path (root path with extension)" {
|
||||
const path = Self.init("/.json");
|
||||
const path = Path.init("/.json");
|
||||
|
||||
try std.testing.expectEqualStrings("/", path.base_path);
|
||||
try std.testing.expectEqualStrings(".json", path.extension.?);
|
||||
}
|
||||
|
||||
test ".directory (with extension, with query)" {
|
||||
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault");
|
||||
const path = Path.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 = Self.init("/foo/bar/baz.html");
|
||||
const path = Path.init("/foo/bar/baz.html");
|
||||
|
||||
try std.testing.expectEqualStrings("/foo/bar", path.directory);
|
||||
}
|
||||
|
||||
test ".directory (without extension, without query)" {
|
||||
const path = Self.init("/foo/bar/baz");
|
||||
const path = Path.init("/foo/bar/baz");
|
||||
|
||||
try std.testing.expectEqualStrings("/foo/bar", path.directory);
|
||||
}
|
||||
|
||||
test ".directory (without extension, without query, root path)" {
|
||||
const path = Self.init("/");
|
||||
const path = Path.init("/");
|
||||
|
||||
try std.testing.expectEqualStrings("/", path.directory);
|
||||
}
|
||||
|
||||
test ".resource_id (with extension, with query)" {
|
||||
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault");
|
||||
const path = Path.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 = Self.init("/foo/bar/baz.html");
|
||||
const path = Path.init("/foo/bar/baz.html");
|
||||
|
||||
try std.testing.expectEqualStrings("baz", path.resource_id);
|
||||
}
|
||||
|
||||
test ".resource_id (without extension, without query)" {
|
||||
const path = Self.init("/foo/bar/baz");
|
||||
const path = Path.init("/foo/bar/baz");
|
||||
|
||||
try std.testing.expectEqualStrings("baz", path.resource_id);
|
||||
}
|
||||
|
||||
test ".resource_id (without extension, without query, without base path)" {
|
||||
const path = Self.init("/baz");
|
||||
const path = Path.init("/baz");
|
||||
|
||||
try std.testing.expectEqualStrings("baz", path.resource_id);
|
||||
}
|
||||
|
||||
test ".resource_id (with trailing slash)" {
|
||||
const path = Self.init("/foo/bar/");
|
||||
const path = Path.init("/foo/bar/");
|
||||
|
||||
try std.testing.expectEqualStrings("bar", path.resource_id);
|
||||
}
|
||||
|
||||
test ".extension (with query)" {
|
||||
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault");
|
||||
const path = Path.init("/foo/bar/baz.html?qux=quux&corge=grault");
|
||||
|
||||
try std.testing.expectEqualStrings(".html", path.extension.?);
|
||||
}
|
||||
|
||||
test ".extension (without query)" {
|
||||
const path = Self.init("/foo/bar/baz.html");
|
||||
const path = Path.init("/foo/bar/baz.html");
|
||||
|
||||
try std.testing.expectEqualStrings(".html", path.extension.?);
|
||||
}
|
||||
|
||||
test ".extension (without extension)" {
|
||||
const path = Self.init("/foo/bar/baz");
|
||||
const path = Path.init("/foo/bar/baz");
|
||||
|
||||
try std.testing.expect(path.extension == null);
|
||||
}
|
||||
|
||||
test ".query (with extension, with query)" {
|
||||
const path = Self.init("/foo/bar/baz.html?qux=quux&corge=grault");
|
||||
const path = Path.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 = Self.init("/foo/bar/baz?qux=quux&corge=grault");
|
||||
const path = Path.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 = Self.init("/foo/bar/baz.json");
|
||||
const path = Path.init("/foo/bar/baz.json");
|
||||
|
||||
try std.testing.expect(path.query == null);
|
||||
}
|
||||
|
||||
test ".query (without extension, without query)" {
|
||||
const path = Self.init("/foo/bar/baz");
|
||||
const path = Path.init("/foo/bar/baz");
|
||||
|
||||
try std.testing.expect(path.query == null);
|
||||
}
|
||||
|
||||
test ".query (with empty query)" {
|
||||
const path = Self.init("/foo/bar/baz?");
|
||||
const path = Path.init("/foo/bar/baz?");
|
||||
|
||||
try std.testing.expect(path.query == null);
|
||||
}
|
||||
|
||||
test ".file_path (with extension, with query)" {
|
||||
const path = Self.init("/foo/bar/baz.json?qux=quux&corge=grault");
|
||||
const path = Path.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 = Self.init("/foo/bar/baz.json");
|
||||
const path = Path.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 = Self.init("/foo/bar/baz");
|
||||
const path = Path.init("/foo/bar/baz");
|
||||
|
||||
try std.testing.expectEqualStrings("/foo/bar/baz", path.file_path);
|
||||
}
|
||||
|
||||
test ".file_path (without extension, with query)" {
|
||||
const path = Self.init("/foo/bar/baz?qux=quux&corge=grault");
|
||||
const path = Path.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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -11,6 +11,16 @@ 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,
|
||||
@ -30,82 +40,85 @@ parsed_multipart: ?*jetzig.data.Data = null,
|
||||
_cookies: ?*jetzig.http.Cookies = null,
|
||||
_session: ?*jetzig.http.Session = null,
|
||||
body: []const u8 = undefined,
|
||||
state: enum { initial, processed } = .initial,
|
||||
response_started: bool = false,
|
||||
state: RequestState = .initial,
|
||||
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,
|
||||
cache: RequestStore,
|
||||
store: RequestStore(jetzig.kv.Store.GeneralStore),
|
||||
cache: RequestStore(jetzig.kv.Store.CacheStore),
|
||||
repo: *jetzig.database.Repo,
|
||||
global: *jetzig.Global,
|
||||
|
||||
/// Wrapper for KV store that uses the request's arena allocator for fetching values.
|
||||
pub const RequestStore = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
store: *jetzig.kv.Store,
|
||||
pub fn RequestStore(T: type) type {
|
||||
return struct {
|
||||
allocator: std.mem.Allocator,
|
||||
store: *T,
|
||||
|
||||
/// 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);
|
||||
}
|
||||
const Self = @This();
|
||||
|
||||
/// 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));
|
||||
}
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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.
|
||||
pub fn remove(self: RequestStore, key: []const u8) !void {
|
||||
try self.store.remove(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);
|
||||
}
|
||||
|
||||
/// 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 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);
|
||||
}
|
||||
|
||||
/// 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));
|
||||
}
|
||||
/// Remove a String to from the key-value store.
|
||||
pub fn remove(self: Self, key: []const u8) !void {
|
||||
try self.store.remove(key);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
/// 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));
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
/// 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));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
/// 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
@ -116,7 +129,11 @@ pub fn init(
|
||||
response: *jetzig.http.Response,
|
||||
repo: *jetzig.database.Repo,
|
||||
) !Request {
|
||||
const method = switch (httpz_request.method) {
|
||||
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) {
|
||||
.DELETE => Method.DELETE,
|
||||
.GET => Method.GET,
|
||||
.PATCH => Method.PATCH,
|
||||
@ -124,6 +141,7 @@ pub fn init(
|
||||
.HEAD => Method.HEAD,
|
||||
.PUT => Method.PUT,
|
||||
.OPTIONS => Method.OPTIONS,
|
||||
.CONNECT, .OTHER => return error.JetzigUnsupportedHttpMethod,
|
||||
};
|
||||
|
||||
const response_data = try allocator.create(jetzig.data.Data);
|
||||
@ -131,7 +149,7 @@ pub fn init(
|
||||
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.path = jetzig.http.Path.init(httpz_request.url.raw),
|
||||
.path = path,
|
||||
.method = method,
|
||||
.headers = jetzig.http.Headers.init(allocator, httpz_request.headers),
|
||||
.server = server,
|
||||
@ -177,6 +195,7 @@ 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.
|
||||
@ -191,10 +210,9 @@ 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.rendered or self.failed) self.rendered_multiple = true;
|
||||
if (self.isRendered()) self.rendered_multiple = true;
|
||||
|
||||
self.rendered = true;
|
||||
if (self.response_started) self.middleware_rendered_during_response = true;
|
||||
self.state = .rendered;
|
||||
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
|
||||
return self.rendered_view.?;
|
||||
}
|
||||
@ -202,15 +220,20 @@ 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.rendered or self.redirected) self.rendered_multiple = true;
|
||||
if (self.isRendered()) self.rendered_multiple = true;
|
||||
|
||||
self.rendered = true;
|
||||
self.failed = true;
|
||||
if (self.response_started) self.middleware_rendered_during_response = true;
|
||||
self.state = .failed;
|
||||
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);
|
||||
@ -224,11 +247,9 @@ pub fn redirect(
|
||||
location: []const u8,
|
||||
redirect_status: enum { moved_permanently, found },
|
||||
) jetzig.views.View {
|
||||
if (self.rendered or self.failed) self.rendered_multiple = true;
|
||||
if (self.isRendered()) self.rendered_multiple = true;
|
||||
|
||||
self.rendered = true;
|
||||
self.redirected = true;
|
||||
if (self.response_started) self.middleware_rendered_during_response = true;
|
||||
self.state = .redirected;
|
||||
|
||||
const status_code = switch (redirect_status) {
|
||||
.moved_permanently => jetzig.http.status_codes.StatusCode.moved_permanently,
|
||||
@ -476,12 +497,14 @@ pub fn cookies(self: *Request) !*jetzig.http.Cookies {
|
||||
/// `jetzig.http.Session`.
|
||||
pub fn session(self: *Request) !*jetzig.http.Session {
|
||||
if (self._session) |capture| return capture;
|
||||
|
||||
const cookie_name = self.server.env.vars.get("JETZIG_SESSION_COOKIE") orelse
|
||||
jetzig.http.Session.default_cookie_name;
|
||||
const local_session = try self.allocator.create(jetzig.http.Session);
|
||||
local_session.* = jetzig.http.Session.init(
|
||||
self.allocator,
|
||||
try self.cookies(),
|
||||
self.server.env.secret,
|
||||
.{ .cookie_name = cookie_name },
|
||||
);
|
||||
local_session.parse() catch |err| {
|
||||
switch (err) {
|
||||
@ -560,25 +583,18 @@ const RequestMail = struct {
|
||||
_ = options;
|
||||
var mail_job = try self.request.job("__jetzig_mail");
|
||||
|
||||
try mail_job.params.put("mailer_name", mail_job.data.string(self.name));
|
||||
|
||||
const from = if (self.mail_params.from) |from| mail_job.data.string(from) else null;
|
||||
try mail_job.params.put("from", from);
|
||||
try mail_job.params.put("mailer_name", self.name);
|
||||
try mail_job.params.put("from", self.mail_params.get(.from));
|
||||
|
||||
var to_array = try mail_job.data.array();
|
||||
if (self.mail_params.to) |capture| {
|
||||
for (capture) |to| try to_array.append(mail_job.data.string(to));
|
||||
if (self.mail_params.get(.to)) |to| {
|
||||
for (to) |each| try to_array.append(.{ .email = each.email, .name = each.name });
|
||||
}
|
||||
try mail_job.params.put("to", to_array);
|
||||
|
||||
const subject = if (self.mail_params.subject) |subject| mail_job.data.string(subject) else null;
|
||||
try mail_job.params.put("subject", subject);
|
||||
|
||||
const html = if (self.mail_params.html) |html| mail_job.data.string(html) else null;
|
||||
try mail_job.params.put("html", html);
|
||||
|
||||
const text = if (self.mail_params.text) |text| mail_job.data.string(text) else null;
|
||||
try mail_job.params.put("text", text);
|
||||
try mail_job.params.put("subject", self.mail_params.get(.subject));
|
||||
try mail_job.params.put("html", self.mail_params.get(.html));
|
||||
try mail_job.params.put("text", self.mail_params.get(.text));
|
||||
|
||||
if (self.request.response_data.value) |value| try mail_job.params.put(
|
||||
"params",
|
||||
@ -600,6 +616,7 @@ const RequestMail = struct {
|
||||
.store = self.request.server.store,
|
||||
.cache = self.request.server.cache,
|
||||
.mutex = undefined,
|
||||
.repo = self.request.repo,
|
||||
},
|
||||
),
|
||||
}
|
||||
@ -687,6 +704,19 @@ 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;
|
||||
@ -745,6 +775,7 @@ 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) {
|
||||
@ -777,8 +808,18 @@ fn isMatch(
|
||||
.resource_id => self.path.directory,
|
||||
};
|
||||
|
||||
// Special case for `/foobar/1/new` -> render `new()`
|
||||
if (route.action == .get and std.mem.eql(u8, self.path.resource_id, "new")) return false;
|
||||
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;
|
||||
}
|
||||
|
||||
return std.mem.eql(u8, path, route.uri_path);
|
||||
}
|
||||
|
||||
@ -9,32 +9,33 @@ const httpz = @import("httpz");
|
||||
allocator: std.mem.Allocator,
|
||||
logger: jetzig.loggers.Logger,
|
||||
env: jetzig.Environment,
|
||||
routes: []*jetzig.views.Route,
|
||||
custom_routes: []jetzig.views.Route,
|
||||
routes: []const *const jetzig.views.Route,
|
||||
custom_routes: []const 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,
|
||||
job_queue: *jetzig.kv.Store,
|
||||
cache: *jetzig.kv.Store,
|
||||
store: *jetzig.kv.Store.GeneralStore,
|
||||
job_queue: *jetzig.kv.Store.JobQueueStore,
|
||||
cache: *jetzig.kv.Store.CacheStore,
|
||||
repo: *jetzig.database.Repo,
|
||||
global: *anyopaque,
|
||||
decoded_static_route_params: []*jetzig.data.Value = &.{},
|
||||
decoded_static_route_params: []const *jetzig.data.Value = &.{},
|
||||
debug_mutex: std.Thread.Mutex = .{},
|
||||
|
||||
const Server = @This();
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
env: jetzig.Environment,
|
||||
routes: []*jetzig.views.Route,
|
||||
custom_routes: []jetzig.views.Route,
|
||||
routes: []const *const jetzig.views.Route,
|
||||
custom_routes: []const jetzig.views.Route,
|
||||
job_definitions: []const jetzig.JobDefinition,
|
||||
mailer_definitions: []const jetzig.MailerDefinition,
|
||||
mime_map: *jetzig.http.mime.MimeMap,
|
||||
store: *jetzig.kv.Store,
|
||||
job_queue: *jetzig.kv.Store,
|
||||
cache: *jetzig.kv.Store,
|
||||
store: *jetzig.kv.Store.GeneralStore,
|
||||
job_queue: *jetzig.kv.Store.JobQueueStore,
|
||||
cache: *jetzig.kv.Store.CacheStore,
|
||||
repo: *jetzig.database.Repo,
|
||||
global: *anyopaque,
|
||||
) Server {
|
||||
@ -73,36 +74,44 @@ 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 = jetzig.config.get(?u16, "thread_count") orelse @intCast(try std.Thread.getCpuCount()),
|
||||
.count = thread_count,
|
||||
.buffer_size = jetzig.config.get(usize, "buffer_size"),
|
||||
},
|
||||
.workers = .{
|
||||
.count = jetzig.config.get(u16, "worker_count"),
|
||||
.count = 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}:{} [{s}]", .{
|
||||
try self.logger.INFO("Listening on http://{s}:{d} [{s}] [workers:{d} threads:{d}]", .{
|
||||
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();
|
||||
}
|
||||
|
||||
@ -111,7 +120,11 @@ 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.logStackTrace(capture, request.arena) catch {};
|
||||
if (stack) |capture| {
|
||||
self.debug_mutex.lock();
|
||||
defer self.debug_mutex.unlock();
|
||||
self.logStackTrace(capture, request.arena) catch {};
|
||||
}
|
||||
|
||||
response.body = "500 Internal Server Error";
|
||||
}
|
||||
@ -123,6 +136,9 @@ 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,
|
||||
@ -131,14 +147,31 @@ pub fn processNextRequest(
|
||||
httpz_request,
|
||||
httpz_response,
|
||||
&response,
|
||||
self.repo,
|
||||
&repo,
|
||||
);
|
||||
|
||||
try request.process();
|
||||
|
||||
var middleware_data = try jetzig.http.middleware.afterRequest(&request);
|
||||
if (try maybeMiddlewareRender(&request, &response)) {
|
||||
try self.logger.logRequest(&request);
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.middleware_rendered) |_| { // Request processing ends when a middleware renders or redirects.
|
||||
try self.renderResponse(&request);
|
||||
try request.response.headers.append("Content-Type", response.content_type);
|
||||
|
||||
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
|
||||
try request.respond();
|
||||
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
|
||||
jetzig.http.middleware.deinit(&middleware_data, &request);
|
||||
|
||||
try self.logger.logRequest(&request);
|
||||
}
|
||||
|
||||
fn maybeMiddlewareRender(request: *jetzig.http.Request, response: *const jetzig.http.Response) !bool {
|
||||
if (request.middleware_rendered) |_| {
|
||||
// Request processing ends when a middleware renders or redirects.
|
||||
if (request.redirect_state) |state| {
|
||||
try request.renderRedirect(state);
|
||||
} else if (request.rendered_view) |rendered| {
|
||||
@ -147,17 +180,8 @@ pub fn processNextRequest(
|
||||
}
|
||||
try request.response.headers.append("Content-Type", response.content_type);
|
||||
try request.respond();
|
||||
} else {
|
||||
try self.renderResponse(&request);
|
||||
try request.response.headers.append("Content-Type", response.content_type);
|
||||
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
|
||||
|
||||
try request.respond();
|
||||
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
|
||||
jetzig.http.middleware.deinit(&middleware_data, &request);
|
||||
}
|
||||
|
||||
try self.logger.logRequest(&request);
|
||||
return true;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
|
||||
@ -197,9 +221,12 @@ 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.failed) {
|
||||
request.setResponse(try self.renderError(request, view.status_code), .{});
|
||||
} else if (request.rendered) {
|
||||
if (request.state == .failed) {
|
||||
request.setResponse(
|
||||
try self.renderError(request, view.status_code, .{}),
|
||||
.{},
|
||||
);
|
||||
} else if (request.state == .rendered) {
|
||||
// TODO: Allow callbacks to set content
|
||||
}
|
||||
return;
|
||||
@ -259,7 +286,9 @@ fn renderHTML(
|
||||
return request.setResponse(rendered_error, .{});
|
||||
};
|
||||
|
||||
return if (request.redirected or request.failed or request.dynamic_assigned_template != null)
|
||||
return if (request.state == .redirected or
|
||||
request.state == .failed or
|
||||
request.dynamic_assigned_template != null)
|
||||
request.setResponse(rendered, .{})
|
||||
else
|
||||
request.setResponse(try self.renderNotFound(request), .{});
|
||||
@ -327,12 +356,12 @@ fn renderView(
|
||||
return try self.renderInternalServerError(request, @errorReturnTrace(), err);
|
||||
};
|
||||
|
||||
if (request.failed) {
|
||||
if (request.state == .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|
|
||||
@ -343,7 +372,7 @@ fn renderView(
|
||||
if (request.rendered_multiple) return error.JetzigMultipleRenderError;
|
||||
|
||||
if (request.rendered_view) |rendered_view| {
|
||||
if (request.redirected) return .{ .view = rendered_view, .content = "" };
|
||||
if (request.state == .redirected) return .{ .view = rendered_view, .content = "" };
|
||||
|
||||
if (template) |capture| {
|
||||
return .{
|
||||
@ -352,12 +381,21 @@ fn renderView(
|
||||
};
|
||||
} else {
|
||||
return switch (request.requestFormat()) {
|
||||
.HTML, .UNKNOWN => try self.renderNotFound(request),
|
||||
.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);
|
||||
},
|
||||
.JSON => .{ .view = rendered_view, .content = "" },
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (!request.redirected) {
|
||||
if (request.state == .processed) {
|
||||
try self.logger.WARN("`request.render` was not invoked. Rendering empty content.", .{});
|
||||
}
|
||||
request.response_data.reset();
|
||||
@ -461,32 +499,42 @@ fn renderInternalServerError(
|
||||
stack_trace: ?*std.builtin.StackTrace,
|
||||
err: anyerror,
|
||||
) !RenderedView {
|
||||
request.response_data.reset();
|
||||
|
||||
try self.logger.logError(stack_trace, err);
|
||||
|
||||
const status = .internal_server_error;
|
||||
return try self.renderError(request, status);
|
||||
const status = jetzig.http.StatusCode.internal_server_error;
|
||||
|
||||
return try self.renderError(request, status, .{ .stack_trace = stack_trace, .err = err });
|
||||
}
|
||||
|
||||
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;
|
||||
@ -494,6 +542,42 @@ fn renderError(
|
||||
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,
|
||||
@ -641,6 +725,13 @@ 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);
|
||||
@ -718,7 +809,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,
|
||||
@ -754,7 +845,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);
|
||||
|
||||
@ -2,12 +2,12 @@ const std = @import("std");
|
||||
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
pub const cookie_name = "_jetzig-session";
|
||||
pub const Cipher = std.crypto.aead.chacha_poly.XChaCha20Poly1305;
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
encryption_key: []const u8,
|
||||
cookies: *jetzig.http.Cookies,
|
||||
cookie_name: []const u8,
|
||||
|
||||
initialized: bool = false,
|
||||
data: jetzig.data.Data,
|
||||
@ -15,22 +15,30 @@ state: enum { parsed, pending } = .pending,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub const default_cookie_name = "_jetzig-session";
|
||||
|
||||
pub const Options = struct {
|
||||
cookie_name: []const u8 = default_cookie_name,
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
cookies: *jetzig.http.Cookies,
|
||||
encryption_key: []const u8,
|
||||
options: Options,
|
||||
) Self {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.data = jetzig.data.Data.init(allocator),
|
||||
.cookies = cookies,
|
||||
.cookie_name = options.cookie_name,
|
||||
.encryption_key = encryption_key,
|
||||
};
|
||||
}
|
||||
|
||||
/// Parse session cookie.
|
||||
pub fn parse(self: *Self) !void {
|
||||
if (self.cookies.get(cookie_name)) |cookie| {
|
||||
if (self.cookies.get(self.cookie_name)) |cookie| {
|
||||
try self.parseSessionCookie(cookie.value);
|
||||
} else {
|
||||
try self.reset();
|
||||
@ -111,7 +119,7 @@ fn save(self: *Self) !void {
|
||||
defer self.allocator.free(encrypted);
|
||||
const encoded = try jetzig.util.base64Encode(self.allocator, encrypted);
|
||||
defer self.allocator.free(encoded);
|
||||
try self.cookies.put(.{ .name = cookie_name, .value = encoded });
|
||||
try self.cookies.put(.{ .name = self.cookie_name, .value = encoded });
|
||||
}
|
||||
|
||||
fn parseSessionCookie(self: *Self, cookie_value: []const u8) !void {
|
||||
@ -180,7 +188,7 @@ test "put and get session key/value" {
|
||||
try cookies.parse();
|
||||
|
||||
const secret: [Cipher.key_length]u8 = [_]u8{0x69} ** Cipher.key_length;
|
||||
var session = Self.init(allocator, &cookies, &secret);
|
||||
var session = Self.init(allocator, &cookies, &secret, .{});
|
||||
defer session.deinit();
|
||||
|
||||
var data = jetzig.data.Data.init(allocator);
|
||||
@ -199,7 +207,7 @@ test "remove session key/value" {
|
||||
try cookies.parse();
|
||||
|
||||
const secret: [Cipher.key_length]u8 = [_]u8{0x69} ** Cipher.key_length;
|
||||
var session = Self.init(allocator, &cookies, &secret);
|
||||
var session = Self.init(allocator, &cookies, &secret, .{});
|
||||
defer session.deinit();
|
||||
|
||||
var data = jetzig.data.Data.init(allocator);
|
||||
@ -224,7 +232,7 @@ test "get value from parsed/decrypted cookie" {
|
||||
try cookies.parse();
|
||||
|
||||
const secret: [Cipher.key_length]u8 = [_]u8{0x69} ** Cipher.key_length;
|
||||
var session = Self.init(allocator, &cookies, &secret);
|
||||
var session = Self.init(allocator, &cookies, &secret, .{});
|
||||
defer session.deinit();
|
||||
|
||||
try session.parse();
|
||||
@ -233,17 +241,32 @@ test "get value from parsed/decrypted cookie" {
|
||||
}
|
||||
|
||||
test "invalid cookie value - too short" {
|
||||
const allocator = std.testing.allocator;
|
||||
var cookies = jetzig.http.Cookies.init(allocator, "_jetzig-session=abc");
|
||||
defer cookies.deinit();
|
||||
try cookies.parse();
|
||||
|
||||
const secret: [Cipher.key_length]u8 = [_]u8{0x69} ** Cipher.key_length;
|
||||
var session = Self.init(allocator, &cookies, &secret, .{});
|
||||
defer session.deinit();
|
||||
|
||||
try std.testing.expectError(error.JetzigInvalidSessionCookie, session.parse());
|
||||
}
|
||||
|
||||
test "custom session cookie name" {
|
||||
const allocator = std.testing.allocator;
|
||||
var cookies = jetzig.http.Cookies.init(
|
||||
allocator,
|
||||
"_jetzig-session=abc",
|
||||
"custom-cookie-name=fPCFwZHvPDT-XCVcsQUSspDLchS3tRuJDqPpB2v3127VXpRP_bPcPLgpHK6RiVkfcP1bMtU",
|
||||
);
|
||||
defer cookies.deinit();
|
||||
try cookies.parse();
|
||||
|
||||
const secret: [Cipher.key_length]u8 = [_]u8{0x69} ** Cipher.key_length;
|
||||
var session = Self.init(allocator, &cookies, &secret);
|
||||
var session = Self.init(allocator, &cookies, &secret, .{ .cookie_name = "custom-cookie-name" });
|
||||
defer session.deinit();
|
||||
|
||||
try std.testing.expectError(error.JetzigInvalidSessionCookie, session.parse());
|
||||
try session.parse();
|
||||
var value = (session.get("foo")).?;
|
||||
try std.testing.expectEqualStrings("bar", try value.toString());
|
||||
}
|
||||
|
||||
@ -48,6 +48,14 @@ pub fn Type(comptime name: MiddlewareEnum()) type {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn afterLaunch(server: *jetzig.http.Server) !void {
|
||||
inline for (middlewares) |middleware| {
|
||||
if (comptime @hasDecl(middleware, "afterLaunch")) {
|
||||
try middleware.afterLaunch(server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
|
||||
var middleware_data = MiddlewareData.init(0) catch unreachable;
|
||||
|
||||
@ -60,8 +68,11 @@ 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(
|
||||
@ -72,7 +83,8 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
|
||||
} else {
|
||||
try @call(.always_inline, middleware.afterRequest, .{request});
|
||||
}
|
||||
if (request.rendered or request.redirected) {
|
||||
|
||||
if (request.state != .after_request) {
|
||||
request.middleware_rendered = .{ .name = @typeName(middleware), .action = "afterRequest" };
|
||||
break;
|
||||
}
|
||||
@ -86,11 +98,11 @@ pub fn beforeResponse(
|
||||
middleware_data: *MiddlewareData,
|
||||
request: *jetzig.http.Request,
|
||||
) !void {
|
||||
request.response_started = true;
|
||||
request.state = .before_response;
|
||||
|
||||
inline for (middlewares, 0..) |middleware, index| {
|
||||
if (comptime !@hasDecl(middleware, "beforeResponse")) continue;
|
||||
if (!request.middleware_rendered_during_response) {
|
||||
if (request.state == .before_response) {
|
||||
if (comptime @hasDecl(middleware, "init")) {
|
||||
const data = middleware_data.get(index).?;
|
||||
try @call(
|
||||
@ -99,11 +111,19 @@ 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.middleware_rendered_during_response) {
|
||||
request.middleware_rendered = .{ .name = @typeName(middleware), .action = "beforeResponse" };
|
||||
|
||||
if (request.state != .before_response) {
|
||||
request.middleware_rendered = .{
|
||||
.name = @typeName(middleware),
|
||||
.action = "beforeResponse",
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,8 +42,8 @@ pub fn expectParams(request: *jetzig.http.Request, T: type) !?T {
|
||||
} else if (@typeInfo(field.type) == .optional) {
|
||||
// if no matching param found and params struct provides a default value, use it,
|
||||
// otherwise set value to null
|
||||
@field(t, field.name) = if (field.default_value) |default_value|
|
||||
@as(*field.type, @ptrCast(@alignCast(@constCast(default_value)))).*
|
||||
@field(t, field.name) = if (field.default_value_ptr) |default_value_ptr|
|
||||
@as(*field.type, @ptrCast(@alignCast(@constCast(default_value_ptr)))).*
|
||||
else
|
||||
null;
|
||||
statuses[index] = .blank;
|
||||
|
||||
@ -16,24 +16,26 @@ pub const JobEnv = struct {
|
||||
/// Environment configured at server launch
|
||||
vars: jetzig.Environment.Vars,
|
||||
/// All routes detected by Jetzig on startup
|
||||
routes: []*const jetzig.Route,
|
||||
routes: []const *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,
|
||||
store: *jetzig.kv.Store.GeneralStore,
|
||||
/// Global cache
|
||||
cache: *jetzig.kv.Store,
|
||||
cache: *jetzig.kv.Store.CacheStore,
|
||||
/// Database repo
|
||||
repo: *jetzig.database.Repo,
|
||||
/// 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,
|
||||
job_queue: *jetzig.kv.Store,
|
||||
cache: *jetzig.kv.Store,
|
||||
store: *jetzig.kv.Store.GeneralStore,
|
||||
job_queue: *jetzig.kv.Store.JobQueueStore,
|
||||
cache: *jetzig.kv.Store.CacheStore,
|
||||
logger: jetzig.loggers.Logger,
|
||||
name: []const u8,
|
||||
definition: ?JobDefinition,
|
||||
@ -45,9 +47,9 @@ const Job = @This();
|
||||
/// Initialize a new Job
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
store: *jetzig.kv.Store,
|
||||
job_queue: *jetzig.kv.Store,
|
||||
cache: *jetzig.kv.Store,
|
||||
store: *jetzig.kv.Store.GeneralStore,
|
||||
job_queue: *jetzig.kv.Store.JobQueueStore,
|
||||
cache: *jetzig.kv.Store.CacheStore,
|
||||
logger: jetzig.loggers.Logger,
|
||||
jobs: []const JobDefinition,
|
||||
name: []const u8,
|
||||
|
||||
@ -5,7 +5,7 @@ const jetzig = @import("../../jetzig.zig");
|
||||
const Pool = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
job_queue: *jetzig.kv.Store,
|
||||
job_queue: *jetzig.kv.Store.JobQueueStore,
|
||||
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,
|
||||
job_queue: *jetzig.kv.Store.JobQueueStore,
|
||||
job_env: jetzig.jobs.JobEnv,
|
||||
) Pool {
|
||||
return .{
|
||||
|
||||
@ -6,14 +6,14 @@ const Worker = @This();
|
||||
allocator: std.mem.Allocator,
|
||||
job_env: jetzig.jobs.JobEnv,
|
||||
id: usize,
|
||||
job_queue: *jetzig.kv.Store,
|
||||
job_queue: *jetzig.kv.Store.JobQueueStore,
|
||||
interval: usize,
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
job_env: jetzig.jobs.JobEnv,
|
||||
id: usize,
|
||||
job_queue: *jetzig.kv.Store,
|
||||
job_queue: *jetzig.kv.Store.JobQueueStore,
|
||||
interval: usize,
|
||||
) Worker {
|
||||
return .{
|
||||
|
||||
@ -1,3 +1,36 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const Store = @import("kv/Store.zig");
|
||||
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;
|
||||
};
|
||||
|
||||
@ -1,87 +1,156 @@
|
||||
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 } = .memory,
|
||||
backend: enum { memory, file, valkey } = .memory,
|
||||
file_options: struct {
|
||||
path: ?[]const u8 = null,
|
||||
address_space_size: u32 = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096),
|
||||
address_space_size: u32 = jetzig.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 };
|
||||
|
||||
/// 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,
|
||||
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,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return .{ .store = store, .options = options };
|
||||
.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,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Free allocated resources/close database file.
|
||||
pub fn deinit(self: *Store) void {
|
||||
self.store.deinit();
|
||||
}
|
||||
/// Role a given store fills. Used in log outputs.
|
||||
pub const Role = enum { jobs, cache, general, custom };
|
||||
|
||||
/// 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());
|
||||
}
|
||||
pub fn Store(comptime options: KVOptions) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
/// 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));
|
||||
}
|
||||
store: jetzig.jetkv.JetKV(jetKVOptions(options)),
|
||||
logger: jetzig.loggers.Logger,
|
||||
options: KVOptions,
|
||||
role: Role,
|
||||
|
||||
/// 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));
|
||||
}
|
||||
/// 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.
|
||||
pub fn remove(self: *Store, key: []const u8) !void {
|
||||
try self.store.remove(key);
|
||||
}
|
||||
return .{ .store = store, .role = role, .logger = logger, .options = options };
|
||||
}
|
||||
|
||||
/// 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());
|
||||
}
|
||||
/// Free allocated resources/close database file.
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.store.deinit();
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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));
|
||||
}
|
||||
/// 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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));
|
||||
/// 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));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn parseValue(data: *jetzig.data.Data, maybe_json: ?[]const u8) !?*jetzig.data.Value {
|
||||
|
||||
@ -12,6 +12,11 @@ pub const NullLogger = @import("loggers/NullLogger.zig");
|
||||
|
||||
pub const LogQueue = @import("loggers/LogQueue.zig");
|
||||
|
||||
pub const LogFile = struct {
|
||||
file: std.fs.File,
|
||||
sync: bool = false,
|
||||
};
|
||||
|
||||
pub const LogLevel = enum(u4) { TRACE, DEBUG, INFO, WARN, ERROR, FATAL };
|
||||
pub const LogFormat = enum { development, production, json, null };
|
||||
|
||||
|
||||
@ -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) |_| if (request.redirected) "redirect" else "render" else "",
|
||||
if (request.middleware_rendered) |_| @tagName(request.state) else "",
|
||||
if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.reset else "",
|
||||
if (request.middleware_rendered) |_| "]" else "",
|
||||
request.path.path,
|
||||
@ -166,9 +166,16 @@ 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.yellow;
|
||||
const identifier_color = jetzig.colors.codes.escape ++ jetzig.colors.codes.white;
|
||||
const reset_color = jetzig.colors.codes.escape ++ jetzig.colors.codes.reset;
|
||||
var buf: [4096]u8 = undefined;
|
||||
var stream = std.io.fixedBufferStream(&buf);
|
||||
@ -221,9 +228,16 @@ 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)) {
|
||||
try writer.print(jetzig.colors.cyan(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});
|
||||
index += token.len;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -6,11 +6,15 @@ const jetzig = @import("../../jetzig.zig");
|
||||
const buffer_size = jetzig.config.get(usize, "log_message_buffer_len");
|
||||
const max_pool_len = jetzig.config.get(usize, "max_log_pool_len");
|
||||
|
||||
const List = std.DoublyLinkedList(Event);
|
||||
const List = std.DoublyLinkedList;
|
||||
const ListNode = struct {
|
||||
event: Event,
|
||||
node: std.DoublyLinkedList.Node = .{},
|
||||
};
|
||||
const Buffer = [buffer_size]u8;
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
node_allocator: std.heap.MemoryPool(List.Node),
|
||||
node_allocator: std.heap.MemoryPool(ListNode),
|
||||
buffer_allocator: std.heap.MemoryPool(Buffer),
|
||||
list: List,
|
||||
read_write_mutex: std.Thread.Mutex,
|
||||
@ -18,7 +22,7 @@ condition: std.Thread.Condition,
|
||||
condition_mutex: std.Thread.Mutex,
|
||||
writer: Writer = undefined,
|
||||
reader: Reader = undefined,
|
||||
node_pool: std.ArrayList(*List.Node),
|
||||
node_pool: std.ArrayList(*ListNode),
|
||||
buffer_pool: std.ArrayList(*Buffer),
|
||||
position: usize,
|
||||
stdout_is_tty: bool = undefined,
|
||||
@ -42,13 +46,13 @@ const Event = struct {
|
||||
pub fn init(allocator: std.mem.Allocator) LogQueue {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.node_allocator = initPool(allocator, List.Node),
|
||||
.node_allocator = initPool(allocator, ListNode),
|
||||
.buffer_allocator = initPool(allocator, Buffer),
|
||||
.list = List{},
|
||||
.condition = std.Thread.Condition{},
|
||||
.condition_mutex = std.Thread.Mutex{},
|
||||
.read_write_mutex = std.Thread.Mutex{},
|
||||
.node_pool = std.ArrayList(*List.Node).init(allocator),
|
||||
.node_pool = std.ArrayList(*ListNode).init(allocator),
|
||||
.buffer_pool = std.ArrayList(*Buffer).init(allocator),
|
||||
.position = 0,
|
||||
};
|
||||
@ -66,7 +70,11 @@ pub fn deinit(self: *LogQueue) void {
|
||||
}
|
||||
|
||||
/// Set the stdout and stderr outputs. Must be called before `print`.
|
||||
pub fn setFiles(self: *LogQueue, stdout_file: std.fs.File, stderr_file: std.fs.File) !void {
|
||||
pub fn setFiles(
|
||||
self: *LogQueue,
|
||||
stdout_file: jetzig.loggers.LogFile,
|
||||
stderr_file: jetzig.loggers.LogFile,
|
||||
) !void {
|
||||
self.writer = Writer{
|
||||
.queue = self,
|
||||
.mutex = std.Thread.Mutex{},
|
||||
@ -76,11 +84,11 @@ pub fn setFiles(self: *LogQueue, stdout_file: std.fs.File, stderr_file: std.fs.F
|
||||
.stderr_file = stderr_file,
|
||||
.queue = self,
|
||||
};
|
||||
self.stdout_is_tty = stdout_file.isTty();
|
||||
self.stderr_is_tty = stderr_file.isTty();
|
||||
self.stdout_is_tty = stdout_file.file.isTty();
|
||||
self.stderr_is_tty = stderr_file.file.isTty();
|
||||
|
||||
self.stdout_colorize = std.io.tty.detectConfig(stdout_file) != .no_color;
|
||||
self.stderr_colorize = std.io.tty.detectConfig(stderr_file) != .no_color;
|
||||
self.stdout_colorize = std.io.tty.detectConfig(stdout_file.file) != .no_color;
|
||||
self.stderr_colorize = std.io.tty.detectConfig(stderr_file.file) != .no_color;
|
||||
|
||||
self.state = .ready;
|
||||
}
|
||||
@ -143,17 +151,21 @@ pub const Writer = struct {
|
||||
/// Reader for `LogQueue`. Reads log events from the queue and writes them to the designated
|
||||
/// target (stdout or stderr).
|
||||
pub const Reader = struct {
|
||||
stdout_file: std.fs.File,
|
||||
stderr_file: std.fs.File,
|
||||
stdout_file: jetzig.loggers.LogFile,
|
||||
stderr_file: jetzig.loggers.LogFile,
|
||||
queue: *LogQueue,
|
||||
|
||||
pub const PublishOptions = struct {
|
||||
oneshot: bool = false,
|
||||
};
|
||||
|
||||
/// Publish log events from the queue. Invoke from a dedicated thread. Sleeps when log queue
|
||||
/// is empty, wakes up when a new event is published.
|
||||
pub fn publish(self: *Reader, options: struct { oneshot: bool = false }) !void {
|
||||
pub fn publish(self: *Reader, options: PublishOptions) !void {
|
||||
std.debug.assert(self.queue.state == .ready);
|
||||
|
||||
const stdout_writer = self.stdout_file.writer();
|
||||
const stderr_writer = self.stderr_file.writer();
|
||||
const stdout_writer = self.stdout_file.file.writer();
|
||||
const stderr_writer = self.stderr_file.file.writer();
|
||||
|
||||
while (true) {
|
||||
self.queue.condition_mutex.lock();
|
||||
@ -173,13 +185,13 @@ pub const Reader = struct {
|
||||
.stdout => {
|
||||
stdout_written = true;
|
||||
if (builtin.os.tag == .windows) {
|
||||
file = self.stdout_file;
|
||||
file = self.stdout_file.file;
|
||||
}
|
||||
},
|
||||
.stderr => {
|
||||
stderr_written = true;
|
||||
if (builtin.os.tag == .windows) {
|
||||
file = self.stderr_file;
|
||||
file = self.stderr_file.file;
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -212,8 +224,8 @@ pub const Reader = struct {
|
||||
}
|
||||
}
|
||||
|
||||
if (stdout_written and !self.queue.stdout_is_tty) try self.stdout_file.sync();
|
||||
if (stderr_written and !self.queue.stderr_is_tty) try self.stderr_file.sync();
|
||||
if (stdout_written and self.stdout_file.sync) try self.stdout_file.file.sync();
|
||||
if (stderr_written and self.stderr_file.sync) try self.stderr_file.file.sync();
|
||||
|
||||
if (options.oneshot) break;
|
||||
}
|
||||
@ -233,8 +245,8 @@ fn append(self: *LogQueue, event: Event) !void {
|
||||
|
||||
self.position += 1;
|
||||
|
||||
node.* = .{ .data = event };
|
||||
self.list.append(node);
|
||||
node.* = .{ .event = event };
|
||||
self.list.append(&node.node);
|
||||
|
||||
self.condition.signal();
|
||||
}
|
||||
@ -245,16 +257,17 @@ fn popFirst(self: *LogQueue) !?Event {
|
||||
defer self.read_write_mutex.unlock();
|
||||
|
||||
if (self.list.popFirst()) |node| {
|
||||
const value = node.data;
|
||||
const list_node: *ListNode = @fieldParentPtr("node", node);
|
||||
const value = list_node.event;
|
||||
self.position -= 1;
|
||||
if (self.position < self.node_pool.items.len) {
|
||||
self.node_pool.items[self.position] = node;
|
||||
self.node_pool.items[self.position] = list_node;
|
||||
} else {
|
||||
if (self.node_pool.items.len >= max_pool_len) {
|
||||
self.node_allocator.destroy(node);
|
||||
self.node_allocator.destroy(list_node);
|
||||
self.position += 1;
|
||||
} else {
|
||||
try self.node_pool.append(node);
|
||||
try self.node_pool.append(list_node);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
@ -280,7 +293,7 @@ test "print to stdout and stderr" {
|
||||
const stderr = try tmp_dir.dir.createFile("stderr.log", .{ .read = true });
|
||||
defer stderr.close();
|
||||
|
||||
try log_queue.setFiles(stdout, stderr);
|
||||
try log_queue.setFiles(.{ .file = stdout }, .{ .file = stderr });
|
||||
try log_queue.print("foo {s}\n", .{"bar"}, .stdout);
|
||||
try log_queue.print("baz {s}\n", .{"qux"}, .stderr);
|
||||
try log_queue.print("quux {s}\n", .{"corge"}, .stdout);
|
||||
@ -324,7 +337,7 @@ test "long messages" {
|
||||
const stderr = try tmp_dir.dir.createFile("stderr.log", .{ .read = true });
|
||||
defer stderr.close();
|
||||
|
||||
try log_queue.setFiles(stdout, stderr);
|
||||
try log_queue.setFiles(.{ .file = stdout }, .{ .file = stderr });
|
||||
try log_queue.print("foo" ** buffer_size, .{}, .stdout);
|
||||
|
||||
try log_queue.reader.publish(.{ .oneshot = true });
|
||||
|
||||
@ -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) |_| if (request.redirected) "redirect" else "render" else "",
|
||||
if (request.middleware_rendered) |_| @tagName(request.state) else "",
|
||||
if (request.middleware_rendered) |_| "]" else "",
|
||||
request.path.path,
|
||||
}, .stdout);
|
||||
|
||||
@ -3,6 +3,7 @@ const std = @import("std");
|
||||
pub const Mail = @import("mail/Mail.zig");
|
||||
pub const SMTPConfig = @import("mail/SMTPConfig.zig");
|
||||
pub const MailParams = @import("mail/MailParams.zig");
|
||||
pub const Address = MailParams.Address;
|
||||
pub const DefaultMailParams = MailParams.DefaultMailParams;
|
||||
pub const components = @import("mail/components.zig");
|
||||
pub const Job = @import("mail/Job.zig");
|
||||
|
||||
@ -46,14 +46,15 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig
|
||||
|
||||
if (env.environment == .development and !jetzig.config.get(bool, "force_development_email_delivery")) {
|
||||
try env.logger.INFO(
|
||||
"Skipping mail delivery in development environment:\n{s}",
|
||||
.{try mail.generateData()},
|
||||
\\Skipping mail delivery in development environment:
|
||||
\\To: {?s}
|
||||
\\{s}
|
||||
,
|
||||
.{ mail.params.get(.to), try mail.generateData() },
|
||||
);
|
||||
} else {
|
||||
try mail.deliver();
|
||||
try env.logger.INFO("Delivered mail to: {s}", .{
|
||||
try std.mem.join(allocator, ", ", mail.params.to.?),
|
||||
});
|
||||
try env.logger.INFO("Delivered mail to: {s}", .{mail.params.to.?});
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,19 +70,32 @@ fn resolveSubject(subject: ?*const jetzig.data.Value) ?[]const u8 {
|
||||
}
|
||||
}
|
||||
|
||||
fn resolveFrom(from: ?*const jetzig.data.Value) ?[]const u8 {
|
||||
fn resolveFrom(from: ?*const jetzig.data.Value) ?jetzig.mail.Address {
|
||||
return if (from) |capture| switch (capture.*) {
|
||||
.null => null,
|
||||
.string => |string| string.value,
|
||||
.string => |string| .{ .email = string.value },
|
||||
.object => |object| .{
|
||||
.email = object.getT(.string, "email") orelse return null,
|
||||
.name = object.getT(.string, "name") orelse return null,
|
||||
},
|
||||
else => unreachable,
|
||||
} else null;
|
||||
}
|
||||
|
||||
fn resolveTo(allocator: std.mem.Allocator, params: *const jetzig.data.Value) !?[]const []const u8 {
|
||||
var to = std.ArrayList([]const u8).init(allocator);
|
||||
fn resolveTo(allocator: std.mem.Allocator, params: *const jetzig.data.Value) !?[]const jetzig.mail.Address {
|
||||
var to = std.ArrayList(jetzig.mail.Address).init(allocator);
|
||||
if (params.get("to")) |capture| {
|
||||
for (capture.items(.array)) |recipient| {
|
||||
try to.append(recipient.string.value);
|
||||
const maybe_address: ?jetzig.mail.Address = switch (recipient.*) {
|
||||
.null => null,
|
||||
.string => |string| .{ .email = string.value },
|
||||
.object => |object| .{
|
||||
.email = object.getT(.string, "email") orelse return error.JetzigMissingEmailField,
|
||||
.name = object.getT(.string, "name"),
|
||||
},
|
||||
else => unreachable,
|
||||
};
|
||||
if (maybe_address) |address| try to.append(address);
|
||||
}
|
||||
}
|
||||
return if (to.items.len > 0) try to.toOwnedSlice() else null;
|
||||
@ -130,7 +144,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|
|
||||
@ -148,7 +162,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|
|
||||
|
||||
@ -30,9 +30,16 @@ pub fn deliver(self: Mail) !void {
|
||||
const data = try self.generateData();
|
||||
defer self.allocator.free(data);
|
||||
|
||||
const to = try self.allocator.alloc(smtp.Message.Address, self.params.to.?.len);
|
||||
defer self.allocator.free(to);
|
||||
|
||||
for (self.params.to.?, 0..) |address, index| {
|
||||
to[index] = .{ .address = address.email, .name = address.name };
|
||||
}
|
||||
|
||||
try smtp.send(.{
|
||||
.from = self.params.from.?,
|
||||
.to = self.params.to.?,
|
||||
.from = .{ .address = self.params.from.?.email, .name = self.params.from.?.name },
|
||||
.to = to,
|
||||
.data = data,
|
||||
}, try self.config.toSMTP(self.allocator, self.env));
|
||||
}
|
||||
@ -147,8 +154,8 @@ test "HTML part only" {
|
||||
.config = .{},
|
||||
.boundary = 123456789,
|
||||
.params = .{
|
||||
.from = "user@example.com",
|
||||
.to = &.{"user@example.com"},
|
||||
.from = .{ .name = "Bob", .email = "user@example.com" },
|
||||
.to = &.{.{ .name = "Alice", .email = "user@example.com" }},
|
||||
.subject = "Test subject",
|
||||
.html = "<div>Hello</div>",
|
||||
},
|
||||
@ -158,7 +165,7 @@ test "HTML part only" {
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
|
||||
\\From: user@example.com
|
||||
\\From: Bob <user@example.com>
|
||||
\\Subject: Test subject
|
||||
\\MIME-Version: 1.0
|
||||
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
|
||||
@ -183,8 +190,8 @@ test "text part only" {
|
||||
.config = .{},
|
||||
.boundary = 123456789,
|
||||
.params = .{
|
||||
.from = "user@example.com",
|
||||
.to = &.{"user@example.com"},
|
||||
.from = .{ .name = "Bob", .email = "user@example.com" },
|
||||
.to = &.{.{ .name = "Alice", .email = "user@example.com" }},
|
||||
.subject = "Test subject",
|
||||
.text = "Hello",
|
||||
},
|
||||
@ -194,7 +201,7 @@ test "text part only" {
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
|
||||
\\From: user@example.com
|
||||
\\From: Bob <user@example.com>
|
||||
\\Subject: Test subject
|
||||
\\MIME-Version: 1.0
|
||||
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
|
||||
@ -219,8 +226,8 @@ test "HTML and text parts" {
|
||||
.config = .{},
|
||||
.boundary = 123456789,
|
||||
.params = .{
|
||||
.from = "user@example.com",
|
||||
.to = &.{"user@example.com"},
|
||||
.from = .{ .name = "Bob", .email = "user@example.com" },
|
||||
.to = &.{.{ .name = "Alice", .email = "user@example.com" }},
|
||||
.subject = "Test subject",
|
||||
.html = "<div>Hello</div>",
|
||||
.text = "Hello",
|
||||
@ -231,7 +238,7 @@ test "HTML and text parts" {
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
|
||||
\\From: user@example.com
|
||||
\\From: Bob <user@example.com>
|
||||
\\Subject: Test subject
|
||||
\\MIME-Version: 1.0
|
||||
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
|
||||
@ -255,6 +262,41 @@ test "HTML and text parts" {
|
||||
try std.testing.expectEqualStrings(expected, actual);
|
||||
}
|
||||
|
||||
test "default email address name" {
|
||||
const mail = Mail{
|
||||
.allocator = std.testing.allocator,
|
||||
.env = undefined,
|
||||
.config = .{},
|
||||
.boundary = 123456789,
|
||||
.params = .{
|
||||
.from = .{ .email = "user@example.com" },
|
||||
.to = &.{.{ .email = "user@example.com" }},
|
||||
.subject = "Test subject",
|
||||
.text = "Hello",
|
||||
},
|
||||
};
|
||||
|
||||
const actual = try generateData(mail);
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
|
||||
\\From: user@example.com <user@example.com>
|
||||
\\Subject: Test subject
|
||||
\\MIME-Version: 1.0
|
||||
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
|
||||
\\--=_alternative_123456789
|
||||
\\Content-Type: text/plain; charset="UTF-8"
|
||||
\\Content-Transfer-Encoding: quoted-printable
|
||||
\\
|
||||
\\Hello
|
||||
\\
|
||||
\\.
|
||||
\\
|
||||
, "\n", "\r\n");
|
||||
defer std.testing.allocator.free(expected);
|
||||
|
||||
try std.testing.expectEqualStrings(expected, actual);
|
||||
}
|
||||
test "long content encoding" {
|
||||
const mail = Mail{
|
||||
.allocator = std.testing.allocator,
|
||||
@ -262,8 +304,8 @@ test "long content encoding" {
|
||||
.config = .{},
|
||||
.boundary = 123456789,
|
||||
.params = .{
|
||||
.from = "user@example.com",
|
||||
.to = &.{"user@example.com"},
|
||||
.from = .{ .name = "Bob", .email = "user@example.com" },
|
||||
.to = &.{.{ .name = "Alice", .email = "user@example.com" }},
|
||||
.subject = "Test subject",
|
||||
.html = "<html><body><div style=\"background-color: black; color: #ff00ff;\">Hellooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo!!!</div></body></html>",
|
||||
.text = "Hello",
|
||||
@ -274,7 +316,7 @@ test "long content encoding" {
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
|
||||
\\From: user@example.com
|
||||
\\From: Bob <user@example.com>
|
||||
\\Subject: Test subject
|
||||
\\MIME-Version: 1.0
|
||||
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
|
||||
@ -312,8 +354,8 @@ test "non-latin alphabet encoding" {
|
||||
.config = .{},
|
||||
.boundary = 123456789,
|
||||
.params = .{
|
||||
.from = "user@example.com",
|
||||
.to = &.{"user@example.com"},
|
||||
.from = .{ .name = "Bob", .email = "user@example.com" },
|
||||
.to = &.{.{ .name = "Alice", .email = "user@example.com" }},
|
||||
.subject = "Test subject",
|
||||
.html = "<html><body><div>你爱学习 Zig 吗?</div></body></html>",
|
||||
|
||||
@ -325,7 +367,7 @@ test "non-latin alphabet encoding" {
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
|
||||
\\From: user@example.com
|
||||
\\From: Bob <user@example.com>
|
||||
\\Subject: Test subject
|
||||
\\MIME-Version: 1.0
|
||||
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
|
||||
|
||||
@ -1,22 +1,31 @@
|
||||
subject: ?[]const u8 = null,
|
||||
from: ?[]const u8 = null,
|
||||
to: ?[]const []const u8 = null,
|
||||
cc: ?[]const []const u8 = null,
|
||||
bcc: ?[]const []const u8 = null, // TODO
|
||||
from: ?Address = null,
|
||||
to: ?[]const Address = null,
|
||||
cc: ?[]const Address = null,
|
||||
bcc: ?[]const Address = null, // TODO
|
||||
html: ?[]const u8 = null,
|
||||
text: ?[]const u8 = null,
|
||||
defaults: ?DefaultMailParams = null,
|
||||
|
||||
pub const DefaultMailParams = struct {
|
||||
subject: ?[]const u8 = null,
|
||||
from: ?[]const u8 = null,
|
||||
to: ?[]const []const u8 = null,
|
||||
cc: ?[]const []const u8 = null,
|
||||
bcc: ?[]const []const u8 = null, // TODO
|
||||
from: ?Address = null,
|
||||
to: ?[]const Address = null,
|
||||
cc: ?[]const Address = null,
|
||||
bcc: ?[]const Address = null, // TODO
|
||||
html: ?[]const u8 = null,
|
||||
text: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const Address = struct {
|
||||
name: ?[]const u8 = null,
|
||||
email: []const u8,
|
||||
|
||||
pub fn format(address: Address, _: anytype, _: anytype, writer: anytype) !void {
|
||||
try writer.print("{s} <{s}>", .{ address.name orelse address.email, address.email });
|
||||
}
|
||||
};
|
||||
|
||||
const MailParams = @This();
|
||||
|
||||
pub fn get(
|
||||
@ -24,10 +33,10 @@ pub fn get(
|
||||
comptime field: enum { subject, from, to, cc, bcc, html, text },
|
||||
) ?switch (field) {
|
||||
.subject => []const u8,
|
||||
.from => []const u8,
|
||||
.to => []const []const u8,
|
||||
.cc => []const []const u8,
|
||||
.bcc => []const []const u8,
|
||||
.from => Address,
|
||||
.to => []const Address,
|
||||
.cc => []const Address,
|
||||
.bcc => []const Address,
|
||||
.html => []const u8,
|
||||
.text => []const u8,
|
||||
} {
|
||||
|
||||
@ -12,7 +12,7 @@ const TokenParams = @Type(.{
|
||||
.name = jetzig.authenticity_token_name ++ "",
|
||||
.type = []const u8,
|
||||
.is_comptime = false,
|
||||
.default_value = null,
|
||||
.default_value_ptr = null,
|
||||
.alignment = @alignOf([]const u8),
|
||||
}},
|
||||
},
|
||||
|
||||
@ -28,17 +28,17 @@ pub fn beforeResponse(request: *jetzig.http.Request, response: *jetzig.http.Resp
|
||||
|
||||
const compressed = switch (encoding) {
|
||||
.gzip => jetzig.util.gzip(request.allocator, response.content, .{}) catch |err|
|
||||
return request.server.logger.logError(err),
|
||||
return request.server.logger.logError(@errorReturnTrace(), err),
|
||||
.deflate => jetzig.util.deflate(request.allocator, response.content, .{}) catch |err|
|
||||
return request.server.logger.logError(err),
|
||||
return request.server.logger.logError(@errorReturnTrace(), err),
|
||||
};
|
||||
|
||||
response.headers.append("Content-Encoding", @tagName(encoding)) catch |err|
|
||||
return request.server.logger.logError(err);
|
||||
return request.server.logger.logError(@errorReturnTrace(), err);
|
||||
|
||||
// Make caching work
|
||||
response.headers.append("Vary", "Accept-Encoding") catch |err|
|
||||
return request.server.logger.logError(err);
|
||||
return request.server.logger.logError(@errorReturnTrace(), err);
|
||||
|
||||
response.content = compressed;
|
||||
}
|
||||
@ -48,9 +48,9 @@ fn detectEncoding(request: *const jetzig.http.Request) ?Encoding {
|
||||
while (headers_it.next()) |header| {
|
||||
var it = std.mem.tokenizeScalar(u8, header.value, ',');
|
||||
while (it.next()) |param| {
|
||||
inline for (@typeInfo(Encoding).Enum.fields) |field| {
|
||||
inline for (@typeInfo(Encoding).@"enum".fields) |field| {
|
||||
if (std.mem.eql(u8, field.name, jetzig.util.strip(param))) {
|
||||
return std.enums.nameCast(Encoding, field.name);
|
||||
return @field(Encoding, field.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,10 +8,10 @@ const HtmxMiddleware = @This();
|
||||
/// request doesn't come via htmx and, when the request does come from htmx, only return the
|
||||
/// content rendered directly by the view function.
|
||||
pub fn afterRequest(request: *jetzig.http.Request) !void {
|
||||
if (request.headers.get("HX-Target")) |target| {
|
||||
if (request.headers.get("HX-Request")) |_| {
|
||||
try request.server.logger.DEBUG(
|
||||
"[middleware-htmx] htmx request detected, disabling layout. (#{s})",
|
||||
.{target},
|
||||
"[middleware-htmx] HX-Request header, disabling layout.",
|
||||
.{},
|
||||
);
|
||||
request.setLayout(null);
|
||||
}
|
||||
@ -19,13 +19,21 @@ pub fn afterRequest(request: *jetzig.http.Request) !void {
|
||||
|
||||
/// If a redirect was issued during request processing, reset any response data, set response
|
||||
/// status to `200 OK` and replace the `Location` header with a `HX-Redirect` header.
|
||||
/// Add Vary response header to prevent caching the page without layout for requests not coming
|
||||
/// from htmx.
|
||||
pub fn beforeResponse(request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
|
||||
if (response.status_code != .moved_permanently and response.status_code != .found) return;
|
||||
if (request.headers.get("HX-Request") == null) return;
|
||||
|
||||
if (response.headers.get("Location")) |location| {
|
||||
response.status_code = .ok;
|
||||
request.response_data.reset();
|
||||
try response.headers.append("HX-Redirect", location);
|
||||
switch (response.status_code) {
|
||||
.moved_permanently, .found => {
|
||||
if (response.headers.get("Location")) |location| {
|
||||
response.status_code = .ok;
|
||||
request.response_data.reset();
|
||||
try response.headers.append("HX-Redirect", location);
|
||||
}
|
||||
},
|
||||
else => {
|
||||
try response.headers.append("Vary", "HX-Request");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,13 +4,14 @@ 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: *jetzig.kv.Store,
|
||||
cache: *jetzig.kv.Store,
|
||||
job_queue: *jetzig.kv.Store,
|
||||
store: *MemoryStore,
|
||||
cache: *MemoryStore,
|
||||
job_queue: *MemoryStore,
|
||||
multipart_boundary: ?[]const u8 = null,
|
||||
logger: jetzig.loggers.Logger,
|
||||
server: Server,
|
||||
@ -54,15 +55,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()),
|
||||
.cache = try createStore(arena.allocator()),
|
||||
.job_queue = try createStore(arena.allocator()),
|
||||
.store = try createStore(arena.allocator(), logger, .general),
|
||||
.cache = try createStore(arena.allocator(), logger, .cache),
|
||||
.job_queue = try createStore(arena.allocator(), logger, .jobs),
|
||||
.logger = logger,
|
||||
.server = .{ .logger = logger },
|
||||
.repo = repo,
|
||||
@ -77,6 +78,7 @@ pub fn init(allocator: std.mem.Allocator, routes_module: type) !App {
|
||||
|
||||
/// Free allocated resources for test app.
|
||||
pub fn deinit(self: *App) void {
|
||||
self.repo.deinit();
|
||||
self.arena.deinit();
|
||||
self.allocator.destroy(self.arena);
|
||||
if (self.logger.test_logger.file) |file| file.close();
|
||||
@ -235,7 +237,7 @@ pub fn initSession(self: *App) !void {
|
||||
const allocator = self.arena.allocator();
|
||||
|
||||
var local_session = try allocator.create(jetzig.http.Session);
|
||||
local_session.* = jetzig.http.Session.init(allocator, self.cookies, jetzig.testing.secret);
|
||||
local_session.* = jetzig.http.Session.init(allocator, self.cookies, jetzig.testing.secret, .{});
|
||||
try local_session.parse();
|
||||
|
||||
self.session = local_session;
|
||||
@ -345,9 +347,12 @@ fn stubbedRequest(
|
||||
.route_data = null,
|
||||
.middlewares = undefined,
|
||||
.address = undefined,
|
||||
.method = std.enums.nameCast(httpz.Method, @tagName(method)),
|
||||
.method = @field(httpz.Method, @tagName(method)),
|
||||
.protocol = .HTTP11,
|
||||
.params = undefined,
|
||||
.conn = undefined,
|
||||
.method_string = undefined,
|
||||
.unread_body = undefined,
|
||||
.headers = request_headers,
|
||||
.body_buffer = if (options.getBody()) |capture|
|
||||
.{ .data = @constCast(capture), .type = .static }
|
||||
@ -391,9 +396,17 @@ fn multiFormKeyValue(allocator: std.mem.Allocator, max: usize) !*httpz.key_value
|
||||
return key_value;
|
||||
}
|
||||
|
||||
fn createStore(allocator: std.mem.Allocator) !*jetzig.kv.Store {
|
||||
const store = try allocator.create(jetzig.kv.Store);
|
||||
store.* = try jetzig.kv.Store.init(allocator, .{});
|
||||
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,
|
||||
);
|
||||
return store;
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ const view_types = @import("view_types.zig");
|
||||
|
||||
const Route = @This();
|
||||
|
||||
pub const Action = enum { index, get, new, post, put, patch, delete, custom };
|
||||
pub const Action = enum { index, get, new, edit, post, put, patch, delete, custom };
|
||||
|
||||
pub const View = union(enum) {
|
||||
with_id: view_types.ViewWithId,
|
||||
@ -32,6 +32,7 @@ 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,
|
||||
@ -114,6 +115,7 @@ 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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user