Compare commits

...

117 Commits

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

Improve development mode SQL syntax highlighting - highlight `SELECT`,
`UPDATE`, `DELETE`, `INSERT` in different bolded colors for clarity.
2024-11-30 12:48:18 +00:00
z1fire
22badde8c0 Jetzigg init direction update 2024-11-30 07:18:06 -05:00
bobf
e8ce232700
Merge pull request #129 from z1fire/wip
Added imports to mailers
2024-11-29 23:20:01 +00:00
z1fire
50bcda1cbe Added imports to mailers 2024-11-29 17:55:09 -05:00
bobf
ac600326a9
Merge pull request #127 from jetzig-framework/jetquery-optionals-rework
JetQuery optionals rework
2024-11-28 22:39:13 +00:00
Bob Farrell
6075708bf4 JetQuery optionals rework
Optionals now supported as param types.
2024-11-28 22:25:39 +00:00
Bob Farrell
54480a7b39 Update JetQuery 2024-11-27 22:39:02 +00:00
Bob Farrell
e27fc06d8c Update JetQuery 2024-11-27 22:32:40 +00:00
bobf
6dbeed7109
Merge pull request #125 from jetzig-framework/schema-reflection-improvements
Schema reflection improvements
2024-11-27 22:20:40 +00:00
Bob Farrell
c91eda51d3 Schema reflection improvements
Reflect foreign keys, fix pk/fk serial type inference, add missing
numeric types.
2024-11-27 22:15:15 +00:00
bobf
a3e3cb6dfe
Merge pull request #124 from jetzig-framework/database-in-jobs
Make database repo available in background jobs
2024-11-27 20:52:08 +00:00
Bob Farrell
1fa6db77df Make database repo available in background jobs 2024-11-27 20:43:35 +00:00
bobf
b4b7251021
Merge pull request #123 from jetzig-framework/jetquery-fix-datetime-coercion
JetQuery: fix datetime coercion
2024-11-26 19:33:17 +00:00
Bob Farrell
8ffbbbbf6a JetQuery: fix datetime coercion
Fix for coercing datetime values in where/insert/etc. params and
managing optional datetimes in schema.
2024-11-26 19:25:35 +00:00
Bob Farrell
4e1a26a5f9 Add zUI to related projects
Add Karl as a contributor (pg.zig and http.zig author + a few PRs direct
to Jetzig/JetQuery)
2024-11-24 21:35:42 +00:00
Bob Farrell
6bd0273e2d Fix default production cookie value 2024-11-24 21:14:28 +00:00
bobf
937efd9121
Merge pull request #122 from jetzig-framework/pseudo-http-verbs-and-edit-action
Routing updates
2024-11-24 20:53:48 +00:00
Bob Farrell
f44a6b33c5 Routing updates
Implement `/foo/1/edit` route/view.

Allow setting HTTP verb by passing e.g. `/_PATCH` as the last segment of
a URL - this allows browsers to submit a `POST` request with a
pseudo-HTTP verb encoded in the URL which Jetzig can translate, i.e.
allowing forms to submit a `PATCH`.
2024-11-24 20:46:56 +00:00
bobf
835885a947
Merge pull request #121 from jetzig-framework/debug-console
Debug console
2024-11-23 20:12:34 +00:00
Bob Farrell
773eb1dc6e Add --debug option to jetzig server
Defaults to true, allow disabling new debug console.
2024-11-23 19:58:52 +00:00
Bob Farrell
9684181c64 Fix middleware render detection 2024-11-23 19:31:37 +00:00
Bob Farrell
1565ae3b73 Debug console
Use `jetzig server` or `zig build -Ddebug_console=true run` to launch a
development server with the debug console enabled.

