├── .github └── workflows │ └── CI.yml ├── .gitignore ├── LICENSE ├── LICENSE-Zig ├── README.md ├── archive.tar.gz ├── build.zig ├── build.zig.zon ├── cli ├── build.zig ├── build.zig.zon ├── cli.zig ├── colors.zig ├── commands │ ├── auth.zig │ ├── bundle.zig │ ├── database.zig │ ├── database │ │ ├── create.zig │ │ ├── drop.zig │ │ ├── migrate.zig │ │ ├── reflect.zig │ │ ├── rollback.zig │ │ ├── seed.zig │ │ ├── setup.zig │ │ └── update.zig │ ├── generate.zig │ ├── generate │ │ ├── job.zig │ │ ├── layout.zig │ │ ├── mailer.zig │ │ ├── middleware.zig │ │ ├── migration.zig │ │ ├── partial.zig │ │ ├── secret.zig │ │ ├── seeder.zig │ │ └── view.zig │ ├── init.zig │ ├── routes.zig │ ├── server.zig │ ├── tests.zig │ └── update.zig ├── compile.zig └── util.zig ├── demo ├── .gitignore ├── .ruby-version ├── Makefile ├── build.zig ├── build.zig.zon ├── compose.yml ├── config │ ├── database.zig │ └── database_template.zig ├── public │ ├── 404.html │ ├── favicon.ico │ ├── jetzig.png │ ├── prism.css │ ├── prism.js │ ├── styles.css │ └── zmpl.png ├── src │ ├── app │ │ ├── config │ │ │ └── quotes.json │ │ ├── database │ │ │ ├── Schema.zig │ │ │ ├── migrations │ │ │ │ ├── 2024-08-25_13-18-52_hello.zig │ │ │ │ └── 2025-03-10_01-36-58_create_users.zig │ │ │ └── seeders │ │ │ │ └── 2025-03-10_01-36-58_create_users.zig │ │ ├── jobs │ │ │ └── example.zig │ │ ├── lib │ │ │ └── example.zig │ │ ├── mailers │ │ │ ├── welcome.zig │ │ │ └── welcome │ │ │ │ ├── html.zmpl │ │ │ │ └── text.zmpl │ │ ├── middleware │ │ │ └── DemoMiddleware.zig │ │ └── views │ │ │ ├── 301.zmpl │ │ │ ├── anti_csrf.zig │ │ │ ├── anti_csrf │ │ │ ├── index.zmpl │ │ │ └── post.zmpl │ │ │ ├── background_jobs.zig │ │ │ ├── basic.zig │ │ │ ├── basic │ │ │ └── index.zmpl │ │ │ ├── cache.zig │ │ │ ├── cache │ │ │ ├── index.zmpl │ │ │ └── post.zmpl │ │ │ ├── channels │ │ │ └── index.zmpl │ │ │ ├── custom │ │ │ ├── foo.zig │ │ │ └── foo │ │ │ │ └── bar.zmpl │ │ │ ├── errors.zig │ │ │ ├── errors │ │ │ └── index.zmpl │ │ │ ├── file_upload.zig │ │ │ ├── file_upload │ │ │ ├── index.zmpl │ │ │ └── post.zmpl │ │ │ ├── format.zig │ │ │ ├── format │ │ │ ├── get.zmpl │ │ │ └── index.zmpl │ │ │ ├── init.zig │ │ │ ├── init │ │ │ ├── _content.zmpl │ │ │ └── index.zmpl │ │ │ ├── kvstore.zig │ │ │ ├── kvstore │ │ │ └── index.zmpl │ │ │ ├── layouts │ │ │ └── application.zmpl │ │ │ ├── login.zig │ │ │ ├── login │ │ │ └── index.zmpl │ │ │ ├── mail.zig │ │ │ ├── mail │ │ │ └── index.zmpl │ │ │ ├── markdown.zig │ │ │ ├── markdown │ │ │ └── index.md.zmpl │ │ │ ├── nested │ │ │ └── route │ │ │ │ ├── example.zig │ │ │ │ ├── example │ │ │ │ └── index.zmpl │ │ │ │ └── markdown.md │ │ │ ├── params.zig │ │ │ ├── params │ │ │ └── post.zmpl │ │ │ ├── quotes.zig │ │ │ ├── quotes │ │ │ ├── get.zmpl │ │ │ └── post.zmpl │ │ │ ├── redirect.zig │ │ │ ├── redirect │ │ │ └── index.zmpl │ │ │ ├── render_template.zig │ │ │ ├── render_template │ │ │ └── index.zmpl │ │ │ ├── render_text.zig │ │ │ ├── render_text │ │ │ └── index.zmpl │ │ │ ├── root.zig │ │ │ ├── root │ │ │ ├── _quotes.zmpl │ │ │ └── index.zmpl │ │ │ ├── session.zig │ │ │ ├── session │ │ │ └── index.zmpl │ │ │ ├── static.zig │ │ │ └── static │ │ │ ├── get.zmpl │ │ │ └── index.zmpl │ └── main.zig └── zmpl_options.zig ├── init └── src │ └── main.zig └── src ├── GenerateMimeTypes.zig ├── Routes.zig ├── assets ├── debug.css └── debug.js ├── cli.gitignore ├── commands ├── auth.zig ├── database.zig ├── routes.zig └── util.zig ├── compile_static_routes.zig ├── jetzig.zig ├── jetzig ├── App.zig ├── DefaultSchema.zig ├── Environment.zig ├── TemplateContext.zig ├── auth.zig ├── callbacks.zig ├── colors.zig ├── config.zig ├── data.zig ├── database.zig ├── debug.zig ├── development_static.zig ├── http.zig ├── http │ ├── Cookies.zig │ ├── File.zig │ ├── Headers.zig │ ├── MultipartQuery.zig │ ├── Path.zig │ ├── Query.zig │ ├── Request.zig │ ├── Response.zig │ ├── Server.zig │ ├── Session.zig │ ├── StaticRequest.zig │ ├── StatusCode.zig │ ├── middleware.zig │ ├── mime.zig │ ├── mime │ │ └── mimeData.json │ ├── params.zig │ └── status_codes.zig ├── jobs.zig ├── jobs │ ├── Job.zig │ ├── Pool.zig │ └── Worker.zig ├── kv.zig ├── kv │ └── Store.zig ├── loggers.zig ├── loggers │ ├── DevelopmentLogger.zig │ ├── JsonLogger.zig │ ├── LogQueue.zig │ ├── NullLogger.zig │ ├── ProductionLogger.zig │ └── TestLogger.zig ├── mail.zig ├── mail │ ├── Job.zig │ ├── Mail.zig │ ├── MailParams.zig │ ├── MailerDefinition.zig │ ├── SMTPConfig.zig │ └── components.zig ├── markdown.zig ├── middleware.zig ├── middleware │ ├── AntiCsrfMiddleware.zig │ ├── AuthMiddleware.zig │ ├── CompressionMiddleware.zig │ └── HtmxMiddleware.zig ├── testing.zig ├── testing │ └── App.zig ├── types.zig ├── types │ └── Timestamp.zig ├── util.zig ├── views.zig └── views │ ├── CustomRoute.zig │ ├── Route.zig │ ├── View.zig │ └── view_types.zig ├── routes_file.zig ├── test_runner.zig └── tests.zig /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | on: 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | branches: [ main ] 10 | schedule: 11 | - cron: '0 0 * * *' #Makes sense, we are testing against master 12 | workflow_dispatch: 13 | 14 | jobs: 15 | build: 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | # Create postgres server 22 | # https://github.com/marketplace/actions/setup-postgresql-for-linux-macos-windows 23 | - uses: ikalnytskyi/action-setup-postgres@v7 24 | 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v3 27 | with: 28 | submodules: true 29 | 30 | - name: Setup Zig 31 | uses: mlugg/setup-zig@main 32 | 33 | - run: zig version 34 | - run: zig env 35 | 36 | - name: Build 37 | run: zig build --verbose 38 | 39 | - name: Run Tests 40 | run: zig build test --summary all 41 | 42 | - name: Run App Tests 43 | run: | 44 | cd demo 45 | zig build -Denvironment=testing jetzig:database:create 46 | zig build -Denvironment=testing jetzig:database:migrate 47 | zig build -Denvironment=testing jetzig:test 48 | env: 49 | JETQUERY_HOSTNAME: 'localhost' 50 | JETQUERY_USERNAME: 'postgres' 51 | JETQUERY_PASSWORD: 'postgres' 52 | JETQUERY_DATABASE: 'jetzig_demo_test' 53 | # Assume a small amount of connections are allowed 54 | # into postgres 55 | JETQUERY_POOL_SIZE: 2 56 | 57 | - name: Build artifacts 58 | if: ${{ matrix.os == 'ubuntu-latest' }} 59 | run: | 60 | declare -a targets=("x86_64-windows" "x86_64-linux" "x86_64-macos" "aarch64-macos") 61 | mkdir -p "artifacts/" 62 | root=$(pwd) 63 | cd cli 64 | for target in "${targets[@]}"; do 65 | mkdir -p $root/artifacts/$target 66 | echo "Building target ${target}..." 67 | if ! zig build -Dtarget=${target} -Doptimize=ReleaseSafe --prefix $root/artifacts/${target}/; then 68 | exit 1 69 | fi 70 | sed -e '1,5d' < $root/README.md > $root/artifacts/${target}/README.md 71 | cp $root/LICENSE $root/artifacts/${target}/ 72 | done 73 | wait 74 | 75 | - name: Upload artifacts Target Windows 76 | if: ${{ matrix.os == 'ubuntu-latest' && !contains(fromJSON('["pull_request"]'), github.event_name) }} 77 | uses: actions/upload-artifact@v4 78 | with: 79 | name: build-windows 80 | path: artifacts/x86_64-windows 81 | - name: Upload artifacts Target Linux 82 | if: ${{ matrix.os == 'ubuntu-latest' && !contains(fromJSON('["pull_request"]'), github.event_name) }} 83 | uses: actions/upload-artifact@v4 84 | with: 85 | name: build-linux 86 | path: artifacts/x86_64-linux 87 | - name: Upload artifacts Target MacOS 88 | if: ${{ matrix.os == 'ubuntu-latest' && !contains(fromJSON('["pull_request"]'), github.event_name) }} 89 | uses: actions/upload-artifact@v4 90 | with: 91 | name: build-macos-x86 92 | path: artifacts/x86_64-macos 93 | - name: Upload artifacts Target MacOS 2 94 | if: ${{ matrix.os == 'ubuntu-latest' && !contains(fromJSON('["pull_request"]'), github.event_name) }} 95 | uses: actions/upload-artifact@v4 96 | with: 97 | name: build-macos-aarch64 98 | path: artifacts/aarch64-macos 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-out/ 2 | zig-cache/ 3 | *.core 4 | .jetzig 5 | .zig-cache/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023-2024 Robert Farrell 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /LICENSE-Zig: -------------------------------------------------------------------------------- 1 | The MIT License (Expat) 2 | 3 | Copyright (c) Zig contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/jetzig-framework/jetzig/actions/workflows/CI.yml/badge.svg)](https://github.com/jetzig-framework/jetzig/actions/workflows/CI.yml) 2 | 3 | ![Jetzig Logo](demo/public/jetzig.png) 4 | 5 | _Jetzig_ is a web framework written in 100% pure [Zig](https://ziglang.org) :lizard: for _Linux_, _OS X_, _Windows_, and any _OS_ that can compile _Zig_ code. 6 | 7 | Official website: [jetzig.dev](https://www.jetzig.dev/) 8 | 9 | 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. 10 | 11 | _Jetzig_ aims to provide a rich set of user-friendly tools for building modern web applications quickly. See the checklist below. 12 | 13 | Join us on Discord ! [https://discord.gg/eufqssz7X6](https://discord.gg/eufqssz7X6). 14 | 15 | If you are interested in _Jetzig_ you will probably find these tools interesting too: 16 | 17 | * [Zap](https://github.com/zigzap/zap) 18 | * [http.zig](https://github.com/karlseguin/http.zig) (_Jetzig_'s backend) 19 | * [tokamak](https://github.com/cztomsik/tokamak) 20 | * [zig-router](https://github.com/Cloudef/zig-router) 21 | * [zig-webui](https://github.com/webui-dev/zig-webui/) 22 | * [ZTS](https://github.com/zigster64/zts) 23 | * [Zine](https://github.com/kristoff-it/zine) 24 | * [Zinc](https://github.com/zon-dev/zinc/) 25 | * [zUI](https://github.com/thienpow/zui) 26 | 27 | ## Checklist 28 | 29 | * :white_check_mark: File system-based routing with [slug] matching. 30 | * :white_check_mark: _HTML_ and _JSON_ response (inferred from extension and/or `Accept` header). 31 | * :white_check_mark: _JSON_-compatible response data builder. 32 | * :white_check_mark: _HTML_ templating (see [Zmpl](https://github.com/jetzig-framework/zmpl)). 33 | * :white_check_mark: Per-request arena allocator. 34 | * :white_check_mark: Sessions. 35 | * :white_check_mark: Cookies. 36 | * :white_check_mark: Error handling. 37 | * :white_check_mark: Static content from /public directory. 38 | * :white_check_mark: Request/response headers. 39 | * :white_check_mark: Stack trace output on error. 40 | * :white_check_mark: Static content generation. 41 | * :white_check_mark: Param/JSON payload parsing/abstracting. 42 | * :white_check_mark: Static content parameter definitions. 43 | * :white_check_mark: Middleware interface. 44 | * :white_check_mark: MIME type inference. 45 | * :white_check_mark: Email delivery. 46 | * :white_check_mark: Background jobs. 47 | * :white_check_mark: General-purpose cache. 48 | * :white_check_mark: Development server auto-reload. 49 | * :white_check_mark: Testing helpers for testing HTTP requests/responses. 50 | * :white_check_mark: Custom/non-conventional routes. 51 | * :white_check_mark: Database integration. 52 | * :x: Environment configurations (development/production/etc.) 53 | * :x: Email receipt (via SendGrid/AWS SES/etc.) 54 | 55 | ## LICENSE 56 | 57 | [MIT](LICENSE) 58 | 59 | ## Contributors 60 | 61 | * [Zackary Housend](https://github.com/z1fire) 62 | * [Andreas Stührk](https://github.com/Trundle) 63 | * [Karl Seguin](https://github.com/karlseguin) 64 | * [Bob Farrell](https://github.com/bobf) 65 | -------------------------------------------------------------------------------- /archive.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetzig-framework/jetzig/1cb27ffec8fb648a30a9aa65c1e6128cf967a2f8/archive.tar.gz -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .jetzig, 3 | .version = "0.0.0", 4 | .fingerprint = 0x93ad8bfa2d209022, 5 | .minimum_zig_version = "0.15.0-dev.355+206bd1ced", 6 | .dependencies = .{ 7 | .jetcommon = .{ 8 | .url = "https://github.com/jetzig-framework/jetcommon/archive/fb4edc13759d87bfcd9b1f5fcefdf93f8c9c62dd.tar.gz", 9 | .hash = "jetcommon-0.1.0-jPY_DS1HAAAP8xp5HSWB_ZY7m9JEYUmm8adQFrse0lwB", 10 | }, 11 | .zmd = .{ 12 | .url = "https://github.com/jetzig-framework/zmd/archive/d6c8aa9a9cde99674ccb096d8f94ed09cba8dab.tar.gz", 13 | .hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163", 14 | }, 15 | .smtp_client = .{ 16 | .url = "https://github.com/karlseguin/smtp_client.zig/archive/5163c66cc42cdd93176a6b1cad45f3db3a291a6a.tar.gz", 17 | .hash = "smtp_client-0.0.1-AAAAAIJkAQCngHtRYVUMsMuncmicSHK_7ugwWibDzQ4S", 18 | }, 19 | .args = .{ 20 | .url = "https://github.com/bobf/zig-args/archive/88cbade9a517a4014824f8f53f3c48c8a0b2ffe1.tar.gz", 21 | .hash = "zig_args-0.0.0-jqtN6P_NAAC97fGpk9hS2K681jkiqPsWP6w3ucb_ctGH", 22 | }, 23 | .jetkv = .{ 24 | .url = "https://github.com/jetzig-framework/jetkv/archive/5a94e3bac0a6e291efc9d6534beb2d311671ff17.tar.gz", 25 | .hash = "jetkv-0.0.0-zCv0fmCGAgCyYqwHjk0P5KrYVRew1MJAtbtAcIO-WPpT", 26 | }, 27 | .zmpl = .{ 28 | .url = "https://github.com/jetzig-framework/zmpl/archive/febec2dd477adadf09c67676ac4bf2079046b1d6.tar.gz", 29 | .hash = "zmpl-0.0.1-SYFGBhilAwAoY1evzcCHqpNFZf1zuB6IhY0P2w-bgM3t", 30 | }, 31 | .httpz = .{ 32 | .url = "https://github.com/karlseguin/http.zig/archive/37d7cb9819b804ade5f4b974b82f8dd0622225ed.tar.gz", 33 | .hash = "httpz-0.0.0-PNVzrEK4BgBpHQGA2m0RPqPGEjnTdDXHodBwzjYDrmps", 34 | }, 35 | .jetquery = .{ 36 | .url = "https://github.com/jetzig-framework/jetquery/archive/907acae15dd36834dbdab06b17c3ba8f576d77cb.tar.gz", 37 | .hash = "jetquery-0.0.0-TNf3zm-5BgDOtCpRuVLEZQMWjkgKWRe1pNlGhrdoyvYE", 38 | }, 39 | }, 40 | 41 | .paths = .{ 42 | "", 43 | "build.zig", 44 | "build.zig.zon", 45 | "src/jetzig", 46 | "LICENSE", 47 | "README.md", 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /cli/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const compile = @import("compile.zig"); 4 | 5 | pub fn build(b: *std.Build) !void { 6 | const target = b.standardTargetOptions(.{}); 7 | const optimize = b.standardOptimizeOption(.{}); 8 | 9 | const exe = b.addExecutable(.{ 10 | .name = "jetzig", 11 | .root_source_file = b.path("cli.zig"), 12 | .target = target, 13 | .optimize = optimize, 14 | }); 15 | 16 | const zig_args_dep = b.dependency("args", .{ .target = target, .optimize = optimize }); 17 | const jetquery_dep = b.dependency("jetquery", .{ 18 | .target = target, 19 | .optimize = optimize, 20 | .jetquery_migrations_path = @as([]const u8, "src/app/database/migrations"), 21 | .jetquery_seeders_path = @as([]const u8, "src/app/database/seeders"), 22 | }); 23 | exe.root_module.addImport("jetquery", jetquery_dep.module("jetquery")); 24 | exe.root_module.addImport("args", zig_args_dep.module("args")); 25 | exe.root_module.addImport("init_data", try compile.initDataModule(b)); 26 | 27 | b.installArtifact(exe); 28 | 29 | const run_cmd = b.addRunArtifact(exe); 30 | run_cmd.step.dependOn(b.getInstallStep()); 31 | if (b.args) |args| { 32 | run_cmd.addArgs(args); 33 | } 34 | const run_step = b.step("run", "Run the app"); 35 | run_step.dependOn(&run_cmd.step); 36 | } 37 | -------------------------------------------------------------------------------- /cli/build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .jetzig_cli, 3 | .fingerprint = 0x73894a3e0616c96a, 4 | .version = "0.0.0", 5 | .minimum_zig_version = "0.12.0", 6 | 7 | .dependencies = .{ 8 | .args = .{ 9 | .url = "https://github.com/bobf/zig-args/archive/88cbade9a517a4014824f8f53f3c48c8a0b2ffe1.tar.gz", 10 | .hash = "zig_args-0.0.0-jqtN6P_NAAC97fGpk9hS2K681jkiqPsWP6w3ucb_ctGH", 11 | }, 12 | .jetquery = .{ 13 | .url = "https://github.com/jetzig-framework/jetquery/archive/907acae15dd36834dbdab06b17c3ba8f576d77cb.tar.gz", 14 | .hash = "jetquery-0.0.0-TNf3zm-5BgDOtCpRuVLEZQMWjkgKWRe1pNlGhrdoyvYE", 15 | }, 16 | }, 17 | .paths = .{ 18 | "", 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /cli/commands/auth.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const args = @import("args"); 3 | const util = @import("../util.zig"); 4 | 5 | /// Command line options for the `update` command. 6 | pub const Options = struct { 7 | pub const meta = .{ 8 | .usage_summary = "[init|create]", 9 | .full_text = 10 | \\Manage user authentication. Initialize with `init` to generate a users table migration. 11 | \\ 12 | \\Example: 13 | \\ 14 | \\ jetzig auth init 15 | \\ jetzig auth create bob@jetzig.dev 16 | , 17 | .option_docs = .{}, 18 | }; 19 | }; 20 | 21 | /// Run the `jetzig database` command. 22 | pub fn run( 23 | parent_allocator: std.mem.Allocator, 24 | options: Options, 25 | writer: anytype, 26 | T: type, 27 | main_options: T, 28 | ) !void { 29 | _ = options; 30 | var arena = std.heap.ArenaAllocator.init(parent_allocator); 31 | defer arena.deinit(); 32 | const allocator = arena.allocator(); 33 | 34 | const Action = enum { init, create }; 35 | const map = std.StaticStringMap(Action).initComptime(.{ 36 | .{ "init", .init }, 37 | .{ "create", .create }, 38 | }); 39 | 40 | const action = if (main_options.positionals.len > 0) 41 | map.get(main_options.positionals[0]) 42 | else 43 | null; 44 | const sub_args: []const []const u8 = if (main_options.positionals.len > 1) 45 | main_options.positionals[1..] 46 | else 47 | &.{}; 48 | 49 | return if (main_options.options.help and action == null) blk: { 50 | try args.printHelp(Options, "jetzig database", writer); 51 | break :blk {}; 52 | } else if (action == null) blk: { 53 | const available_help = try std.mem.join(allocator, "|", map.keys()); 54 | std.debug.print("Missing sub-command. Expected: [{s}]\n", .{available_help}); 55 | break :blk error.JetzigCommandError; 56 | } else if (action) |capture| 57 | switch (capture) { 58 | .init => { 59 | const argv = [_][]const u8{ 60 | "jetzig", 61 | "generate", 62 | "migration", 63 | "create_users", 64 | "table:users", 65 | "column:email:string:index:unique", 66 | "column:password_hash:string", 67 | }; 68 | try util.runCommand(allocator, &argv); 69 | try util.print(.success, "Migration created. Run `jetzig database update` to run migration and reflect database.", .{}); 70 | }, 71 | .create => blk: { 72 | if (sub_args.len < 1) { 73 | std.debug.print("Missing argument. Expected an email/username parameter.\n", .{}); 74 | break :blk error.JetzigCommandError; 75 | } else { 76 | var argv = std.ArrayList([]const u8).init(allocator); 77 | try argv.append("zig"); 78 | try argv.append("build"); 79 | try argv.append(util.environmentBuildOption(main_options.options.environment)); 80 | try argv.append(try std.mem.concat(allocator, u8, &.{ "-Dauth_username=", sub_args[0] })); 81 | if (sub_args.len > 1) { 82 | try argv.append(try std.mem.concat(allocator, u8, &.{ "-Dauth_password=", sub_args[1] })); 83 | } 84 | try argv.append("jetzig:auth:user:create"); 85 | try util.execCommand(allocator, argv.items); 86 | } 87 | }, 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /cli/commands/database.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const args = @import("args"); 4 | 5 | const util = @import("../util.zig"); 6 | const cli = @import("../cli.zig"); 7 | const migrate = @import("database/migrate.zig"); 8 | const seed = @import("database/seed.zig"); 9 | const rollback = @import("database/rollback.zig"); 10 | const create = @import("database/create.zig"); 11 | const drop = @import("database/drop.zig"); 12 | const reflect = @import("database/reflect.zig"); 13 | const update = @import("database/update.zig"); 14 | const setup = @import("database/setup.zig"); 15 | 16 | pub const confirm_drop_env = "JETZIG_DROP_PRODUCTION_DATABASE"; 17 | 18 | /// Command line options for the `database` command. 19 | pub const Options = struct { 20 | pub const meta = .{ 21 | .usage_summary = "[setup|create|drop|migrate|rollback|reflect|update]", 22 | .full_text = 23 | \\Manage the application's database. 24 | \\ 25 | \\Pass `--help` to any command for more information, e.g.: 26 | \\ 27 | \\ jetzig database migrate --help 28 | \\ 29 | , 30 | }; 31 | }; 32 | 33 | /// Run the `jetzig database` command. 34 | pub fn run( 35 | allocator: std.mem.Allocator, 36 | options: Options, 37 | writer: anytype, 38 | T: type, 39 | main_options: T, 40 | ) !void { 41 | var arena = std.heap.ArenaAllocator.init(allocator); 42 | defer arena.deinit(); 43 | const alloc = arena.allocator(); 44 | 45 | const Action = enum { 46 | migrate, 47 | seed, 48 | rollback, 49 | create, 50 | drop, 51 | reflect, 52 | update, 53 | setup, 54 | }; 55 | const map = std.StaticStringMap(Action).initComptime(.{ 56 | .{ "migrate", .migrate }, 57 | .{ "seed", .seed }, 58 | .{ "rollback", .rollback }, 59 | .{ "create", .create }, 60 | .{ "drop", .drop }, 61 | .{ "reflect", .reflect }, 62 | .{ "update", .update }, 63 | .{ "setup", .setup }, 64 | }); 65 | 66 | const action = if (main_options.positionals.len > 0) 67 | map.get(main_options.positionals[0]) 68 | else 69 | null; 70 | const sub_args: []const []const u8 = if (main_options.positionals.len > 1) 71 | main_options.positionals[1..] 72 | else 73 | &.{}; 74 | 75 | return if (main_options.options.help and action == null) blk: { 76 | try args.printHelp(Options, "jetzig database", writer); 77 | break :blk {}; 78 | } else if (action == null) blk: { 79 | const available_help = try std.mem.join(alloc, "|", map.keys()); 80 | std.debug.print("Missing sub-command. Expected: [{s}]\n", .{available_help}); 81 | break :blk error.JetzigCommandError; 82 | } else if (action) |capture| blk: { 83 | var cwd = try util.detectJetzigProjectDir(); 84 | defer cwd.close(); 85 | 86 | break :blk switch (capture) { 87 | .migrate => migrate.run(alloc, cwd, sub_args, options, T, main_options), 88 | .seed => seed.run(alloc, cwd, sub_args, options, T, main_options), 89 | .rollback => rollback.run(alloc, cwd, sub_args, options, T, main_options), 90 | .create => create.run(alloc, cwd, sub_args, options, T, main_options), 91 | .drop => drop.run(alloc, cwd, sub_args, options, T, main_options), 92 | .reflect => reflect.run(alloc, cwd, sub_args, options, T, main_options), 93 | .update => update.run(alloc, cwd, sub_args, options, T, main_options), 94 | .setup => setup.run(alloc, cwd, sub_args, options, T, main_options), 95 | }; 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /cli/commands/database/create.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const cli = @import("../../cli.zig"); 4 | const util = @import("../../util.zig"); 5 | 6 | pub fn run( 7 | allocator: std.mem.Allocator, 8 | cwd: std.fs.Dir, 9 | args: []const []const u8, 10 | options: cli.database.Options, 11 | T: type, 12 | main_options: T, 13 | ) !void { 14 | _ = cwd; 15 | _ = options; 16 | if (main_options.options.help or args.len != 0) { 17 | std.debug.print( 18 | \\Create a database. 19 | \\ 20 | \\Example: 21 | \\ 22 | \\ jetzig database create 23 | \\ jetzig --environment=testing database create 24 | \\ 25 | , .{}); 26 | 27 | return if (main_options.options.help) {} else error.JetzigCommandError; 28 | } 29 | 30 | try util.execCommand(allocator, &.{ 31 | "zig", 32 | "build", 33 | util.environmentBuildOption(main_options.options.environment), 34 | "jetzig:database:create", 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /cli/commands/database/drop.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const cli = @import("../../cli.zig"); 4 | const util = @import("../../util.zig"); 5 | 6 | pub fn run( 7 | allocator: std.mem.Allocator, 8 | cwd: std.fs.Dir, 9 | args: []const []const u8, 10 | options: cli.database.Options, 11 | T: type, 12 | main_options: T, 13 | ) !void { 14 | _ = cwd; 15 | _ = options; 16 | if (main_options.options.help or args.len != 0) { 17 | std.debug.print( 18 | \\Drop database. 19 | \\ 20 | \\Example: 21 | \\ 22 | \\ jetzig database drop 23 | \\ jetzig --environment=testing database drop 24 | \\ 25 | \\To drop a production database, set the environment variable `{s}` to the name of the database you want to drop, e.g.: 26 | \\ 27 | \\ {0s}=my_production_production jetzig --environment=production database drop 28 | \\ 29 | , .{cli.database.confirm_drop_env}); 30 | 31 | return if (main_options.options.help) {} else error.JetzigCommandError; 32 | } 33 | 34 | try util.execCommand(allocator, &.{ 35 | "zig", 36 | "build", 37 | util.environmentBuildOption(main_options.options.environment), 38 | "jetzig:database:drop", 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /cli/commands/database/migrate.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const cli = @import("../../cli.zig"); 4 | const util = @import("../../util.zig"); 5 | 6 | pub fn run( 7 | allocator: std.mem.Allocator, 8 | cwd: std.fs.Dir, 9 | args: []const []const u8, 10 | options: cli.database.Options, 11 | T: type, 12 | main_options: T, 13 | ) !void { 14 | _ = cwd; 15 | _ = options; 16 | if (main_options.options.help or args.len != 0) { 17 | std.debug.print( 18 | \\Run database migrations. 19 | \\ 20 | \\Example: 21 | \\ 22 | \\ jetzig database migrate 23 | \\ jetzig --environment=testing database migrate 24 | \\ 25 | , .{}); 26 | 27 | return if (main_options.options.help) {} else error.JetzigCommandError; 28 | } 29 | 30 | try util.execCommand(allocator, &.{ 31 | "zig", 32 | "build", 33 | util.environmentBuildOption(main_options.options.environment), 34 | "jetzig:database:migrate", 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /cli/commands/database/reflect.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const cli = @import("../../cli.zig"); 4 | const util = @import("../../util.zig"); 5 | 6 | pub fn run( 7 | allocator: std.mem.Allocator, 8 | cwd: std.fs.Dir, 9 | args: []const []const u8, 10 | options: cli.database.Options, 11 | T: type, 12 | main_options: T, 13 | ) !void { 14 | _ = cwd; 15 | _ = options; 16 | if (main_options.options.help or args.len != 0) { 17 | std.debug.print( 18 | \\Generate a JetQuery schema file and save to `src/app/database/Schema.zig`. 19 | \\ 20 | \\Example: 21 | \\ 22 | \\ jetzig database reflect 23 | \\ 24 | , .{}); 25 | 26 | return if (main_options.options.help) {} else error.JetzigCommandError; 27 | } 28 | 29 | try util.execCommand(allocator, &.{ 30 | "zig", 31 | "build", 32 | util.environmentBuildOption(main_options.options.environment), 33 | "jetzig:database:reflect", 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /cli/commands/database/rollback.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const cli = @import("../../cli.zig"); 4 | const util = @import("../../util.zig"); 5 | 6 | pub fn run( 7 | allocator: std.mem.Allocator, 8 | cwd: std.fs.Dir, 9 | args: []const []const u8, 10 | options: cli.database.Options, 11 | T: type, 12 | main_options: T, 13 | ) !void { 14 | _ = cwd; 15 | _ = options; 16 | if (main_options.options.help or args.len != 0) { 17 | std.debug.print( 18 | \\Roll back a database migration. 19 | \\ 20 | \\Example: 21 | \\ 22 | \\ jetzig database rollback 23 | \\ jetzig --environment=testing database rollback 24 | \\ 25 | , .{}); 26 | 27 | return if (main_options.options.help) {} else error.JetzigCommandError; 28 | } 29 | 30 | try util.execCommand(allocator, &.{ 31 | "zig", 32 | "build", 33 | util.environmentBuildOption(main_options.options.environment), 34 | "jetzig:database:rollback", 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /cli/commands/database/seed.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const cli = @import("../../cli.zig"); 4 | const util = @import("../../util.zig"); 5 | 6 | pub fn run( 7 | allocator: std.mem.Allocator, 8 | cwd: std.fs.Dir, 9 | args: []const []const u8, 10 | options: cli.database.Options, 11 | T: type, 12 | main_options: T, 13 | ) !void { 14 | _ = cwd; 15 | _ = options; 16 | if (main_options.options.help or args.len != 0) { 17 | std.debug.print( 18 | \\Run database seeders. 19 | \\ 20 | \\Example: 21 | \\ 22 | \\ jetzig database seed 23 | \\ jetzig --environment=testing database seed 24 | \\ 25 | , .{}); 26 | 27 | return if (main_options.options.help) {} else error.JetzigCommandError; 28 | } 29 | 30 | try util.execCommand(allocator, &.{ 31 | "zig", 32 | "build", 33 | util.environmentBuildOption(main_options.options.environment), 34 | "jetzig:database:seed", 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /cli/commands/database/setup.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const cli = @import("../../cli.zig"); 4 | const util = @import("../../util.zig"); 5 | 6 | pub fn run( 7 | allocator: std.mem.Allocator, 8 | cwd: std.fs.Dir, 9 | args: []const []const u8, 10 | options: cli.database.Options, 11 | T: type, 12 | main_options: T, 13 | ) !void { 14 | _ = cwd; 15 | _ = options; 16 | if (main_options.options.help or args.len != 0) { 17 | std.debug.print( 18 | \\Set up a database: create a database, run migrations, reflect schema. 19 | \\ 20 | \\Convenience wrapper for: 21 | \\ 22 | \\* jetzig database create 23 | \\* jetzig database update 24 | \\ 25 | \\Example: 26 | \\ 27 | \\ jetzig database setup 28 | \\ jetzig --environment=testing setup 29 | \\ 30 | , .{}); 31 | 32 | return if (main_options.options.help) {} else error.JetzigCommandError; 33 | } 34 | 35 | const env = main_options.options.environment; 36 | try runCommand(allocator, env, "create"); 37 | try runCommand(allocator, env, "migrate"); 38 | try runCommand(allocator, env, "reflect"); 39 | 40 | try util.print( 41 | .success, 42 | "Database created, migrations applied, and Schema generated successfully.", 43 | .{}, 44 | ); 45 | } 46 | 47 | fn runCommand(allocator: std.mem.Allocator, environment: anytype, comptime action: []const u8) !void { 48 | try util.runCommand(allocator, &.{ 49 | "zig", 50 | "build", 51 | util.environmentBuildOption(environment), 52 | "jetzig:database:" ++ action, 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /cli/commands/database/update.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const cli = @import("../../cli.zig"); 4 | const util = @import("../../util.zig"); 5 | 6 | pub fn run( 7 | allocator: std.mem.Allocator, 8 | cwd: std.fs.Dir, 9 | args: []const []const u8, 10 | options: cli.database.Options, 11 | T: type, 12 | main_options: T, 13 | ) !void { 14 | _ = cwd; 15 | _ = options; 16 | if (main_options.options.help or args.len != 0) { 17 | std.debug.print( 18 | \\Update a database: run migrations and reflect schema. 19 | \\ 20 | \\Convenience wrapper for `jetzig database migrate` and `jetzig database reflect`. 21 | \\ 22 | \\Example: 23 | \\ 24 | \\ jetzig database update 25 | \\ jetzig --environment=testing update 26 | \\ 27 | , .{}); 28 | 29 | return if (main_options.options.help) {} else error.JetzigCommandError; 30 | } 31 | 32 | try util.runCommand(allocator, &.{ 33 | "zig", 34 | "build", 35 | util.environmentBuildOption(main_options.options.environment), 36 | "jetzig:database:migrate", 37 | }); 38 | 39 | try util.runCommand(allocator, &.{ 40 | "zig", 41 | "build", 42 | util.environmentBuildOption(main_options.options.environment), 43 | "jetzig:database:reflect", 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /cli/commands/generate.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const args = @import("args"); 3 | const secret = @import("generate/secret.zig"); 4 | const util = @import("../util.zig"); 5 | 6 | const view = @import("generate/view.zig"); 7 | const partial = @import("generate/partial.zig"); 8 | const layout = @import("generate/layout.zig"); 9 | const middleware = @import("generate/middleware.zig"); 10 | const job = @import("generate/job.zig"); 11 | const mailer = @import("generate/mailer.zig"); 12 | const migration = @import("generate/migration.zig"); 13 | const seeder = @import("generate/seeder.zig"); 14 | 15 | /// Command line options for the `generate` command. 16 | pub const Options = struct { 17 | pub const meta = .{ 18 | .usage_summary = "[view|partial|layout|mailer|middleware|job|secret|migration|seeder] [options]", 19 | .full_text = 20 | \\Generate scaffolding for views, middleware, and other objects. 21 | \\ 22 | \\Pass `--help` to any generator for more information, e.g.: 23 | \\ 24 | \\ jetzig generate view --help 25 | \\ 26 | , 27 | }; 28 | }; 29 | 30 | /// Run the `jetzig generate` command. 31 | pub fn run( 32 | allocator: std.mem.Allocator, 33 | options: Options, 34 | writer: anytype, 35 | T: type, 36 | main_options: T, 37 | ) !void { 38 | var cwd = try util.detectJetzigProjectDir(); 39 | defer cwd.close(); 40 | 41 | _ = options; 42 | 43 | const Generator = enum { 44 | view, 45 | partial, 46 | layout, 47 | mailer, 48 | middleware, 49 | job, 50 | secret, 51 | migration, 52 | seeder, 53 | }; 54 | var sub_args = std.ArrayList([]const u8).init(allocator); 55 | defer sub_args.deinit(); 56 | 57 | var available_buf = std.ArrayList([]const u8).init(allocator); 58 | defer available_buf.deinit(); 59 | 60 | const map = std.StaticStringMap(Generator).initComptime(.{ 61 | .{ "view", .view }, 62 | .{ "partial", .partial }, 63 | .{ "layout", .layout }, 64 | .{ "job", .job }, 65 | .{ "mailer", .mailer }, 66 | .{ "middleware", .middleware }, 67 | .{ "secret", .secret }, 68 | .{ "migration", .migration }, 69 | .{ "seeder", .seeder }, 70 | }); 71 | for (map.keys()) |key| try available_buf.append(key); 72 | 73 | const available_help = try std.mem.join(allocator, "|", available_buf.items); 74 | defer allocator.free(available_help); 75 | 76 | var arena_allocator = std.heap.ArenaAllocator.init(allocator); 77 | defer arena_allocator.deinit(); 78 | const arena = arena_allocator.allocator(); 79 | 80 | const generate_type: ?Generator = if (main_options.positionals.len > 0) 81 | map.get(main_options.positionals[0]) 82 | else 83 | null; 84 | 85 | if (main_options.positionals.len > 1) { 86 | for (main_options.positionals[1..]) |arg| try sub_args.append(arg); 87 | } 88 | 89 | if (main_options.options.help and generate_type == null) { 90 | try args.printHelp(Options, "jetzig generate", writer); 91 | return; 92 | } else if (generate_type == null) { 93 | std.debug.print("Missing sub-command. Expected: [{s}]\n", .{available_help}); 94 | return error.JetzigCommandError; 95 | } 96 | 97 | if (generate_type) |capture| { 98 | return switch (capture) { 99 | .view => view.run(arena, cwd, sub_args.items, main_options.options.help), 100 | .partial => partial.run(arena, cwd, sub_args.items, main_options.options.help), 101 | .layout => layout.run(arena, cwd, sub_args.items, main_options.options.help), 102 | .mailer => mailer.run(arena, cwd, sub_args.items, main_options.options.help), 103 | .job => job.run(arena, cwd, sub_args.items, main_options.options.help), 104 | .middleware => middleware.run(arena, cwd, sub_args.items, main_options.options.help), 105 | .secret => secret.run(arena, cwd, sub_args.items, main_options.options.help), 106 | .migration => migration.run(arena, cwd, sub_args.items, main_options.options.help), 107 | .seeder => seeder.run(arena, cwd, sub_args.items, main_options.options.help), 108 | }; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /cli/commands/generate/job.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Run the job generator. Create a job in `src/app/jobs/` 4 | pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, help: bool) !void { 5 | if (help or args.len != 1) { 6 | std.debug.print( 7 | \\Generate a new Job. Jobs can be scheduled to run in the background. 8 | \\Use a Job when you need to return a request immediately and perform 9 | \\another task asynchronously. 10 | \\ 11 | \\Example: 12 | \\ 13 | \\ jetzig generate job iguana 14 | \\ 15 | , .{}); 16 | 17 | if (help) return; 18 | 19 | return error.JetzigCommandError; 20 | } 21 | 22 | const dir_path = try std.fs.path.join(allocator, &[_][]const u8{ "src", "app", "jobs" }); 23 | defer allocator.free(dir_path); 24 | 25 | var dir = try cwd.makeOpenPath(dir_path, .{}); 26 | defer dir.close(); 27 | 28 | const filename = try std.mem.concat(allocator, u8, &[_][]const u8{ args[0], ".zig" }); 29 | defer allocator.free(filename); 30 | 31 | const file = dir.createFile(filename, .{ .exclusive = true }) catch |err| { 32 | switch (err) { 33 | error.PathAlreadyExists => { 34 | std.debug.print("Job already exists: {s}\n", .{filename}); 35 | return error.JetzigCommandError; 36 | }, 37 | else => return err, 38 | } 39 | }; 40 | 41 | try file.writeAll( 42 | \\const std = @import("std"); 43 | \\const jetzig = @import("jetzig"); 44 | \\ 45 | \\// The `run` function for a job is invoked every time the job is processed by a queue worker 46 | \\// (or by the Jetzig server if the job is processed in-line). 47 | \\// 48 | \\// Arguments: 49 | \\// * allocator: Arena allocator for use during the job execution process. 50 | \\// * params: Params assigned to a job (from a request, values added to response data). 51 | \\// * env: Provides the following fields: 52 | \\// - logger: Logger attached to the same stream as the Jetzig server. 53 | \\// - environment: Enum of `{ production, development }`. 54 | \\pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig.jobs.JobEnv) !void { 55 | \\ _ = allocator; 56 | \\ _ = params; 57 | \\ // Job execution code goes here. Add any code that you would like to run in the background. 58 | \\ try env.logger.INFO("Running a job.", .{}); 59 | \\} 60 | \\ 61 | ); 62 | 63 | file.close(); 64 | 65 | const realpath = try dir.realpathAlloc(allocator, filename); 66 | defer allocator.free(realpath); 67 | std.debug.print("Generated job: {s}\n", .{realpath}); 68 | } 69 | -------------------------------------------------------------------------------- /cli/commands/generate/layout.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Run the layout generator. Create a layout template in `src/app/views/layouts` 4 | pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, help: bool) !void { 5 | if (help or args.len != 1) { 6 | std.debug.print( 7 | \\Generate a layout. Layouts encapsulate common boilerplate mark-up. 8 | \\ 9 | \\Specify a layout name to create a new Zmpl template in src/app/views/layouts/ 10 | \\ 11 | \\Example: 12 | \\ 13 | \\ jetzig generate layout standard 14 | \\ 15 | , .{}); 16 | 17 | if (help) return; 18 | 19 | return error.JetzigCommandError; 20 | } 21 | 22 | const dir_path = try std.fs.path.join( 23 | allocator, 24 | &[_][]const u8{ "src", "app", "views", "layouts" }, 25 | ); 26 | defer allocator.free(dir_path); 27 | 28 | var dir = try cwd.makeOpenPath(dir_path, .{}); 29 | defer dir.close(); 30 | 31 | const filename = try std.mem.concat(allocator, u8, &[_][]const u8{ args[0], ".zmpl" }); 32 | defer allocator.free(filename); 33 | 34 | const file = dir.createFile(filename, .{ .exclusive = true }) catch |err| { 35 | switch (err) { 36 | error.PathAlreadyExists => { 37 | std.debug.print("Layout already exists: {s}\n", .{filename}); 38 | return error.JetzigCommandError; 39 | }, 40 | else => return err, 41 | } 42 | }; 43 | 44 | try file.writeAll( 45 | \\ 46 | \\ 47 | \\ 48 | \\
{{zmpl.content}}
49 | \\ 50 | \\ 51 | \\ 52 | ); 53 | 54 | file.close(); 55 | 56 | const realpath = try dir.realpathAlloc(allocator, filename); 57 | defer allocator.free(realpath); 58 | std.debug.print("Generated layout: {s}\n", .{realpath}); 59 | } 60 | -------------------------------------------------------------------------------- /cli/commands/generate/middleware.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const util = @import("../../util.zig"); 3 | 4 | /// Run the middleware generator. Create a middleware file in `src/app/middleware/` 5 | pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, help: bool) !void { 6 | if (help or args.len != 1 or !util.isCamelCase(args[0])) { 7 | std.debug.print( 8 | \\Generate a middleware module. Module name must be in CamelCase. 9 | \\ 10 | \\Example: 11 | \\ 12 | \\ jetzig generate middleware IguanaBrain 13 | \\ 14 | , .{}); 15 | 16 | if (help) return; 17 | 18 | return error.JetzigCommandError; 19 | } 20 | 21 | const dir_path = try std.fs.path.join(allocator, &[_][]const u8{ "src", "app", "middleware" }); 22 | defer allocator.free(dir_path); 23 | 24 | var dir = try cwd.makeOpenPath(dir_path, .{}); 25 | defer dir.close(); 26 | 27 | const filename = try std.mem.concat(allocator, u8, &[_][]const u8{ args[0], ".zig" }); 28 | defer allocator.free(filename); 29 | 30 | const file = dir.createFile(filename, .{ .exclusive = true }) catch |err| { 31 | switch (err) { 32 | error.PathAlreadyExists => { 33 | std.debug.print("Middleware already exists: {s}\n", .{filename}); 34 | return error.JetzigCommandError; 35 | }, 36 | else => return err, 37 | } 38 | }; 39 | 40 | try file.writeAll(middleware_content); 41 | 42 | file.close(); 43 | 44 | const realpath = try dir.realpathAlloc(allocator, filename); 45 | defer allocator.free(realpath); 46 | std.debug.print( 47 | \\Generated middleware: {s} 48 | \\ 49 | \\Edit `src/main.zig` and add the new middleware to the `jetzig_options.middleware` declaration: 50 | \\ 51 | \\ pub const jetzig_options = struct {{ 52 | \\ pub const middleware: []const type = &.{{ 53 | \\ @import("app/middleware/{s}.zig"), 54 | \\ }}; 55 | \\ }}; 56 | \\ 57 | \\Middleware are invoked in the order they appear in `jetzig_options.middleware`. 58 | \\ 59 | \\ 60 | , .{ realpath, args[0] }); 61 | } 62 | 63 | const middleware_content = 64 | \\const std = @import("std"); 65 | \\const jetzig = @import("jetzig"); 66 | \\ 67 | \\/// Define any custom data fields you want to store here. Assigning to these fields in the `init` 68 | \\/// function allows you to access them in the `beforeRequest` and `afterRequest` functions, where 69 | \\/// they can also be modified. 70 | \\my_custom_value: []const u8, 71 | \\ 72 | \\const Self = @This(); 73 | \\ 74 | \\/// Initialize middleware. 75 | \\pub fn init(request: *jetzig.http.Request) !*Self { 76 | \\ var middleware = try request.allocator.create(Self); 77 | \\ middleware.my_custom_value = "initial value"; 78 | \\ return middleware; 79 | \\} 80 | \\ 81 | \\/// Invoked immediately after the request is received but before it has started processing. 82 | \\/// Any calls to `request.render` or `request.redirect` will prevent further processing of the 83 | \\/// request, including any other middleware in the chain. 84 | \\pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void { 85 | \\ request.server.logger.debug("[middleware] my_custom_value: {s}", .{self.my_custom_value}); 86 | \\ self.my_custom_value = @tagName(request.method); 87 | \\} 88 | \\ 89 | \\/// Invoked immediately before the response renders to the client. 90 | \\/// The response can be modified here if needed. 91 | \\pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void { 92 | \\ request.server.logger.debug( 93 | \\ "[middleware] my_custom_value: {s}, response status: {s}", 94 | \\ .{ self.my_custom_value, @tagName(response.status_code) }, 95 | \\ ); 96 | \\} 97 | \\ 98 | \\/// Invoked immediately after the response has been finalized and sent to the client. 99 | \\/// Response data can be accessed for logging, but any modifications will have no impact. 100 | \\pub fn afterResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) void { 101 | \\ request.allocator.destroy(self); 102 | \\} 103 | \\ 104 | \\/// Invoked after `afterResponse` is called. Use this function to do any clean-up. 105 | \\/// Note that `request.allocator` is an arena allocator, so any allocations are automatically 106 | \\/// freed before the next request starts processing. 107 | \\pub fn deinit(self: *Self, request: *jetzig.http.Request) void { 108 | \\ request.allocator.destroy(self); 109 | \\} 110 | \\ 111 | ; 112 | -------------------------------------------------------------------------------- /cli/commands/generate/migration.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const jetquery = @import("jetquery"); 4 | 5 | /// Run the migration generator. Create a migration in `src/app/database/migrations/` 6 | pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, help: bool) !void { 7 | if (help or args.len < 1) { 8 | std.debug.print( 9 | \\Generate a new Migration. Migrations modify the application's database schema. 10 | \\ 11 | \\Example: 12 | \\ 13 | \\ jetzig generate migration create_iguanas 14 | \\ jetzig generate migration create_iguanas table:create:iguanas column:name:string:index column:age:integer 15 | \\ 16 | \\ More information: https://www.jetzig.dev/documentation/sections/database/command_line_tools 17 | \\ 18 | , .{}); 19 | 20 | if (help) return; 21 | 22 | return error.JetzigCommandError; 23 | } 24 | 25 | const name = args[0]; 26 | const command = if (args.len > 1) 27 | try std.mem.join(allocator, " ", args[1..]) 28 | else 29 | null; 30 | 31 | const migrations_dir = try cwd.makeOpenPath( 32 | try std.fs.path.join(allocator, &.{ "src", "app", "database", "migrations" }), 33 | .{}, 34 | ); 35 | const migration = jetquery.Migration.init( 36 | allocator, 37 | name, 38 | .{ 39 | .migrations_path = try migrations_dir.realpathAlloc(allocator, "."), 40 | .command = command, 41 | }, 42 | ); 43 | const path = migration.save() catch |err| { 44 | switch (err) { 45 | error.InvalidMigrationCommand => { 46 | std.log.err("Invalid migration command: {?s}", .{command}); 47 | return; 48 | }, 49 | else => return err, 50 | } 51 | }; 52 | 53 | std.log.info("Saved migration: {s}", .{path}); 54 | } 55 | -------------------------------------------------------------------------------- /cli/commands/generate/partial.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Run the partial generator. Create a partial template in `src/app/views/` 4 | pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, help: bool) !void { 5 | if (help or args.len != 2) { 6 | std.debug.print( 7 | \\Generate a partial template. Expects a view name followed by a partial name. 8 | \\ 9 | \\Example: 10 | \\ 11 | \\ jetzig generate partial iguanas ziglet 12 | \\ 13 | , .{}); 14 | 15 | if (help) return; 16 | 17 | return error.JetzigCommandError; 18 | } 19 | 20 | const dir_path = try std.fs.path.join(allocator, &[_][]const u8{ "src", "app", "views", args[0] }); 21 | defer allocator.free(dir_path); 22 | 23 | var dir = try cwd.makeOpenPath(dir_path, .{}); 24 | defer dir.close(); 25 | 26 | const filename = try std.mem.concat(allocator, u8, &[_][]const u8{ "_", args[1], ".zmpl" }); 27 | defer allocator.free(filename); 28 | 29 | const file = dir.createFile(filename, .{ .exclusive = true }) catch |err| { 30 | switch (err) { 31 | error.PathAlreadyExists => { 32 | std.debug.print("Partial already exists: {s}\n", .{filename}); 33 | return error.JetzigCommandError; 34 | }, 35 | else => return err, 36 | } 37 | }; 38 | 39 | try file.writeAll( 40 | \\
Partial content goes here.
41 | \\ 42 | ); 43 | 44 | file.close(); 45 | 46 | const realpath = try dir.realpathAlloc(allocator, filename); 47 | defer allocator.free(realpath); 48 | std.debug.print("Generated partial template: {s}\n", .{realpath}); 49 | } 50 | -------------------------------------------------------------------------------- /cli/commands/generate/secret.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Generate a secure random secret and output to stdout. 4 | pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, help: bool) !void { 5 | if (help) { 6 | std.debug.print( 7 | \\Generate a secure random secret suitable for use as the `JETZIG_SECRET` environment variable. 8 | \\ 9 | , .{}); 10 | return; 11 | } 12 | 13 | _ = allocator; 14 | _ = args; 15 | _ = cwd; 16 | const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 17 | const len = 128; 18 | var secret: [len]u8 = undefined; 19 | 20 | for (0..len) |index| { 21 | secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len - 1)]; 22 | } 23 | 24 | try std.io.getStdOut().writer().print("{s}\n", .{secret}); 25 | } 26 | -------------------------------------------------------------------------------- /cli/commands/generate/seeder.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const jetquery = @import("jetquery"); 4 | 5 | /// Run the seeder generator. Create a seed in `src/app/database/seeders/` 6 | pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, help: bool) !void { 7 | if (help or args.len < 1) { 8 | std.debug.print( 9 | \\Generate a new Seeder. Seeders is a way to set up some inital data for your application. 10 | \\ 11 | \\Example: 12 | \\ 13 | \\ jetzig generate seeder iguana 14 | \\ 15 | \\ More information: https://www.jetzig.dev/documentation/sections/database/command_line_tools 16 | \\ 17 | , .{}); 18 | 19 | if (help) return; 20 | 21 | return error.JetzigCommandError; 22 | } 23 | 24 | const name = args[0]; 25 | 26 | const seeders_dir = try cwd.makeOpenPath( 27 | try std.fs.path.join(allocator, &.{ "src", "app", "database", "seeders" }), 28 | .{}, 29 | ); 30 | const seed = jetquery.Seeder.init( 31 | allocator, 32 | name, 33 | .{ 34 | .seeders_path = try seeders_dir.realpathAlloc(allocator, "."), 35 | }, 36 | ); 37 | const path = try seed.save(); 38 | 39 | std.log.info("Saved seed: {s}", .{path}); 40 | } 41 | -------------------------------------------------------------------------------- /cli/commands/routes.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const args = @import("args"); 3 | const util = @import("../util.zig"); 4 | 5 | /// Command line options for the `routes` command. 6 | pub const Options = struct { 7 | pub const meta = .{ 8 | .usage_summary = "", 9 | .full_text = 10 | \\Output all available routes for this app. 11 | \\ 12 | \\Example: 13 | \\ 14 | \\ jetzig routes 15 | , 16 | }; 17 | }; 18 | 19 | /// Run the `jetzig routes` command. 20 | pub fn run( 21 | allocator: std.mem.Allocator, 22 | options: Options, 23 | writer: anytype, 24 | T: type, 25 | main_options: T, 26 | ) !void { 27 | _ = options; 28 | if (main_options.options.help) { 29 | try args.printHelp(Options, "jetzig routes", writer); 30 | return; 31 | } 32 | 33 | var cwd = try util.detectJetzigProjectDir(); 34 | defer cwd.close(); 35 | 36 | const realpath = try std.fs.realpathAlloc(allocator, "."); 37 | defer allocator.free(realpath); 38 | 39 | try util.runCommandStreaming(allocator, realpath, &[_][]const u8{ 40 | "zig", 41 | "build", 42 | "jetzig:routes", 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /cli/commands/tests.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const util = @import("../util.zig"); 3 | 4 | /// Command line options for the `generate` command. 5 | pub const Options = struct { 6 | @"fail-fast": bool = false, 7 | 8 | pub const shorthands = .{ 9 | .f = "fail-fast", 10 | }; 11 | 12 | pub const meta = .{ 13 | .usage_summary = "[--fail-fast]", 14 | .full_text = 15 | \\Run app tests. 16 | \\ 17 | \\Execute all tests found in `src/main.zig` 18 | \\ 19 | , 20 | .option_docs = .{ 21 | .path = "Set the output path relative to the current directory (default: current directory)", 22 | }, 23 | }; 24 | }; 25 | 26 | /// Run the job generator. Create a job in `src/app/jobs/` 27 | pub fn run( 28 | allocator: std.mem.Allocator, 29 | options: Options, 30 | writer: anytype, 31 | T: type, 32 | main_options: T, 33 | ) !void { 34 | _ = options; 35 | _ = writer; 36 | _ = main_options; 37 | try util.execCommand(allocator, &.{ 38 | "zig", 39 | "build", 40 | "-Denvironment=testing", 41 | "jetzig:test", 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /cli/commands/update.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const args = @import("args"); 3 | const util = @import("../util.zig"); 4 | 5 | /// Command line options for the `update` command. 6 | pub const Options = struct { 7 | pub const meta = .{ 8 | .usage_summary = "[NAME=jetzig]", 9 | .full_text = 10 | \\Updates the current project to the latest version of Jetzig. 11 | \\ 12 | \\Optionally pass a positional argument to save the dependency to `build.zig.zon` with an 13 | \\alternative name. 14 | \\ 15 | \\Equivalent to running `zig fetch --save=jetzig https://github.com/jetzig-framework/jetzig/archive/.tar.gz` 16 | \\ 17 | \\Example: 18 | \\ 19 | \\ jetzig update 20 | \\ jetzig update web 21 | , 22 | .option_docs = .{ 23 | .path = "Set the output path relative to the current directory (default: current directory)", 24 | }, 25 | }; 26 | }; 27 | 28 | /// Run the `jetzig update` command. 29 | pub fn run( 30 | allocator: std.mem.Allocator, 31 | options: Options, 32 | writer: anytype, 33 | T: type, 34 | main_options: T, 35 | ) !void { 36 | _ = options; 37 | if (main_options.options.help) { 38 | try args.printHelp(Options, "jetzig update", writer); 39 | return; 40 | } 41 | 42 | if (main_options.positionals.len > 1) { 43 | std.debug.print( 44 | "Expected at most 1 positional argument, found {}\n", 45 | .{main_options.positionals.len}, 46 | ); 47 | return error.JetzigCommandError; 48 | } 49 | 50 | const name = if (main_options.positionals.len > 0) main_options.positionals[0] else "jetzig"; 51 | 52 | const github_url = try util.githubUrl(allocator); 53 | defer allocator.free(github_url); 54 | 55 | const save_arg = try std.mem.concat(allocator, u8, &[_][]const u8{ "--save=", name }); 56 | defer allocator.free(save_arg); 57 | 58 | try util.runCommand( 59 | allocator, 60 | &[_][]const u8{ 61 | "zig", 62 | "fetch", 63 | save_arg, 64 | github_url, 65 | }, 66 | ); 67 | 68 | std.debug.print( 69 | \\Update complete. 70 | \\ 71 | , .{}); 72 | } 73 | -------------------------------------------------------------------------------- /cli/compile.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | fn base64Encode(allocator: std.mem.Allocator, input: []const u8) []const u8 { 4 | const encoder = std.base64.Base64Encoder.init( 5 | std.base64.url_safe_no_pad.alphabet_chars, 6 | std.base64.url_safe_no_pad.pad_char, 7 | ); 8 | const size = encoder.calcSize(input.len); 9 | const ptr = allocator.alloc(u8, size) catch @panic("OOM"); 10 | _ = encoder.encode(ptr, input); 11 | return ptr; 12 | } 13 | 14 | pub fn initDataModule(build: *std.Build) !*std.Build.Module { 15 | const root_path = build.pathFromRoot(".."); 16 | 17 | var buf = std.ArrayList(u8).init(build.allocator); 18 | defer buf.deinit(); 19 | 20 | const writer = buf.writer(); 21 | 22 | const paths = .{ 23 | "init/src/main.zig", 24 | "demo/build.zig", 25 | "demo/src/app/middleware/DemoMiddleware.zig", 26 | "demo/src/app/views/init.zig", 27 | "demo/src/app/views/init/index.zmpl", 28 | "demo/src/app/views/init/_content.zmpl", 29 | "demo/public/jetzig.png", 30 | "demo/public/zmpl.png", 31 | "demo/public/favicon.ico", 32 | "demo/public/styles.css", 33 | "demo/config/database_template.zig", 34 | ".gitignore", 35 | }; 36 | 37 | try writer.writeAll( 38 | \\pub const init_data = .{ 39 | \\ 40 | ); 41 | 42 | var dir = try std.fs.openDirAbsolute(root_path, .{}); 43 | defer dir.close(); 44 | 45 | inline for (paths) |path| { 46 | const stat = try dir.statFile(path); 47 | const encoded = base64Encode( 48 | build.allocator, 49 | try dir.readFileAlloc(build.allocator, path, @intCast(stat.size)), 50 | ); 51 | defer build.allocator.free(encoded); 52 | 53 | const output = try std.fmt.allocPrint( 54 | build.allocator, 55 | \\.{{ .path = "{s}", .data = "{s}" }}, 56 | , 57 | .{ path, encoded }, 58 | ); 59 | defer build.allocator.free(output); 60 | 61 | try writer.writeAll(output); 62 | } 63 | 64 | try writer.writeAll( 65 | \\}; 66 | \\ 67 | ); 68 | 69 | const write_files = build.addWriteFiles(); 70 | const init_data_source = write_files.add("init_data.zig", buf.items); 71 | return build.createModule(.{ .root_source_file = init_data_source }); 72 | } 73 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-out/ 3 | static/ 4 | src/app/views/**/.*.zig 5 | .DS_Store 6 | log/ 7 | src/routes.zig 8 | -------------------------------------------------------------------------------- /demo/.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.2 2 | -------------------------------------------------------------------------------- /demo/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | # 3 | # Use this Makefile to set up a local Docker PostgreSQL database and run tests, or launch a local 4 | # development database. 5 | # 6 | ## Tests 7 | # 8 | # Set up test database and run application tests: 9 | # 10 | # ``` 11 | # make test 12 | # ``` 13 | # 14 | ## Development 15 | # 16 | # Set up development database and launch the demo Jetzig app: 17 | # 18 | # ``` 19 | # make dev 20 | # ``` 21 | # 22 | # TODO: Move all of this into `build.zig` 23 | test_database=jetzig_demo_test 24 | dev_database=jetzig_demo_dev 25 | port=14173 26 | 27 | export JETQUERY_HOSTNAME=localhost 28 | export JETQUERY_USERNAME=postgres 29 | export JETQUERY_PASSWORD=postgres 30 | export JETQUERY_POOL_SIZE=2 31 | 32 | .PHONY: test 33 | test: env=JETQUERY_DATABASE=${test_database} JETQUERY_PORT=${port} 34 | test: 35 | docker compose up --detach --wait --renew-anon-volumes --remove-orphans --force-recreate 36 | ${env} zig build -Denvironment=testing jetzig:database:setup 37 | ${env} zig build -Denvironment=testing jetzig:test 38 | 39 | .PHONY: dev 40 | dev: env=JETQUERY_DATABASE=${dev_database} JETQUERY_PORT=${port} 41 | dev: 42 | docker compose up --detach --wait --renew-anon-volumes --remove-orphans 43 | ${env} zig build -Denvironment=testing jetzig:database:setup 44 | ${env} jetzig server 45 | -------------------------------------------------------------------------------- /demo/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | pub fn build(b: *std.Build) !void { 5 | const target = b.standardTargetOptions(.{}); 6 | const optimize = b.standardOptimizeOption(.{}); 7 | 8 | const exe = b.addExecutable(.{ 9 | .name = "jetzig-demo", 10 | .root_source_file = b.path("src/main.zig"), 11 | .target = target, 12 | .optimize = optimize, 13 | }); 14 | 15 | // Example Dependency 16 | // ------------------- 17 | // const iguanas_dep = b.dependency("iguanas", .{ .optimize = optimize, .target = target }); 18 | // exe.root_module.addImport("iguanas", iguanas_dep.module("iguanas")); 19 | // 20 | // ^ Add all dependencies before `jetzig.jetzigInit()` ^ 21 | 22 | try jetzig.jetzigInit(b, exe, .{}); 23 | 24 | b.installArtifact(exe); 25 | 26 | const run_cmd = b.addRunArtifact(exe); 27 | run_cmd.step.dependOn(b.getInstallStep()); 28 | 29 | if (b.args) |args| run_cmd.addArgs(args); 30 | 31 | const run_step = b.step("run", "Run the app"); 32 | run_step.dependOn(&run_cmd.step); 33 | 34 | const lib_unit_tests = b.addTest(.{ 35 | .root_source_file = b.path("src/main.zig"), 36 | .target = target, 37 | .optimize = optimize, 38 | }); 39 | 40 | const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); 41 | 42 | const exe_unit_tests = b.addTest(.{ 43 | .root_source_file = b.path("src/main.zig"), 44 | .target = target, 45 | .optimize = optimize, 46 | }); 47 | 48 | const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); 49 | 50 | const test_step = b.step("test", "Run unit tests"); 51 | test_step.dependOn(&run_lib_unit_tests.step); 52 | test_step.dependOn(&run_exe_unit_tests.step); 53 | } 54 | -------------------------------------------------------------------------------- /demo/build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .jetzig_demo, 3 | .version = "0.0.0", 4 | .minimum_zig_version = "0.12.0", 5 | .fingerprint = 0x3877c19710a92a5c, 6 | .dependencies = .{ 7 | .jetzig = .{ 8 | .path = "../", 9 | }, 10 | }, 11 | .paths = .{ 12 | // This makes *all* files, recursively, included in this package. It is generally 13 | // better to explicitly list the files and directories instead, to insure that 14 | // fetching from tarballs, file system paths, and version control all result 15 | // in the same contents hash. 16 | "", 17 | // For example... 18 | //"build.zig", 19 | //"build.zig.zon", 20 | //"src", 21 | //"LICENSE", 22 | //"README.md", 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /demo/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:17 4 | ports: 5 | - 14173:5432 6 | environment: 7 | POSTGRES_PASSWORD: 'postgres' 8 | -------------------------------------------------------------------------------- /demo/config/database.zig: -------------------------------------------------------------------------------- 1 | pub const database = .{ 2 | .development = .{ 3 | .adapter = .postgresql, 4 | .username = "postgres", 5 | .password = "postgres", 6 | .hostname = "localhost", 7 | .database = "jetzig_demo_dev", 8 | .port = 14173, // See `compose.yml` 9 | }, 10 | // This configuration is used for CI 11 | // in GitHub 12 | .testing = .{ 13 | .adapter = .postgresql, 14 | .database = "jetzig_demo_test", 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /demo/config/database_template.zig: -------------------------------------------------------------------------------- 1 | pub const database = .{ 2 | // Null adapter fails when a database call is invoked. 3 | .development = .{ 4 | .adapter = .null, 5 | }, 6 | .testing = .{ 7 | .adapter = .null, 8 | }, 9 | .production = .{ 10 | .adapter = .null, 11 | }, 12 | // PostgreSQL adapter configuration. 13 | // 14 | // All options except `adapter` can be configured using environment variables: 15 | // 16 | // * JETQUERY_HOSTNAME 17 | // * JETQUERY_PORT 18 | // * JETQUERY_USERNAME 19 | // * JETQUERY_PASSWORD 20 | // * JETQUERY_DATABASE 21 | // 22 | // .testing = .{ 23 | // .adapter = .postgresql, 24 | // .hostname = "localhost", 25 | // .port = 5432, 26 | // .username = "postgres", 27 | // .password = "password", 28 | // .database = "myapp_testing", 29 | // }, 30 | // 31 | // .development = .{ 32 | // .adapter = .postgresql, 33 | // .hostname = "localhost", 34 | // .port = 5432, 35 | // .username = "postgres", 36 | // .password = "password", 37 | // .database = "myapp_development", 38 | // }, 39 | // 40 | // .production = .{ 41 | // .adapter = .postgresql, 42 | // .hostname = "localhost", 43 | // .port = 5432, 44 | // .username = "postgres", 45 | // .password = "password", 46 | // .database = "myapp_production", 47 | // }, 48 | }; 49 | -------------------------------------------------------------------------------- /demo/public/404.html: -------------------------------------------------------------------------------- 1 | 13 |
14 | 15 |