When an error is encountered a formatted stack trace is dumped to the
browser along with the current response data. Both outputs are
syntax-highlighted using Prism JS.
2024-11-23 18:07:19 +00:00
Bob Farrell
8095bbcb76 Implement request.renderTemplate
Clean up request state.
2024-11-23 13:42:14 +00:00
Bob Farrell
036aec1682 Misc. tidying 2024-11-23 13:14:11 +00:00
bobf
130a7c81a8
Merge pull request #120 from jetzig-framework/anti-csrf
Closes #108: Anti-CSRF middleware
2024-11-23 12:58:03 +00:00
bobf
b1becfd6ec
Merge pull request #119 from karlseguin/update_karl_deps
update pg.zig dep (out of bound fix)
2024-11-21 18:15:22 +00:00
Karl Seguin
7f71d1d6da update pg.zig dep (out of bound fix)
update smtp_client (merged tls PR)
2024-11-21 10:06:09 +08:00
75 changed files with 1967 additions and 700 deletions

View File

@ -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
View 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.

View File

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

View File

@ -5,6 +5,9 @@ pub const GenerateMimeTypes = @import("src/GenerateMimeTypes.zig");
const zmpl_build = @import("zmpl");
const Environment = enum { development, testing, production };
const builtin = @import("builtin");
const use_llvm_default = builtin.os.tag != .linux;
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
@ -36,6 +39,7 @@ pub fn build(b: *std.Build) !void {
.{
.target = target,
.optimize = optimize,
.use_llvm = b.option(bool, "use_llvm", "Use LLVM") orelse use_llvm_default,
.zmpl_templates_paths = templates_paths,
.zmpl_auto_build = false,
.zmpl_markdown_fragments = try generateMarkdownFragments(b),
@ -46,6 +50,11 @@ pub fn build(b: *std.Build) !void {
},
);
const zmpl_steps = zmpl_dep.builder.top_level_steps;
const zmpl_compile_step = zmpl_steps.get("compile").?;
const compile_step = b.step("compile", "Compile Zmpl templates");
compile_step.dependOn(&zmpl_compile_step.step);
const zmpl_module = zmpl_dep.module("zmpl");
const jetkv_dep = b.dependency("jetkv", .{ .target = target, .optimize = optimize });
@ -58,18 +67,15 @@ pub fn build(b: *std.Build) !void {
const jetcommon_dep = b.dependency("jetcommon", .{ .target = target, .optimize = optimize });
const zmd_dep = b.dependency("zmd", .{ .target = target, .optimize = optimize });
const httpz_dep = b.dependency("httpz", .{ .target = target, .optimize = optimize });
const pg_dep = b.dependency("pg", .{ .target = target, .optimize = optimize });
// This is the way to make it look nice in the zig build script
// If we would do it the other way around, we would have to do
// b.dependency("jetzig",.{}).builder.dependency("zmpl",.{}).module("zmpl");
b.modules.put("zmpl", zmpl_dep.module("zmpl")) catch @panic("Out of memory");
b.modules.put("zmd", zmd_dep.module("zmd")) catch @panic("Out of memory");
b.modules.put("pg", pg_dep.module("pg")) catch @panic("Out of memory");
b.modules.put("jetquery", jetquery_dep.module("jetquery")) catch @panic("Out of memory");
b.modules.put("jetcommon", jetcommon_dep.module("jetcommon")) catch @panic("Out of memory");
b.modules.put("jetquery_migrate", jetquery_dep.module("jetquery_migrate")) catch @panic("Out of memory");
jetquery_dep.module("jetquery").addImport("pg", pg_dep.module("pg"));
const smtp_client_dep = b.dependency("smtp_client", .{
.target = target,
@ -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,

View File

@ -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",
},
},

View File

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

View File

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

View File

@ -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)});
\\}

View File

@ -43,7 +43,7 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he
const action_args = if (args.len > 1)
args[1..]
else
&[_][]const u8{ "index", "get", "new", "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} = .{{

View File

@ -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 },
.{},
);
}

View File

@ -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;

View File

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

View File