404

16 |
17 | -------------------------------------------------------------------------------- /demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetzig-framework/jetzig/1cb27ffec8fb648a30a9aa65c1e6128cf967a2f8/demo/public/favicon.ico -------------------------------------------------------------------------------- /demo/public/jetzig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetzig-framework/jetzig/1cb27ffec8fb648a30a9aa65c1e6128cf967a2f8/demo/public/jetzig.png -------------------------------------------------------------------------------- /demo/public/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+json+zig&plugins=file-highlight */ 3 | code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-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-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;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} 4 | -------------------------------------------------------------------------------- /demo/public/styles.css: -------------------------------------------------------------------------------- 1 | /* Root stylesheet. Load into your Zmpl template with: 2 | * 3 | * 4 | * 5 | */ 6 | 7 | .message { 8 | font-weight: bold; 9 | font-size: 3rem; 10 | } 11 | -------------------------------------------------------------------------------- /demo/public/zmpl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetzig-framework/jetzig/1cb27ffec8fb648a30a9aa65c1e6128cf967a2f8/demo/public/zmpl.png -------------------------------------------------------------------------------- /demo/src/app/database/Schema.zig: -------------------------------------------------------------------------------- 1 | const jetquery = @import("jetzig").jetquery; 2 | 3 | pub const User = jetquery.Model( 4 | @This(), 5 | "users", 6 | struct { 7 | id: i32, 8 | email: []const u8, 9 | password_hash: []const u8, 10 | created_at: jetquery.DateTime, 11 | updated_at: jetquery.DateTime, 12 | }, 13 | .{}, 14 | ); 15 | -------------------------------------------------------------------------------- /demo/src/app/database/migrations/2024-08-25_13-18-52_hello.zig: -------------------------------------------------------------------------------- 1 | const jetquery = @import("jetquery"); 2 | 3 | pub fn up(repo: anytype) !void { 4 | _ = repo; 5 | } 6 | 7 | pub fn down(repo: anytype) !void { 8 | _ = repo; 9 | } 10 | -------------------------------------------------------------------------------- /demo/src/app/database/migrations/2025-03-10_01-36-58_create_users.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetquery = @import("jetquery"); 3 | const t = jetquery.schema.table; 4 | const jetzig = @import("jetzig"); 5 | 6 | pub fn up(repo: anytype) !void { 7 | try repo.createTable( 8 | "users", 9 | &.{ 10 | t.primaryKey("id", .{}), 11 | t.column("email", .string, .{ .unique = true, .index = true }), 12 | t.column("password_hash", .string, .{}), 13 | t.timestamps(.{}), 14 | }, 15 | .{}, 16 | ); 17 | } 18 | 19 | pub fn down(repo: anytype) !void { 20 | try repo.dropTable("users", .{}); 21 | } 22 | -------------------------------------------------------------------------------- /demo/src/app/database/seeders/2025-03-10_01-36-58_create_users.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const jetzig = @import("jetzig"); 4 | 5 | pub fn run(repo: anytype) !void { 6 | try repo.insert( 7 | .User, 8 | .{ 9 | .email = "iguana@jetzig.dev", 10 | .password_hash = try jetzig.auth.hashPassword(repo.allocator, "password"), 11 | }, 12 | ); 13 | 14 | try repo.insert( 15 | .User, 16 | .{ 17 | .email = "admin@jetzig.dev", 18 | .password_hash = try jetzig.auth.hashPassword(repo.allocator, "admin"), 19 | }, 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /demo/src/app/jobs/example.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | /// The `run` function for all jobs receives an arena allocator, the params passed to the job 5 | /// when it was created, and an environment which provides a logger, the current server 6 | /// environment `{ development, production }`. 7 | pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig.jobs.JobEnv) !void { 8 | try env.logger.INFO("Job received params: {s}", .{try params.toJson()}); 9 | 10 | const mail = jetzig.mail.Mail.init( 11 | allocator, 12 | env, 13 | .{ 14 | .subject = "Hello!!!", 15 | .from = .{ .email = "bob@jetzig.dev" }, 16 | .to = &.{.{ .email = "bob@jetzig.dev" }}, 17 | .html = "
Hello!
", 18 | .text = "Hello!", 19 | }, 20 | ); 21 | 22 | try mail.deliver(); 23 | } 24 | -------------------------------------------------------------------------------- /demo/src/app/lib/example.zig: -------------------------------------------------------------------------------- 1 | pub fn exampleFunction(a: i64, b: i64, c: i64) i64 { 2 | return a * b * c; 3 | } 4 | -------------------------------------------------------------------------------- /demo/src/app/mailers/welcome.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | // Default values for this mailer. 5 | pub const defaults: jetzig.mail.DefaultMailParams = .{ 6 | .from = .{ .email = "no-reply@example.com" }, 7 | .subject = "Default subject", 8 | }; 9 | 10 | // The `deliver` function is invoked every time this mailer is used to send an email. 11 | // Use this function to set default mail params (e.g. a default `from` address or 12 | // `subject`) before the mail is delivered. 13 | // 14 | // A mailer can provide two Zmpl templates for rendering email content: 15 | // * `src/app/mailers//html.zmpl 16 | // * `src/app/mailers//text.zmpl 17 | // 18 | // Arguments: 19 | // * allocator: Arena allocator for use during the mail delivery process. 20 | // * mail: Mail parameters (from, to, subject, etc.). Inspect or override any values 21 | // assigned when the mail was created. 22 | // * data: Provides `data.string()` etc. for generating Jetzig Values. 23 | // * params: Template data for `text.zmpl` and `html.zmpl`. Inherits all response data 24 | // assigned in a view function and can be modified for email-specific content. 25 | // * env: Provides the following fields: 26 | // - logger: Logger attached to the same stream as the Jetzig server. 27 | // - environment: Enum of `{ production, development }`. 28 | pub fn deliver( 29 | allocator: std.mem.Allocator, 30 | mail: *jetzig.mail.MailParams, 31 | params: *jetzig.data.Value, 32 | env: jetzig.jobs.JobEnv, 33 | ) !void { 34 | _ = allocator; 35 | try params.put("email_message", "Custom email message"); 36 | 37 | try env.logger.INFO("Delivering email with subject: '{?s}'", .{mail.get(.subject)}); 38 | } 39 | -------------------------------------------------------------------------------- /demo/src/app/mailers/welcome/html.zmpl: -------------------------------------------------------------------------------- 1 |
{{.message}}
2 |
{{.email_message}}
3 | -------------------------------------------------------------------------------- /demo/src/app/mailers/welcome/text.zmpl: -------------------------------------------------------------------------------- 1 | {{.message}} 2 | 3 | {{.email_message}} 4 | -------------------------------------------------------------------------------- /demo/src/app/middleware/DemoMiddleware.zig: -------------------------------------------------------------------------------- 1 | /// Demo middleware. Assign middleware by declaring `pub const middleware` in the 2 | /// `jetzig_options` defined in your application's `src/main.zig`. 3 | /// 4 | /// Middleware is called before and after the request, providing full access to the active 5 | /// request, allowing you to execute any custom code for logging, tracking, inserting response 6 | /// headers, etc. 7 | /// 8 | /// This middleware is configured in the demo app's `src/main.zig`: 9 | /// 10 | /// ``` 11 | /// pub const jetzig_options = struct { 12 | /// pub const middleware: []const type = &.{@import("app/middleware/DemoMiddleware.zig")}; 13 | /// }; 14 | /// ``` 15 | const std = @import("std"); 16 | const jetzig = @import("jetzig"); 17 | 18 | /// Define any custom data fields you want to store here. Assigning to these fields in the `init` 19 | /// function allows you to access them in various middleware callbacks defined below, where they 20 | /// can also be modified. 21 | my_custom_value: []const u8, 22 | 23 | const DemoMiddleware = @This(); 24 | 25 | /// Initialize middleware. 26 | pub fn init(request: *jetzig.http.Request) !*DemoMiddleware { 27 | var middleware = try request.allocator.create(DemoMiddleware); 28 | middleware.my_custom_value = "initial value"; 29 | return middleware; 30 | } 31 | 32 | /// Invoked immediately after the request is received but before it has started processing. 33 | /// Any calls to `request.render` or `request.redirect` will prevent further processing of the 34 | /// request, including any other middleware in the chain. 35 | pub fn afterRequest(self: *DemoMiddleware, request: *jetzig.http.Request) !void { 36 | // Middleware can invoke `request.redirect` or `request.render`. All request processing stops 37 | // and the response is immediately returned if either of these two functions are called 38 | // during middleware processing. 39 | // _ = request.redirect("/foobar", .moved_permanently); 40 | // _ = request.render(.unauthorized); 41 | 42 | try request.server.logger.DEBUG( 43 | "[DemoMiddleware:afterRequest] my_custom_value: {s}", 44 | .{self.my_custom_value}, 45 | ); 46 | self.my_custom_value = @tagName(request.method); 47 | } 48 | 49 | /// Invoked immediately before the response renders to the client. 50 | /// The response can be modified here if needed. 51 | pub fn beforeResponse( 52 | self: *DemoMiddleware, 53 | request: *jetzig.http.Request, 54 | response: *jetzig.http.Response, 55 | ) !void { 56 | try request.server.logger.DEBUG( 57 | "[DemoMiddleware:beforeResponse] my_custom_value: {s}, response status: {s}", 58 | .{ self.my_custom_value, @tagName(response.status_code) }, 59 | ); 60 | } 61 | 62 | /// Invoked immediately after the response has been finalized and sent to the client. 63 | /// Response data can be accessed for logging, but any modifications will have no impact. 64 | pub fn afterResponse( 65 | self: *DemoMiddleware, 66 | request: *jetzig.http.Request, 67 | response: *jetzig.http.Response, 68 | ) !void { 69 | _ = self; 70 | _ = response; 71 | try request.server.logger.DEBUG("[DemoMiddleware:afterResponse] response completed", .{}); 72 | } 73 | 74 | /// Invoked after `afterResponse` is called. Use this function to do any clean-up. 75 | /// Note that `request.allocator` is an arena allocator, so any allocations are automatically 76 | /// freed before the next request starts processing. 77 | pub fn deinit(self: *DemoMiddleware, request: *jetzig.http.Request) void { 78 | request.allocator.destroy(self); 79 | } 80 | -------------------------------------------------------------------------------- /demo/src/app/views/301.zmpl: -------------------------------------------------------------------------------- 1 | Redirecting to {{.location}} 2 | -------------------------------------------------------------------------------- /demo/src/app/views/anti_csrf.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | // Anti-CSRF middleware can be included in the view's `actions` declaration to apply CSRF 5 | // protection just to this specific view, or it can be added to your application's global 6 | // middleware stack defined in `jetzig_options` in `src/main.zig`. 7 | // 8 | // Use `{{context.authenticityToken()}}` or `{{context.authenticityFormField()}}` in a Zmpl 9 | // template to generate a token, store it in the user's session, and inject it into the page. 10 | 11 | pub const actions = .{ 12 | .before = .{jetzig.middleware.AntiCsrfMiddleware}, 13 | }; 14 | 15 | pub fn post(request: *jetzig.Request) !jetzig.View { 16 | var root = try request.data(.object); 17 | 18 | const Params = struct { spam: []const u8 }; 19 | const params = try request.expectParams(Params) orelse { 20 | return request.fail(.unprocessable_entity); 21 | }; 22 | 23 | try root.put("spam", params.spam); 24 | 25 | return request.render(.created); 26 | } 27 | 28 | pub fn index(request: *jetzig.Request) !jetzig.View { 29 | return request.render(.ok); 30 | } 31 | 32 | test "post with missing token" { 33 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 34 | defer app.deinit(); 35 | 36 | const response = try app.request(.POST, "/anti_csrf", .{}); 37 | try response.expectStatus(.forbidden); 38 | } 39 | 40 | test "post with invalid token" { 41 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 42 | defer app.deinit(); 43 | 44 | const response = try app.request(.POST, "/anti_csrf", .{}); 45 | try response.expectStatus(.forbidden); 46 | } 47 | 48 | test "post with valid token but missing expected params" { 49 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 50 | defer app.deinit(); 51 | 52 | _ = try app.request(.GET, "/anti_csrf", .{}); 53 | const token = app.session.getT(.string, jetzig.authenticity_token_name).?; 54 | const response = try app.request( 55 | .POST, 56 | "/anti_csrf", 57 | .{ .params = .{ ._jetzig_authenticity_token = token } }, 58 | ); 59 | try response.expectStatus(.unprocessable_entity); 60 | } 61 | 62 | test "post with valid token and expected params" { 63 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 64 | defer app.deinit(); 65 | 66 | _ = try app.request(.GET, "/anti_csrf", .{}); 67 | const token = app.session.getT(.string, jetzig.authenticity_token_name).?; 68 | const response = try app.request( 69 | .POST, 70 | "/anti_csrf", 71 | .{ .params = .{ ._jetzig_authenticity_token = token, .spam = "Spam" } }, 72 | ); 73 | try response.expectStatus(.created); 74 | } 75 | 76 | test "index" { 77 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 78 | defer app.deinit(); 79 | 80 | const response = try app.request(.GET, "/anti_csrf", .{}); 81 | try response.expectStatus(.ok); 82 | } 83 | -------------------------------------------------------------------------------- /demo/src/app/views/anti_csrf/index.zmpl: -------------------------------------------------------------------------------- 1 |
2 | {{context.authenticityFormElement()}} 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 |
Try clearing `_jetzig-session` cookie before clicking "Submit Spam"
11 | -------------------------------------------------------------------------------- /demo/src/app/views/anti_csrf/post.zmpl: -------------------------------------------------------------------------------- 1 |

Spam Submitted Successfully

2 | 3 |

Spam:

4 | 5 |
{{$.spam}}
6 | -------------------------------------------------------------------------------- /demo/src/app/views/background_jobs.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | /// This example demonstrates usage of Jetzig's background jobs. 5 | pub fn index(request: *jetzig.Request) !jetzig.View { 6 | // Prepare a job using `src/app/jobs/example.zig`. 7 | var job = try request.job("example"); 8 | 9 | // Add a param `foo` to the job. 10 | try job.params.put("foo", "bar"); 11 | try job.params.put("id", std.crypto.random.int(u32)); 12 | 13 | // Schedule the job for background processing. 14 | try job.schedule(); 15 | 16 | return request.render(.ok); 17 | } 18 | 19 | test "index" { 20 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 21 | defer app.deinit(); 22 | 23 | const response = try app.request(.GET, "/background_jobs", .{}); 24 | try response.expectJob("example", .{ .foo = "bar" }); 25 | } 26 | -------------------------------------------------------------------------------- /demo/src/app/views/basic.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | pub fn index(request: *jetzig.Request) !jetzig.View { 5 | return request.render(.ok); 6 | } 7 | -------------------------------------------------------------------------------- /demo/src/app/views/basic/index.zmpl: -------------------------------------------------------------------------------- 1 |
2 | Content goes here 3 |
Hello
4 |
5 | -------------------------------------------------------------------------------- /demo/src/app/views/cache.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | pub fn index(request: *jetzig.Request) !jetzig.View { 5 | var root = try request.data(.object); 6 | try root.put("message", try request.cache.get("message")); 7 | 8 | return request.render(.ok); 9 | } 10 | 11 | pub fn post(request: *jetzig.Request) !jetzig.View { 12 | var root = try request.data(.object); 13 | 14 | const params = try request.params(); 15 | 16 | if (params.get("message")) |message| { 17 | try request.cache.put("message", message); 18 | try root.put("message", message); 19 | } else { 20 | try root.put("message", "[no message param detected]"); 21 | } 22 | 23 | return request.render(.ok); 24 | } 25 | 26 | test "index" { 27 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 28 | defer app.deinit(); 29 | 30 | _ = try app.request(.POST, "/cache", .{ .params = .{ .message = "test message" } }); 31 | 32 | const response = try app.request(.GET, "/cache", .{}); 33 | try response.expectBodyContains( 34 | \\ Cached value: test message 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /demo/src/app/views/cache/index.zmpl: -------------------------------------------------------------------------------- 1 |
2 | Cached value: {{.message}} 3 |
4 | -------------------------------------------------------------------------------- /demo/src/app/views/cache/post.zmpl: -------------------------------------------------------------------------------- 1 |
2 | Value "{{.message}}" added to cache 3 |
4 | -------------------------------------------------------------------------------- /demo/src/app/views/channels/index.zmpl: -------------------------------------------------------------------------------- 1 |
2 | Content goes here 3 |
4 | -------------------------------------------------------------------------------- /demo/src/app/views/custom/foo.zig: -------------------------------------------------------------------------------- 1 | const jetzig = @import("jetzig"); 2 | 3 | pub fn bar(id: []const u8, request: *jetzig.Request) !jetzig.View { 4 | var root = try request.data(.object); 5 | try root.put("id", id); 6 | return request.render(.ok); 7 | } 8 | -------------------------------------------------------------------------------- /demo/src/app/views/custom/foo/bar.zmpl: -------------------------------------------------------------------------------- 1 | {{jetzig_view}} 2 | {{jetzig_action}} 3 | {{.id}} 4 | -------------------------------------------------------------------------------- /demo/src/app/views/errors.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | // Generic handler for all errors. 5 | // Use `jetzig.http.status_codes.get(request.status_code)` to get a value that provides string 6 | // versions of the error code and message for use in templates. 7 | pub fn index(request: *jetzig.Request) !jetzig.View { 8 | var root = try request.data(.object); 9 | var error_info = try root.put("error", .object); 10 | 11 | const status = jetzig.http.status_codes.get(request.status_code); 12 | 13 | try error_info.put("code", status.getCode()); 14 | try error_info.put("message", status.getMessage()); 15 | 16 | // Render with the original error status code, or override if preferred. 17 | return request.render(request.status_code); 18 | } 19 | -------------------------------------------------------------------------------- /demo/src/app/views/errors/index.zmpl: -------------------------------------------------------------------------------- 1 | 13 |
14 | 15 |

{{.error.code}}

16 |

{{.error.message}}

17 |
18 | -------------------------------------------------------------------------------- /demo/src/app/views/file_upload.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | pub fn index(request: *jetzig.Request) !jetzig.View { 5 | return request.render(.ok); 6 | } 7 | 8 | pub fn post(request: *jetzig.Request) !jetzig.View { 9 | var root = try request.data(.object); 10 | 11 | const params = try request.params(); 12 | 13 | if (try request.file("upload")) |file| { 14 | try root.put("description", params.getT(.string, "description")); 15 | try root.put("filename", file.filename); 16 | try root.put("content", file.content); 17 | try root.put("uploaded", true); 18 | } 19 | 20 | return request.render(.created); 21 | } 22 | 23 | test "index" { 24 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 25 | defer app.deinit(); 26 | 27 | const response = try app.request(.GET, "/file_upload", .{}); 28 | try response.expectStatus(.ok); 29 | } 30 | 31 | test "post" { 32 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 33 | defer app.deinit(); 34 | 35 | const response = try app.request(.POST, "/file_upload", .{ 36 | .body = app.multipart(.{ 37 | .description = "example description", 38 | .upload = jetzig.testing.file("example.txt", "example file content"), 39 | }), 40 | }); 41 | 42 | try response.expectStatus(.created); 43 | try response.expectBodyContains("example description"); 44 | try response.expectBodyContains("example.txt"); 45 | try response.expectBodyContains("example file content"); 46 | } 47 | -------------------------------------------------------------------------------- /demo/src/app/views/file_upload/index.zmpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 |
8 | -------------------------------------------------------------------------------- /demo/src/app/views/file_upload/post.zmpl: -------------------------------------------------------------------------------- 1 |

File Uploaded Successfully

2 | 3 |

Description

4 |

{{.description}}

5 | 6 |

Filename

7 |

{{.filename}}

8 | 9 |

Content

10 |

{{.content}}

11 | -------------------------------------------------------------------------------- /demo/src/app/views/format.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | // Define `pub const formats` to apply constraints to specific view functions. By default, all 5 | // view functions respond to `json` and `html` requests. Use this feature to override those 6 | // defaults. 7 | pub const formats: jetzig.Route.Formats = .{ 8 | .index = &.{ .json, .html }, 9 | .get = &.{.html}, 10 | }; 11 | 12 | pub fn index(request: *jetzig.Request) !jetzig.View { 13 | return request.render(.ok); 14 | } 15 | 16 | pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View { 17 | _ = id; 18 | return request.render(.ok); 19 | } 20 | 21 | test "index (json)" { 22 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 23 | defer app.deinit(); 24 | 25 | const response = try app.request(.GET, "/format.json", .{}); 26 | try response.expectStatus(.ok); 27 | } 28 | 29 | test "index (html)" { 30 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 31 | defer app.deinit(); 32 | 33 | const response = try app.request(.GET, "/format.html", .{}); 34 | try response.expectStatus(.ok); 35 | } 36 | 37 | test "get (html)" { 38 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 39 | defer app.deinit(); 40 | 41 | const response = try app.request(.GET, "/format/example-id.html", .{}); 42 | try response.expectStatus(.ok); 43 | } 44 | 45 | test "get (json)" { 46 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 47 | defer app.deinit(); 48 | 49 | const response = try app.request(.GET, "/format/example-id.json", .{}); 50 | try response.expectStatus(.not_found); 51 | } 52 | -------------------------------------------------------------------------------- /demo/src/app/views/format/get.zmpl: -------------------------------------------------------------------------------- 1 |
2 | Content goes here 3 |
4 | -------------------------------------------------------------------------------- /demo/src/app/views/format/index.zmpl: -------------------------------------------------------------------------------- 1 |
2 | Content goes here 3 |
4 | -------------------------------------------------------------------------------- /demo/src/app/views/init.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | /// `src/app/views/root.zig` represents the root URL `/` 5 | /// The `index` view function is invoked when when the HTTP verb is `GET`. 6 | /// Other view types are invoked either by passing a resource ID value (e.g. `/1234`) or by using 7 | /// a different HTTP verb: 8 | /// 9 | /// GET / => index(request, data) 10 | /// GET /1234 => get(id, request, data) 11 | /// POST / => post(request, data) 12 | /// PUT /1234 => put(id, request, data) 13 | /// PATCH /1234 => patch(id, request, data) 14 | /// DELETE /1234 => delete(id, request, data) 15 | pub fn index(request: *jetzig.Request) !jetzig.View { 16 | // Sets the root response data value. 17 | // JSON requests return a JSON string representation of the root data value. 18 | // Zmpl templates can access all values in the root data value. 19 | var root = try request.data(.object); 20 | 21 | // Add a string to the root object. 22 | try root.put("welcome_message", "Welcome to Jetzig!"); 23 | 24 | // Request params have the same type as a `data.object()` so they can be inserted them 25 | // directly into the response data. Fetch `http://localhost:8080/?message=hello` to set the 26 | // param. JSON data is also accepted when the `content-type: application/json` header is 27 | // present. 28 | const params = try request.params(); 29 | 30 | try root.put("message_param", params.get("message")); 31 | 32 | // Set arbitrary response headers as required. `content-type` is automatically assigned for 33 | // HTML, JSON responses. 34 | // 35 | // Static files located in `public/` in the root of your project directory are accessible 36 | // from the root path (e.g. `public/jetzig.png`) is available at `/jetzig.png` and the 37 | // content type is inferred from the extension using MIME types. 38 | try request.response.headers.append("x-example-header", "example header value"); 39 | 40 | // Render the response and set the response code. 41 | return request.render(.ok); 42 | } 43 | -------------------------------------------------------------------------------- /demo/src/app/views/init/_content.zmpl: -------------------------------------------------------------------------------- 1 | @args message: []const u8 2 |

{{message}}

3 | 4 |
5 | 6 |
7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 |
Visit jetzig.dev to get started.
15 |
Join our Discord server and introduce yourself:
16 | 17 |
18 | https://discord.gg/eufqssz7X6 19 |
20 |
21 | Follow the project on 22 | 23 | GitHub 24 | GitHub Repo stars 25 | 26 |
27 |
28 | -------------------------------------------------------------------------------- /demo/src/app/views/init/index.zmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 14 |

{{$.message_param}}

15 | 16 | 17 |
18 | @partial init/content(message: $.welcome_message) 19 |
20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/src/app/views/kvstore.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | /// This example demonstrates usage of Jetzig's KV store. 5 | pub fn index(request: *jetzig.Request) !jetzig.View { 6 | var root = try request.data(.object); 7 | 8 | // Fetch a string from the KV store. If it exists, store it in the root data object, 9 | // otherwise store a string value to be picked up by the next request. 10 | if (try request.store.get("example-key")) |capture| { 11 | try root.put("stored_string", capture); 12 | } else { 13 | try root.put("stored_string", null); 14 | try request.store.put("example-key", "example-value"); 15 | } 16 | 17 | // Left-pop an item from an array and store it in the root data object. This will empty the 18 | // array after multiple requests. 19 | // If the array is empty or not found, append some new values to the array. 20 | if (try request.store.popFirst("example-array")) |value| { 21 | try root.put("popped", value); 22 | } else { 23 | // Store some values in an array in the KV store. 24 | try request.store.append("example-array", "hello"); 25 | try request.store.append("example-array", "goodbye"); 26 | try request.store.append("example-array", "hello again"); 27 | 28 | try root.put("popped", null); 29 | } 30 | 31 | return request.render(.ok); 32 | } 33 | 34 | test "index" { 35 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 36 | defer app.deinit(); 37 | 38 | const response1 = try app.request(.GET, "/kvstore.json", .{}); 39 | try response1.expectStatus(.ok); 40 | try response1.expectJson(".stored_string", null); 41 | 42 | const response2 = try app.request(.GET, "/kvstore.json", .{}); 43 | try response2.expectJson(".stored_string", "example-value"); 44 | try response2.expectJson(".popped", "hello"); 45 | try (try app.request(.GET, "/kvstore.json", .{})).expectJson(".popped", "goodbye"); 46 | try (try app.request(.GET, "/kvstore.json", .{})).expectJson(".popped", "hello again"); 47 | } 48 | -------------------------------------------------------------------------------- /demo/src/app/views/kvstore/index.zmpl: -------------------------------------------------------------------------------- 1 |
Stored string: {{.stored_string}}
2 |
Left-popped array value: {{.popped}}
3 | 4 |
Refresh this page to cycle through values
5 | -------------------------------------------------------------------------------- /demo/src/app/views/layouts/application.zmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
{{zmpl.content}}
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /demo/src/app/views/login.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | const auth = @import("jetzig").auth; 4 | 5 | pub fn index(request: *jetzig.Request) !jetzig.View { 6 | var root = try request.data(.object); 7 | 8 | if (request.middleware(.auth).user) |user| { 9 | try root.put("user", .{ .email = user.email }); 10 | } 11 | 12 | return request.render(.ok); 13 | } 14 | 15 | pub fn post(request: *jetzig.Request) !jetzig.View { 16 | const Logout = struct { logout: []const u8 }; 17 | const Login = struct { email: []const u8, password: []const u8 }; 18 | 19 | if (try request.expectParams(Logout)) |_| { 20 | try auth.signOut(request); 21 | return request.redirect("/login", .found); 22 | } 23 | 24 | const params = try request.expectParams(Login) orelse { 25 | return request.fail(.forbidden); 26 | }; 27 | 28 | // Lookup the user by email 29 | const user = try jetzig.database.Query(.User).findBy( 30 | .{ .email = params.email }, 31 | ).execute(request.repo) orelse { 32 | return request.fail(.forbidden); 33 | }; 34 | 35 | // Check that the password matches 36 | if (!try auth.verifyPassword( 37 | request.allocator, 38 | user.password_hash, 39 | params.password, 40 | )) return request.fail(.forbidden); 41 | 42 | try auth.signIn(request, user.id); 43 | return request.redirect("/login", .found); 44 | } 45 | 46 | test "post" { 47 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 48 | defer app.deinit(); 49 | 50 | const hashed_pass = try auth.hashPassword(std.testing.allocator, "test"); 51 | defer std.testing.allocator.free(hashed_pass); 52 | 53 | try jetzig.database.Query(.User).deleteAll().execute(app.repo); 54 | try app.repo.insert(.User, .{ 55 | .id = 1, 56 | .email = "test@test.com", 57 | .password_hash = hashed_pass, 58 | }); 59 | 60 | const response = try app.request(.POST, "/login", .{ 61 | .json = .{ 62 | .email = "test@test.com", 63 | .password = "test", 64 | }, 65 | }); 66 | try response.expectStatus(.found); 67 | } 68 | -------------------------------------------------------------------------------- /demo/src/app/views/login/index.zmpl: -------------------------------------------------------------------------------- 1 | @if ($.user) |user| 2 |
Logged in as {{user.email}}
3 | 4 |
5 | 6 | 7 |
8 | @else 9 |
10 | 11 | 12 | 13 | 14 | 15 |
16 | @end 17 | -------------------------------------------------------------------------------- /demo/src/app/views/mail.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | pub fn index(request: *jetzig.Request) !jetzig.View { 5 | var root = try request.data(.object); 6 | try root.put("message", "Welcome to Jetzig!"); 7 | 8 | // Create a new mail using `src/app/mailers/welcome.zig`. 9 | // HTML and text parts are rendered using Zmpl templates: 10 | // * `src/app/mailers/welcome/html.zmpl` 11 | // * `src/app/mailers/welcome/text.zmpl` 12 | // All mailer templates have access to the same template data as a view template. 13 | const mail = request.mail("welcome", .{ .to = &.{.{ .email = "hello@jetzig.dev" }} }); 14 | 15 | // Deliver the email asynchronously via a built-in mail Job. Use `.now` to send the email 16 | // synchronously (i.e. before the request has returned). 17 | try mail.deliver(.background, .{}); 18 | 19 | return request.render(.ok); 20 | } 21 | -------------------------------------------------------------------------------- /demo/src/app/views/mail/index.zmpl: -------------------------------------------------------------------------------- 1 |
2 | Your email has been sent! 3 |
{{.message}}
4 |
5 | -------------------------------------------------------------------------------- /demo/src/app/views/markdown.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | pub const layout = "application"; 5 | 6 | pub fn index(request: *jetzig.Request) !jetzig.View { 7 | return request.render(.ok); 8 | } 9 | 10 | test "index" { 11 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 12 | defer app.deinit(); 13 | const response = try app.request(.GET, "/markdown", .{}); 14 | try response.expectBodyContains("You can still use Zmpl references, modes, and partials."); 15 | } 16 | -------------------------------------------------------------------------------- /demo/src/app/views/markdown/index.md.zmpl: -------------------------------------------------------------------------------- 1 | # Markdown Example 2 | 3 | ![jetzig logo](https://www.jetzig.dev/jetzig.png) 4 | 5 | _Markdown_ is rendered by [zmd](https://github.com/jetzig-framework/zmd). 6 | 7 | You can use a `StaticRequest` in your view if you prefer to render your _Markdown_ at build time, or use `Request` in development to render at run time without a server restart. 8 | 9 | Simply create a `.md.zmpl` file instead of a `.zmpl` file, e.g. `src/app/views/iguanas/index.md.zmpl` and _Markdown_ will be rendered. You can still use _Zmpl_ references, modes, and partials. In fact, a `.md.zmpl` template is simply a regular _Zmpl_ template with the root mode set to `markdown`. 10 | 11 | ## An _ordered_ list 12 | 13 | 1. List item with a [link](https://ziglang.org/) 14 | 1. List item with some **bold** and _italic_ text 15 | 1. List item 3 16 | 17 | ## An _unordered_ list 18 | 19 | * List item 1 20 | * List item 2 21 | * List item 3 22 | 23 | ## Define your own formatters in `src/main.zig` 24 | 25 | ```zig 26 | pub const jetzig_options = struct { 27 | pub const markdown_fragments = struct { 28 | pub const root = .{ 29 | "
", 30 | "
", 31 | }; 32 | pub const h1 = .{ 33 | "

", 34 | "

", 35 | }; 36 | pub const h2 = .{ 37 | "

", 38 | "

", 39 | }; 40 | pub const h3 = .{ 41 | "

", 42 | "

", 43 | }; 44 | pub const paragraph = .{ 45 | "

", 46 | "

", 47 | }; 48 | pub const code = .{ 49 | "", 50 | "", 51 | }; 52 | 53 | pub fn block(allocator: std.mem.Allocator, node: jetzig.zmd.Node) ![]const u8 { 54 | return try std.fmt.allocPrint(allocator, 55 | \\
{s}
56 | , .{try jetzig.zmd.html.escape(allocator, node.content)}); 57 | } 58 | 59 | pub fn link(allocator: std.mem.Allocator, node: jetzig.zmd.Node) ![]const u8 { 60 | return try std.fmt.allocPrint(allocator, 61 | \\{1s} 62 | , .{ node.href.?, node.title.? }); 63 | } 64 | }; 65 | } 66 | ``` 67 | -------------------------------------------------------------------------------- /demo/src/app/views/nested/route/example.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | pub fn index(request: *jetzig.Request) !jetzig.View { 5 | return request.render(.ok); 6 | } 7 | 8 | pub const static_params = .{ 9 | .get = .{ 10 | .{ .id = "foo", .params = .{ .foo = "bar" } }, 11 | .{ .id = "foo" }, 12 | }, 13 | }; 14 | 15 | pub fn get(id: []const u8, request: *jetzig.StaticRequest) !jetzig.View { 16 | var object = try request.data(.object); 17 | try object.put("id", id); 18 | const params = try request.params(); 19 | if (params.get("foo")) |value| try object.put("foo", value); 20 | return request.render(.ok); 21 | } 22 | -------------------------------------------------------------------------------- /demo/src/app/views/nested/route/example/index.zmpl: -------------------------------------------------------------------------------- 1 |
2 | Content goes here 3 |
4 | -------------------------------------------------------------------------------- /demo/src/app/views/nested/route/markdown.md: -------------------------------------------------------------------------------- 1 | # Dynamic Markdown Routes 2 | 3 | _Markdown_ can be stored in any path in `src/app/views/` and _Jetzig_ will automatically render it if it matches a URI. 4 | 5 | This _Markdown_ page can be accessed at `/nested/route/markdown.html` and `/nested/route/markdown`. 6 | 7 | This functionality is particularly useful if you want to load _Markdown_ content with [htmx](https://htmx.org/). 8 | -------------------------------------------------------------------------------- /demo/src/app/views/params.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | pub fn post(request: *jetzig.Request) !jetzig.View { 5 | const Params = struct { 6 | // Required param - `expectParams` returns `null` if not present: 7 | name: []const u8, 8 | // Enum params are converted from string, `expectParams` returns `null` if no match: 9 | favorite_animal: enum { cat, dog, raccoon }, 10 | // Optional params are not required. Numbers are coerced from strings. `expectParams` 11 | // returns `null` if a type coercion fails. 12 | age: ?u8 = 100, 13 | }; 14 | const params = try request.expectParams(Params) orelse { 15 | // Inspect information about the failed params with `request.paramsInfo()`: 16 | // std.debug.print("{?}\n", .{try request.paramsInfo()}); 17 | return request.fail(.unprocessable_entity); 18 | }; 19 | 20 | var root = try request.data(.object); 21 | try root.put("info", params); 22 | 23 | return request.render(.created); 24 | } 25 | 26 | test "post query params" { 27 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 28 | defer app.deinit(); 29 | 30 | const response1 = try app.request(.POST, "/params", .{ 31 | .params = .{ 32 | .name = "Bob", 33 | .favorite_animal = "raccoon", 34 | }, 35 | }); 36 | try response1.expectStatus(.created); 37 | 38 | const response2 = try app.request(.POST, "/params", .{ 39 | .params = .{ 40 | .name = "Bob", 41 | .favorite_animal = "platypus", 42 | }, 43 | }); 44 | try response2.expectStatus(.unprocessable_entity); 45 | 46 | const response3 = try app.request(.POST, "/params", .{ 47 | .params = .{ 48 | .name = "", // empty param 49 | .favorite_animal = "raccoon", 50 | }, 51 | }); 52 | try response3.expectStatus(.unprocessable_entity); 53 | } 54 | 55 | test "post json" { 56 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 57 | defer app.deinit(); 58 | 59 | const response1 = try app.request(.POST, "/params", .{ 60 | .json = .{ 61 | .name = "Bob", 62 | .favorite_animal = "raccoon", 63 | }, 64 | }); 65 | try response1.expectJson("$.info.name", "Bob"); 66 | try response1.expectJson("$.info.favorite_animal", "raccoon"); 67 | try response1.expectJson("$.info.age", 100); 68 | 69 | const response2 = try app.request(.POST, "/params", .{ 70 | .json = .{ 71 | .name = "Hercules", 72 | .favorite_animal = "cat", 73 | .age = 11, 74 | }, 75 | }); 76 | try response2.expectJson("$.info.name", "Hercules"); 77 | try response2.expectJson("$.info.favorite_animal", "cat"); 78 | try response2.expectJson("$.info.age", 11); 79 | 80 | const response3 = try app.request(.POST, "/params", .{ 81 | .json = .{ 82 | .name = "Hercules", 83 | .favorite_animal = "platypus", 84 | .age = 11, 85 | }, 86 | }); 87 | try response3.expectStatus(.unprocessable_entity); 88 | } 89 | -------------------------------------------------------------------------------- /demo/src/app/views/params/post.zmpl: -------------------------------------------------------------------------------- 1 |
2 | Content goes here 3 |
4 | -------------------------------------------------------------------------------- /demo/src/app/views/quotes.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | pub const layout = "application"; 5 | 6 | pub fn get(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { 7 | var body = try data.object(); 8 | 9 | const random_quote = try randomQuote(request.allocator); 10 | 11 | if (std.mem.eql(u8, id, "random")) { 12 | try body.put("quote", data.string(random_quote.quote)); 13 | try body.put("author", data.string(random_quote.author)); 14 | } else { 15 | try body.put("quote", data.string("If you can dream it, you can achieve it.")); 16 | try body.put("author", data.string("Zig Ziglar")); 17 | } 18 | 19 | return request.render(.ok); 20 | } 21 | 22 | pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { 23 | var root = try data.object(); 24 | const params = try request.params(); 25 | try root.put("param", params.get("foo").?); 26 | 27 | return request.render(.ok); 28 | } 29 | 30 | const Quote = struct { 31 | quote: []const u8, 32 | author: []const u8, 33 | }; 34 | 35 | // Quotes taken from: https://gist.github.com/natebass/b0a548425a73bdf8ea5c618149fe1fce 36 | fn randomQuote(allocator: std.mem.Allocator) !Quote { 37 | const path = "src/app/config/quotes.json"; 38 | const stat = try std.fs.cwd().statFile(path); 39 | const json = try std.fs.cwd().readFileAlloc(allocator, path, @intCast(stat.size)); 40 | const quotes = try std.json.parseFromSlice([]Quote, allocator, json, .{}); 41 | return quotes.value[std.crypto.random.intRangeLessThan(usize, 0, quotes.value.len)]; 42 | } 43 | 44 | test "get" { 45 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 46 | defer app.deinit(); 47 | 48 | const response = try app.request(.GET, "/quotes/initial", .{}); 49 | try response.expectBodyContains("If you can dream it, you can achieve it."); 50 | } 51 | -------------------------------------------------------------------------------- /demo/src/app/views/quotes/get.zmpl: -------------------------------------------------------------------------------- 1 |
"{{.quote}}"
2 |
--{{.author}}
3 | -------------------------------------------------------------------------------- /demo/src/app/views/quotes/post.zmpl: -------------------------------------------------------------------------------- 1 |
{{.param}}
2 | -------------------------------------------------------------------------------- /demo/src/app/views/redirect.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | pub fn index(request: *jetzig.Request) !jetzig.View { 5 | const params = try request.params(); 6 | if (params.get("redirect")) |location| { 7 | switch (location.*) { 8 | // Value is `.null` when param is empty, e.g.: 9 | // `http://localhost:8080/redirect?redirect` 10 | .null => return request.redirect("http://www.example.com/", .moved_permanently), 11 | // Value is `.string` when param is present, e.g.: 12 | // `http://localhost:8080/redirect?redirect=https://jetzig.dev/` 13 | .string => |string| return request.redirect(string.value, .moved_permanently), 14 | else => unreachable, 15 | } 16 | } 17 | 18 | try request.response.headers.append("foobar", "hello"); 19 | return request.render(.ok); 20 | } 21 | -------------------------------------------------------------------------------- /demo/src/app/views/redirect/index.zmpl: -------------------------------------------------------------------------------- 1 |
2 | Content goes here 3 |
4 | -------------------------------------------------------------------------------- /demo/src/app/views/render_template.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | pub fn index(request: *jetzig.Request) !jetzig.View { 5 | return request.renderTemplate("basic/index", .ok); 6 | } 7 | 8 | test "index" { 9 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 10 | defer app.deinit(); 11 | 12 | const response = try app.request(.GET, "/render_template", .{}); 13 | try response.expectStatus(.ok); 14 | try response.expectBodyContains("Hello"); 15 | } 16 | -------------------------------------------------------------------------------- /demo/src/app/views/render_template/index.zmpl: -------------------------------------------------------------------------------- 1 |
2 | Content goes here 3 |
4 | -------------------------------------------------------------------------------- /demo/src/app/views/render_text.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | pub fn index(request: *jetzig.Request) !jetzig.View { 5 | request.response.content_type = "text/xml"; 6 | return request.renderText("baz", .ok); 7 | } 8 | 9 | test "index" { 10 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 11 | defer app.deinit(); 12 | 13 | const response = try app.request(.GET, "/render_text", .{}); 14 | try response.expectStatus(.ok); 15 | try response.expectBodyContains("baz"); 16 | try response.expectHeader("content-type", "text/xml"); 17 | } 18 | -------------------------------------------------------------------------------- /demo/src/app/views/render_text/index.zmpl: -------------------------------------------------------------------------------- 1 |
2 | Content goes here 3 |
4 | -------------------------------------------------------------------------------- /demo/src/app/views/root.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | const importedFunction = @import("../lib/example.zig").exampleFunction; 5 | 6 | pub const layout = "application"; 7 | 8 | pub fn index(request: *jetzig.Request) !jetzig.View { 9 | var root = try request.data(.object); 10 | try root.put("message", "Welcome to Jetzig!"); 11 | try root.put("custom_number", customFunction(100, 200, 300)); 12 | try root.put("imported_number", importedFunction(100, 200, 300)); 13 | 14 | try request.response.headers.append("x-example-header", "example header value"); 15 | 16 | return request.render(.ok); 17 | } 18 | 19 | pub fn edit(id: []const u8, request: *jetzig.Request) !jetzig.View { 20 | var root = try request.data(.object); 21 | try root.put("id", id); 22 | return request.render(.ok); 23 | } 24 | 25 | fn customFunction(a: i32, b: i32, c: i32) i32 { 26 | return a + b + c; 27 | } 28 | 29 | test "404 Not Found" { 30 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 31 | defer app.deinit(); 32 | 33 | const response = try app.request(.GET, "/foobar", .{}); 34 | try response.expectStatus(.not_found); 35 | } 36 | 37 | test "200 OK" { 38 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 39 | defer app.deinit(); 40 | 41 | const response = try app.request(.GET, "/", .{}); 42 | try response.expectStatus(.ok); 43 | } 44 | 45 | test "response body" { 46 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 47 | defer app.deinit(); 48 | 49 | const response = try app.request(.GET, "/", .{}); 50 | try response.expectBodyContains("Welcome to Jetzig!"); 51 | } 52 | 53 | test "header" { 54 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 55 | defer app.deinit(); 56 | 57 | const response = try app.request(.GET, "/", .{}); 58 | try response.expectHeader("content-type", "text/html"); 59 | } 60 | 61 | test "json" { 62 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 63 | defer app.deinit(); 64 | 65 | const response = try app.request(.GET, "/.json", .{}); 66 | try response.expectJson(".message", "Welcome to Jetzig!"); 67 | try response.expectJson(".custom_number", 600); 68 | } 69 | 70 | test "json from header" { 71 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 72 | defer app.deinit(); 73 | 74 | const response = try app.request( 75 | .GET, 76 | "/", 77 | .{ .headers = .{ .accept = "application/json" } }, 78 | ); 79 | try response.expectJson(".message", "Welcome to Jetzig!"); 80 | try response.expectJson(".custom_number", 600); 81 | } 82 | 83 | test "public file" { 84 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 85 | defer app.deinit(); 86 | 87 | const response = try app.request(.GET, "/jetzig.png", .{}); 88 | try response.expectStatus(.ok); 89 | } 90 | -------------------------------------------------------------------------------- /demo/src/app/views/root/_quotes.zmpl: -------------------------------------------------------------------------------- 1 | @args message: *ZmplValue 2 |
3 |

{{message}}

4 |
5 | 6 | 7 | 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /demo/src/app/views/root/index.zmpl: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 |
7 | @partial root/quotes(message: $.message) 8 |
9 | 10 |
11 | 12 | 13 | 14 |
15 | 16 |
Take a look at the /demo/src/app/ directory to see how this application works.
17 |
Visit jetzig.dev to get started.
18 |
19 | -------------------------------------------------------------------------------- /demo/src/app/views/session.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig"); 3 | 4 | pub fn index(request: *jetzig.Request) !jetzig.View { 5 | var root = try request.data(.object); 6 | 7 | const session = try request.session(); 8 | 9 | if (session.get("message")) |message| { 10 | try root.put("message", message); 11 | } else { 12 | try root.put("message", "No message saved yet"); 13 | } 14 | 15 | return request.render(.ok); 16 | } 17 | 18 | pub fn edit(id: []const u8, request: *jetzig.Request) !jetzig.View { 19 | try request.server.logger.INFO("id: {s}", .{id}); 20 | return request.render(.ok); 21 | } 22 | 23 | pub fn post(request: *jetzig.Request) !jetzig.View { 24 | const params = try request.params(); 25 | var session = try request.session(); 26 | 27 | if (params.get("message")) |message| { 28 | if (std.mem.eql(u8, message.string.value, "delete")) { 29 | _ = try session.remove("message"); 30 | } else { 31 | try session.put("message", message); 32 | } 33 | } 34 | 35 | return request.redirect("/session", .moved_permanently); 36 | } 37 | -------------------------------------------------------------------------------- /demo/src/app/views/session/index.zmpl: -------------------------------------------------------------------------------- 1 |
2 | Saved message in session: {{.message}} 3 |
4 | 5 |
6 | 7 |
8 | 9 | 10 |
11 | -------------------------------------------------------------------------------- /demo/src/app/views/static.zig: -------------------------------------------------------------------------------- 1 | /// This example demonstrates static site generation (SSG). 2 | /// 3 | /// Any view function that receives `*jetzig.StaticRequest` is considered as a SSG view, which 4 | /// will be invoked at build time and its content (both JSON and HTML) rendered to `static/` in 5 | /// the root project directory. 6 | /// 7 | /// Define `pub const static_params` as a struct with fields named after each view function, with 8 | /// the value for each field being an array of structs with fields `params` and, where 9 | /// applicable (i.e. `get`, `put`, `patch`, and `delete`), `id`. 10 | /// 11 | /// For each item in the provided array, a separate JSON and HTML output will be generated. At 12 | /// run time, requests are matched to the relevant content by comparing the request params and 13 | /// resource ID to locate the relevant content. 14 | /// 15 | /// Launch the demo app and try the following requests: 16 | /// 17 | /// ```console 18 | /// curl -H "Accept: application/json" \ 19 | /// --data-binary '{"foo":"hello", "bar":"goodbye"}' \ 20 | /// --request GET \ 21 | /// 'http://localhost:8080/static' 22 | /// ``` 23 | /// 24 | /// ```console 25 | /// curl 'http://localhost:8080/static.html?foo=hi&bar=bye' 26 | /// ``` 27 | /// 28 | /// ```console 29 | /// curl 'http://localhost:8080/static/123.html?foo=hi&bar=bye' 30 | /// ``` 31 | const std = @import("std"); 32 | const jetzig = @import("jetzig"); 33 | 34 | pub const static_params = .{ 35 | .index = .{ 36 | .{ .params = .{ .foo = "hi", .bar = "bye" } }, 37 | .{ .params = .{ .foo = "hello", .bar = "goodbye" } }, 38 | }, 39 | .get = .{ 40 | .{ .id = "123", .params = .{ .foo = "hi", .bar = "bye" } }, 41 | .{ .id = "456", .params = .{ .foo = "hello", .bar = "goodbye" } }, 42 | }, 43 | }; 44 | 45 | pub fn index(request: *jetzig.StaticRequest, data: *jetzig.Data) !jetzig.View { 46 | var root = try data.root(.object); 47 | 48 | const params = try request.params(); 49 | 50 | try root.put("foo", params.get("foo")); 51 | try root.put("bar", params.get("bar")); 52 | 53 | return request.render(.ok); 54 | } 55 | 56 | pub fn get(id: []const u8, request: *jetzig.StaticRequest, data: *jetzig.Data) !jetzig.View { 57 | var root = try data.root(.object); 58 | 59 | const params = try request.params(); 60 | 61 | if (std.mem.eql(u8, id, "123")) { 62 | try root.put("message", "id is '123'"); 63 | } else { 64 | try root.put("message", "id is not '123'"); 65 | } 66 | 67 | try root.put("foo", params.get("foo")); 68 | try root.put("bar", params.get("bar")); 69 | 70 | return request.render(.created); 71 | } 72 | 73 | test "index json" { 74 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 75 | defer app.deinit(); 76 | 77 | const response = try app.request( 78 | .GET, 79 | "/static.json", 80 | .{ .json = .{ .foo = "hello", .bar = "goodbye" } }, 81 | ); 82 | 83 | try response.expectStatus(.ok); 84 | try response.expectJson(".foo", "hello"); 85 | try response.expectJson(".bar", "goodbye"); 86 | } 87 | 88 | test "get json" { 89 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 90 | defer app.deinit(); 91 | 92 | const response = try app.request( 93 | .GET, 94 | "/static/123.json", 95 | .{ .json = .{ .foo = "hi", .bar = "bye" } }, 96 | ); 97 | 98 | try response.expectStatus(.ok); 99 | try response.expectJson(".message", "id is '123'"); 100 | } 101 | 102 | test "index html" { 103 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 104 | defer app.deinit(); 105 | 106 | const response = try app.request( 107 | .GET, 108 | "/static.html", 109 | .{ .params = .{ .foo = "hello", .bar = "goodbye" } }, 110 | ); 111 | 112 | try response.expectStatus(.ok); 113 | try response.expectBodyContains("foo: hello"); 114 | try response.expectBodyContains("bar: goodbye"); 115 | } 116 | 117 | test "get html" { 118 | var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); 119 | defer app.deinit(); 120 | 121 | const response = try app.request( 122 | .GET, 123 | "/static/123.html", 124 | .{ .params = .{ .foo = "hi", .bar = "bye" } }, 125 | ); 126 | 127 | try response.expectStatus(.ok); 128 | } 129 | -------------------------------------------------------------------------------- /demo/src/app/views/static/get.zmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 |
{{.message}}
4 |
foo: {{.foo}}
5 |
bar: {{.bar}}
6 | 7 | 8 | -------------------------------------------------------------------------------- /demo/src/app/views/static/index.zmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 |
You made a request to /static with:
4 |
foo: {{.foo}}
5 |
bar: {{.bar}}
6 | 7 | 8 | -------------------------------------------------------------------------------- /demo/zmpl_options.zig: -------------------------------------------------------------------------------- 1 | hello -------------------------------------------------------------------------------- /src/GenerateMimeTypes.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const JsonMimeType = struct { 4 | name: []const u8, 5 | fileTypes: [][]const u8, 6 | }; 7 | 8 | /// Invoked at build time to parse mimeData.json into an array of `MimeType` which can then be 9 | /// written out as a Zig struct and imported at runtime. 10 | pub fn generateMimeModule(build: *std.Build) !*std.Build.Module { 11 | const file = try std.fs.openFileAbsolute(build.pathFromRoot("src/jetzig/http/mime/mimeData.json"), .{}); 12 | const stat = try file.stat(); 13 | const json = try file.readToEndAlloc(build.allocator, @intCast(stat.size)); 14 | defer build.allocator.free(json); 15 | 16 | const parsed_mime_types = try std.json.parseFromSlice( 17 | []JsonMimeType, 18 | build.allocator, 19 | json, 20 | .{ .ignore_unknown_fields = true }, 21 | ); 22 | 23 | var buf = std.ArrayList(u8).init(build.allocator); 24 | defer buf.deinit(); 25 | 26 | const writer = buf.writer(); 27 | 28 | try writer.writeAll("pub const MimeType = struct { name: []const u8, file_type: []const u8 };"); 29 | try writer.writeAll("pub const mime_types = [_]MimeType{\n"); 30 | for (parsed_mime_types.value) |mime_type| { 31 | for (mime_type.fileTypes) |file_type| { 32 | const entry = try std.fmt.allocPrint( 33 | build.allocator, 34 | \\.{{ .name = "{s}", .file_type = "{s}" }}, 35 | \\ 36 | , 37 | .{ mime_type.name, file_type }, 38 | ); 39 | try writer.writeAll(entry); 40 | } 41 | } 42 | try writer.writeAll("};\n"); 43 | 44 | const write_files = build.addWriteFiles(); 45 | const generated_file = write_files.add("mime_types.zig", buf.items); 46 | return build.createModule(.{ .root_source_file = generated_file }); 47 | } 48 | -------------------------------------------------------------------------------- /src/assets/debug.css: -------------------------------------------------------------------------------- 1 | /* reset https://www.joshwcomeau.com/css/custom-css-reset/ */ 2 | *, *::before, *::after { 3 | box-sizing: border-box; 4 | } 5 | 6 | * { 7 | margin: 0; 8 | } 9 | 10 | body { 11 | line-height: 1.5; 12 | -webkit-font-smoothing: antialiased; 13 | } 14 | 15 | img, picture, video, canvas, svg { 16 | display: block; 17 | max-width: 100%; 18 | } 19 | 20 | input, button, textarea, select { 21 | font: inherit; 22 | } 23 | 24 | p, h1, h2, h3, h4, h5, h6 { 25 | overflow-wrap: break-word; 26 | } 27 | 28 | p { 29 | text-wrap: pretty; 30 | } 31 | 32 | h1, h2, h3, h4, h5, h6 { 33 | text-wrap: balance; 34 | } 35 | 36 | #root, #__next { 37 | isolation: isolate; 38 | } 39 | 40 | /* styles */ 41 | 42 | body { 43 | background-color: #222; 44 | } 45 | 46 | h1 { 47 | font-family: monospace; 48 | color: #e55; 49 | padding: 1rem; 50 | font-size: 1.6rem; 51 | } 52 | 53 | h2 { 54 | font-family: monospace; 55 | color: #a0a0a0; 56 | padding: 1rem; 57 | font-size: 1.6rem; 58 | } 59 | 60 | .stack-trace { 61 | /* background-color: #e555; */ 62 | padding: 1rem; 63 | font-family: monospace; 64 | } 65 | 66 | .stack-trace .stack-source-line .file-name { 67 | color: #90bfd7; 68 | display: block; 69 | padding: 0.4rem; 70 | font-weight: bold; 71 | } 72 | 73 | .stack-trace .stack-source-line { 74 | background-color: #333; 75 | padding: 1rem; 76 | margin: 0; 77 | border-bottom: 1px solid #ffa3; 78 | } 79 | 80 | .stack-trace .stack-source-line:last-child { 81 | border-bottom: none; 82 | } 83 | 84 | .stack-trace .stack-source-line .line-content { 85 | margin: 0; 86 | padding: 0; 87 | } 88 | 89 | .stack-trace .stack-source-line .line-content.surrounding { 90 | color: #ffa; 91 | } 92 | 93 | .stack-trace .stack-source-line .line-content.target { 94 | color: #faa; 95 | background-color: #e552; 96 | } 97 | 98 | .stack-trace .stack-source-line .line-content .line-number { 99 | display: inline; 100 | 101 | } 102 | 103 | pre { 104 | display: inline; 105 | margin-right: 1rem; 106 | } 107 | 108 | .response-data { 109 | color: #fff; 110 | background-color: #333; 111 | padding: 1rem; 112 | margin: 1rem; 113 | } 114 | 115 | /* PrismJS 1.29.0 116 | https://prismjs.com/download.html#themes=prism-tomorrow&languages=json+zig */ 117 | 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} 118 | 119 | .stack-trace .stack-source-line .line-number { 120 | color: #ffa !important; 121 | } 122 | -------------------------------------------------------------------------------- /src/cli.gitignore: -------------------------------------------------------------------------------- 1 | zig-out/ 2 | zig-cache/ 3 | -------------------------------------------------------------------------------- /src/commands/auth.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const build_options = @import("build_options"); 4 | 5 | const jetquery = @import("jetquery"); 6 | const jetzig = @import("jetzig"); 7 | const Schema = @import("Schema"); 8 | const Action = enum { @"user:create" }; 9 | 10 | pub fn main() !void { 11 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 12 | defer std.debug.assert(gpa.deinit() == .ok); 13 | 14 | const gpa_allocator = gpa.allocator(); 15 | var arena = std.heap.ArenaAllocator.init(gpa_allocator); 16 | defer arena.deinit(); 17 | 18 | const allocator = arena.allocator(); 19 | 20 | const args = try std.process.argsAlloc(allocator); 21 | 22 | if (args.len < 3) return error.JetzigMissingArgument; 23 | 24 | const map = std.StaticStringMap(Action).initComptime(.{ 25 | .{ "user:create", .@"user:create" }, 26 | }); 27 | 28 | const action = map.get(args[1]) orelse return error.JetzigUnrecognizedArgument; 29 | const env = try jetzig.Environment.init(allocator, .{ .silent = true }); 30 | 31 | switch (action) { 32 | .@"user:create" => { 33 | const Repo = jetzig.jetquery.Repo(jetzig.database.adapter, Schema); 34 | var repo = try Repo.loadConfig( 35 | allocator, 36 | @field(jetzig.jetquery.Environment, @tagName(jetzig.environment)), 37 | .{ .env = try jetzig.database.repoEnv(env), .context = .cli }, 38 | ); 39 | defer repo.deinit(); 40 | 41 | const model = comptime jetzig.config.get(jetzig.auth.AuthOptions, "auth").user_model; 42 | const stdin = std.io.getStdIn(); 43 | const reader = stdin.reader(); 44 | 45 | const password = if (stdin.isTty() and args.len < 4) blk: { 46 | std.debug.print("Enter password: ", .{}); 47 | var buf: [1024]u8 = undefined; 48 | if (try reader.readUntilDelimiterOrEof(&buf, '\n')) |input| { 49 | break :blk std.mem.trim(u8, input, &std.ascii.whitespace); 50 | } else { 51 | std.debug.print("Blank password. Exiting.\n", .{}); 52 | return; 53 | } 54 | } else if (args.len >= 4) 55 | args[3] 56 | else { 57 | std.debug.print("Blank password. Exiting.\n", .{}); 58 | return; 59 | }; 60 | 61 | const email = args[2]; 62 | 63 | try repo.insert(@field(std.meta.DeclEnum(Schema), model), .{ 64 | .email = email, 65 | .password_hash = try hashPassword(allocator, password), 66 | }); 67 | std.debug.print("Created user: `{s}`.\n", .{email}); 68 | }, 69 | } 70 | } 71 | 72 | fn hashPassword(allocator: std.mem.Allocator, password: []const u8) ![]const u8 { 73 | const buf = try allocator.alloc(u8, 128); 74 | return try std.crypto.pwhash.argon2.strHash( 75 | password, 76 | .{ 77 | .allocator = allocator, 78 | .params = .{ .t = 3, .m = 32, .p = 4 }, 79 | }, 80 | buf, 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /src/commands/routes.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const routes = @import("routes"); 3 | const app = @import("app"); 4 | const jetzig = @import("jetzig"); 5 | 6 | pub const jetzig_options = app.jetzig_options; 7 | 8 | pub fn main() !void { 9 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 10 | const allocator = gpa.allocator(); 11 | 12 | comptime var max_uri_path_len: usize = 0; 13 | 14 | log("Jetzig Routes:", .{}); 15 | 16 | const environment = try jetzig.Environment.init(allocator, .{ .silent = true }); 17 | const initHook: ?*const fn (*jetzig.App) anyerror!void = if (@hasDecl(app, "init")) app.init else null; 18 | 19 | inline for (routes.routes) |route| max_uri_path_len = @max(route.uri_path.len + 5, max_uri_path_len); 20 | const padded_path = std.fmt.comptimePrint("{{s: <{}}}", .{max_uri_path_len}); 21 | 22 | inline for (routes.routes) |route| { 23 | const action = comptime switch (route.action) { 24 | .get => jetzig.colors.cyan("{s: <7}"), 25 | .index => jetzig.colors.blue("{s: <7}"), 26 | .new => jetzig.colors.green("{s: <7}"), 27 | .edit => jetzig.colors.bold(.yellow, "{s: <7}"), 28 | .post => jetzig.colors.yellow("{s: <7}"), 29 | .put => jetzig.colors.magenta("{s: <7}"), 30 | .patch => jetzig.colors.bold(.magenta, "{s: <7}"), 31 | .delete => jetzig.colors.red("{s: <7}"), 32 | .custom => unreachable, 33 | }; 34 | 35 | log(" " ++ action ++ " " ++ padded_path ++ " {?s}", .{ 36 | @tagName(route.action), 37 | route.uri_path ++ switch (route.action) { 38 | .index, .post => "", 39 | .new => "/new", 40 | .edit => "/:id/edit", 41 | .get, .put, .patch, .delete => "/:id", 42 | .custom => "", 43 | }, 44 | route.path, 45 | }); 46 | } 47 | 48 | var jetzig_app = jetzig.App{ 49 | .env = environment, 50 | .allocator = allocator, 51 | .custom_routes = std.ArrayList(jetzig.views.Route).init(allocator), 52 | .initHook = initHook, 53 | }; 54 | 55 | if (initHook) |hook| try hook(&jetzig_app); 56 | 57 | for (jetzig_app.custom_routes.items) |route| { 58 | log( 59 | " " ++ jetzig.colors.bold(.white, "{s: <7}") ++ " " ++ padded_path ++ " {s}:{s}", 60 | .{ route.name, route.uri_path, route.view_name, route.name }, 61 | ); 62 | } 63 | } 64 | 65 | fn log(comptime message: []const u8, args: anytype) void { 66 | std.debug.print(message ++ "\n", args); 67 | } 68 | 69 | fn sortedRoutes(comptime unordered_routes: []const jetzig.views.Route) void { 70 | comptime std.sort.pdq(jetzig.views.Route, unordered_routes, {}, lessThanFn); 71 | } 72 | pub fn lessThanFn(context: void, lhs: jetzig.views.Route, rhs: jetzig.views.Route) bool { 73 | _ = context; 74 | return std.mem.order(u8, lhs.uri_path, rhs.uri_path).compare(std.math.CompareOperator.lt); 75 | } 76 | -------------------------------------------------------------------------------- /src/commands/util.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const colors = @import("jetzig").colors; 3 | 4 | const icons = .{ 5 | .check = "✅", 6 | .cross = "❌", 7 | }; 8 | 9 | /// Print a success confirmation. 10 | pub fn printSuccess() void { 11 | std.debug.print(" " ++ icons.check ++ "\n", .{}); 12 | } 13 | 14 | /// Print a failure confirmation. 15 | pub fn printFailure() void { 16 | std.debug.print(" " ++ icons.cross ++ "\n", .{}); 17 | } 18 | 19 | const PrintContext = enum { success, failure }; 20 | /// Print some output in with a given context to stderr. 21 | pub fn print(comptime context: PrintContext, comptime message: []const u8, args: anytype) !void { 22 | const writer = std.io.getStdErr().writer(); 23 | switch (context) { 24 | .success => try writer.print( 25 | std.fmt.comptimePrint("{s} {s}\n", .{ icons.check, colors.green(message) }), 26 | args, 27 | ), 28 | .failure => try writer.print( 29 | std.fmt.comptimePrint("{s} {s}\n", .{ icons.cross, colors.red(message) }), 30 | args, 31 | ), 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/jetzig.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const zmpl = @import("zmpl").zmpl; 4 | pub const zmd = @import("zmd").zmd; 5 | pub const jetkv = @import("jetkv").jetkv; 6 | pub const jetquery = @import("jetquery"); 7 | pub const jetcommon = @import("jetcommon"); 8 | 9 | pub const http = @import("jetzig/http.zig"); 10 | pub const loggers = @import("jetzig/loggers.zig"); 11 | pub const data = @import("jetzig/data.zig"); 12 | pub const views = @import("jetzig/views.zig"); 13 | pub const colors = @import("jetzig/colors.zig"); 14 | pub const middleware = @import("jetzig/middleware.zig"); 15 | pub const util = @import("jetzig/util.zig"); 16 | pub const types = @import("jetzig/types.zig"); 17 | pub const markdown = @import("jetzig/markdown.zig"); 18 | pub const jobs = @import("jetzig/jobs.zig"); 19 | pub const mail = @import("jetzig/mail.zig"); 20 | pub const kv = @import("jetzig/kv.zig"); 21 | pub const database = @import("jetzig/database.zig"); 22 | pub const testing = @import("jetzig/testing.zig"); 23 | pub const config = @import("jetzig/config.zig"); 24 | pub const auth = @import("jetzig/auth.zig"); 25 | pub const callbacks = @import("jetzig/callbacks.zig"); 26 | pub const debug = @import("jetzig/debug.zig"); 27 | pub const TemplateContext = @import("jetzig/TemplateContext.zig"); 28 | 29 | pub const DateTime = jetcommon.types.DateTime; 30 | pub const Time = jetcommon.types.Time; 31 | pub const Date = jetcommon.types.Date; 32 | 33 | pub const authenticity_token_name = config.get([]const u8, "authenticity_token_name"); 34 | 35 | pub const build_options = @import("build_options"); 36 | pub const environment = @field( 37 | Environment.EnvironmentName, 38 | @tagName(build_options.environment), 39 | ); 40 | 41 | /// The primary interface for a Jetzig application. Create an `App` in your application's 42 | /// `src/main.zig` and call `start` to launch the application. 43 | pub const App = @import("jetzig/App.zig"); 44 | 45 | /// Configuration options for the application server with command-line argument parsing. 46 | pub const Environment = @import("jetzig/Environment.zig"); 47 | 48 | /// An HTTP request which is passed to (dynamic) view functions and provides access to params, 49 | /// headers, and functions to render a response. 50 | pub const Request = http.Request; 51 | 52 | /// A build-time request. Provides a similar interface to a `Request` but outputs are generated 53 | /// when building the application and then returned immediately to the client for matching 54 | /// requests. 55 | pub const StaticRequest = http.StaticRequest; 56 | 57 | /// An HTTP response generated during request processing. 58 | pub const Response = http.Response; 59 | 60 | /// Generic, JSON-compatible data type. Provides `Value` which in turn provides `Object`, 61 | /// `Array`, `String`, `Integer`, `Float`, `Boolean`, and `NullType`. 62 | pub const Data = data.Data; 63 | 64 | /// The return value of all view functions. Call `request.render(.ok)` in a view function to 65 | /// generate a `View`. 66 | pub const View = views.View; 67 | 68 | /// A route definition. Generated at build type by `Routes.zig`. 69 | pub const Route = views.Route; 70 | 71 | /// A middleware route definition. Allows middleware to define custom routes in order to serve 72 | /// content. 73 | pub const MiddlewareRoute = middleware.MiddlewareRoute; 74 | 75 | /// An asynchronous job that runs outside of the request/response flow. Create via `Request.job` 76 | /// and set params with `Job.put`, then call `Job.schedule()` to add to the 77 | /// job queue. 78 | pub const Job = jobs.Job; 79 | 80 | /// A container for a job definition, includes the job name and run function. 81 | pub const JobDefinition = jobs.Job.JobDefinition; 82 | 83 | /// A container for a mailer definition, includes mailer name and mail function. 84 | pub const MailerDefinition = mail.MailerDefinition; 85 | 86 | /// A generic logger type. Provides all standard log levels as functions (`INFO`, `WARN`, 87 | /// `ERROR`, etc.). Note that all log functions are CAPITALIZED. 88 | pub const Logger = loggers.Logger; 89 | 90 | pub const root = @import("root"); 91 | pub const Global = if (@hasDecl(root, "Global")) root.Global else DefaultGlobal; 92 | pub const DefaultGlobal = struct { comptime __jetzig_default: bool = true }; 93 | pub const default_global = DefaultGlobal{}; 94 | 95 | pub const initHook: ?*const fn (*App) anyerror!void = if (@hasDecl(root, "init")) root.init else null; 96 | 97 | /// Initialize a new Jetzig app. Call this from `src/main.zig` and then call 98 | /// `start(@import("routes").routes)` on the returned value. 99 | pub fn init(allocator: std.mem.Allocator) !App { 100 | const env = try Environment.init(allocator, .{}); 101 | 102 | return .{ 103 | .env = env, 104 | .allocator = allocator, 105 | .custom_routes = std.ArrayList(views.Route).init(allocator), 106 | .initHook = initHook, 107 | }; 108 | } 109 | -------------------------------------------------------------------------------- /src/jetzig/DefaultSchema.zig: -------------------------------------------------------------------------------- 1 | // An empty schema imported when no `src/app/database/Schema.zig` is present. 2 | -------------------------------------------------------------------------------- /src/jetzig/TemplateContext.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const http = @import("http.zig"); 4 | pub const config = @import("config.zig"); 5 | 6 | /// Context available in every Zmpl template as `context`. 7 | pub const TemplateContext = @This(); 8 | 9 | request: ?*http.Request = null, 10 | 11 | pub fn authenticityToken(self: TemplateContext) !?[]const u8 { 12 | return if (self.request) |request| 13 | try request.authenticityToken() 14 | else 15 | null; 16 | } 17 | 18 | pub fn authenticityFormElement(self: TemplateContext) !?[]const u8 { 19 | return if (self.request) |request| blk: { 20 | const token = try request.authenticityToken(); 21 | break :blk try std.fmt.allocPrint(request.allocator, 22 | \\ 23 | , .{ config.get([]const u8, "authenticity_token_name"), token }); 24 | } else null; 25 | } 26 | -------------------------------------------------------------------------------- /src/jetzig/auth.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const jetzig = @import("../jetzig.zig"); 4 | 5 | pub const IdType = enum { string, integer }; 6 | 7 | pub const AuthOptions = struct { 8 | user_model: []const u8, 9 | }; 10 | 11 | pub fn getUserId(comptime id_type: IdType, request: *jetzig.Request) !?switch (id_type) { 12 | .integer => i128, 13 | .string => []const u8, 14 | } { 15 | const session = try request.session(); 16 | 17 | return session.getT(@field(jetzig.data.ValueType, @tagName(id_type)), "_jetzig_user_id"); 18 | } 19 | 20 | pub fn signIn(request: *jetzig.Request, user_id: anytype) !void { 21 | var session = try request.session(); 22 | try session.put("_jetzig_user_id", user_id); 23 | } 24 | 25 | pub fn signOut(request: *jetzig.Request) !void { 26 | var session = try request.session(); 27 | _ = try session.remove("_jetzig_user_id"); 28 | } 29 | 30 | pub fn verifyPassword( 31 | allocator: std.mem.Allocator, 32 | hash: []const u8, 33 | password: []const u8, 34 | ) !bool { 35 | const verify_error = std.crypto.pwhash.argon2.strVerify( 36 | hash, 37 | password, 38 | .{ .allocator = allocator }, 39 | ); 40 | 41 | return if (verify_error) 42 | true 43 | else |err| switch (err) { 44 | error.AuthenticationFailed, error.PasswordVerificationFailed => false, 45 | else => err, 46 | }; 47 | } 48 | 49 | pub fn hashPassword(allocator: std.mem.Allocator, password: []const u8) ![]const u8 { 50 | var buf: [128]u8 = undefined; 51 | const hash = try std.crypto.pwhash.argon2.strHash( 52 | password, 53 | .{ 54 | .allocator = allocator, 55 | .params = .{ .t = 3, .m = 32, .p = 4 }, 56 | }, 57 | &buf, 58 | ); 59 | const result = try allocator.alloc(u8, hash.len); 60 | @memcpy(result, hash); 61 | return result; 62 | } 63 | -------------------------------------------------------------------------------- /src/jetzig/callbacks.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const jetzig = @import("../jetzig.zig"); 4 | 5 | pub const BeforeCallback = *const fn ( 6 | *jetzig.http.Request, 7 | jetzig.views.Route, 8 | ) anyerror!void; 9 | 10 | pub const AfterCallback = *const fn ( 11 | *jetzig.http.Request, 12 | *jetzig.http.Response, 13 | jetzig.views.Route, 14 | ) anyerror!void; 15 | 16 | pub const Context = enum { before, after }; 17 | 18 | pub fn beforeCallbacks(view: type) []const BeforeCallback { 19 | comptime { 20 | return buildCallbacks(.before, view); 21 | } 22 | } 23 | 24 | pub fn afterCallbacks(view: type) []const AfterCallback { 25 | comptime { 26 | return buildCallbacks(.after, view); 27 | } 28 | } 29 | 30 | fn buildCallbacks(comptime context: Context, view: type) switch (context) { 31 | .before => []const BeforeCallback, 32 | .after => []const AfterCallback, 33 | } { 34 | comptime { 35 | if (!@hasDecl(view, "actions")) return &.{}; 36 | if (!@hasField(@TypeOf(view.actions), @tagName(context))) return &.{}; 37 | 38 | var size: usize = 0; 39 | for (@field(view.actions, @tagName(context))) |module| { 40 | if (isCallback(context, module)) { 41 | size += 1; 42 | } else { 43 | @compileError(std.fmt.comptimePrint( 44 | "`{0s}` callbacks must be either a function `{1s}` or a type that defines " ++ 45 | "`pub const {0s}Render`. Found: `{2s}`", 46 | .{ 47 | @tagName(context), 48 | switch (context) { 49 | .before => @typeName(BeforeCallback), 50 | .after => @typeName(AfterCallback), 51 | }, 52 | if (@TypeOf(module) == type) 53 | @typeName(module) 54 | else 55 | @typeName(@TypeOf(&module)), 56 | }, 57 | )); 58 | } 59 | } 60 | 61 | var callbacks: [size]switch (context) { 62 | .before => BeforeCallback, 63 | .after => AfterCallback, 64 | } = undefined; 65 | var index: usize = 0; 66 | for (@field(view.actions, @tagName(context))) |module| { 67 | if (!isCallback(context, module)) continue; 68 | 69 | callbacks[index] = if (@TypeOf(module) == type) 70 | @field(module, @tagName(context) ++ "Render") 71 | else 72 | &module; 73 | 74 | index += 1; 75 | } 76 | 77 | const final = callbacks; 78 | return &final; 79 | } 80 | } 81 | 82 | fn isCallback(comptime context: Context, comptime module: anytype) bool { 83 | comptime { 84 | if (@typeInfo(@TypeOf(module)) == .@"fn") { 85 | const expected = switch (context) { 86 | .before => BeforeCallback, 87 | .after => AfterCallback, 88 | }; 89 | 90 | const info = @typeInfo(@TypeOf(module)).@"fn"; 91 | 92 | const actual_params = info.params; 93 | const expected_params = @typeInfo(@typeInfo(expected).pointer.child).@"fn".params; 94 | 95 | if (actual_params.len != expected_params.len) return false; 96 | 97 | for (actual_params, expected_params) |actual_param, expected_param| { 98 | if (actual_param.type != expected_param.type) return false; 99 | } 100 | 101 | if (@typeInfo(info.return_type.?) != .error_union) return false; 102 | if (@typeInfo(info.return_type.?).error_union.payload != void) return false; 103 | 104 | return true; 105 | } 106 | 107 | return if (@TypeOf(module) == type and @hasDecl(module, @tagName(context) ++ "Render")) 108 | true 109 | else 110 | false; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/jetzig/data.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const zmpl = @import("zmpl").zmpl; 4 | 5 | pub const Writer = zmpl.Data.Writer; 6 | pub const Data = zmpl.Data; 7 | pub const Value = zmpl.Data.Value; 8 | pub const NullType = zmpl.Data.NullType; 9 | pub const Float = zmpl.Data.Float; 10 | pub const Integer = zmpl.Data.Integer; 11 | pub const Boolean = zmpl.Data.Boolean; 12 | pub const String = zmpl.Data.String; 13 | pub const Object = zmpl.Data.Object; 14 | pub const Array = zmpl.Data.Array; 15 | pub const ValueType = zmpl.Data.ValueType; 16 | -------------------------------------------------------------------------------- /src/jetzig/database.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const jetzig = @import("../jetzig.zig"); 4 | 5 | pub const adapter = @field( 6 | jetzig.jetquery.adapters.Name, 7 | @tagName(@field(jetzig.jetquery.config.database, @tagName(jetzig.environment)).adapter), 8 | ); 9 | 10 | pub const Schema = jetzig.config.get(type, "Schema"); 11 | pub const Repo = jetzig.jetquery.Repo(adapter, Schema); 12 | 13 | pub fn Query(comptime model: anytype) type { 14 | return jetzig.jetquery.Query(adapter, Schema, model); 15 | } 16 | 17 | pub fn repo(allocator: std.mem.Allocator, app: anytype) !Repo { 18 | const Callback = struct { 19 | var jetzig_app: @TypeOf(app) = undefined; 20 | pub fn callbackFn(event: jetzig.jetquery.events.Event) !void { 21 | try eventCallback(event, jetzig_app); 22 | } 23 | }; 24 | Callback.jetzig_app = app; 25 | 26 | return try Repo.loadConfig( 27 | allocator, 28 | @field(jetzig.jetquery.Environment, @tagName(jetzig.environment)), 29 | .{ 30 | .eventCallback = Callback.callbackFn, 31 | .lazy_connect = switch (jetzig.environment) { 32 | .development, .production => true, 33 | .testing => false, 34 | }, 35 | // Checking field presence here makes setting up test App a bit simpler. 36 | .env = if (@TypeOf(app) == *const jetzig.App) try repoEnv(app.env) else .{}, 37 | }, 38 | ); 39 | } 40 | 41 | fn eventCallback(event: jetzig.jetquery.events.Event, app: anytype) !void { 42 | try app.server.logger.logSql(event); 43 | if (event.err) |err| { 44 | try app.server.logger.ERROR("[database] {?s}", .{err.message}); 45 | } 46 | } 47 | 48 | pub fn repoEnv(env: jetzig.Environment) !Repo.AdapterOptions { 49 | return switch (comptime adapter) { 50 | .null => .{}, 51 | .postgresql => .{ 52 | .hostname = env.vars.get("JETQUERY_HOSTNAME"), 53 | .port = try env.vars.getT(u16, "JETQUERY_PORT"), 54 | .username = env.vars.get("JETQUERY_USERNAME"), 55 | .password = env.vars.get("JETQUERY_PASSWORD"), 56 | .database = env.vars.get("JETQUERY_DATABASE"), 57 | .pool_size = try env.vars.getT(u16, "JETQUERY_POOL_SIZE"), 58 | .timeout = try env.vars.getT(u32, "JETQUERY_TIMEOUT"), 59 | }, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /src/jetzig/development_static.zig: -------------------------------------------------------------------------------- 1 | pub const compiled = [_]Compiled{}; 2 | 3 | const StaticOutput = struct { 4 | json: ?[]const u8 = null, 5 | html: ?[]const u8 = null, 6 | params: ?[]const u8, 7 | }; 8 | 9 | const Compiled = struct { 10 | route_id: []const u8, 11 | output: StaticOutput, 12 | }; 13 | -------------------------------------------------------------------------------- /src/jetzig/http.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | pub const build_options = @import("build_options"); 5 | 6 | pub const Server = @import("http/Server.zig"); 7 | pub const Request = @import("http/Request.zig"); 8 | pub const StaticRequest = if (build_options.environment == .development) 9 | Request 10 | else 11 | @import("http/StaticRequest.zig"); 12 | pub const Response = @import("http/Response.zig"); 13 | pub const Session = @import("http/Session.zig"); 14 | pub const Cookies = @import("http/Cookies.zig"); 15 | pub const Headers = @import("http/Headers.zig"); 16 | pub const Query = @import("http/Query.zig"); 17 | pub const MultipartQuery = @import("http/MultipartQuery.zig"); 18 | pub const File = @import("http/File.zig"); 19 | pub const Path = @import("http/Path.zig"); 20 | pub const status_codes = @import("http/status_codes.zig"); 21 | pub const StatusCode = status_codes.StatusCode; 22 | pub const middleware = @import("http/middleware.zig"); 23 | pub const mime = @import("http/mime.zig"); 24 | pub const params = @import("http/params.zig"); 25 | 26 | pub const SimplifiedRequest = struct { 27 | location: ?[]const u8, 28 | }; 29 | 30 | pub const default_content_type = "application/octet-stream"; 31 | -------------------------------------------------------------------------------- /src/jetzig/http/File.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | filename: []const u8, 4 | content: []const u8, 5 | -------------------------------------------------------------------------------- /src/jetzig/http/MultipartQuery.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const httpz = @import("httpz"); 4 | 5 | const jetzig = @import("../../jetzig.zig"); 6 | 7 | allocator: std.mem.Allocator, 8 | key_value: *httpz.key_value.MultiFormKeyValue, 9 | 10 | const MultipartQuery = @This(); 11 | 12 | /// Fetch a file from multipart form data, if present. 13 | pub fn getFile(self: MultipartQuery, key: []const u8) ?jetzig.http.File { 14 | const keys = self.key_value.keys; 15 | const values = self.key_value.values; 16 | 17 | for (keys[0..self.key_value.len], values[0..self.key_value.len]) |name, field| { 18 | const filename = field.filename orelse continue; 19 | 20 | if (std.mem.eql(u8, name, key)) return jetzig.http.File{ 21 | .filename = filename, 22 | .content = field.value, 23 | }; 24 | } 25 | 26 | return null; 27 | } 28 | 29 | /// Return all params in a multipart form submission **excluding** files. Use 30 | /// `jetzig.http.Request.getFile` to read a file object (includes filename and data). 31 | pub fn params(self: MultipartQuery) !*jetzig.data.Data { 32 | const data = try self.allocator.create(jetzig.data.Data); 33 | data.* = jetzig.data.Data.init(self.allocator); 34 | var root = try data.root(.object); 35 | 36 | const keys = self.key_value.keys; 37 | const values = self.key_value.values; 38 | 39 | for (keys[0..self.key_value.len], values[0..self.key_value.len]) |name, field| { 40 | if (field.filename != null) continue; 41 | 42 | try root.put(name, field.value); 43 | } 44 | 45 | return data; 46 | } 47 | -------------------------------------------------------------------------------- /src/jetzig/http/Response.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const httpz = @import("httpz"); 4 | 5 | const jetzig = @import("../../jetzig.zig"); 6 | const http = @import("../http.zig"); 7 | 8 | const Self = @This(); 9 | 10 | allocator: std.mem.Allocator, 11 | headers: jetzig.http.Headers, 12 | content: []const u8, 13 | status_code: http.status_codes.StatusCode, 14 | content_type: ?[]const u8 = null, 15 | httpz_response: *httpz.Response, 16 | 17 | pub fn init( 18 | allocator: std.mem.Allocator, 19 | httpz_response: *httpz.Response, 20 | ) !Self { 21 | return .{ 22 | .allocator = allocator, 23 | .httpz_response = httpz_response, 24 | .status_code = .no_content, 25 | .content = "", 26 | .headers = jetzig.http.Headers.init(allocator, &httpz_response.headers), 27 | }; 28 | } 29 | 30 | pub inline fn contentType(self: *const jetzig.http.Response) []const u8 { 31 | return self.content_type orelse jetzig.http.default_content_type; 32 | } 33 | -------------------------------------------------------------------------------- /src/jetzig/http/StaticRequest.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const StaticRequest = @This(); 3 | const jetzig = @import("../../jetzig.zig"); 4 | 5 | response_data: *jetzig.data.Data, 6 | allocator: std.mem.Allocator, 7 | json: []const u8, 8 | 9 | pub fn init(allocator: std.mem.Allocator, json: []const u8) !StaticRequest { 10 | return .{ 11 | .allocator = allocator, 12 | .response_data = try allocator.create(jetzig.data.Data), 13 | .json = json, 14 | }; 15 | } 16 | 17 | pub fn deinit(self: *StaticRequest) void { 18 | _ = self; 19 | } 20 | 21 | pub fn render(self: *StaticRequest, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View { 22 | return .{ .data = self.response_data, .status_code = status_code }; 23 | } 24 | 25 | pub fn data(self: *StaticRequest, comptime root: @TypeOf(.enum_literal)) !*jetzig.Data.Value { 26 | return try self.response_data.root(root); 27 | } 28 | 29 | pub fn resourceId(self: *StaticRequest) ![]const u8 { 30 | var params_data = try self.allocator.create(jetzig.data.Data); 31 | params_data.* = jetzig.data.Data.init(self.allocator); 32 | defer self.allocator.destroy(params_data); 33 | defer params_data.deinit(); 34 | 35 | try params_data.fromJson(self.json); 36 | // Routes generator rejects missing `.id` option so this should always be present. 37 | // Note that static requests are never rendered at runtime so we can be unsafe here and risk 38 | // failing a build (which would not be coherent if we allowed it to complete). 39 | return try self.allocator.dupe(u8, params_data.value.?.get("id").?.string.value); 40 | } 41 | 42 | /// Returns the static params defined by `pub const static_params` in the relevant view. 43 | pub fn params(self: *StaticRequest) !*jetzig.data.Value { 44 | var params_data = try self.allocator.create(jetzig.data.Data); 45 | params_data.* = jetzig.data.Data.init(self.allocator); 46 | try params_data.fromJson(self.json); 47 | return params_data.value.?.get("params") orelse params_data.object(); 48 | } 49 | -------------------------------------------------------------------------------- /src/jetzig/http/StatusCode.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const jetzig = @import("../jetzig.zig"); 4 | 5 | pub fn StatusCodeType(comptime code: []const u8, comptime message: []const u8) type { 6 | return struct { 7 | code: []const u8 = code, 8 | message: []const u8 = message, 9 | 10 | const Self = @This(); 11 | 12 | pub fn format(self: Self) []const u8 { 13 | _ = self; 14 | 15 | const full_message = code ++ " " ++ message; 16 | 17 | if (std.mem.startsWith(u8, code, "2")) { 18 | return jetzig.colors.green(full_message); 19 | } else if (std.mem.startsWith(u8, code, "3")) { 20 | return jetzig.colors.blue(full_message); 21 | } else if (std.mem.startsWith(u8, code, "4")) { 22 | return jetzig.colors.yellow(full_message); 23 | } else if (std.mem.startsWith(u8, code, "5")) { 24 | return jetzig.colors.red(full_message); 25 | } else { 26 | return full_message; 27 | } 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/jetzig/http/mime.zig: -------------------------------------------------------------------------------- 1 | // Mime types borrowed from here: 2 | // https://mimetype.io/all-types 3 | // https://github.com/patrickmccallum/mimetype-io/blob/master/src/mimeData.json 4 | 5 | const std = @import("std"); 6 | 7 | const mime_types = @import("mime_types").mime_types; // Generated at build time. 8 | 9 | /// Provides information about a given MIME Type. 10 | pub const MimeType = struct { 11 | name: []const u8, 12 | }; 13 | 14 | /// Attempts to map a given extension to a mime type. 15 | pub fn fromExtension(extension: []const u8) ?MimeType { 16 | for (mime_types) |mime_type| { 17 | if (std.mem.eql(u8, extension, mime_type.file_type)) return .{ .name = mime_type.name }; 18 | } 19 | return null; 20 | } 21 | 22 | pub const MimeMap = struct { 23 | allocator: std.mem.Allocator, 24 | map: std.StringHashMap([]const u8), 25 | 26 | pub fn init(allocator: std.mem.Allocator) MimeMap { 27 | return .{ 28 | .allocator = allocator, 29 | .map = std.StringHashMap([]const u8).init(allocator), 30 | }; 31 | } 32 | 33 | pub fn deinit(self: *MimeMap) void { 34 | self.map.deinit(); 35 | } 36 | 37 | pub fn build(self: *MimeMap) !void { 38 | for (mime_types) |mime_type| { 39 | try self.map.put( 40 | mime_type.file_type, 41 | mime_type.name, 42 | ); 43 | } 44 | } 45 | 46 | pub fn get(self: *MimeMap, file_type: []const u8) ?[]const u8 { 47 | return self.map.get(file_type); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/jetzig/jobs.zig: -------------------------------------------------------------------------------- 1 | pub const Job = @import("jobs/Job.zig"); 2 | pub const JobEnv = Job.JobEnv; 3 | pub const JobDefinition = Job.JobDefinition; 4 | pub const Pool = @import("jobs/Pool.zig"); 5 | pub const Worker = @import("jobs/Worker.zig"); 6 | -------------------------------------------------------------------------------- /src/jetzig/jobs/Job.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("../../jetzig.zig"); 3 | 4 | /// Job name and run function, used when generating an array of job definitions at build time. 5 | pub const JobDefinition = struct { 6 | name: []const u8, 7 | runFn: *const fn (std.mem.Allocator, *jetzig.data.Value, JobEnv) anyerror!void, 8 | }; 9 | 10 | /// Environment passed to all jobs. 11 | pub const JobEnv = struct { 12 | /// The Jetzig server logger 13 | logger: jetzig.loggers.Logger, 14 | /// The current server environment, `enum { development, production }` 15 | environment: jetzig.Environment.EnvironmentName, 16 | /// Environment configured at server launch 17 | vars: jetzig.Environment.Vars, 18 | /// All routes detected by Jetzig on startup 19 | routes: []const *const jetzig.Route, 20 | /// All mailers detected by Jetzig on startup 21 | mailers: []const jetzig.MailerDefinition, 22 | /// All jobs detected by Jetzig on startup 23 | jobs: []const jetzig.JobDefinition, 24 | /// Global key-value store 25 | store: *jetzig.kv.Store.GeneralStore, 26 | /// Global cache 27 | cache: *jetzig.kv.Store.CacheStore, 28 | /// Database repo 29 | repo: *jetzig.database.Repo, 30 | /// Global mutex - use with caution if it is necessary to guarantee thread safety/consistency 31 | /// between concurrent job workers 32 | mutex: *std.Thread.Mutex, 33 | }; 34 | 35 | allocator: std.mem.Allocator, 36 | store: *jetzig.kv.Store.GeneralStore, 37 | job_queue: *jetzig.kv.Store.JobQueueStore, 38 | cache: *jetzig.kv.Store.CacheStore, 39 | logger: jetzig.loggers.Logger, 40 | name: []const u8, 41 | definition: ?JobDefinition, 42 | data: *jetzig.data.Data, 43 | params: *jetzig.data.Value, 44 | 45 | const Job = @This(); 46 | 47 | /// Initialize a new Job 48 | pub fn init( 49 | allocator: std.mem.Allocator, 50 | store: *jetzig.kv.Store.GeneralStore, 51 | job_queue: *jetzig.kv.Store.JobQueueStore, 52 | cache: *jetzig.kv.Store.CacheStore, 53 | logger: jetzig.loggers.Logger, 54 | jobs: []const JobDefinition, 55 | name: []const u8, 56 | ) Job { 57 | var definition: ?JobDefinition = null; 58 | 59 | for (jobs) |job_definition| { 60 | if (std.mem.eql(u8, job_definition.name, name)) { 61 | definition = job_definition; 62 | break; 63 | } 64 | } 65 | 66 | const data = allocator.create(jetzig.data.Data) catch @panic("OOM"); 67 | data.* = jetzig.data.Data.init(allocator); 68 | 69 | return .{ 70 | .allocator = allocator, 71 | .store = store, 72 | .job_queue = job_queue, 73 | .cache = cache, 74 | .logger = logger, 75 | .name = name, 76 | .definition = definition, 77 | .data = data, 78 | .params = data.object() catch @panic("OOM"), 79 | }; 80 | } 81 | 82 | /// Deinitialize the Job and frees memory 83 | pub fn deinit(self: *Job) void { 84 | self.data.deinit(); 85 | self.allocator.destroy(self.data); 86 | } 87 | 88 | /// Add a Job to the queue 89 | pub fn schedule(self: *Job) !void { 90 | try self.params.put("__jetzig_job_name", self.data.string(self.name)); 91 | try self.job_queue.append("__jetzig_jobs", self.data.value.?); 92 | try self.logger.INFO("Scheduled job: {s}", .{self.name}); 93 | } 94 | -------------------------------------------------------------------------------- /src/jetzig/jobs/Pool.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const jetzig = @import("../../jetzig.zig"); 4 | 5 | const Pool = @This(); 6 | 7 | allocator: std.mem.Allocator, 8 | job_queue: *jetzig.kv.Store.JobQueueStore, 9 | job_env: jetzig.jobs.JobEnv, 10 | pool: std.Thread.Pool = undefined, 11 | workers: std.ArrayList(*jetzig.jobs.Worker), 12 | 13 | /// Initialize a new worker thread pool. 14 | pub fn init( 15 | allocator: std.mem.Allocator, 16 | job_queue: *jetzig.kv.Store.JobQueueStore, 17 | job_env: jetzig.jobs.JobEnv, 18 | ) Pool { 19 | return .{ 20 | .allocator = allocator, 21 | .job_queue = job_queue, 22 | .job_env = job_env, 23 | .workers = std.ArrayList(*jetzig.jobs.Worker).init(allocator), 24 | }; 25 | } 26 | 27 | /// Free pool resources and destroy workers. 28 | pub fn deinit(self: *Pool) void { 29 | self.pool.deinit(); 30 | for (self.workers.items) |worker| self.allocator.destroy(worker); 31 | self.workers.deinit(); 32 | } 33 | 34 | /// Spawn a given number of threads and start processing jobs, sleep for a given interval (ms) 35 | /// when no jobs are in the queue. Each worker operates its own work loop. 36 | pub fn work(self: *Pool, threads: usize, interval: usize) !void { 37 | try self.pool.init(.{ .allocator = self.allocator }); 38 | 39 | for (0..threads) |index| { 40 | const worker = try self.allocator.create(jetzig.jobs.Worker); 41 | worker.* = jetzig.jobs.Worker.init( 42 | self.allocator, 43 | self.job_env, 44 | index, 45 | self.job_queue, 46 | interval, 47 | ); 48 | try self.workers.append(worker); 49 | try self.pool.spawn(jetzig.jobs.Worker.work, .{worker}); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/jetzig/jobs/Worker.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const jetzig = @import("../../jetzig.zig"); 4 | const Worker = @This(); 5 | 6 | allocator: std.mem.Allocator, 7 | job_env: jetzig.jobs.JobEnv, 8 | id: usize, 9 | job_queue: *jetzig.kv.Store.JobQueueStore, 10 | interval: usize, 11 | 12 | pub fn init( 13 | allocator: std.mem.Allocator, 14 | job_env: jetzig.jobs.JobEnv, 15 | id: usize, 16 | job_queue: *jetzig.kv.Store.JobQueueStore, 17 | interval: usize, 18 | ) Worker { 19 | return .{ 20 | .allocator = allocator, 21 | .job_env = job_env, 22 | .id = id, 23 | .job_queue = job_queue, 24 | .interval = interval * 1000 * 1000, // millisecond => nanosecond 25 | }; 26 | } 27 | 28 | /// Begin working through jobs in the queue. 29 | pub fn work(self: *const Worker) void { 30 | self.log(.INFO, "[worker-{}] Job worker started.", .{self.id}); 31 | 32 | while (true) { 33 | var data = jetzig.data.Data.init(self.allocator); 34 | defer data.deinit(); 35 | const maybe_value = self.job_queue.popFirst(&data, "__jetzig_jobs") catch |err| blk: { 36 | self.log(.ERROR, "Error fetching job from queue: {s}", .{@errorName(err)}); 37 | break :blk null; // FIXME: Probably close thread here ? 38 | }; 39 | 40 | if (maybe_value) |value| { 41 | if (self.matchJob(value)) |job_definition| { 42 | self.processJob(job_definition, value); 43 | } 44 | } else { 45 | std.time.sleep(self.interval); 46 | } 47 | } 48 | 49 | self.log(.INFO, "[worker-{}] Job worker exited.", .{self.id}); 50 | } 51 | 52 | // Do a minimal parse of JSON job data to identify job name, then match on known job definitions. 53 | fn matchJob(self: Worker, value: *const jetzig.data.Value) ?jetzig.jobs.JobDefinition { 54 | const job_name = value.getT(.string, "__jetzig_job_name") orelse { 55 | self.log( 56 | .ERROR, 57 | "[worker-{}] Missing expected job name field `__jetzig_job_name`", 58 | .{self.id}, 59 | ); 60 | return null; 61 | }; 62 | 63 | // TODO: Hashmap 64 | for (self.job_env.jobs) |job_definition| { 65 | if (std.mem.eql(u8, job_definition.name, job_name)) { 66 | return job_definition; 67 | } 68 | } else { 69 | self.log(.WARN, "[worker-{}] Tried to process unknown job: {s}", .{ self.id, job_name }); 70 | return null; 71 | } 72 | } 73 | 74 | // Fully parse JSON job data and invoke the defined job's run function, passing the parsed params 75 | // as a `*jetzig.data.Value`. 76 | fn processJob(self: Worker, job_definition: jetzig.JobDefinition, params: *jetzig.data.Value) void { 77 | var arena = std.heap.ArenaAllocator.init(self.allocator); 78 | defer arena.deinit(); 79 | 80 | job_definition.runFn(arena.allocator(), params, self.job_env) catch |err| { 81 | self.log( 82 | .ERROR, 83 | "[worker-{}] Encountered error processing job `{s}`: {s}", 84 | .{ self.id, job_definition.name, @errorName(err) }, 85 | ); 86 | return; 87 | }; 88 | self.log(.INFO, "[worker-{}] Job completed: {s}", .{ self.id, job_definition.name }); 89 | } 90 | 91 | // Log with error handling and fallback. Prefix with worker ID. 92 | fn log( 93 | self: Worker, 94 | comptime level: jetzig.loggers.LogLevel, 95 | comptime message: []const u8, 96 | args: anytype, 97 | ) void { 98 | self.job_env.logger.log(level, message, args) catch |err| { 99 | // XXX: In (daemonized) deployment stderr will not be available, find a better solution. 100 | // Note that this only occurs if logging itself fails. 101 | std.debug.print("[worker-{}] Logger encountered error: {s}\n", .{ self.id, @errorName(err) }); 102 | }; 103 | } 104 | -------------------------------------------------------------------------------- /src/jetzig/kv.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const config = @import("config.zig"); 4 | 5 | pub const Store = struct { 6 | /// Configuration for JetKV. Encompasses all backends: 7 | /// * valkey 8 | /// * memory 9 | /// * file 10 | /// 11 | /// The Valkey backend is recommended for production deployment. `memory` and `file` can be 12 | /// used in local development for convenience. All backends have a unified interface, i.e. 13 | /// they can be swapped out without any code changes. 14 | pub const Options = @import("kv/Store.zig").KVOptions; 15 | 16 | // For backward compatibility - `jetzig.kv.Options` is preferred. 17 | pub const KVOptions = Options; 18 | 19 | /// General-purpose store. Use for storing data with no expiry. 20 | pub const GeneralStore = @import("kv/Store.zig").Store(config.get(Store.Options, "store")); 21 | 22 | /// Store ephemeral data. 23 | pub const CacheStore = @import("kv/Store.zig").Store(config.get(Store.Options, "cache")); 24 | 25 | /// Background job storage. 26 | pub const JobQueueStore = @import("kv/Store.zig").Store(config.get(Store.Options, "job_queue")); 27 | 28 | /// Generic store type. Create a custom store by passing `Options`, e.g.: 29 | /// ```zig 30 | /// var store = Generic(.{ .backend = .memory }).init(allocator, logger, .custom); 31 | /// ``` 32 | pub const Generic = @import("kv/Store.zig").Store; 33 | 34 | /// Role a given store fills. Used in log outputs. 35 | pub const Role = @import("kv/Store.zig").Role; 36 | }; 37 | -------------------------------------------------------------------------------- /src/jetzig/loggers.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const jetzig = @import("../jetzig.zig"); 4 | 5 | const Self = @This(); 6 | 7 | pub const DevelopmentLogger = @import("loggers/DevelopmentLogger.zig"); 8 | pub const JsonLogger = @import("loggers/JsonLogger.zig"); 9 | pub const TestLogger = @import("loggers/TestLogger.zig"); 10 | pub const ProductionLogger = @import("loggers/ProductionLogger.zig"); 11 | pub const NullLogger = @import("loggers/NullLogger.zig"); 12 | 13 | pub const LogQueue = @import("loggers/LogQueue.zig"); 14 | 15 | pub const LogFile = struct { 16 | file: std.fs.File, 17 | sync: bool = false, 18 | }; 19 | 20 | pub const LogLevel = enum(u4) { TRACE, DEBUG, INFO, WARN, ERROR, FATAL }; 21 | pub const LogFormat = enum { development, production, json, null }; 22 | 23 | /// Infer a log target (stdout or stderr) from a given log level. 24 | pub inline fn logTarget(comptime level: LogLevel) LogQueue.Target { 25 | return switch (level) { 26 | .TRACE, .DEBUG, .INFO => .stdout, 27 | .WARN, .ERROR, .FATAL => .stderr, 28 | }; 29 | } 30 | pub const Logger = union(enum) { 31 | development_logger: DevelopmentLogger, 32 | json_logger: JsonLogger, 33 | test_logger: TestLogger, 34 | production_logger: ProductionLogger, 35 | null_logger: NullLogger, 36 | 37 | /// Log a TRACE level message to the configured logger. 38 | pub fn TRACE(self: *const Logger, comptime message: []const u8, args: anytype) !void { 39 | switch (self.*) { 40 | inline else => |*logger| try logger.log(.TRACE, message, args), 41 | } 42 | } 43 | 44 | /// Log a DEBUG level message to the configured logger. 45 | pub fn DEBUG(self: *const Logger, comptime message: []const u8, args: anytype) !void { 46 | switch (self.*) { 47 | inline else => |*logger| try logger.log(.DEBUG, message, args), 48 | } 49 | } 50 | 51 | /// Log an INFO level message to the configured logger. 52 | pub fn INFO(self: *const Logger, comptime message: []const u8, args: anytype) !void { 53 | switch (self.*) { 54 | inline else => |*logger| try logger.log(.INFO, message, args), 55 | } 56 | } 57 | 58 | /// Log a WARN level message to the configured logger. 59 | pub fn WARN(self: *const Logger, comptime message: []const u8, args: anytype) !void { 60 | switch (self.*) { 61 | inline else => |*logger| try logger.log(.WARN, message, args), 62 | } 63 | } 64 | 65 | /// Log an ERROR level message to the configured logger. 66 | pub fn ERROR(self: *const Logger, comptime message: []const u8, args: anytype) !void { 67 | switch (self.*) { 68 | inline else => |*logger| try logger.log(.ERROR, message, args), 69 | } 70 | } 71 | 72 | /// Log a FATAL level message to the configured logger. 73 | pub fn FATAL(self: *const Logger, comptime message: []const u8, args: anytype) !void { 74 | switch (self.*) { 75 | inline else => |*logger| try logger.log(.FATAL, message, args), 76 | } 77 | } 78 | 79 | pub fn logRequest(self: *const Logger, request: *const jetzig.http.Request) !void { 80 | switch (self.*) { 81 | inline else => |*logger| try logger.logRequest(request), 82 | } 83 | } 84 | 85 | pub fn logSql(self: *const Logger, request: jetzig.jetquery.events.Event) !void { 86 | switch (self.*) { 87 | inline else => |*logger| try logger.logSql(request), 88 | } 89 | } 90 | 91 | pub fn logError( 92 | self: *const Logger, 93 | stack_trace: ?*std.builtin.StackTrace, 94 | err: anyerror, 95 | ) !void { 96 | switch (self.*) { 97 | inline else => |*logger| try logger.logError(stack_trace, err), 98 | } 99 | } 100 | 101 | pub fn log( 102 | self: *const Logger, 103 | comptime level: LogLevel, 104 | comptime message: []const u8, 105 | args: anytype, 106 | ) !void { 107 | switch (self.*) { 108 | inline else => |*logger| try logger.log(level, message, args), 109 | } 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /src/jetzig/loggers/JsonLogger.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const jetzig = @import("../../jetzig.zig"); 4 | 5 | const JsonLogger = @This(); 6 | 7 | const Timestamp = jetzig.types.Timestamp; 8 | const LogLevel = jetzig.loggers.LogLevel; 9 | const LogMessage = struct { 10 | level: []const u8, 11 | timestamp: []const u8, 12 | message: []const u8, 13 | }; 14 | 15 | const RequestLogMessage = struct { 16 | level: []const u8, 17 | timestamp: []const u8, 18 | method: []const u8, 19 | status: []const u8, 20 | path: []const u8, 21 | duration: i64, 22 | }; 23 | 24 | allocator: std.mem.Allocator, 25 | log_queue: *jetzig.loggers.LogQueue, 26 | level: LogLevel, 27 | 28 | /// Initialize a new JSON Logger. 29 | pub fn init( 30 | allocator: std.mem.Allocator, 31 | level: LogLevel, 32 | log_queue: *jetzig.loggers.LogQueue, 33 | ) JsonLogger { 34 | return .{ 35 | .allocator = allocator, 36 | .level = level, 37 | .log_queue = log_queue, 38 | }; 39 | } 40 | 41 | /// Generic log function, receives log level, message (format string), and args for format string. 42 | pub fn log( 43 | self: *const JsonLogger, 44 | comptime level: LogLevel, 45 | comptime message: []const u8, 46 | args: anytype, 47 | ) !void { 48 | if (@intFromEnum(level) < @intFromEnum(self.level)) return; 49 | 50 | const output = try std.fmt.allocPrint(self.allocator, message, args); 51 | defer self.allocator.free(output); 52 | 53 | const timestamp = Timestamp.init(std.time.timestamp()); 54 | var timestamp_buf: [256]u8 = undefined; 55 | const iso8601 = try timestamp.iso8601(×tamp_buf); 56 | 57 | const log_message = LogMessage{ .level = @tagName(level), .timestamp = iso8601, .message = output }; 58 | 59 | const json = try std.json.stringifyAlloc(self.allocator, log_message, .{ .whitespace = .minified }); 60 | defer self.allocator.free(json); 61 | 62 | try self.log_queue.print("{s}\n", .{json}, jetzig.loggers.logTarget(level)); 63 | } 64 | 65 | /// Log a one-liner including response status code, path, method, duration, etc. 66 | pub fn logRequest(self: *const JsonLogger, request: *const jetzig.http.Request) !void { 67 | const level: LogLevel = .INFO; 68 | 69 | const duration = jetzig.util.duration(request.start_time); 70 | 71 | const timestamp = Timestamp.init(std.time.timestamp()); 72 | var timestamp_buf: [256]u8 = undefined; 73 | const iso8601 = try timestamp.iso8601(×tamp_buf); 74 | 75 | const status = switch (request.response.status_code) { 76 | inline else => |status_code| @unionInit( 77 | jetzig.http.status_codes.TaggedStatusCode, 78 | @tagName(status_code), 79 | .{}, 80 | ), 81 | }; 82 | 83 | const message = RequestLogMessage{ 84 | .level = @tagName(level), 85 | .timestamp = iso8601, 86 | .method = @tagName(request.method), 87 | .status = status.getCode(), 88 | .path = request.path.path, 89 | .duration = duration, 90 | }; 91 | 92 | var buf: [4096]u8 = undefined; 93 | var stream = std.io.fixedBufferStream(&buf); 94 | std.json.stringify(message, .{ .whitespace = .minified }, stream.writer()) catch |err| { 95 | switch (err) { 96 | error.NoSpaceLeft => {}, // TODO: Spill to heap 97 | else => return err, 98 | } 99 | }; 100 | 101 | try self.log_queue.print("{s}\n", .{stream.getWritten()}, .stdout); 102 | } 103 | 104 | pub fn logSql(self: *const JsonLogger, event: jetzig.jetquery.events.Event) !void { 105 | var buf: [4096]u8 = undefined; 106 | var stream = std.io.fixedBufferStream(&buf); 107 | try std.json.stringify( 108 | .{ .sql = event.sql, .duration = event.duration }, 109 | .{ .whitespace = .minified }, 110 | stream.writer(), 111 | ); 112 | try self.log_queue.print("{s}\n", .{stream.getWritten()}, .stdout); 113 | } 114 | 115 | pub fn logError( 116 | self: *const JsonLogger, 117 | stack_trace: ?*std.builtin.StackTrace, 118 | err: anyerror, 119 | ) !void { 120 | // TODO: Format this as JSON and include line number/column if available. 121 | _ = stack_trace; 122 | try self.log(.ERROR, "Encountered error: {s}", .{@errorName(err)}); 123 | } 124 | 125 | fn getFile(self: JsonLogger, level: LogLevel) std.fs.File { 126 | return switch (level) { 127 | .TRACE, .DEBUG, .INFO => self.stdout, 128 | .WARN, .ERROR, .FATAL => self.stderr, 129 | }; 130 | } 131 | -------------------------------------------------------------------------------- /src/jetzig/loggers/NullLogger.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const jetzig = @import("../../jetzig.zig"); 4 | 5 | pub inline fn log(self: @This(), comptime level: jetzig.loggers.LogLevel, comptime message: []const u8, args: anytype) !void { 6 | _ = self; 7 | _ = level; 8 | _ = message; 9 | _ = args; 10 | } 11 | 12 | pub inline fn logSql(self: @This(), event: jetzig.jetquery.events.Event) !void { 13 | _ = self; 14 | _ = event; 15 | } 16 | 17 | pub inline fn logRequest(self: @This(), request: *const jetzig.http.Request) !void { 18 | _ = self; 19 | _ = request; 20 | } 21 | 22 | pub inline fn logError(self: @This(), stack_trace: ?*std.builtin.StackTrace, err: anyerror) !void { 23 | _ = self; 24 | _ = stack_trace; 25 | std.debug.print("Error: {s}\n", .{@errorName(err)}); 26 | } 27 | -------------------------------------------------------------------------------- /src/jetzig/loggers/ProductionLogger.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const jetzig = @import("../../jetzig.zig"); 4 | 5 | const ProductionLogger = @This(); 6 | 7 | const Timestamp = jetzig.types.Timestamp; 8 | const LogLevel = jetzig.loggers.LogLevel; 9 | 10 | allocator: std.mem.Allocator, 11 | level: LogLevel, 12 | log_queue: *jetzig.loggers.LogQueue, 13 | 14 | /// Initialize a new Development Logger. 15 | pub fn init( 16 | allocator: std.mem.Allocator, 17 | level: LogLevel, 18 | log_queue: *jetzig.loggers.LogQueue, 19 | ) ProductionLogger { 20 | return .{ 21 | .allocator = allocator, 22 | .level = level, 23 | .log_queue = log_queue, 24 | }; 25 | } 26 | 27 | /// Generic log function, receives log level, message (format string), and args for format string. 28 | pub fn log( 29 | self: *const ProductionLogger, 30 | comptime level: LogLevel, 31 | comptime message: []const u8, 32 | args: anytype, 33 | ) !void { 34 | if (@intFromEnum(level) < @intFromEnum(self.level)) return; 35 | 36 | const output = try std.fmt.allocPrint(self.allocator, message, args); 37 | defer self.allocator.free(output); 38 | 39 | const timestamp = Timestamp.init(std.time.timestamp()); 40 | var timestamp_buf: [256]u8 = undefined; 41 | const iso8601 = try timestamp.iso8601(×tamp_buf); 42 | 43 | const target = jetzig.loggers.logTarget(level); 44 | 45 | try self.log_queue.print( 46 | "{s} [{s}] {s}\n", 47 | .{ @tagName(level), iso8601, output }, 48 | target, 49 | ); 50 | } 51 | 52 | /// Log a one-liner including response status code, path, method, duration, etc. 53 | pub fn logRequest(self: ProductionLogger, request: *const jetzig.http.Request) !void { 54 | if (@intFromEnum(LogLevel.INFO) < @intFromEnum(self.level)) return; 55 | 56 | var duration_buf: [256]u8 = undefined; 57 | const formatted_duration = try jetzig.colors.duration( 58 | &duration_buf, 59 | jetzig.util.duration(request.start_time), 60 | false, 61 | ); 62 | 63 | const status: jetzig.http.status_codes.TaggedStatusCode = switch (request.response.status_code) { 64 | inline else => |status_code| @unionInit( 65 | jetzig.http.status_codes.TaggedStatusCode, 66 | @tagName(status_code), 67 | .{}, 68 | ), 69 | }; 70 | 71 | const formatted_status = status.getFormatted(.{}); 72 | const timestamp = Timestamp.init(std.time.timestamp()); 73 | var timestamp_buf: [256]u8 = undefined; 74 | const iso8601 = try timestamp.iso8601(×tamp_buf); 75 | 76 | const formatted_level = @tagName(.INFO); 77 | 78 | try self.log_queue.print("{s} [{s}] [{s}/{s}/{s}]{s}{s}{s}{s}{s}{s}{s} {s}\n", .{ 79 | formatted_level, 80 | iso8601, 81 | formatted_duration, 82 | request.fmtMethod(false), 83 | formatted_status, 84 | if (request.middleware_rendered) |_| " [" else "", 85 | if (request.middleware_rendered) |middleware| middleware.name else "", 86 | if (request.middleware_rendered) |_| ":" else "", 87 | if (request.middleware_rendered) |middleware| middleware.action else "", 88 | if (request.middleware_rendered) |_| ":" else "", 89 | if (request.middleware_rendered) |_| @tagName(request.state) else "", 90 | if (request.middleware_rendered) |_| "]" else "", 91 | request.path.path, 92 | }, .stdout); 93 | } 94 | 95 | pub fn logSql(self: *const ProductionLogger, event: jetzig.jetquery.events.Event) !void { 96 | var duration_buf: [256]u8 = undefined; 97 | const formatted_duration = if (event.duration) |duration| try jetzig.colors.duration( 98 | &duration_buf, 99 | duration, 100 | false, 101 | ) else ""; 102 | 103 | const timestamp = Timestamp.init(std.time.timestamp()); 104 | var timestamp_buf: [256]u8 = undefined; 105 | const iso8601 = try timestamp.iso8601(×tamp_buf); 106 | 107 | try self.log_queue.print( 108 | "{s} [{s}] [database] [sql:{s}] [duration:{s}]\n", 109 | .{ @tagName(.INFO), iso8601, event.sql orelse "", formatted_duration }, 110 | .stdout, 111 | ); 112 | } 113 | 114 | const sql_tokens = .{ 115 | "SELECT", 116 | "INSERT", 117 | "UPDATE", 118 | "DELETE", 119 | "WHERE", 120 | "SET", 121 | "ANY", 122 | "FROM", 123 | "INTO", 124 | "IN", 125 | "ON", 126 | "IS", 127 | "NOT", 128 | "NULL", 129 | "LIMIT", 130 | "ORDER BY", 131 | "GROUP BY", 132 | "HAVING", 133 | "LEFT OUTER JOIN", 134 | "INNER JOIN", 135 | "ASC", 136 | "DESC", 137 | "MAX", 138 | "MIN", 139 | "COUNT", 140 | "SUM", 141 | "VALUES", 142 | }; 143 | 144 | pub fn logError(self: *const ProductionLogger, stack_trace: ?*std.builtin.StackTrace, err: anyerror) !void { 145 | // TODO: Include line number/column if available. 146 | _ = stack_trace; 147 | try self.log(.ERROR, "Encountered Error: {s}", .{@errorName(err)}); 148 | } 149 | -------------------------------------------------------------------------------- /src/jetzig/loggers/TestLogger.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const jetzig = @import("../../jetzig.zig"); 4 | 5 | const TestLogger = @This(); 6 | 7 | mode: enum { stream, file, disable }, 8 | file: ?std.fs.File = null, 9 | 10 | pub fn TRACE(self: TestLogger, comptime message: []const u8, args: anytype) !void { 11 | try self.log(.TRACE, message, args); 12 | } 13 | 14 | pub fn DEBUG(self: TestLogger, comptime message: []const u8, args: anytype) !void { 15 | try self.log(.DEBUG, message, args); 16 | } 17 | 18 | pub fn INFO(self: TestLogger, comptime message: []const u8, args: anytype) !void { 19 | try self.log(.INFO, message, args); 20 | } 21 | 22 | pub fn WARN(self: TestLogger, comptime message: []const u8, args: anytype) !void { 23 | try self.log(.WARN, message, args); 24 | } 25 | 26 | pub fn ERROR(self: TestLogger, comptime message: []const u8, args: anytype) !void { 27 | try self.log(.ERROR, message, args); 28 | } 29 | 30 | pub fn FATAL(self: TestLogger, comptime message: []const u8, args: anytype) !void { 31 | try self.log(.FATAL, message, args); 32 | } 33 | 34 | pub fn logRequest(self: TestLogger, request: *const jetzig.http.Request) !void { 35 | const status = jetzig.http.status_codes.get(request.response.status_code); 36 | var buf: [256]u8 = undefined; 37 | try self.log(.INFO, "[{s}|{s}|{s}] {s}", .{ 38 | request.fmtMethod(true), 39 | try jetzig.colors.duration(&buf, jetzig.util.duration(request.start_time), true), 40 | status.getFormatted(.{ .colorized = true }), 41 | request.path.path, 42 | }); 43 | } 44 | 45 | pub fn logSql(self: TestLogger, event: jetzig.jetquery.events.Event) !void { 46 | try self.log(.INFO, "[database] {?s}", .{event.sql}); 47 | } 48 | 49 | pub fn logError(self: TestLogger, stack_trace: ?*std.builtin.StackTrace, err: anyerror) !void { 50 | // TODO: Output useful debug info from stack trace 51 | _ = stack_trace; 52 | try self.log(.ERROR, "Encountered error: {s}", .{@errorName(err)}); 53 | } 54 | 55 | pub fn log( 56 | self: TestLogger, 57 | comptime level: jetzig.loggers.LogLevel, 58 | comptime message: []const u8, 59 | args: anytype, 60 | ) !void { 61 | const template = "-- test logger: " ++ @tagName(level) ++ " " ++ message ++ "\n"; 62 | switch (self.mode) { 63 | .stream => std.debug.print(template, args), 64 | .file => try self.file.?.writer().print(template, args), 65 | .disable => {}, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/jetzig/mail.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const Mail = @import("mail/Mail.zig"); 4 | pub const SMTPConfig = @import("mail/SMTPConfig.zig"); 5 | pub const MailParams = @import("mail/MailParams.zig"); 6 | pub const Address = MailParams.Address; 7 | pub const DefaultMailParams = MailParams.DefaultMailParams; 8 | pub const components = @import("mail/components.zig"); 9 | pub const Job = @import("mail/Job.zig"); 10 | pub const MailerDefinition = @import("mail/MailerDefinition.zig"); 11 | -------------------------------------------------------------------------------- /src/jetzig/mail/MailParams.zig: -------------------------------------------------------------------------------- 1 | subject: ?[]const u8 = null, 2 | from: ?Address = null, 3 | to: ?[]const Address = null, 4 | cc: ?[]const Address = null, 5 | bcc: ?[]const Address = null, // TODO 6 | html: ?[]const u8 = null, 7 | text: ?[]const u8 = null, 8 | defaults: ?DefaultMailParams = null, 9 | 10 | pub const DefaultMailParams = struct { 11 | subject: ?[]const u8 = null, 12 | from: ?Address = null, 13 | to: ?[]const Address = null, 14 | cc: ?[]const Address = null, 15 | bcc: ?[]const Address = null, // TODO 16 | html: ?[]const u8 = null, 17 | text: ?[]const u8 = null, 18 | }; 19 | 20 | pub const Address = struct { 21 | name: ?[]const u8 = null, 22 | email: []const u8, 23 | 24 | pub fn format(address: Address, _: anytype, _: anytype, writer: anytype) !void { 25 | try writer.print("{s} <{s}>", .{ address.name orelse address.email, address.email }); 26 | } 27 | }; 28 | 29 | const MailParams = @This(); 30 | 31 | pub fn get( 32 | self: MailParams, 33 | comptime field: enum { subject, from, to, cc, bcc, html, text }, 34 | ) ?switch (field) { 35 | .subject => []const u8, 36 | .from => Address, 37 | .to => []const Address, 38 | .cc => []const Address, 39 | .bcc => []const Address, 40 | .html => []const u8, 41 | .text => []const u8, 42 | } { 43 | return @field(self, @tagName(field)) orelse if (self.defaults) |defaults| 44 | @field(defaults, @tagName(field)) 45 | else 46 | null; 47 | } 48 | -------------------------------------------------------------------------------- /src/jetzig/mail/MailerDefinition.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("../../jetzig.zig"); 3 | 4 | pub const DeliverFn = *const fn ( 5 | std.mem.Allocator, 6 | *jetzig.mail.MailParams, 7 | *jetzig.data.Value, 8 | jetzig.jobs.JobEnv, 9 | ) anyerror!void; 10 | 11 | name: []const u8, 12 | deliverFn: DeliverFn, 13 | defaults: ?jetzig.mail.DefaultMailParams, 14 | text_template: []const u8, 15 | html_template: []const u8, 16 | -------------------------------------------------------------------------------- /src/jetzig/mail/SMTPConfig.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const smtp = @import("smtp"); 4 | 5 | const jetzig = @import("../../jetzig.zig"); 6 | 7 | port: u16 = 25, 8 | encryption: enum { none, insecure, tls, start_tls } = .none, 9 | host: []const u8 = "localhost", 10 | username: ?[]const u8 = null, 11 | password: ?[]const u8 = null, 12 | 13 | const SMTPConfig = @This(); 14 | 15 | pub fn toSMTP( 16 | self: SMTPConfig, 17 | allocator: std.mem.Allocator, 18 | env: jetzig.jobs.JobEnv, 19 | ) !smtp.Config { 20 | return smtp.Config{ 21 | .allocator = allocator, 22 | .port = try env.vars.getT(u16, "JETZIG_SMTP_PORT") orelse self.port, 23 | .encryption = try env.vars.getT(smtp.Encryption, "JETZIG_SMTP_ENCRYPTION") orelse 24 | self.getEncryption(), 25 | .host = env.vars.get("JETZIG_SMTP_HOST") orelse self.host, 26 | .username = env.vars.get("JETZIG_SMTP_USERNAME") orelse self.username, 27 | .password = env.vars.get("JETZIG_SMTP_PASSWORD") orelse self.password, 28 | }; 29 | } 30 | 31 | fn getEncryption(self: SMTPConfig) smtp.Encryption { 32 | return switch (self.encryption) { 33 | .none => smtp.Encryption.none, 34 | .insecure => smtp.Encryption.insecure, 35 | .tls => smtp.Encryption.tls, 36 | .start_tls => smtp.Encryption.start_tls, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/jetzig/mail/components.zig: -------------------------------------------------------------------------------- 1 | pub const header = 2 | "MIME-Version: 1.0\r\n" ++ 3 | "Content-Type: multipart/alternative; boundary=\"=_alternative_{0}\"\r\n"; 4 | 5 | pub const footer = 6 | "\r\n.\r\n"; 7 | 8 | pub const text = 9 | "--=_alternative_{0}\r\n" ++ 10 | "Content-Type: text/plain; charset=\"UTF-8\"\r\n" ++ 11 | "Content-Transfer-Encoding: quoted-printable\r\n\r\n" ++ 12 | "{1s}\r\n"; 13 | 14 | pub const html = 15 | "--=_alternative_{0}\r\n" ++ 16 | "Content-Type: text/html; charset=\"UTF-8\"\r\n" ++ 17 | "Content-Transfer-Encoding: quoted-printable\r\n\r\n" ++ 18 | "{1s}\r\n"; 19 | -------------------------------------------------------------------------------- /src/jetzig/markdown.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Zmd = @import("zmd").Zmd; 4 | 5 | const jetzig = @import("../jetzig.zig"); 6 | 7 | pub const MarkdownRenderOptions = struct { 8 | fragments: ?type = null, 9 | }; 10 | 11 | pub fn render( 12 | allocator: std.mem.Allocator, 13 | content: []const u8, 14 | comptime options: MarkdownRenderOptions, 15 | ) ![]const u8 { 16 | const fragments = options.fragments orelse jetzig.config.get(type, "markdown_fragments"); 17 | 18 | var zmd = Zmd.init(allocator); 19 | defer zmd.deinit(); 20 | 21 | try zmd.parse(content); 22 | return try zmd.toHtml(fragments); 23 | } 24 | 25 | pub fn renderFile( 26 | allocator: std.mem.Allocator, 27 | path: []const u8, 28 | comptime options: MarkdownRenderOptions, 29 | ) !?[]const u8 { 30 | var path_buf = std.ArrayList([]const u8).init(allocator); 31 | defer path_buf.deinit(); 32 | 33 | try path_buf.appendSlice(&[_][]const u8{ "src", "app", "views" }); 34 | 35 | var it = std.mem.splitScalar(u8, path, '/'); 36 | while (it.next()) |segment| { 37 | try path_buf.append(segment); 38 | } 39 | 40 | const base_path = try std.fs.path.join(allocator, path_buf.items); 41 | defer allocator.free(base_path); 42 | 43 | const full_path = try std.mem.concat(allocator, u8, &[_][]const u8{ base_path, ".md" }); 44 | defer allocator.free(full_path); 45 | 46 | const stat = std.fs.cwd().statFile(full_path) catch |err| { 47 | return switch (err) { 48 | error.FileNotFound => null, 49 | else => err, 50 | }; 51 | }; 52 | const content = std.fs.cwd().readFileAlloc(allocator, full_path, @intCast(stat.size)) catch |err| { 53 | switch (err) { 54 | error.FileNotFound => return null, 55 | else => return err, 56 | } 57 | }; 58 | 59 | return try render(allocator, content, options); 60 | } 61 | -------------------------------------------------------------------------------- /src/jetzig/middleware.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("../jetzig.zig"); 3 | 4 | pub const HtmxMiddleware = @import("middleware/HtmxMiddleware.zig"); 5 | pub const CompressionMiddleware = @import("middleware/CompressionMiddleware.zig"); 6 | pub const AuthMiddleware = @import("middleware/AuthMiddleware.zig"); 7 | pub const AntiCsrfMiddleware = @import("middleware/AntiCsrfMiddleware.zig"); 8 | 9 | const RouteOptions = struct { 10 | content: ?[]const u8 = null, 11 | content_type: []const u8 = "text/html", 12 | status: jetzig.http.StatusCode = .ok, 13 | }; 14 | 15 | pub const MiddlewareRoute = struct { 16 | method: jetzig.http.Request.Method, 17 | path: []const u8, 18 | content: ?[]const u8, 19 | content_type: []const u8, 20 | status: jetzig.http.StatusCode, 21 | 22 | pub fn match(self: MiddlewareRoute, request: *const jetzig.http.Request) bool { 23 | if (self.method != request.method) return false; 24 | if (!std.mem.eql(u8, self.path, request.path.file_path)) return false; 25 | 26 | return true; 27 | } 28 | }; 29 | 30 | pub fn route(method: jetzig.http.Request.Method, path: []const u8, options: RouteOptions) MiddlewareRoute { 31 | return .{ 32 | .method = method, 33 | .path = path, 34 | .content = options.content, 35 | .content_type = options.content_type, 36 | .status = options.status, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/jetzig/middleware/AntiCsrfMiddleware.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("../../jetzig.zig"); 3 | 4 | pub const middleware_name = "anti_csrf"; 5 | 6 | const TokenParams = @Type(.{ 7 | .@"struct" = .{ 8 | .layout = .auto, 9 | .is_tuple = false, 10 | .decls = &.{}, 11 | .fields = &.{.{ 12 | .name = jetzig.authenticity_token_name ++ "", 13 | .type = []const u8, 14 | .is_comptime = false, 15 | .default_value_ptr = null, 16 | .alignment = @alignOf([]const u8), 17 | }}, 18 | }, 19 | }); 20 | 21 | pub fn afterRequest(request: *jetzig.http.Request) !void { 22 | try verifyCsrfToken(request); 23 | } 24 | 25 | pub fn beforeRender(request: *jetzig.http.Request, route: jetzig.views.Route) !void { 26 | _ = route; 27 | try verifyCsrfToken(request); 28 | } 29 | 30 | fn logFailure(request: *jetzig.http.Request) !void { 31 | _ = request.fail(.forbidden); 32 | try request.server.logger.DEBUG("Anti-CSRF token validation failed. Request aborted.", .{}); 33 | } 34 | 35 | fn verifyCsrfToken(request: *jetzig.http.Request) !void { 36 | switch (request.method) { 37 | .DELETE, .PATCH, .PUT, .POST => {}, 38 | else => return, 39 | } 40 | 41 | switch (request.requestFormat()) { 42 | .HTML, .UNKNOWN => {}, 43 | // We do not authenticate JSON requests. Users must implement their own authentication 44 | // system or disable JSON endpoints that should be protected. 45 | .JSON => return, 46 | } 47 | 48 | const session = try request.session(); 49 | 50 | if (session.getT(.string, jetzig.authenticity_token_name)) |token| { 51 | const params = try request.expectParams(TokenParams) orelse { 52 | return logFailure(request); 53 | }; 54 | 55 | if (token.len != 32 or @field(params, jetzig.authenticity_token_name).len != 32) { 56 | return try logFailure(request); 57 | } 58 | 59 | var actual: [32]u8 = undefined; 60 | var expected: [32]u8 = undefined; 61 | 62 | @memcpy(&actual, token[0..32]); 63 | @memcpy(&expected, @field(params, jetzig.authenticity_token_name)[0..32]); 64 | 65 | const valid = std.crypto.timing_safe.eql([32]u8, expected, actual); 66 | 67 | if (!valid) { 68 | return try logFailure(request); 69 | } 70 | } else { 71 | return try logFailure(request); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/jetzig/middleware/AuthMiddleware.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("../../jetzig.zig"); 3 | 4 | pub const middleware_name = "auth"; 5 | 6 | // Default model is `.User`. 7 | const user_model = jetzig.config.get(jetzig.auth.AuthOptions, "auth").user_model; 8 | 9 | /// Define any custom data fields you want to store here. Assigning to these fields in the `init` 10 | /// function allows you to access them in the `beforeRequest` and `afterRequest` functions, where 11 | /// they can also be modified. 12 | user: ?@TypeOf(jetzig.database.Query(user_model).find(0)).ResultType, 13 | 14 | const AuthMiddleware = @This(); 15 | 16 | /// Initialize middleware. 17 | pub fn init(request: *jetzig.http.Request) !*AuthMiddleware { 18 | const middleware = try request.allocator.create(AuthMiddleware); 19 | middleware.* = .{ .user = null }; 20 | return middleware; 21 | } 22 | 23 | const map = std.StaticStringMap(void).initComptime(.{ 24 | .{ ".html", void }, 25 | .{ ".json", void }, 26 | }); 27 | 28 | /// For HTML/JSON requests, fetch a user ID from the encrypted session cookie and execute a 29 | /// database query to match the user ID to a database record. Expects a `User` model defined in 30 | /// the schema, configurable with `auth.user_model`. 31 | /// 32 | /// User ID is accessible from a request: 33 | /// ```zig 34 | /// if (request.middleware(.auth).user) |user| { 35 | /// try request.server.log(.DEBUG, "{}", .{user.id}); 36 | /// } 37 | /// ``` 38 | pub fn afterRequest(self: *AuthMiddleware, request: *jetzig.http.Request) !void { 39 | if (request.path.extension) |extension| { 40 | if (map.get(extension) == null) return; 41 | } 42 | const user_id = try jetzig.auth.getUserId(.integer, request) orelse return; 43 | 44 | const query = jetzig.database.Query(user_model).find(user_id); 45 | if (try request.repo.execute(query)) |user| { 46 | self.user = user; 47 | } 48 | } 49 | 50 | /// Invoked after `afterRequest` is called, use this function to do any clean-up. 51 | /// Note that `request.allocator` is an arena allocator, so any allocations are automatically 52 | /// done before the next request starts processing. 53 | pub fn deinit(self: *AuthMiddleware, request: *jetzig.http.Request) void { 54 | request.allocator.destroy(self); 55 | } 56 | -------------------------------------------------------------------------------- /src/jetzig/middleware/CompressionMiddleware.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("../../jetzig.zig"); 3 | 4 | fn isCompressable(content_type: []const u8) bool { 5 | const type_list = .{ 6 | "text/html", 7 | "application/xhtml+xml", 8 | "application/xml", 9 | "text/css", 10 | "text/javascript", 11 | "application/json", 12 | "application/pdf", 13 | "image/svg+xml", 14 | }; 15 | 16 | inline for (type_list) |content| { 17 | if (std.mem.eql(u8, content_type, content)) return true; 18 | } 19 | return false; 20 | } 21 | 22 | const Encoding = enum { gzip, deflate }; 23 | /// Parse accepted encoding, encode responses if possible, set appropriate headers, and 24 | /// modify the response accordingly to decrease response size 25 | pub fn beforeResponse(request: *jetzig.http.Request, response: *jetzig.http.Response) !void { 26 | if (!isCompressable(response.content_type)) return; 27 | const encoding = detectEncoding(request) orelse return; 28 | 29 | const compressed = switch (encoding) { 30 | .gzip => jetzig.util.gzip(request.allocator, response.content, .{}) catch |err| 31 | return request.server.logger.logError(@errorReturnTrace(), err), 32 | .deflate => jetzig.util.deflate(request.allocator, response.content, .{}) catch |err| 33 | return request.server.logger.logError(@errorReturnTrace(), err), 34 | }; 35 | 36 | response.headers.append("Content-Encoding", @tagName(encoding)) catch |err| 37 | return request.server.logger.logError(@errorReturnTrace(), err); 38 | 39 | // Make caching work 40 | response.headers.append("Vary", "Accept-Encoding") catch |err| 41 | return request.server.logger.logError(@errorReturnTrace(), err); 42 | 43 | response.content = compressed; 44 | } 45 | 46 | fn detectEncoding(request: *const jetzig.http.Request) ?Encoding { 47 | var headers_it = request.headers.getAllIterator("Accept-Encoding"); 48 | while (headers_it.next()) |header| { 49 | var it = std.mem.tokenizeScalar(u8, header.value, ','); 50 | while (it.next()) |param| { 51 | inline for (@typeInfo(Encoding).@"enum".fields) |field| { 52 | if (std.mem.eql(u8, field.name, jetzig.util.strip(param))) { 53 | return @field(Encoding, field.name); 54 | } 55 | } 56 | } 57 | } 58 | 59 | return null; 60 | } 61 | -------------------------------------------------------------------------------- /src/jetzig/middleware/HtmxMiddleware.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("../../jetzig.zig"); 3 | 4 | const HtmxMiddleware = @This(); 5 | 6 | /// Detects the `HX-Request` header and, if present, disables the default layout for the current 7 | /// request. This allows a view to specify a layout that will render the full page when the 8 | /// request doesn't come via htmx and, when the request does come from htmx, only return the 9 | /// content rendered directly by the view function. 10 | pub fn afterRequest(request: *jetzig.http.Request) !void { 11 | if (request.headers.get("HX-Request")) |_| { 12 | try request.server.logger.DEBUG( 13 | "[middleware-htmx] HX-Request header, disabling layout.", 14 | .{}, 15 | ); 16 | request.setLayout(null); 17 | } 18 | } 19 | 20 | /// If a redirect was issued during request processing, reset any response data, set response 21 | /// status to `200 OK` and replace the `Location` header with a `HX-Redirect` header. 22 | /// Add Vary response header to prevent caching the page without layout for requests not coming 23 | /// from htmx. 24 | pub fn beforeResponse(request: *jetzig.http.Request, response: *jetzig.http.Response) !void { 25 | if (request.headers.get("HX-Request") == null) return; 26 | 27 | switch (response.status_code) { 28 | .moved_permanently, .found => { 29 | if (response.headers.get("Location")) |location| { 30 | response.status_code = .ok; 31 | request.response_data.reset(); 32 | try response.headers.append("HX-Redirect", location); 33 | } 34 | }, 35 | else => { 36 | try response.headers.append("Vary", "HX-Request"); 37 | }, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/jetzig/types.zig: -------------------------------------------------------------------------------- 1 | pub const Timestamp = @import("types/Timestamp.zig"); 2 | -------------------------------------------------------------------------------- /src/jetzig/types/Timestamp.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Self = @This(); 4 | 5 | timestamp: i64, 6 | 7 | const constants = struct { 8 | pub const seconds_in_day: i64 = 60 * 60 * 24; 9 | pub const seconds_in_year: i64 = 60 * 60 * 24 * 365.25; 10 | pub const months: [12]i64 = .{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; 11 | pub const epoch_year: i64 = 1970; 12 | }; 13 | 14 | pub fn init(timestamp: i64) Self { 15 | return .{ .timestamp = timestamp }; 16 | } 17 | 18 | pub fn iso8601(self: *const Self, buf: *[256]u8) ![]const u8 { 19 | const u32_year: u32 = @intCast(self.year()); 20 | const u32_month: u32 = @intCast(self.month()); 21 | const u32_day_of_month: u32 = @intCast(self.dayOfMonth()); 22 | const u32_hour: u32 = @intCast(self.hour()); 23 | const u32_minute: u32 = @intCast(self.minute()); 24 | const u32_second: u32 = @intCast(self.second()); 25 | return try std.fmt.bufPrint(buf, "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}", .{ 26 | u32_year, 27 | u32_month, 28 | u32_day_of_month, 29 | u32_hour, 30 | u32_minute, 31 | u32_second, 32 | }); 33 | } 34 | 35 | pub fn year(self: *const Self) i64 { 36 | return constants.epoch_year + @divTrunc(self.timestamp, constants.seconds_in_year); 37 | } 38 | 39 | pub fn month(self: *const Self) usize { 40 | const day_of_year = self.dayOfYear(); 41 | var total_days: i64 = 0; 42 | for (constants.months, 1..) |days, index| { 43 | total_days += days; 44 | if (day_of_year <= total_days) return index; 45 | } 46 | unreachable; 47 | } 48 | 49 | pub fn dayOfYear(self: *const Self) i64 { 50 | return @divTrunc(self.daysSinceEpoch(), constants.seconds_in_day); 51 | } 52 | 53 | pub fn dayOfMonth(self: *const Self) i64 { 54 | const day_of_year = self.dayOfYear(); 55 | var total_days: i64 = 0; 56 | for (constants.months) |days| { 57 | total_days += days; 58 | if (day_of_year <= total_days) return days + (day_of_year - total_days) + 1; 59 | } 60 | unreachable; 61 | } 62 | 63 | pub fn daysSinceEpoch(self: *const Self) i64 { 64 | return self.timestamp - ((self.year() - constants.epoch_year) * constants.seconds_in_year); 65 | } 66 | 67 | pub fn dayOfWeek(self: *const Self) i64 { 68 | const currentDay = std.math.mod(i64, self.daysSinceEpoch(), 7) catch unreachable; 69 | return std.math.mod(i64, currentDay + 4, 7) catch unreachable; 70 | } 71 | 72 | pub fn hour(self: *const Self) i64 { 73 | const seconds = std.math.mod(i64, self.timestamp, constants.seconds_in_day) catch unreachable; 74 | return @divTrunc(seconds, @as(i64, 60 * 60)); 75 | } 76 | 77 | pub fn minute(self: *const Self) i64 { 78 | const seconds = std.math.mod(i64, self.timestamp, @as(i64, 60 * 60)) catch unreachable; 79 | return @divTrunc(seconds, @as(i64, 60)); 80 | } 81 | 82 | pub fn second(self: *const Self) i64 { 83 | return std.math.mod(i64, self.timestamp, @as(i64, 60)) catch unreachable; 84 | } 85 | -------------------------------------------------------------------------------- /src/jetzig/views.zig: -------------------------------------------------------------------------------- 1 | pub const Route = @import("views/Route.zig"); 2 | pub const View = @import("views/View.zig"); 3 | pub const CustomRoute = @import("views/CustomRoute.zig"); 4 | -------------------------------------------------------------------------------- /src/jetzig/views/CustomRoute.zig: -------------------------------------------------------------------------------- 1 | const jetzig = @import("../../jetzig.zig"); 2 | 3 | method: jetzig.http.Request.Method, 4 | path: []const u8, 5 | view: union(enum) { 6 | with_id: *const fn (id: []const u8, *jetzig.http.Request, *jetzig.data.Data) anyerror!jetzig.views.View, 7 | without_id: *const fn (*jetzig.http.Request, *jetzig.data.Data) anyerror!jetzig.views.View, 8 | }, 9 | -------------------------------------------------------------------------------- /src/jetzig/views/View.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Self = @This(); 4 | 5 | const jetzig = @import("../../jetzig.zig"); 6 | 7 | data: *jetzig.data.Data, 8 | status_code: jetzig.http.status_codes.StatusCode = .ok, 9 | 10 | pub fn deinit(self: Self) void { 11 | _ = self; 12 | } 13 | -------------------------------------------------------------------------------- /src/jetzig/views/view_types.zig: -------------------------------------------------------------------------------- 1 | const jetzig = @import("../../jetzig.zig"); 2 | 3 | pub const ViewWithoutId = *const fn ( 4 | *jetzig.http.Request, 5 | ) anyerror!jetzig.views.View; 6 | 7 | pub const ViewWithId = *const fn ( 8 | id: []const u8, 9 | *jetzig.http.Request, 10 | ) anyerror!jetzig.views.View; 11 | 12 | pub const ViewWithArgs = *const fn ( 13 | []const []const u8, 14 | *jetzig.http.Request, 15 | ) anyerror!jetzig.views.View; 16 | 17 | pub const StaticViewWithoutId = *const fn ( 18 | *jetzig.http.StaticRequest, 19 | ) anyerror!jetzig.views.View; 20 | 21 | pub const StaticViewWithId = *const fn ( 22 | id: []const u8, 23 | *jetzig.http.StaticRequest, 24 | ) anyerror!jetzig.views.View; 25 | 26 | pub const StaticViewWithArgs = *const fn ( 27 | []const []const u8, 28 | *jetzig.http.StaticRequest, 29 | ) anyerror!jetzig.views.View; 30 | 31 | // Legacy view types receive a `data` argument. This made sense when `data.string(...)` etc. were 32 | // needed to create a string, but now we use type inference/coercion when adding values to 33 | // response data. 34 | // `Array.append(.array)`, `Array.append(.object)`, `Object.put(key, .array)`, and 35 | // `Object.put(key, .object)` also remove the need to use `data.array()` and `data.object()`. 36 | // The only remaining use is `data.root(.object)` and `data.root(.array)` which we can move to 37 | // `request.data(.object)` and `request.data(.array)`. 38 | pub const LegacyViewWithoutId = *const fn ( 39 | *jetzig.http.Request, 40 | *jetzig.data.Data, 41 | ) anyerror!jetzig.views.View; 42 | 43 | pub const LegacyViewWithId = *const fn ( 44 | id: []const u8, 45 | *jetzig.http.Request, 46 | *jetzig.data.Data, 47 | ) anyerror!jetzig.views.View; 48 | 49 | pub const LegacyStaticViewWithoutId = *const fn ( 50 | *jetzig.http.StaticRequest, 51 | *jetzig.data.Data, 52 | ) anyerror!jetzig.views.View; 53 | 54 | pub const LegacyViewWithArgs = *const fn ( 55 | []const []const u8, 56 | *jetzig.http.Request, 57 | *jetzig.data.Data, 58 | ) anyerror!jetzig.views.View; 59 | 60 | pub const LegacyStaticViewWithId = *const fn ( 61 | id: []const u8, 62 | *jetzig.http.StaticRequest, 63 | *jetzig.data.Data, 64 | ) anyerror!jetzig.views.View; 65 | 66 | pub const LegacyStaticViewWithArgs = *const fn ( 67 | []const []const u8, 68 | *jetzig.http.StaticRequest, 69 | ) anyerror!jetzig.views.View; 70 | -------------------------------------------------------------------------------- /src/routes_file.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Routes = @import("Routes.zig"); 3 | 4 | pub fn main() !void { 5 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 6 | defer std.debug.assert(gpa.deinit() == .ok); 7 | const gpa_allocator = gpa.allocator(); 8 | 9 | var arena = std.heap.ArenaAllocator.init(gpa_allocator); 10 | const allocator = arena.allocator(); 11 | defer arena.deinit(); 12 | 13 | var it = try std.process.argsWithAllocator(allocator); 14 | _ = it.next().?; 15 | const output_path = it.next().?; 16 | const root_path = it.next().?; 17 | const src_path = it.next().?; 18 | const templates_path = it.next().?; 19 | const views_path = it.next().?; 20 | const jobs_path = it.next().?; 21 | const mailers_path = it.next().?; 22 | 23 | var routes = try Routes.init( 24 | allocator, 25 | root_path, 26 | templates_path, 27 | views_path, 28 | jobs_path, 29 | mailers_path, 30 | ); 31 | const generated_routes = try routes.generateRoutes(); 32 | var src_dir = try std.fs.cwd().openDir(src_path, .{ .iterate = true }); 33 | defer src_dir.close(); 34 | var walker = try src_dir.walk(allocator); 35 | defer walker.deinit(); 36 | 37 | while (try walker.next()) |entry| { 38 | if (entry.kind == .file) { 39 | const stat = try src_dir.statFile(entry.path); 40 | const src_data = try src_dir.readFileAlloc(allocator, entry.path, @intCast(stat.size)); 41 | const relpath = try std.fs.path.join(allocator, &[_][]const u8{ "src", entry.path }); 42 | var dir = try std.fs.cwd().openDir(std.fs.path.dirname(output_path).?, .{}); 43 | const dest_dir = try dir.makeOpenPath(std.fs.path.dirname(relpath).?, .{}); 44 | const src_file = try dest_dir.createFile(std.fs.path.basename(relpath), .{}); 45 | try src_file.writeAll(src_data); 46 | src_file.close(); 47 | } 48 | } 49 | 50 | const file = try std.fs.cwd().createFile(output_path, .{ .truncate = true }); 51 | try file.writeAll(generated_routes); 52 | file.close(); 53 | } 54 | -------------------------------------------------------------------------------- /src/tests.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const jetzig = @import("jetzig.zig"); 3 | 4 | test { 5 | std.debug.assert(jetzig.jetquery.jetcommon == jetzig.zmpl.jetcommon); 6 | std.debug.assert(jetzig.zmpl.jetcommon == jetzig.jetcommon); 7 | _ = @import("jetzig/http/Query.zig"); 8 | _ = @import("jetzig/http/Headers.zig"); 9 | _ = @import("jetzig/http/Cookies.zig"); 10 | _ = @import("jetzig/http/Session.zig"); 11 | _ = @import("jetzig/http/Path.zig"); 12 | _ = @import("jetzig/jobs/Job.zig"); 13 | _ = @import("jetzig/mail/Mail.zig"); 14 | _ = @import("jetzig/loggers/LogQueue.zig"); 15 | } 16 | --------------------------------------------------------------------------------