@ -23,8 +23,8 @@ const icons = .{
};
/// Print a success confirmation.
pub fn printSuccess() void {
std.debug.print(" " ++ icons.check ++ "\n", .{});
pub fn printSuccess(message: ?[]const u8) void {
std.debug.print(" " ++ icons.check ++ " {s}\n", .{message orelse ""});
}
/// Print a failure confirmation.
@ -153,7 +153,7 @@ pub fn runCommandStreaming(allocator: std.mem.Allocator, install_path: []const u
pub fn runCommand(allocator: std.mem.Allocator, argv: []const []const u8) !void {
var dir = try detectJetzigProjectDir();
defer dir.close();
try runCommandInDir(allocator, argv, .{ .dir = dir });
try runCommandInDir(allocator, argv, .{ .dir = dir }, .{});
}
const Dir = union(enum) {
@ -161,48 +161,118 @@ const Dir = union(enum) {
dir: std.fs.Dir,
};
pub const RunOptions = struct {
output: enum { stream, capture } = .capture,
wait: bool = true,
};
/// Runs a command as a child process in the given directory and verifies successful exit code.
pub fn runCommandInDir(allocator: std.mem.Allocator, argv: []const []const u8, dir: Dir) !void {
pub fn runCommandInDir(allocator: std.mem.Allocator, argv: []const []const u8, dir: Dir, options: RunOptions) !void {
const cwd_path = switch (dir) {
.path => |capture| capture,
.dir => |capture| try capture.realpathAlloc(allocator, "."),
};
defer if (dir == .dir) allocator.free(cwd_path);
const result = try std.process.Child.run(.{
.allocator = allocator,
.argv = argv,
.cwd = cwd_path,
});
const output_behaviour: std.process.Child.StdIo = switch (options.output) {
.stream => .Inherit,
.capture => .Pipe,
};
var child = std.process.Child.init(argv, allocator);
child.stdin_behavior = .Ignore;
child.stdout_behavior = output_behaviour;
child.stderr_behavior = output_behaviour;
if (options.output == .stream) {
child.stdout = std.io.getStdOut();
child.stderr = std.io.getStdErr();
}
child.cwd = cwd_path;
var stdout = try std.ArrayListUnmanaged(u8).initCapacity(allocator, 0);
var stderr = try std.ArrayListUnmanaged(u8).initCapacity(allocator, 0);
errdefer {
stdout.deinit(allocator);
stderr.deinit(allocator);
}
try child.spawn();
switch (options.output) {
.capture => try collectOutput(child, allocator, &stdout, &stderr),
.stream => {},
}
if (!options.wait) return;
const result = std.process.Child.RunResult{
.term = try child.wait(),
.stdout = try stdout.toOwnedSlice(allocator),
.stderr = try stderr.toOwnedSlice(allocator),
};
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
const command = try std.mem.join(allocator, " ", argv);
defer allocator.free(command);
std.debug.print("[exec] {s}", .{command});
if (result.term.Exited != 0) {
printFailure();
std.debug.print(
\\
\\Error running command: {s}
\\
\\[stdout]:
\\
\\{s}
\\
\\[stderr]:
\\
\\{s}
\\
, .{ command, result.stdout, result.stderr });
if (result.stdout.len > 0) {
std.debug.print("\n[stdout]:\n{s}\n", .{result.stdout});
}
if (result.stderr.len > 0) {
std.debug.print("\n[stderr]:\n{s}\n", .{result.stderr});
}
return error.JetzigCommandError;
} else {
printSuccess();
const message = try std.mem.join(allocator, " ", argv);
defer allocator.free(message);
printSuccess(message);
}
}
fn collectOutput(
child: std.process.Child,
allocator: std.mem.Allocator,
stdout: *std.ArrayListUnmanaged(u8),
stderr: *std.ArrayListUnmanaged(u8),
) !void {
const max_output_bytes = 50 * 1024;
std.debug.assert(child.stdout_behavior == .Pipe);
std.debug.assert(child.stderr_behavior == .Pipe);
var poller = std.io.poll(allocator, enum { stdout, stderr }, .{
.stdout = child.stdout.?,
.stderr = child.stderr.?,
});
defer poller.deinit();
std.debug.print("(working) .", .{});
while (try poller.poll()) {
if (poller.fifo(.stdout).count > max_output_bytes)
return error.StdoutStreamTooLong;
if (poller.fifo(.stderr).count > max_output_bytes)
return error.StderrStreamTooLong;
std.debug.print(".", .{});
}
std.debug.print(" (done)\n", .{});
try writeFifoDataToArrayList(allocator, stdout, poller.fifo(.stdout));
try writeFifoDataToArrayList(allocator, stderr, poller.fifo(.stderr));
}
// Borrowed from `std.process.Child.writeFifoDataToArrayList` - non-public but needed in our
// modified `collectOutput`
fn writeFifoDataToArrayList(allocator: std.mem.Allocator, list: *std.ArrayListUnmanaged(u8), fifo: *std.io.PollFifo) !void {
if (fifo.head != 0) fifo.realign();
if (list.capacity == 0) {
list.* = .{
.items = fifo.buf[0..fifo.count],
.capacity = fifo.buf.len,
};
fifo.* = std.io.PollFifo.init(fifo.allocator);
} else {
try list.appendSlice(allocator, fifo.buf[0..fifo.count]);
}
}
/// Generate a full GitHub URL for passing to `zig fetch`.
pub fn githubUrl(allocator: std.mem.Allocator) ![]const u8 {
var client = std.http.Client{ .allocator = allocator };
@ -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
View File

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

44
demo/Makefile Normal file
View File

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

View File

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

7
demo/compose.yml Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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");
}

View File

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

View File

@ -5,8 +5,8 @@ const importedFunction = @import("../lib/example.zig").exampleFunction;
pub const layout = "application";
pub fn index(request: *jetzig.Request, 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;
}

View File

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

View File

@ -15,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();

View File

@ -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,

View File

@ -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
View 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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -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 },
);
}

View File

@ -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.

View File

@ -24,8 +24,7 @@ const AppOptions = struct {
};
/// Starts an application. `routes` should be `@import("routes").routes`, a generated file
/// automatically created at build time. `templates` should be
/// `@import("src/app/views/zmpl.manifest.zig").templates`, created by Zmpl at compile time.
/// automatically created at build time.
pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
defer self.env.deinit();
@ -35,7 +34,7 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
defer mime_map.deinit();
try mime_map.build();
const routes = try createRoutes(self.allocator, &routes_module.routes);
const routes = try createRoutes(self.allocator, if (@hasDecl(routes_module, "routes")) &routes_module.routes else &.{});
defer {
for (routes) |var_route| {
var_route.deinitParams();
@ -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| {

View File

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

View File

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

View File

@ -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 {

View File

@ -18,7 +18,10 @@ pub const mail = @import("mail.zig");
pub const kv = @import("kv.zig");
pub const db = @import("database.zig");
pub const Environment = @import("Environment.zig");
pub const environment = std.enums.nameCast(Environment.EnvironmentName, build_options.environment);
pub const environment = @field(
Environment.EnvironmentName,
@tagName(build_options.environment),
);
pub const build_options = @import("build_options");
const root = @import("root");
@ -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 = "/",
},
};

View File

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

218
src/jetzig/debug.zig Normal file
View 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>
\\
;

View File

@ -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);
}

View File

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

View File

@ -11,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);
}

View File

@ -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);

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -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 .{

View File

@ -6,14 +6,14 @@ const Worker = @This();
allocator: std.mem.Allocator,
job_env: jetzig.jobs.JobEnv,
id: usize,
job_queue: *jetzig.kv.Store,
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 .{

View File

@ -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;
};

View File

@ -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 {

View File

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

View File

@ -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;
}

View File

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

View File

@ -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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,10 +8,10 @@ const HtmxMiddleware = @This();
/// request doesn't come via htmx and, when the request does come from htmx, only return the
/// content rendered directly by the view function.
pub fn afterRequest(request: *jetzig.http.Request) !void {
if (request.headers.get("HX-Target")) |target| {
if (request.headers.get("HX-Request")) |_| {
try request.server.logger.DEBUG(
"[middleware-htmx] htmx request detected, disabling layout. (#{s})",
.{target},
"[middleware-htmx] HX-Request header, disabling layout.",
.{},
);
request.setLayout(null);
}
@ -19,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");
},
}
}

View File

@ -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;
}

View File

@ -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,