Compare commits

..

No commits in common. "websockets" and "main" have entirely different histories.

47 changed files with 886 additions and 2596 deletions

View File

@ -12,22 +12,11 @@ const use_llvm_default = builtin.os.tag != .linux;
pub fn build(b: *std.Build) !void { pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
var jetzig_templates_path = std.ArrayList([]const u8).init(b.allocator);
try jetzig_templates_path.append("/");
var it = std.mem.splitSequence(
u8,
try b.path("src/jetzig/templates").getPath3(b, null).toString(b.allocator),
std.fs.path.sep_str,
);
while (it.next()) |segment| {
try jetzig_templates_path.append(segment);
}
const templates_paths = try zmpl_build.templatesPaths( const templates_paths = try zmpl_build.templatesPaths(
b.allocator, b.allocator,
&.{ &.{
.{ .prefix = "views", .path = &.{ "src", "app", "views" } }, .{ .prefix = "views", .path = &.{ "src", "app", "views" } },
.{ .prefix = "mailers", .path = &.{ "src", "app", "mailers" } }, .{ .prefix = "mailers", .path = &.{ "src", "app", "mailers" } },
.{ .prefix = "jetzig", .path = jetzig_templates_path.items },
}, },
); );

View File

@ -25,9 +25,8 @@
.hash = "jetkv-0.0.0-zCv0fmCGAgCyYqwHjk0P5KrYVRew1MJAtbtAcIO-WPpT", .hash = "jetkv-0.0.0-zCv0fmCGAgCyYqwHjk0P5KrYVRew1MJAtbtAcIO-WPpT",
}, },
.zmpl = .{ .zmpl = .{
// .url = "https://github.com/jetzig-framework/zmpl/archive/89ee0ce9b4c96c316cc0575266fb66c864f24a49.tar.gz", .url = "https://github.com/jetzig-framework/zmpl/archive/c57fc9b83027e8c1459d9625c3509f59f0fb89f3.tar.gz",
// .hash = "zmpl-0.0.1-SYFGBtuNAwCj2YbqnoEJt3bk1iFIZjGK6JwMc72toZBR", .hash = "zmpl-0.0.1-SYFGBgdqAwDeA6xm4KAhpKoNrWs5CMQK6x447zhWclCs",
.path = "../zmpl",
}, },
.httpz = .{ .httpz = .{
.url = "https://github.com/karlseguin/http.zig/archive/37d7cb9819b804ade5f4b974b82f8dd0622225ed.tar.gz", .url = "https://github.com/karlseguin/http.zig/archive/37d7cb9819b804ade5f4b974b82f8dd0622225ed.tar.gz",

View File

@ -1,139 +0,0 @@
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
background-color: #f0f0f0;
overflow-x: hidden;
position: relative;
padding-top: 5rem;
}
#board {
display: grid;
grid-template-columns: repeat(3, 100px);
grid-gap: 5px;
margin: 20px;
}
.cell {
width: 100px;
height: 100px;
background: white;
border: 2px solid #333;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
cursor: pointer;
}
.cell:hover {
background: #e0e0e0;
}
#status {
font-size: 24px;
margin-bottom: 20px;
}
#reset-button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
display: inline;
}
#reset-button:hover {
background-color: #45a049;
}
#reset-wrapper {
text-align: center;
}
#party-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
}
.animal {
position: absolute;
font-size: 50px;
opacity: 0;
}
.run-dog {
animation: runAcross 2s linear forwards;
}
.run-cat {
animation: runAcross 2s linear forwards;
}
.run-lizard {
animation: runAcross 2s linear forwards;
}
.run-jet {
animation: runAcross 2s linear forwards;
}
@keyframes runAcross {
0% {
left: -100px;
opacity: 1;
}
100% {
left: 100%;
opacity: 1;
}
}
.confetti {
position: absolute;
font-size: 30px;
opacity: 0.8;
}
.fall {
animation: fall 3s linear forwards;
}
@keyframes fall {
0% {
top: -50px;
opacity: 1;
transform: rotate(0deg);
}
100% {
top: 100%;
opacity: 0.5;
transform: rotate(360deg);
}
}
#results {
display: grid;
grid-template-columns: repeat(2, 5rem);
text-align: center;
font-weight: bold;
font-family: monospace;
}
#results-wrapper {
display: flex;
justify-content: space-between;
}
.trophy {
font-size: 5rem;
}
#victor {
margin: 3rem;
font-size: 3.5rem;
text-align: center;
vertical-align: center;
}
#grid {
transition: background-color 0.5s ease;
}
.flash-animation {
animation: flashRed 0.5s ease;
}
@keyframes flashRed {
0% { background-color: #e55; }
100% { background-color: white; }
}

View File

@ -1,47 +0,0 @@
function triggerPartyAnimation() {
const container = document.getElementById('party-container');
container.innerHTML = ''; // Clear previous animations
// Define entities
const entities = [
{ type: 'dog', emoji: '🐶' },
{ type: 'cat', emoji: '🐱' },
{ type: 'lizard', emoji: '🦎' },
{ type: 'jet', emoji: '✈' }
];
// Create random number of each entity (2-5 per type)
entities.forEach(entity => {
const count = Math.floor(Math.random() * 4) + 2; // Random 2-5
for (let i = 0; i < count; i++) {
const div = document.createElement('div');
div.className = 'animal';
div.innerHTML = entity.emoji;
// Random vertical position (between 20% and 80% of screen height)
div.style.top = `${20 + Math.random() * 60}%`;
// Random delay (0 to 1.5s)
div.style.animationDelay = `${Math.random() * 1.5}s`;
container.appendChild(div);
// Trigger animation
setTimeout(() => {
div.classList.add(`run-${entity.type}`);
}, 10);
}
});
// Create confetti (20 pieces)
for (let i = 0; i < 20; i++) {
const div = document.createElement('div');
div.className = 'confetti';
div.innerHTML = '&#127881;';
// Random horizontal position
div.style.left = `${Math.random() * 100}%`;
// Random delay (0 to 2s)
div.style.animationDelay = `${Math.random() * 2}s`;
container.appendChild(div);
// Trigger fall animation
setTimeout(() => {
div.classList.add('fall');
}, 10);
}
}

View File

@ -1,83 +0,0 @@
const std = @import("std");
const jetzig = @import("jetzig");
grid: Grid,
victor: ?State = null,
pub const Grid = [9]State;
pub const State = enum { empty, player, cpu, tie };
const Game = @This();
pub fn gridFromValues(values: []*jetzig.data.Value) Grid {
var grid: [9]Game.State = undefined;
for (0..9) |id| {
if (values[id].* != .null) {
grid[id] = if (values[id].eql("player")) .player else .cpu;
} else {
grid[id] = .empty;
}
}
return grid;
}
pub fn movePlayer(game: *Game, cell: usize) bool {
if (cell >= game.grid.len) return false;
if (game.grid[cell] != .empty) return false;
game.grid[cell] = .player;
game.evaluate();
return true;
}
pub fn moveCpu(game: *Game) usize {
std.debug.assert(game.victor == null);
var available: [9]usize = undefined;
var available_len: usize = 0;
for (game.grid, 0..) |cell, cell_index| {
if (cell == .empty) {
available[available_len] = cell_index;
available_len += 1;
}
}
std.debug.assert(available_len > 0);
const choice = available[std.crypto.random.intRangeAtMost(usize, 0, available_len - 1)];
game.grid[choice] = .cpu;
game.evaluate();
return choice;
}
pub fn evaluate(game: *Game) void {
var full = true;
for (game.grid) |cell| {
if (cell == .empty) full = false;
}
if (full) {
game.victor = .tie;
return;
}
const patterns = [_][3]usize{
.{ 0, 1, 2 },
.{ 3, 4, 5 },
.{ 6, 7, 8 },
.{ 0, 3, 6 },
.{ 1, 4, 7 },
.{ 2, 5, 8 },
.{ 0, 4, 8 },
.{ 2, 4, 6 },
};
for (patterns) |pattern| {
var cpu_victor = true;
var player_victor = true;
for (pattern) |cell_index| {
if (game.grid[cell_index] != .cpu) cpu_victor = false;
if (game.grid[cell_index] != .player) player_victor = false;
}
std.debug.assert(!(cpu_victor and player_victor));
if (cpu_victor) game.victor = .cpu;
if (player_victor) game.victor = .player;
}
}

View File

@ -1 +0,0 @@
<title>My Inertia App</title>

View File

@ -6,12 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/prism.css" /> <link rel="stylesheet" href="/prism.css" />
{{context.middleware.renderHeader()}}
</head> </head>
<body> <body>
<main>{{zmpl.content}}</main> <main>{{zmpl.content}}</main>
<script src="/prism.js"></script> <script src="/prism.js"></script>
{{context.middleware.renderFooter()}}
</body> </body>
</html> </html>

View File

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

View File

@ -1,77 +0,0 @@
const std = @import("std");
const jetzig = @import("jetzig");
const Game = @import("../lib/Game.zig");
pub const layout = "application";
pub fn index(request: *jetzig.Request) !jetzig.View {
return request.render(.ok);
}
pub const Channel = struct {
pub fn open(channel: jetzig.channels.Channel) !void {
if (channel.get("cells") == null) try initGame(channel);
try channel.sync();
}
pub const Actions = struct {
pub fn move(channel: jetzig.channels.Channel, cell: usize) !void {
const cells = channel.getT(.array, "cells") orelse return;
const grid = Game.gridFromValues(cells.items());
var game = Game{ .grid = grid };
game.evaluate();
if (game.victor != null) {
try channel.invoke(.game_over, .{});
return;
} else {
try movePlayer(channel, &game, cells, cell);
try channel.sync();
}
}
pub fn reset(channel: jetzig.channels.Channel) !void {
try resetGame(channel);
try channel.sync();
}
};
fn resetGame(channel: jetzig.channels.Channel) !void {
try channel.put("victor", null);
var cells = try channel.put("cells", .array);
for (0..9) |_| try cells.append(null);
}
fn initGame(channel: jetzig.channels.Channel) !void {
var results = try channel.put("results", .object);
try results.put("cpu", 0);
try results.put("player", 0);
try results.put("tie", 0);
try resetGame(channel);
}
fn movePlayer(
channel: jetzig.channels.Channel,
game: *Game,
cells: *const jetzig.data.Array,
cell: usize,
) !void {
const values = cells.items();
if (game.movePlayer(cell)) {
values[cell] = channel.data.string("player");
if (game.victor == null) {
values[game.moveCpu()] = channel.data.string("cpu");
}
if (game.victor) |victor| try setVictor(channel, victor);
}
}
fn setVictor(channel: jetzig.channels.Channel, victor: Game.State) !void {
try channel.put("victor", @tagName(victor));
var results = channel.getT(.object, "results") orelse return;
const count = results.getT(.integer, @tagName(victor)) orelse return;
try results.put(@tagName(victor), count + 1);
try channel.invoke(.victor, .{ .type = @tagName(victor) });
}
};

View File

@ -1,64 +0,0 @@
<link rel="stylesheet" href="/party.css" />
<div id="party-container"></div>
<div id="results-wrapper">
<span class="trophy">&#127942;</span>
<div id="results">
<div>Player</div>
<div id="player-wins" jetzig-connect="$.results.player"></div>
<div>CPU</div>
<div id="cpu-wins" jetzig-connect="$.results.cpu"></div>
<div>Tie</div>
<div id="ties" jetzig-connect="$.results.tie"></div>
</div>
<span class="trophy">&#127942;</span>
</div>
<div class="board" id="board">
@for (0..9) |index| {
<div
class="cell"
jetzig-connect="$.cells.{{index}}"
jetzig-transform="{ player: '&#9992;&#65039;', cpu: '&#129422;', tie: '&#129309;' }[value] || ''"
jetzig-click="move"
id="tic-tac-toe-cell-{{index}}"
data-cell="{{index}}"
>
</div>
}
</div>
<div id="reset-wrapper">
<button jetzig-click="reset" id="reset-button">Reset Game</button>
</div>
<div jetzig-style="{ visibility: $.victor === null ? 'hidden' : 'visible' }" id="victor">
<span>&#127942;</span>
<span
jetzig-connect="$.victor"
jetzig-transform="{ player: '&#9992;&#65039;', cpu: '&#129422;', tie: '&#129309;' }[value] || ''"
></span>
<span>&#127942;</span>
</div>
<script>
@// jetzig.channel.onStateChanged(state => {
@// });
@//
@// jetzig.channel.onMessage(data => {
@// });
@//
@// jetzig.channel.receive("victor", data => {
@// triggerPartyAnimation();
@// });
@//
@// jetzig.channel.receive("game_over", data => {
@// const element = document.querySelector("#board");
@// element.classList.remove('flash-animation');
@// void element.offsetWidth;
@// element.classList.add('flash-animation');
@// });
@//
</script>

View File

@ -7,8 +7,6 @@ const zmd = @import("zmd");
pub const routes = @import("routes"); pub const routes = @import("routes");
pub const static = @import("static"); pub const static = @import("static");
pub const std_options = jetzig.std_options;
// Override default settings in `jetzig.config` here: // Override default settings in `jetzig.config` here:
pub const jetzig_options = struct { pub const jetzig_options = struct {
/// Middleware chain. Add any custom middleware here, or use middleware provided in /// Middleware chain. Add any custom middleware here, or use middleware provided in
@ -16,9 +14,7 @@ pub const jetzig_options = struct {
pub const middleware: []const type = &.{ pub const middleware: []const type = &.{
// jetzig.middleware.AuthMiddleware, // jetzig.middleware.AuthMiddleware,
// jetzig.middleware.AntiCsrfMiddleware, // jetzig.middleware.AntiCsrfMiddleware,
jetzig.middleware.HtmxMiddleware, // jetzig.middleware.HtmxMiddleware,
jetzig.middleware.ChannelsMiddleware,
// jetzig.middleware.InertiaMiddleware,
// jetzig.middleware.CompressionMiddleware, // jetzig.middleware.CompressionMiddleware,
// @import("app/middleware/DemoMiddleware.zig"), // @import("app/middleware/DemoMiddleware.zig"),
}; };

View File

@ -11,13 +11,9 @@ mailers_path: []const u8,
buffer: std.ArrayList(u8), buffer: std.ArrayList(u8),
dynamic_routes: std.ArrayList(Function), dynamic_routes: std.ArrayList(Function),
static_routes: std.ArrayList(Function), static_routes: std.ArrayList(Function),
channel_routes: std.ArrayList([]const u8), module_paths: std.ArrayList([]const u8),
channel_actions: std.StringHashMap(std.StringHashMap([]const []const u8)),
module_paths: std.StringHashMap(void),
data: *jetzig.data.Data, data: *jetzig.data.Data,
const receive_message = "receiveMessage";
const Routes = @This(); const Routes = @This();
const Function = struct { const Function = struct {
@ -124,9 +120,7 @@ pub fn init(
.buffer = std.ArrayList(u8).init(allocator), .buffer = std.ArrayList(u8).init(allocator),
.static_routes = std.ArrayList(Function).init(allocator), .static_routes = std.ArrayList(Function).init(allocator),
.dynamic_routes = std.ArrayList(Function).init(allocator), .dynamic_routes = std.ArrayList(Function).init(allocator),
.channel_routes = std.ArrayList([]const u8).init(allocator), .module_paths = std.ArrayList([]const u8).init(allocator),
.channel_actions = std.StringHashMap(std.StringHashMap([]const []const u8)).init(allocator),
.module_paths = std.StringHashMap(void).init(allocator),
.data = data, .data = data,
}; };
} }
@ -136,7 +130,6 @@ pub fn deinit(self: *Routes) void {
self.buffer.deinit(); self.buffer.deinit();
self.static_routes.deinit(); self.static_routes.deinit();
self.dynamic_routes.deinit(); self.dynamic_routes.deinit();
self.channel_routes.deinit();
} }
/// Generates the complete route set for the application /// Generates the complete route set for the application
@ -144,7 +137,6 @@ pub fn generateRoutes(self: *Routes) ![]const u8 {
const writer = self.buffer.writer(); const writer = self.buffer.writer();
try writer.writeAll( try writer.writeAll(
\\const std = @import("std");
\\const jetzig = @import("jetzig"); \\const jetzig = @import("jetzig");
\\ \\
\\pub const routes = [_]jetzig.Route{ \\pub const routes = [_]jetzig.Route{
@ -156,16 +148,6 @@ pub fn generateRoutes(self: *Routes) ![]const u8 {
\\ \\
); );
try writer.writeAll(
\\pub const channel_routes = std.StaticStringMap(jetzig.channels.Route).initComptime(.{
\\
);
try self.writeChannelRoutes(writer);
try writer.writeAll(
\\});
\\
);
try writer.writeAll( try writer.writeAll(
\\ \\
\\pub const mailers = [_]jetzig.MailerDefinition{ \\pub const mailers = [_]jetzig.MailerDefinition{
@ -189,29 +171,16 @@ pub fn generateRoutes(self: *Routes) ![]const u8 {
\\ \\
); );
try writer.writeAll(
\\
\\pub const View = struct { name: []const u8, module: type };
\\pub const views = std.StaticStringMap(View).initComptime(.{
\\
);
try self.writeViewsMap(writer);
try writer.writeAll(
\\});
\\
);
try writer.writeAll( try writer.writeAll(
\\test { \\test {
\\ \\
); );
var it = self.module_paths.keyIterator(); for (self.module_paths.items) |module_path| {
while (it.next()) |module_path| {
try writer.print( try writer.print(
\\ _ = @import("{s}"); \\ _ = @import("{s}");
\\ \\
, .{module_path.*}); , .{module_path});
} }
try writer.writeAll( try writer.writeAll(
@ -285,10 +254,6 @@ fn writeRoutes(self: *Routes, writer: anytype) !void {
for (view_routes.dynamic) |view_route| { for (view_routes.dynamic) |view_route| {
try self.dynamic_routes.append(view_route); try self.dynamic_routes.append(view_route);
} }
for (view_routes.channel) |view_route| {
try self.channel_routes.append(view_route);
}
} }
std.sort.pdq(Function, self.static_routes.items, {}, Function.lessThanFn); std.sort.pdq(Function, self.static_routes.items, {}, Function.lessThanFn);
@ -378,7 +343,7 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
unreachable; unreachable;
std.mem.replaceScalar(u8, module_path, '\\', '/'); std.mem.replaceScalar(u8, module_path, '\\', '/');
try self.module_paths.put(try self.allocator.dupe(u8, module_path), {}); try self.module_paths.append(try self.allocator.dupe(u8, module_path));
var buf: [32]u8 = undefined; var buf: [32]u8 = undefined;
const id = jetzig.util.generateVariableName(&buf); const id = jetzig.util.generateVariableName(&buf);
@ -402,49 +367,8 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
const RouteSet = struct { const RouteSet = struct {
dynamic: []Function, dynamic: []Function,
static: []Function, static: []Function,
channel: [][]const u8,
}; };
fn writeChannelRoutes(self: *Routes, writer: anytype) !void {
for (self.channel_routes.items) |path| {
const module_path = try self.relativePathFrom(.root, path, .posix);
defer self.allocator.free(module_path);
const relative_path = try self.relativePathFrom(.views, path, .posix);
defer self.allocator.free(relative_path);
const view_name = chompExtension(relative_path);
var actions_buf = std.ArrayList(u8).init(self.allocator);
const actions_writer = actions_buf.writer();
if (self.channel_actions.get(path)) |actions| {
var it = actions.iterator();
while (it.next()) |entry| {
try actions_writer.print(
\\.{{ .name = "{s}", .params = &.{{
, .{entry.key_ptr.*});
for (entry.value_ptr.*, 0..) |param, index| {
if (index == 0) continue; // Skip `self` argument.
try actions_writer.print(
\\.{{ .name = "{s}" }},
, .{param});
}
try actions_writer.writeAll("},},");
}
}
try writer.print(
\\.{{
\\ "{0s}",
\\ jetzig.channels.Route.initComptime(
\\ @import("{1s}"),
\\ "{0s}",
\\ &.{{{2s}}}
\\ ),
\\}}
\\
, .{ view_name, module_path, actions_buf.items });
}
}
fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !RouteSet { fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !RouteSet {
const stat = try dir.statFile(path); const stat = try dir.statFile(path);
const source = try dir.readFileAllocOptions( const source = try dir.readFileAllocOptions(
@ -461,37 +385,30 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
var static_routes = std.ArrayList(Function).init(self.allocator); var static_routes = std.ArrayList(Function).init(self.allocator);
var dynamic_routes = std.ArrayList(Function).init(self.allocator); var dynamic_routes = std.ArrayList(Function).init(self.allocator);
var channel_routes = std.ArrayList([]const u8).init(self.allocator);
var static_params: ?*jetzig.data.Value = null; var static_params: ?*jetzig.data.Value = null;
for (self.ast.nodes.items(.tag), 0..) |tag, index| { for (self.ast.nodes.items(.tag), 0..) |tag, index| {
switch (tag) { switch (tag) {
.fn_proto_multi, .fn_proto_one, .fn_proto_simple => |function_tag| { .fn_proto_multi, .fn_proto_one, .fn_proto_simple => |function_tag| {
var maybe_function = try self.parseFunction( var function = try self.parseFunction(function_tag, @enumFromInt(index), path, source);
function_tag, if (function) |*capture| {
@enumFromInt(index), if (capture.args.len == 0) {
path,
source,
);
if (maybe_function) |*function| {
if (!std.mem.eql(u8, function.name, receive_message) and function.args.len == 0) {
std.debug.print( std.debug.print(
"Expected at least 1 argument for view function `{s}` in `{s}`", "Expected at least 1 argument for view function `{s}` in `{s}`",
.{ function.name, path }, .{ capture.name, path },
); );
return error.JetzigMissingViewArgument; return error.JetzigMissingViewArgument;
} }
for (function.args, 0..) |arg, arg_index| { for (capture.args, 0..) |arg, arg_index| {
if (std.mem.eql(u8, try arg.typeBasename(), "StaticRequest")) { if (std.mem.eql(u8, try arg.typeBasename(), "StaticRequest")) {
function.static = jetzig.build_options.build_static; capture.static = jetzig.build_options.build_static;
function.legacy = arg_index + 1 < function.args.len; capture.legacy = arg_index + 1 < capture.args.len;
try static_routes.append(function.*); try static_routes.append(capture.*);
} else if (std.mem.eql(u8, try arg.typeBasename(), "Request")) { } else if (std.mem.eql(u8, try arg.typeBasename(), "Request")) {
function.static = false; capture.static = false;
function.legacy = arg_index + 1 < function.args.len; capture.legacy = arg_index + 1 < capture.args.len;
try dynamic_routes.append(function.*); try dynamic_routes.append(capture.*);
} }
} }
} }
@ -505,28 +422,6 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
static_params = self.data.value; static_params = self.data.value;
} }
}, },
.container_decl_two,
.container_decl_two_trailing,
.container_decl,
.container_decl_trailing,
=> |container_tag| {
var buf: [2]std.zig.Ast.Node.Index = undefined;
const container = switch (container_tag) {
.container_decl_two,
.container_decl_two_trailing,
=> self.ast.containerDeclTwo(&buf, @enumFromInt(index)),
.container_decl,
.container_decl_trailing,
=> self.ast.containerDecl(@enumFromInt(index)),
else => unreachable,
};
const container_token = container.ast.main_token;
const decl_name = self.ast.tokenSlice(container_token - 2);
if (std.mem.eql(u8, decl_name, "Channel")) {
try channel_routes.append(path);
try self.parseChannel(container, path);
}
},
else => {}, else => {},
} }
} }
@ -552,87 +447,9 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
return .{ return .{
.dynamic = dynamic_routes.items, .dynamic = dynamic_routes.items,
.static = static_routes.items, .static = static_routes.items,
.channel = channel_routes.items,
}; };
} }
// Although we mostly evaluate channel routes at comptime, we need to parse the function
// signatures in `Actions` to get argument names (Zig only reflects the types).
fn parseChannel(self: *Routes, channel: std.zig.Ast.full.ContainerDecl, path: []const u8) !void {
for (channel.ast.members) |member| {
const tag = self.ast.nodeTag(member);
switch (tag) {
.simple_var_decl => {
const var_decl = self.ast.simpleVarDecl(member);
const var_name = self.ast.tokenSlice(self.ast.nodeMainToken(member) + 1);
if (std.mem.eql(u8, var_name, "Actions")) {
const init_node = var_decl.ast.init_node.unwrap() orelse continue;
switch (self.ast.nodeTag(init_node)) {
.container_decl_two,
.container_decl_two_trailing,
.container_decl,
.container_decl_trailing,
=> |container_tag| {
var buf: [2]std.zig.Ast.Node.Index = undefined;
const container = switch (container_tag) {
.container_decl_two,
.container_decl_two_trailing,
=> self.ast.containerDeclTwo(&buf, init_node),
.container_decl,
.container_decl_trailing,
=> self.ast.containerDecl(init_node),
else => unreachable,
};
const container_token = container.ast.main_token;
const decl_name = self.ast.tokenSlice(container_token - 2);
if (std.mem.eql(u8, decl_name, "Actions")) {
try self.parseChannelActions(container, path);
}
},
else => continue,
}
}
},
else => {},
}
}
}
fn parseChannelActions(self: *Routes, actions: std.zig.Ast.full.ContainerDecl, path: []const u8) !void {
for (actions.ast.members) |member| {
const tag = self.ast.nodes.items(.tag)[@intFromEnum(member)];
switch (tag) {
.fn_proto,
.fn_proto_multi,
.fn_proto_one,
.fn_proto_simple,
.fn_decl,
=> {
var buf: [1]std.zig.Ast.Node.Index = undefined;
const func = self.ast.fullFnProto(&buf, member).?;
const visib_token = func.visib_token orelse continue;
if (!std.mem.eql(u8, self.ast.tokenSlice(visib_token), "pub")) continue;
const func_name_token = func.name_token orelse continue;
const func_name = self.ast.tokenSlice(func_name_token);
var params_buf = std.ArrayList([]const u8).init(self.allocator);
var params_it = func.iterate(&self.ast);
while (params_it.next()) |param| {
try params_buf.append(
try self.allocator.dupe(u8, self.ast.tokenSlice(param.name_token.?)),
);
}
const result = try self.channel_actions.getOrPut(path);
if (!result.found_existing) {
result.value_ptr.* = std.StringHashMap([]const []const u8).init(self.allocator);
}
try result.value_ptr.put(try self.allocator.dupe(u8, func_name), try params_buf.toOwnedSlice());
},
else => {},
}
}
}
// Parse the `pub const static_params` definition and into a `jetzig.data.Value`. // Parse the `pub const static_params` definition and into a `jetzig.data.Value`.
fn parseStaticParamsDecl(self: *Routes, decl: std.zig.Ast.full.VarDecl, params: *jetzig.data.Value) !void { fn parseStaticParamsDecl(self: *Routes, decl: std.zig.Ast.full.VarDecl, params: *jetzig.data.Value) !void {
const init_node = decl.ast.init_node.unwrap() orelse return; const init_node = decl.ast.init_node.unwrap() orelse return;
@ -801,10 +618,6 @@ fn parseFunction(
var it = fn_proto.iterate(&self.ast); var it = fn_proto.iterate(&self.ast);
while (it.next()) |arg| { while (it.next()) |arg| {
// We don't need to resolve args for `receiveMessage` as it only has one form (it was
// added after the removal of the `data` arg from view functions).
if (std.mem.eql(u8, receive_message, function_name)) continue;
if (arg.name_token) |arg_token| { if (arg.name_token) |arg_token| {
const arg_name = self.ast.tokenSlice(arg_token); const arg_name = self.ast.tokenSlice(arg_token);
const node = self.ast.nodes.get(@intFromEnum(arg.type_expr.?)); const node = self.ast.nodes.get(@intFromEnum(arg.type_expr.?));
@ -832,7 +645,7 @@ fn parseFunction(
fn parseTypeExpr(self: *Routes, node: std.zig.Ast.Node) ![]const u8 { fn parseTypeExpr(self: *Routes, node: std.zig.Ast.Node) ![]const u8 {
switch (node.tag) { switch (node.tag) {
// Currently all expected params are pointers, keeping this here in case that changes in future: // Currently all expected params are pointers, keeping this here in case that changes in future:
.identifier => return self.ast.tokenSlice(@as(u32, @intCast(node.main_token))), .identifier => {},
.ptr_type_aligned => { .ptr_type_aligned => {
var buf = std.ArrayList([]const u8).init(self.allocator); var buf = std.ArrayList([]const u8).init(self.allocator);
defer buf.deinit(); defer buf.deinit();
@ -850,14 +663,6 @@ fn parseTypeExpr(self: *Routes, node: std.zig.Ast.Node) ![]const u8 {
else => {}, else => {},
} }
// TODO: Output source line
std.log.err(
"Unexpected token type `{s}` in `{s}`",
.{
@tagName(node.tag),
self.ast.tokenSlice(@as(u32, @intCast(node.main_token))),
},
);
return error.JetzigAstParserError; return error.JetzigAstParserError;
} }
@ -866,7 +671,7 @@ fn isActionFunctionName(name: []const u8) bool {
if (std.mem.eql(u8, field.name, name)) return true; if (std.mem.eql(u8, field.name, name)) return true;
} }
return std.mem.eql(u8, receive_message, name); return false;
} }
inline fn chompExtension(path: []const u8) []const u8 { inline fn chompExtension(path: []const u8) []const u8 {
@ -998,18 +803,3 @@ fn writeJobs(self: Routes, writer: anytype) !void {
std.debug.print("[jetzig] Imported {} job(s)\n", .{count}); std.debug.print("[jetzig] Imported {} job(s)\n", .{count});
} }
fn writeViewsMap(self: Routes, writer: anytype) !void {
var it = self.module_paths.keyIterator();
while (it.next()) |path| {
try writer.print(
\\.{{ "{0s}", View{{ .name = "{0s}", .module = @import("{1s}") }} }},
\\
,
.{
chompExtension(try self.relativePathFrom(.views, path.*, .posix)),
path.*,
},
);
}
}

View File

@ -147,7 +147,7 @@ fn renderMarkdown(
if (zmpl.findPrefixed("views", prefixed_name)) |layout| { if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
view.data.content = .{ .data = content }; view.data.content = .{ .data = content };
return try layout.render(view.data, jetzig.TemplateContext, .{}, &.{}, .{}); return try layout.render(view.data, jetzig.TemplateContext, .{}, .{});
} else { } else {
std.debug.print("Unknown layout: {s}\n", .{layout_name}); std.debug.print("Unknown layout: {s}\n", .{layout_name});
return content; return content;
@ -174,7 +174,6 @@ fn renderZmplTemplate(
view.data, view.data,
jetzig.TemplateContext, jetzig.TemplateContext,
.{}, .{},
&.{},
.{ .layout = layout }, .{ .layout = layout },
); );
} else { } else {
@ -182,7 +181,7 @@ fn renderZmplTemplate(
return try allocator.dupe(u8, ""); return try allocator.dupe(u8, "");
} }
} else { } else {
return try template.render(view.data, jetzig.TemplateContext, .{}, &.{}, .{}); return try template.render(view.data, jetzig.TemplateContext, .{}, .{});
} }
} else return null; } else return null;
} }

View File

@ -25,8 +25,6 @@ pub const auth = @import("jetzig/auth.zig");
pub const callbacks = @import("jetzig/callbacks.zig"); pub const callbacks = @import("jetzig/callbacks.zig");
pub const debug = @import("jetzig/debug.zig"); pub const debug = @import("jetzig/debug.zig");
pub const TemplateContext = @import("jetzig/TemplateContext.zig"); pub const TemplateContext = @import("jetzig/TemplateContext.zig");
pub const websockets = @import("jetzig/websockets.zig");
pub const channels = @import("jetzig/channels.zig");
pub const DateTime = jetcommon.types.DateTime; pub const DateTime = jetcommon.types.DateTime;
pub const Time = jetcommon.types.Time; pub const Time = jetcommon.types.Time;
@ -40,20 +38,6 @@ pub const environment = @field(
@tagName(build_options.environment), @tagName(build_options.environment),
); );
pub fn logFn(
comptime level: std.log.Level,
comptime scope: @Type(.enum_literal),
comptime format: []const u8,
args: anytype,
) void {
if (scope == .websocket) return; // We handle our own websocket event logging.
std.log.defaultLog(level, scope, format, args);
}
pub const std_options: std.Options = .{
.logFn = logFn,
};
/// The primary interface for a Jetzig application. Create an `App` in your application's /// The primary interface for a Jetzig application. Create an `App` in your application's
/// `src/main.zig` and call `start` to launch the application. /// `src/main.zig` and call `start` to launch the application.
pub const App = @import("jetzig/App.zig"); pub const App = @import("jetzig/App.zig");

View File

@ -11,7 +11,7 @@ env: jetzig.Environment,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
custom_routes: std.ArrayList(jetzig.views.Route), custom_routes: std.ArrayList(jetzig.views.Route),
initHook: ?*const fn (*App) anyerror!void, initHook: ?*const fn (*App) anyerror!void,
server: *anyopaque = undefined, server: *jetzig.http.Server = undefined,
pub fn deinit(self: *const App) void { pub fn deinit(self: *const App) void {
@constCast(self).custom_routes.deinit(); @constCast(self).custom_routes.deinit();
@ -34,15 +34,7 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
defer mime_map.deinit(); defer mime_map.deinit();
try mime_map.build(); try mime_map.build();
inline for (jetzig.http.middleware.middlewares) |middleware| { const routes = try createRoutes(self.allocator, if (@hasDecl(routes_module, "routes")) &routes_module.routes else &.{});
if (@hasDecl(middleware, "setup")) try middleware.setup(@constCast(self));
}
const routes = try createRoutes(self.allocator, if (@hasDecl(routes_module, "routes"))
&routes_module.routes
else
&.{});
defer { defer {
for (routes) |var_route| { for (routes) |var_route| {
var_route.deinitParams(); var_route.deinitParams();
@ -55,11 +47,6 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
self.allocator.free(custom_route.template); self.allocator.free(custom_route.template);
}; };
const channel_routes = if (@hasDecl(routes_module, "channel_routes"))
routes_module.channel_routes
else
std.StaticStringMap(jetzig.channels.Route).initComptime(.{});
var store = try jetzig.kv.Store.GeneralStore.init(self.allocator, self.env.logger, .general); var store = try jetzig.kv.Store.GeneralStore.init(self.allocator, self.env.logger, .general);
defer store.deinit(); defer store.deinit();
@ -69,9 +56,6 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
var cache = try jetzig.kv.Store.CacheStore.init(self.allocator, self.env.logger, .cache); var cache = try jetzig.kv.Store.CacheStore.init(self.allocator, self.env.logger, .cache);
defer cache.deinit(); defer cache.deinit();
var channels = try jetzig.kv.Store.CacheStore.init(self.allocator, self.env.logger, .channels);
defer channels.deinit();
var repo = try jetzig.database.repo(self.allocator, self); var repo = try jetzig.database.repo(self.allocator, self);
defer repo.deinit(); defer repo.deinit();
@ -97,11 +81,10 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
std.process.exit(0); std.process.exit(0);
} }
var server = jetzig.http.Server.RoutedServer(routes_module).init( var server = jetzig.http.Server.init(
self.allocator, self.allocator,
self.env, self.env,
routes, routes,
channel_routes,
self.custom_routes.items, self.custom_routes.items,
if (@hasDecl(routes_module, "jobs")) &routes_module.jobs else &.{}, if (@hasDecl(routes_module, "jobs")) &routes_module.jobs else &.{},
if (@hasDecl(routes_module, "jobs")) &routes_module.mailers else &.{}, if (@hasDecl(routes_module, "jobs")) &routes_module.mailers else &.{},
@ -109,7 +92,6 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
&store, &store,
&job_queue, &job_queue,
&cache, &cache,
&channels,
&repo, &repo,
options.global, options.global,
); );

View File

@ -1,43 +1,13 @@
const std = @import("std"); const std = @import("std");
pub const http = @import("http.zig"); pub const http = @import("http.zig");
pub const views = @import("views.zig");
pub const config = @import("config.zig"); pub const config = @import("config.zig");
/// Context available in every Zmpl template as `context`. /// Context available in every Zmpl template as `context`.
pub const TemplateContext = @This(); pub const TemplateContext = @This();
request: ?*http.Request = null, request: ?*http.Request = null,
route: ?views.Route = null,
middleware: Middleware = .{},
pub const Middleware = struct {
context: *TemplateContext = undefined,
pub inline fn renderHeader(middleware: Middleware) ![]const u8 {
return try middleware.render("header");
}
pub inline fn renderFooter(middleware: Middleware) ![]const u8 {
return try middleware.render("footer");
}
fn render(middleware: Middleware, comptime section: []const u8) ![]const u8 {
var buf = std.ArrayList(u8).init(middleware.context.*.request.?.allocator);
const writer = buf.writer();
inline for (http.middleware.middlewares) |middleware_type| {
if (@hasDecl(middleware_type, "Blocks") and @hasDecl(middleware_type.Blocks, section)) {
const renderFn = @field(middleware_type.Blocks, section);
try renderFn(middleware.context.*, writer);
}
}
return try buf.toOwnedSlice();
}
};
/// Return an authenticity token stored in the current request's session. If no token exists,
/// generate and store before returning.
/// Use to create a form element which can be verified by `AntiCsrfMiddleware`.
pub fn authenticityToken(self: TemplateContext) !?[]const u8 { pub fn authenticityToken(self: TemplateContext) !?[]const u8 {
return if (self.request) |request| return if (self.request) |request|
try request.authenticityToken() try request.authenticityToken()
@ -45,8 +15,6 @@ pub fn authenticityToken(self: TemplateContext) !?[]const u8 {
null; null;
} }
/// Generate a hidden form element containing an authenticity token provided by
/// `authenticityToken`. Use as `{{context.authenticityFormElement()}}` in a Zmpl template.
pub fn authenticityFormElement(self: TemplateContext) !?[]const u8 { pub fn authenticityFormElement(self: TemplateContext) !?[]const u8 {
return if (self.request) |request| blk: { return if (self.request) |request| blk: {
const token = try request.authenticityToken(); const token = try request.authenticityToken();
@ -55,10 +23,3 @@ pub fn authenticityFormElement(self: TemplateContext) !?[]const u8 {
, .{ config.get([]const u8, "authenticity_token_name"), token }); , .{ config.get([]const u8, "authenticity_token_name"), token });
} else null; } else null;
} }
pub fn path(self: TemplateContext) ?[]const u8 {
return if (self.request) |request|
request.path.path
else
null;
}

View File

@ -1,7 +0,0 @@
pub const RoutedChannel = @import("channels/Channel.zig").RoutedChannel;
pub const Message = @import("channels/Message.zig");
pub const Route = @import("channels/Route.zig");
pub const ActionRouter = @import("channels/ActionRouter.zig");
// For convenience in channel callback functions implemented by users.
pub const Channel = RoutedChannel(@import("root").routes);

View File

@ -1,235 +0,0 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
pub const Action = struct {
view: []const u8,
name: []const u8,
params: []const std.builtin.Type.Fn.Param,
};
pub const ActionRouter = struct {
actions: []const Action,
routes: type,
encoded_params: std.StaticStringMap([]const u8),
pub fn invoke(
comptime router: ActionRouter,
allocator: std.mem.Allocator,
path: []const u8,
data: []const u8,
Channel: type,
channel: Channel,
) !?[]const u8 {
inline for (router.actions) |action| {
if (match(action, path, data)) {
var d = jetzig.data.Data.init(allocator);
defer d.deinit();
// Format should be at least e.g.: `_invoke:foo:[]`
if (data.len < prefix(action).len + 2) return error.InvalidChannelActionArguments;
try d.fromJson(data[prefix(action).len..]);
const received_args = switch (d.value.?.*) {
.array => |array| array.array.items,
else => return error.InvalidChannelActionArguments,
};
const view = router.routes.views.get(action.view).?;
const func = @field(view.module.Channel.Actions, action.name);
const Args = std.meta.ArgsTuple(@TypeOf(func));
var args: Args = undefined;
const expected_args = std.meta.fields(Args);
if (expected_args.len < 1 or received_args.len != expected_args.len - 1) {
return error.InvalidChannelActionArguments;
}
args[0] = channel;
if (comptime action.params.len > 1) {
inline for (action.params[1..], 0..) |param, index| {
args[index + 1] = try coerce(param.type.?, received_args[index].*);
}
}
try @call(.auto, func, args);
return action.name;
}
}
return null;
}
pub fn encodedParams(comptime router: ActionRouter, route: jetzig.channels.Route) ?[]const u8 {
if (router.routes.channel_routes.get(route.path)) |matched_route| {
_ = matched_route;
}
}
fn match(comptime action: Action, path: []const u8, data: []const u8) bool {
return (std.mem.eql(u8, action.view, path)) and std.mem.startsWith(
u8,
data,
prefix(action),
);
}
inline fn prefix(comptime action: Action) []const u8 {
return "_invoke:" ++ action.name ++ ":";
}
fn coerce(T: type, value: jetzig.data.Value) !T {
return switch (T) {
[]const u8 => switch (value) {
.string => |v| v.value,
else => error.InvalidChannelActionArguments,
},
else => switch (@typeInfo(T)) {
.int => switch (value) {
.integer => |v| @intCast(v.value),
else => error.InvalidChannelActionArguments,
},
.float => switch (value) {
.float => |v| @floatCast(v.value),
else => error.InvalidChannelActionArguments,
},
.bool => switch (value) {
.boolean => |v| v.value,
else => error.InvalidChannelActionArguments,
},
else => error.InvalidChannelActionArguments,
},
};
}
};
pub fn initComptime(Routes: type) ActionRouter {
comptime {
var len: usize = 0;
for (Routes.views.values()) |view| {
if (!@hasDecl(view.module, "Channel")) continue;
if (!@hasDecl(view.module.Channel, "Actions")) continue;
const actions = view.module.Channel.Actions;
for (std.meta.declarations(actions)) |_| {
len += 1;
}
}
var actions: [len]Action = undefined;
var index: usize = 0;
for (Routes.views.values()) |view| {
if (!@hasDecl(view.module, "Channel")) continue;
if (!@hasDecl(view.module.Channel, "Actions")) continue;
const channel_actions = view.module.Channel.Actions;
const decls = std.meta.declarations(channel_actions);
for (decls) |decl| {
const params = @typeInfo(
@TypeOf(@field(view.module.Channel.Actions, decl.name)),
).@"fn".params;
actions[index] = .{
.view = view.name,
.name = decl.name,
.params = params,
};
index += 1;
}
}
const encoded_params = try encodeParams(Routes);
const result = actions;
return .{ .actions = &result, .routes = Routes, .encoded_params = encoded_params };
}
}
fn encodeParams(Routes: type) !std.StaticStringMap([]const u8) {
// We do a bit of awkward encoding here to ensure that we have a pre-compiled JSON string
// that we can send to the websocket after intialization to give the Jetzig Javascript code a
// spec for all available actions.
comptime {
const Spec = struct {
actions: []ActionSpec,
pub const ActionSpec = struct {
name: []const u8,
params: []const ParamSpec,
pub const ParamSpec = struct {
type: []const u8,
name: []const u8,
};
};
};
const Tuple = std.meta.Tuple(&.{ []const u8, []const u8 });
var map: [Routes.views.keys().len]Tuple = undefined;
for (Routes.views.values(), 0..) |view, view_index| {
const has_actions = @hasDecl(view.module, "Channel") and
@hasDecl(view.module.Channel, "Actions");
const channel_actions = if (has_actions) view.module.Channel.Actions else struct {};
const decls = std.meta.declarations(channel_actions);
var channel_params: Spec = undefined;
var actions: [decls.len]Spec.ActionSpec = undefined;
for (decls, 0..) |decl, decl_index| {
switch (@typeInfo(@TypeOf(@field(view.module.Channel.Actions, decl.name)))) {
.@"fn" => |info| {
verifyParams(info.params, view.name, decl.name);
const route = Routes.channel_routes.get(view.name).?;
const action = for (route.actions) |action| {
if (std.mem.eql(u8, action.name, decl.name)) break action;
} else unreachable;
if (info.params.len > 1) {
var params: [info.params.len - 1]Spec.ActionSpec.ParamSpec = undefined;
for (info.params[1..], 0..) |param, param_index| {
params[param_index] = .{
.type = jsonTypeName(param.type.?),
.name = action.params[param_index].name,
};
}
actions[decl_index] = .{ .name = decl.name, .params = &params };
} else {
actions[decl_index] = .{ .name = decl.name, .params = &.{} };
}
},
else => {},
}
}
channel_params.actions = &actions;
var counting_stream = std.io.countingWriter(std.io.null_writer);
try std.json.stringify(channel_params, .{}, counting_stream.writer());
var buf: [counting_stream.bytes_written]u8 = undefined;
var stream = std.io.fixedBufferStream(&buf);
try std.json.stringify(channel_params, .{}, stream.writer());
const written = buf;
map[view_index] = .{ view.name, &written };
}
return std.StaticStringMap([]const u8).initComptime(map);
}
}
fn verifyParams(
params: []const std.builtin.Type.Fn.Param,
view: []const u8,
action: []const u8,
) void {
const humanized = std.fmt.comptimePrint("Channel Action {s}:{s}", .{ view, action });
const too_few_params = "Expected at least 1 parameter for " ++ humanized;
const missing_param = "Incorrect first argument (must be jetzig.channels.Channel) for " ++ humanized;
if (params.len < 1) @compileError(too_few_params);
if (params[0].type.? != jetzig.channels.Channel) @compileError(missing_param);
}
fn jsonTypeName(T: type) []const u8 {
return switch (T) {
[]const u8 => "string",
else => switch (@typeInfo(T)) {
.float, .comptime_float => "float",
.int, .comptime_int => "integer",
.bool => "bool",
else => @compileError("Unsupported Channel Action argument type: " ++ @typeName(T)),
},
};
}

View File

@ -1,82 +0,0 @@
const std = @import("std");
const httpz = @import("httpz");
const jetzig = @import("../../jetzig.zig");
pub fn RoutedChannel(Routes: type) type {
return struct {
const Channel = @This();
allocator: std.mem.Allocator,
websocket: *jetzig.websockets.RoutedWebsocket(Routes),
state: *jetzig.data.Value,
data: *jetzig.data.Data,
pub fn publish(channel: Channel, data: anytype) !void {
var stack_fallback = std.heap.stackFallback(4096, channel.allocator);
const allocator = stack_fallback.get();
var write_buffer = channel.websocket.connection.writeBuffer(allocator, .text);
defer write_buffer.deinit();
const writer = write_buffer.writer();
try std.json.stringify(data, .{}, writer);
try write_buffer.flush();
channel.websocket.logger.DEBUG(
"Published Channel message for `{s}`",
.{channel.websocket.route.path},
) catch {};
}
pub fn invoke(
channel: Channel,
comptime method: @TypeOf(.enum_literal),
args: anytype,
) !void {
// TODO: DRY
var stack_fallback = std.heap.stackFallback(4096, channel.allocator);
const allocator = stack_fallback.get();
var write_buffer = channel.websocket.connection.writeBuffer(allocator, .text);
defer write_buffer.deinit();
const writer = write_buffer.writer();
try writer.writeAll("__jetzig_event__:");
try std.json.stringify(.{ .method = method, .params = args }, .{}, writer);
try write_buffer.flush();
channel.websocket.logger.DEBUG(
"Invoked Javascript function `{s}` for `{s}`",
.{ @tagName(method), channel.websocket.route.path },
) catch {};
}
pub fn getT(
channel: Channel,
comptime T: jetzig.data.Data.ValueType,
key: []const u8,
) @TypeOf(channel.state.getT(T, key)) {
return channel.state.getT(T, key);
}
pub fn get(channel: Channel, key: []const u8) ?*jetzig.data.Value {
return channel.state.get(key);
}
pub fn put(
channel: Channel,
key: []const u8,
value: anytype,
) @TypeOf(channel.state.put(key, value)) {
return try channel.state.put(key, value);
}
pub fn remove(channel: Channel, key: []const u8) bool {
return channel.state.remove(key);
}
pub fn sync(channel: Channel) !void {
try channel.websocket.syncState(channel);
}
};
}

View File

@ -1,54 +0,0 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
const Message = @This();
allocator: std.mem.Allocator,
payload: []const u8,
data: *jetzig.data.Data,
channel: jetzig.channels.Channel,
pub fn init(
allocator: std.mem.Allocator,
channel: jetzig.channels.Channel,
payload: []const u8,
) Message {
return .{
.allocator = allocator,
.channel = channel,
.data = channel.data,
.payload = payload,
};
}
pub fn params(message: Message) !?*jetzig.data.Value {
var d = try message.allocator.create(jetzig.data.Data);
d.* = jetzig.data.Data.init(message.allocator);
d.fromJson(message.payload) catch |err| {
switch (err) {
error.SyntaxError => {
message.channel.websocket.logger.ERROR("Invalid JSON received in Channel message.", .{}) catch {};
},
else => {
message.channel.websocket.logger.logError(@errorReturnTrace(), err) catch {};
},
}
return null;
};
return d.value;
}
test "message with payload" {
const message = Message.init(
std.testing.allocator,
jetzig.channels.Channel{
.websocket = undefined,
.state = undefined,
.allocator = undefined,
.data = undefined,
},
"foo",
);
try std.testing.expectEqualStrings(message.payload, "foo");
}

View File

@ -1,36 +0,0 @@
const jetzig = @import("../../jetzig.zig");
const Route = @This();
receiveMessageFn: ?*const fn (jetzig.channels.Message) anyerror!void = null,
openConnectionFn: ?*const fn (jetzig.channels.Channel) anyerror!void = null,
path: []const u8,
actions: []const Action,
pub const Action = struct {
name: []const u8,
params: []const Param,
pub const Param = struct {
name: []const u8,
};
};
pub fn receiveMessage(route: Route, message: jetzig.channels.Message) !void {
if (route.receiveMessageFn) |func| try func(message);
}
pub fn initComptime(T: type, path: []const u8, actions: []const Action) Route {
comptime {
if (!@hasDecl(T, "Channel")) return .{};
const openConnectionFn = if (@hasDecl(T.Channel, "open")) T.Channel.open else null;
const receiveMessageFn = if (@hasDecl(T.Channel, "receive")) T.Channel.receive else null;
return .{
.openConnectionFn = openConnectionFn,
.receiveMessageFn = receiveMessageFn,
.path = path,
.actions = actions,
};
}
}

View File

@ -145,19 +145,6 @@ pub const cache: kv.Store.Options = .{
// }, // },
}; };
/// Channels. Identical to `store` options, but allows using different
/// backends (e.g. `.memory` for key-value store, `.file` for cache.
/// Channel state data is stored here.
pub const channels: kv.Store.Options = .{
.backend = .memory,
// .backend = .file,
// .file_options = .{
// .path = "/path/to/jetkv-channels.db",
// .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.JetKV.addressSpace(4096),
// },
};
/// SMTP configuration for Jetzig Mail. /// SMTP configuration for Jetzig Mail.
pub const smtp: mail.SMTPConfig = .{ pub const smtp: mail.SMTPConfig = .{
.port = 25, .port = 25,

View File

@ -39,10 +39,9 @@ pub fn repo(allocator: std.mem.Allocator, app: anytype) !Repo {
} }
fn eventCallback(event: jetzig.jetquery.events.Event, app: anytype) !void { fn eventCallback(event: jetzig.jetquery.events.Event, app: anytype) !void {
var server: *jetzig.http.Server.RoutedServer(@import("root").routes) = @ptrCast(@alignCast(app.server)); try app.server.logger.logSql(event);
try server.logger.logSql(event);
if (event.err) |err| { if (event.err) |err| {
try server.logger.ERROR("[database] {?s}", .{err.message}); try app.server.logger.ERROR("[database] {?s}", .{err.message});
} }
} }

View File

@ -41,12 +41,6 @@ pub fn get(self: Headers, name: []const u8) ?[]const u8 {
return self.httpz_headers.get(lower); return self.httpz_headers.get(lower);
} }
/// Get the first value for a given header identified by `name`, which is assumed to be lower case.
pub fn getLower(self: Headers, name: []const u8) ?[]const u8 {
std.debug.assert(name.len <= max_bytes_header_name);
return self.httpz_headers.get(name);
}
/// Get all values for a given header identified by `name`. Names are case insensitive. /// Get all values for a given header identified by `name`. Names are case insensitive.
pub fn getAll(self: Headers, name: []const u8) []const []const u8 { pub fn getAll(self: Headers, name: []const u8) []const []const u8 {
var headers = std.ArrayList([]const u8).init(self.allocator); var headers = std.ArrayList([]const u8).init(self.allocator);

View File

@ -13,7 +13,6 @@ path: []const u8,
base_path: []const u8, base_path: []const u8,
directory: []const u8, directory: []const u8,
file_path: []const u8, file_path: []const u8,
view_name: []const u8,
resource_id: []const u8, resource_id: []const u8,
extension: ?[]const u8, extension: ?[]const u8,
query: ?[]const u8, query: ?[]const u8,
@ -30,7 +29,6 @@ pub fn init(path: []const u8) Path {
.base_path = base_path, .base_path = base_path,
.directory = getDirectory(base_path), .directory = getDirectory(base_path),
.file_path = getFilePath(path), .file_path = getFilePath(path),
.view_name = std.mem.trimLeft(u8, base_path, "/"),
.resource_id = getResourceId(base_path), .resource_id = getResourceId(base_path),
.extension = getExtension(path), .extension = getExtension(path),
.query = getQuery(path), .query = getQuery(path),
@ -416,8 +414,3 @@ test ".method (/foo/bar/1/_PATCH" {
const path = Path.init("/foo/bar/1/_PATCH"); const path = Path.init("/foo/bar/1/_PATCH");
try std.testing.expect(path.method.? == .PATCH); try std.testing.expect(path.method.? == .PATCH);
} }
test ".view_name" {
const path = Path.init("/foo/bar");
try std.testing.expectEqualStrings("foo/bar", path.view_name);
}

View File

@ -15,9 +15,7 @@ pub const RequestState = enum {
initial, // No processing has taken place initial, // No processing has taken place
processed, // Request headers have been processed processed, // Request headers have been processed
after_request, // Initial middleware processing after_request, // Initial middleware processing
after_view, // View returned, response data ready for full response render
rendered, // Rendered by middleware or view rendered, // Rendered by middleware or view
rendered_content, // Rendered a plain string by middleware or view
redirected, // Redirected by middleware or view redirected, // Redirected by middleware or view
failed, // Failed by middleware or view failed, // Failed by middleware or view
before_response, // Post middleware processing before_response, // Post middleware processing
@ -28,7 +26,7 @@ allocator: std.mem.Allocator,
path: jetzig.http.Path, path: jetzig.http.Path,
method: Method, method: Method,
headers: jetzig.http.Headers, headers: jetzig.http.Headers,
host: []const u8, server: *jetzig.http.Server,
httpz_request: *httpz.Request, httpz_request: *httpz.Request,
httpz_response: *httpz.Response, httpz_response: *httpz.Response,
response: *jetzig.http.Response, response: *jetzig.http.Response,
@ -54,14 +52,8 @@ rendered_view: ?jetzig.views.View = null,
start_time: i128, start_time: i128,
store: RequestStore(jetzig.kv.Store.GeneralStore), store: RequestStore(jetzig.kv.Store.GeneralStore),
cache: RequestStore(jetzig.kv.Store.CacheStore), cache: RequestStore(jetzig.kv.Store.CacheStore),
job_queue: RequestStore(jetzig.kv.Store.JobQueueStore),
job_definitions: []const jetzig.JobDefinition,
mailer_definitions: []const jetzig.MailerDefinition,
repo: *jetzig.database.Repo, repo: *jetzig.database.Repo,
global: *jetzig.Global, global: *jetzig.Global,
env: jetzig.Environment,
routes: []const *const jetzig.views.Route,
logger: jetzig.loggers.Logger,
/// Wrapper for KV store that uses the request's arena allocator for fetching values. /// Wrapper for KV store that uses the request's arena allocator for fetching values.
pub fn RequestStore(T: type) type { pub fn RequestStore(T: type) type {
@ -130,20 +122,12 @@ pub fn RequestStore(T: type) type {
pub fn init( pub fn init(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
server: *jetzig.http.Server,
start_time: i128, start_time: i128,
httpz_request: *httpz.Request, httpz_request: *httpz.Request,
httpz_response: *httpz.Response, httpz_response: *httpz.Response,
response: *jetzig.http.Response, response: *jetzig.http.Response,
repo: *jetzig.database.Repo, repo: *jetzig.database.Repo,
env: jetzig.Environment,
routes: []const *const jetzig.views.Route,
logger: jetzig.loggers.Logger,
store: *jetzig.kv.Store.GeneralStore,
cache: *jetzig.kv.Store.CacheStore,
job_queue: *jetzig.kv.Store.JobQueueStore,
job_definitions: []const jetzig.JobDefinition,
mailer_definitions: []const jetzig.MailerDefinition,
global: *anyopaque,
) !Request { ) !Request {
const path = jetzig.http.Path.init(httpz_request.url.raw); const path = jetzig.http.Path.init(httpz_request.url.raw);
@ -162,33 +146,25 @@ pub fn init(
const response_data = try allocator.create(jetzig.data.Data); const response_data = try allocator.create(jetzig.data.Data);
response_data.* = jetzig.data.Data.init(allocator); response_data.* = jetzig.data.Data.init(allocator);
const headers = jetzig.http.Headers.init(allocator, httpz_request.headers);
const host = headers.getLower("host") orelse "";
return .{ return .{
.allocator = allocator, .allocator = allocator,
.path = path, .path = path,
.method = method, .method = method,
.headers = headers, .headers = jetzig.http.Headers.init(allocator, httpz_request.headers),
.host = host, .server = server,
.response = response, .response = response,
.response_data = response_data, .response_data = response_data,
.httpz_request = httpz_request, .httpz_request = httpz_request,
.httpz_response = httpz_response, .httpz_response = httpz_response,
.start_time = start_time, .start_time = start_time,
.store = .{ .store = store, .allocator = allocator }, .store = .{ .store = server.store, .allocator = allocator },
.cache = .{ .store = cache, .allocator = allocator }, .cache = .{ .store = server.cache, .allocator = allocator },
.job_queue = .{ .store = job_queue, .allocator = allocator },
.job_definitions = job_definitions,
.mailer_definitions = mailer_definitions,
.env = env,
.routes = routes,
.logger = logger,
.repo = repo, .repo = repo,
.global = if (@hasField(jetzig.Global, "__jetzig_default")) .global = if (@hasField(jetzig.Global, "__jetzig_default"))
undefined undefined
else else
@ptrCast(@alignCast(global)), @ptrCast(@alignCast(server.global)),
}; };
} }
@ -241,20 +217,6 @@ pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode)
return self.rendered_view.?; return self.rendered_view.?;
} }
/// Render a response with pre-rendered content. This function can only be called once per
/// request (repeat calls will trigger an error).
pub fn renderContent(
self: *Request,
status_code: jetzig.http.status_codes.StatusCode,
content: []const u8,
) jetzig.views.View {
if (self.isRendered()) self.rendered_multiple = true;
self.rendered_view = .{ .data = self.response_data, .status_code = status_code, .content = content };
self.state = .rendered_content;
return self.rendered_view.?;
}
/// Render an error. This function can only be called once per request (repeat calls will /// Render an error. This function can only be called once per request (repeat calls will
/// trigger an error). /// trigger an error).
pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View { pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
@ -268,7 +230,7 @@ pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) je
pub inline fn isRendered(self: *const Request) bool { pub inline fn isRendered(self: *const Request) bool {
return switch (self.state) { return switch (self.state) {
.initial, .processed, .after_request, .before_response => false, .initial, .processed, .after_request, .before_response => false,
.after_view, .rendered, .rendered_content, .redirected, .failed, .finalized => true, .rendered, .redirected, .failed, .finalized => true,
}; };
} }
@ -336,9 +298,6 @@ pub fn renderRedirect(self: *Request, state: RedirectState) !void {
var root = try self.response_data.root(.object); var root = try self.response_data.root(.object);
try root.put("location", self.response_data.string(state.location)); try root.put("location", self.response_data.string(state.location));
var template_context = jetzig.TemplateContext{ .request = self };
template_context.middleware.context = &template_context;
const content = switch (self.requestFormat()) { const content = switch (self.requestFormat()) {
.HTML, .UNKNOWN => if (maybe_template) |template| blk: { .HTML, .UNKNOWN => if (maybe_template) |template| blk: {
try view.data.addConst("jetzig_view", view.data.string("internal")); try view.data.addConst("jetzig_view", view.data.string("internal"));
@ -346,8 +305,7 @@ pub fn renderRedirect(self: *Request, state: RedirectState) !void {
break :blk try template.render( break :blk try template.render(
self.response_data, self.response_data,
jetzig.TemplateContext, jetzig.TemplateContext,
template_context, .{ .request = self },
&.{},
.{}, .{},
); );
} else try std.fmt.allocPrint(self.allocator, "Redirecting to {s}", .{state.location}), } else try std.fmt.allocPrint(self.allocator, "Redirecting to {s}", .{state.location}),
@ -539,19 +497,19 @@ pub fn cookies(self: *Request) !*jetzig.http.Cookies {
/// `jetzig.http.Session`. /// `jetzig.http.Session`.
pub fn session(self: *Request) !*jetzig.http.Session { pub fn session(self: *Request) !*jetzig.http.Session {
if (self._session) |capture| return capture; if (self._session) |capture| return capture;
const cookie_name = self.env.vars.get("JETZIG_SESSION_COOKIE") orelse const cookie_name = self.server.env.vars.get("JETZIG_SESSION_COOKIE") orelse
jetzig.http.Session.default_cookie_name; jetzig.http.Session.default_cookie_name;
const local_session = try self.allocator.create(jetzig.http.Session); const local_session = try self.allocator.create(jetzig.http.Session);
local_session.* = jetzig.http.Session.init( local_session.* = jetzig.http.Session.init(
self.allocator, self.allocator,
try self.cookies(), try self.cookies(),
self.env.secret, self.server.env.secret,
.{ .cookie_name = cookie_name }, .{ .cookie_name = cookie_name },
); );
local_session.parse() catch |err| { local_session.parse() catch |err| {
switch (err) { switch (err) {
error.JetzigInvalidSessionCookie => { error.JetzigInvalidSessionCookie => {
try self.logger.DEBUG("Invalid session cookie detected. Resetting session.", .{}); try self.server.logger.DEBUG("Invalid session cookie detected. Resetting session.", .{});
try local_session.reset(); try local_session.reset();
}, },
else => return err, else => return err,
@ -603,11 +561,11 @@ pub fn job(self: *Request, job_name: []const u8) !*jetzig.Job {
const background_job = try self.allocator.create(jetzig.Job); const background_job = try self.allocator.create(jetzig.Job);
background_job.* = jetzig.Job.init( background_job.* = jetzig.Job.init(
self.allocator, self.allocator,
self.store.store, self.server.store,
self.job_queue.store, self.server.job_queue,
self.cache.store, self.server.cache,
self.logger, self.server.logger,
self.job_definitions, self.server.job_definitions,
job_name, job_name,
); );
return background_job; return background_job;
@ -649,14 +607,14 @@ const RequestMail = struct {
self.request.allocator, self.request.allocator,
mail_job.params, mail_job.params,
jetzig.jobs.JobEnv{ jetzig.jobs.JobEnv{
.vars = self.request.env.vars, .vars = self.request.server.env.vars,
.environment = self.request.env.environment, .environment = self.request.server.env.environment,
.logger = self.request.logger, .logger = self.request.server.logger,
.routes = self.request.routes, .routes = self.request.server.routes,
.mailers = self.request.mailer_definitions, .mailers = self.request.server.mailer_definitions,
.jobs = self.request.job_definitions, .jobs = self.request.server.job_definitions,
.store = self.request.store.store, .store = self.request.server.store,
.cache = self.request.cache.store, .cache = self.request.server.cache,
.mutex = undefined, .mutex = undefined,
.repo = self.request.repo, .repo = self.request.repo,
}, },

View File

@ -6,36 +6,29 @@ const zmpl = @import("zmpl");
const zmd = @import("zmd"); const zmd = @import("zmd");
const httpz = @import("httpz"); const httpz = @import("httpz");
pub const RenderedView = struct { view: jetzig.views.View, content: []const u8 }; allocator: std.mem.Allocator,
logger: jetzig.loggers.Logger,
env: jetzig.Environment,
routes: []const *const jetzig.views.Route,
custom_routes: []const jetzig.views.Route,
job_definitions: []const jetzig.JobDefinition,
mailer_definitions: []const jetzig.MailerDefinition,
mime_map: *jetzig.http.mime.MimeMap,
initialized: bool = false,
store: *jetzig.kv.Store.GeneralStore,
job_queue: *jetzig.kv.Store.JobQueueStore,
cache: *jetzig.kv.Store.CacheStore,
repo: *jetzig.database.Repo,
global: *anyopaque,
decoded_static_route_params: []const *jetzig.data.Value = &.{},
debug_mutex: std.Thread.Mutex = .{},
pub fn RoutedServer(Routes: type) type { const Server = @This();
return struct {
allocator: std.mem.Allocator,
logger: jetzig.loggers.Logger,
env: jetzig.Environment,
routes: []const *const jetzig.views.Route,
channel_routes: std.StaticStringMap(jetzig.channels.Route),
custom_routes: []const jetzig.views.Route,
job_definitions: []const jetzig.JobDefinition,
mailer_definitions: []const jetzig.MailerDefinition,
mime_map: *jetzig.http.mime.MimeMap,
initialized: bool = false,
store: *jetzig.kv.Store.GeneralStore,
job_queue: *jetzig.kv.Store.JobQueueStore,
cache: *jetzig.kv.Store.CacheStore,
channels: *jetzig.kv.Store.ChannelStore,
repo: *jetzig.database.Repo,
global: *anyopaque,
decoded_static_route_params: []const *jetzig.data.Value = &.{},
debug_mutex: std.Thread.Mutex = .{},
const Server = @This(); pub fn init(
pub fn init(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
env: jetzig.Environment, env: jetzig.Environment,
routes: []const *const jetzig.views.Route, routes: []const *const jetzig.views.Route,
channel_routes: std.StaticStringMap(jetzig.channels.Route),
custom_routes: []const jetzig.views.Route, custom_routes: []const jetzig.views.Route,
job_definitions: []const jetzig.JobDefinition, job_definitions: []const jetzig.JobDefinition,
mailer_definitions: []const jetzig.MailerDefinition, mailer_definitions: []const jetzig.MailerDefinition,
@ -43,16 +36,14 @@ pub fn RoutedServer(Routes: type) type {
store: *jetzig.kv.Store.GeneralStore, store: *jetzig.kv.Store.GeneralStore,
job_queue: *jetzig.kv.Store.JobQueueStore, job_queue: *jetzig.kv.Store.JobQueueStore,
cache: *jetzig.kv.Store.CacheStore, cache: *jetzig.kv.Store.CacheStore,
channels: *jetzig.kv.Store.ChannelStore,
repo: *jetzig.database.Repo, repo: *jetzig.database.Repo,
global: *anyopaque, global: *anyopaque,
) Server { ) Server {
return .{ return .{
.allocator = allocator, .allocator = allocator,
.logger = env.logger, .logger = env.logger,
.env = env, .env = env,
.routes = routes, .routes = routes,
.channel_routes = channel_routes,
.custom_routes = custom_routes, .custom_routes = custom_routes,
.job_definitions = job_definitions, .job_definitions = job_definitions,
.mailer_definitions = mailer_definitions, .mailer_definitions = mailer_definitions,
@ -60,36 +51,33 @@ pub fn RoutedServer(Routes: type) type {
.store = store, .store = store,
.job_queue = job_queue, .job_queue = job_queue,
.cache = cache, .cache = cache,
.channels = channels,
.repo = repo, .repo = repo,
.global = global, .global = global,
}; };
} }
pub fn deinit(self: *Server) void { pub fn deinit(self: *Server) void {
self.allocator.free(self.env.secret); self.allocator.free(self.env.secret);
self.allocator.free(self.env.bind); self.allocator.free(self.env.bind);
} }
const HttpzHandler = struct { const Dispatcher = struct {
server: *Server, server: *Server,
pub const WebsocketHandler = jetzig.websockets.RoutedWebsocket(Routes); pub fn handle(self: Dispatcher, request: *httpz.Request, response: *httpz.Response) void {
pub fn handle(self: HttpzHandler, request: *httpz.Request, response: *httpz.Response) void {
self.server.processNextRequest(request, response) catch |err| { self.server.processNextRequest(request, response) catch |err| {
self.server.errorHandlerFn(request, response, err) catch {}; self.server.errorHandlerFn(request, response, err) catch {};
}; };
} }
}; };
pub fn listen(self: *Server) !void { pub fn listen(self: *Server) !void {
try self.decodeStaticParams(); try self.decodeStaticParams();
const worker_count = jetzig.config.get(u16, "worker_count"); const worker_count = jetzig.config.get(u16, "worker_count");
const thread_count: u16 = jetzig.config.get(?u16, "thread_count") orelse @intCast(try std.Thread.getCpuCount()); const thread_count: u16 = jetzig.config.get(?u16, "thread_count") orelse @intCast(try std.Thread.getCpuCount());
var httpz_server = try httpz.Server(HttpzHandler).init( var httpz_server = try httpz.Server(Dispatcher).init(
self.allocator, self.allocator,
.{ .{
.port = self.env.port, .port = self.env.port,
@ -108,7 +96,7 @@ pub fn RoutedServer(Routes: type) type {
.max_body_size = jetzig.config.get(usize, "max_bytes_request_body"), .max_body_size = jetzig.config.get(usize, "max_bytes_request_body"),
}, },
}, },
HttpzHandler{ .server = self }, Dispatcher{ .server = self },
); );
defer httpz_server.deinit(); defer httpz_server.deinit();
@ -125,9 +113,9 @@ pub fn RoutedServer(Routes: type) type {
try jetzig.http.middleware.afterLaunch(self); try jetzig.http.middleware.afterLaunch(self);
return try httpz_server.listen(); return try httpz_server.listen();
} }
pub fn errorHandlerFn(self: *Server, request: *httpz.Request, response: *httpz.Response, err: anyerror) !void { pub fn errorHandlerFn(self: *Server, request: *httpz.Request, response: *httpz.Response, err: anyerror) !void {
if (isBadHttpError(err)) return; if (isBadHttpError(err)) return;
self.logger.ERROR("Encountered error: {s} {s}", .{ @errorName(err), request.url.raw }) catch {}; self.logger.ERROR("Encountered error: {s} {s}", .{ @errorName(err), request.url.raw }) catch {};
@ -139,13 +127,13 @@ pub fn RoutedServer(Routes: type) type {
} }
response.body = "500 Internal Server Error"; response.body = "500 Internal Server Error";
} }
pub fn processNextRequest( pub fn processNextRequest(
self: *Server, self: *Server,
httpz_request: *httpz.Request, httpz_request: *httpz.Request,
httpz_response: *httpz.Response, httpz_response: *httpz.Response,
) !void { ) !void {
const start_time = std.time.nanoTimestamp(); const start_time = std.time.nanoTimestamp();
var repo = try self.repo.bindConnect(.{ .allocator = httpz_response.arena }); var repo = try self.repo.bindConnect(.{ .allocator = httpz_response.arena });
@ -154,27 +142,14 @@ pub fn RoutedServer(Routes: type) type {
var response = try jetzig.http.Response.init(httpz_response.arena, httpz_response); var response = try jetzig.http.Response.init(httpz_response.arena, httpz_response);
var request = try jetzig.http.Request.init( var request = try jetzig.http.Request.init(
httpz_response.arena, httpz_response.arena,
self,
start_time, start_time,
httpz_request, httpz_request,
httpz_response, httpz_response,
&response, &response,
&repo, &repo,
self.env,
self.routes,
self.logger,
self.store,
self.cache,
self.job_queue,
self.job_definitions,
self.mailer_definitions,
self.global,
); );
if (try self.upgradeWebsocket(httpz_request, httpz_response, &request)) {
try self.logger.DEBUG("Websocket upgrade request successful.", .{});
return;
}
try request.process(); try request.process();
var middleware_data = try jetzig.http.middleware.afterRequest(&request); var middleware_data = try jetzig.http.middleware.afterRequest(&request);
@ -192,45 +167,9 @@ pub fn RoutedServer(Routes: type) type {
jetzig.http.middleware.deinit(&middleware_data, &request); jetzig.http.middleware.deinit(&middleware_data, &request);
try self.logger.logRequest(&request); try self.logger.logRequest(&request);
} }
/// Attempt to match a channel name to a view with a Channel implementation. fn maybeMiddlewareRender(request: *jetzig.http.Request, response: *const jetzig.http.Response) !bool {
pub fn matchChannelRoute(self: *const Server, channel_name: []const u8) ?jetzig.channels.Route {
// TODO: Detect root path correctly.
return self.channel_routes.get(channel_name);
}
fn upgradeWebsocket(
self: *const Server,
httpz_request: *httpz.Request,
httpz_response: *httpz.Response,
request: *jetzig.http.Request,
) !bool {
const route = self.matchChannelRoute(request.path.view_name) orelse return false;
const session = try request.session();
const session_id = session.getT(.string, "_id") orelse blk: {
try session.reset();
break :blk session.getT(.string, "_id") orelse {
try self.logger.ERROR("Error fetching session ID for websocket, aborting.", .{});
return false;
};
};
return try httpz.upgradeWebsocket(
jetzig.websockets.RoutedWebsocket(Routes),
httpz_request,
httpz_response,
jetzig.websockets.Context{
.allocator = self.allocator,
.route = route,
.session_id = session_id,
.channels = self.channels,
.logger = self.logger,
},
);
}
fn maybeMiddlewareRender(request: *jetzig.http.Request, response: *const jetzig.http.Response) !bool {
if (request.middleware_rendered) |_| { if (request.middleware_rendered) |_| {
// Request processing ends when a middleware renders or redirects. // Request processing ends when a middleware renders or redirects.
if (request.redirect_state) |state| { if (request.redirect_state) |state| {
@ -243,9 +182,9 @@ pub fn RoutedServer(Routes: type) type {
try request.respond(); try request.respond();
return true; return true;
} else return false; } else return false;
} }
fn renderResponse(self: *Server, request: *jetzig.http.Request) !void { fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
const static_resource = self.matchStaticResource(request) catch |err| { const static_resource = self.matchStaticResource(request) catch |err| {
if (isUnhandledError(err)) return err; if (isUnhandledError(err)) return err;
@ -312,20 +251,20 @@ pub fn RoutedServer(Routes: type) type {
} }
if (request.redirect_state) |state| try request.renderRedirect(state); if (request.redirect_state) |state| try request.renderRedirect(state);
} }
fn renderStatic(resource: StaticResource, request: *jetzig.http.Request) !void { fn renderStatic(resource: StaticResource, request: *jetzig.http.Request) !void {
request.setResponse( request.setResponse(
.{ .view = .{ .data = request.response_data }, .content = resource.content }, .{ .view = .{ .data = request.response_data }, .content = resource.content },
.{ .content_type = resource.mime_type }, .{ .content_type = resource.mime_type },
); );
} }
fn renderHTML( fn renderHTML(
self: *Server, self: *Server,
request: *jetzig.http.Request, request: *jetzig.http.Request,
route: ?jetzig.views.Route, route: ?jetzig.views.Route,
) !void { ) !void {
if (route) |matched_route| { if (route) |matched_route| {
if (zmpl.findPrefixed("views", matched_route.template)) |template| { if (zmpl.findPrefixed("views", matched_route.template)) |template| {
const rendered = self.renderView(matched_route, request, template) catch |err| { const rendered = self.renderView(matched_route, request, template) catch |err| {
@ -347,7 +286,9 @@ pub fn RoutedServer(Routes: type) type {
return request.setResponse(rendered_error, .{}); return request.setResponse(rendered_error, .{});
}; };
return if (request.isRendered() or request.dynamic_assigned_template != null) return if (request.state == .redirected or
request.state == .failed or
request.dynamic_assigned_template != null)
request.setResponse(rendered, .{}) request.setResponse(rendered, .{})
else else
request.setResponse(try self.renderNotFound(request), .{}); request.setResponse(try self.renderNotFound(request), .{});
@ -360,13 +301,13 @@ pub fn RoutedServer(Routes: type) type {
return request.setResponse(try self.renderNotFound(request), .{}); return request.setResponse(try self.renderNotFound(request), .{});
} }
} }
} }
fn renderJSON( fn renderJSON(
self: *Server, self: *Server,
request: *jetzig.http.Request, request: *jetzig.http.Request,
route: ?jetzig.views.Route, route: ?jetzig.views.Route,
) !void { ) !void {
if (route) |matched_route| { if (route) |matched_route| {
var rendered = try self.renderView(matched_route, request, null); var rendered = try self.renderView(matched_route, request, null);
var data = rendered.view.data; var data = rendered.view.data;
@ -382,9 +323,9 @@ pub fn RoutedServer(Routes: type) type {
} else { } else {
request.setResponse(try self.renderNotFound(request), .{}); request.setResponse(try self.renderNotFound(request), .{});
} }
} }
fn renderMarkdown(self: *Server, request: *jetzig.http.Request) !?RenderedView { fn renderMarkdown(self: *Server, request: *jetzig.http.Request) !?RenderedView {
_ = self; _ = self;
// No route recognized, but we can still render a static markdown file if it matches the URI: // No route recognized, but we can still render a static markdown file if it matches the URI:
if (request.method != .GET) return null; if (request.method != .GET) return null;
@ -396,14 +337,16 @@ pub fn RoutedServer(Routes: type) type {
} else { } else {
return null; return null;
} }
} }
fn renderView( pub const RenderedView = struct { view: jetzig.views.View, content: []const u8 };
fn renderView(
self: *Server, self: *Server,
route: jetzig.views.Route, route: jetzig.views.Route,
request: *jetzig.http.Request, request: *jetzig.http.Request,
maybe_template: ?zmpl.Template, maybe_template: ?zmpl.Template,
) !RenderedView { ) !RenderedView {
// View functions return a `View` to encourage users to return from a view function with // View functions return a `View` to encourage users to return from a view function with
// `return request.render(.ok)`, but the actual rendered view is stored in // `return request.render(.ok)`, but the actual rendered view is stored in
// `request.rendered_view`. // `request.rendered_view`.
@ -430,10 +373,6 @@ pub fn RoutedServer(Routes: type) type {
if (request.rendered_view) |rendered_view| { if (request.rendered_view) |rendered_view| {
if (request.state == .redirected) return .{ .view = rendered_view, .content = "" }; if (request.state == .redirected) return .{ .view = rendered_view, .content = "" };
if (request.state == .rendered_content) return .{
.view = rendered_view,
.content = rendered_view.content.?,
};
if (template) |capture| { if (template) |capture| {
return .{ return .{
@ -465,19 +404,18 @@ pub fn RoutedServer(Routes: type) type {
.content = "", .content = "",
}; };
} }
} }
fn renderTemplateWithLayout( fn renderTemplateWithLayout(
self: *Server, self: *Server,
request: *jetzig.http.Request, request: *jetzig.http.Request,
template: zmpl.Template, template: zmpl.Template,
view: jetzig.views.View, view: jetzig.views.View,
route: jetzig.views.Route, route: jetzig.views.Route,
) ![]const u8 { ) ![]const u8 {
try addTemplateConstants(view, route); try addTemplateConstants(view, route);
var template_context = jetzig.TemplateContext{ .request = request }; const template_context = jetzig.TemplateContext{ .request = request };
template_context.middleware.context = &template_context;
if (request.getLayout(route)) |layout_name| { if (request.getLayout(route)) |layout_name| {
// TODO: Allow user to configure layouts directory other than src/app/views/layouts/ // TODO: Allow user to configure layouts directory other than src/app/views/layouts/
@ -493,7 +431,6 @@ pub fn RoutedServer(Routes: type) type {
view.data, view.data,
jetzig.TemplateContext, jetzig.TemplateContext,
template_context, template_context,
&.{},
.{ .layout = layout }, .{ .layout = layout },
); );
} else { } else {
@ -502,7 +439,6 @@ pub fn RoutedServer(Routes: type) type {
view.data, view.data,
jetzig.TemplateContext, jetzig.TemplateContext,
template_context, template_context,
&.{},
.{}, .{},
); );
} }
@ -510,12 +446,11 @@ pub fn RoutedServer(Routes: type) type {
view.data, view.data,
jetzig.TemplateContext, jetzig.TemplateContext,
template_context, template_context,
&.{},
.{}, .{},
); );
} }
fn addTemplateConstants(view: jetzig.views.View, route: jetzig.views.Route) !void { fn addTemplateConstants(view: jetzig.views.View, route: jetzig.views.Route) !void {
const action = switch (route.action) { const action = switch (route.action) {
.custom => route.name, .custom => route.name,
else => |tag| @tagName(tag), else => |tag| @tagName(tag),
@ -523,23 +458,23 @@ pub fn RoutedServer(Routes: type) type {
try view.data.addConst("jetzig_action", view.data.string(action)); try view.data.addConst("jetzig_action", view.data.string(action));
try view.data.addConst("jetzig_view", view.data.string(route.view_name)); try view.data.addConst("jetzig_view", view.data.string(route.view_name));
} }
fn isBadRequest(err: anyerror) bool { fn isBadRequest(err: anyerror) bool {
return switch (err) { return switch (err) {
error.JetzigBodyParseError, error.JetzigQueryParseError => true, error.JetzigBodyParseError, error.JetzigQueryParseError => true,
else => false, else => false,
}; };
} }
fn isUnhandledError(err: anyerror) bool { fn isUnhandledError(err: anyerror) bool {
return switch (err) { return switch (err) {
error.OutOfMemory => true, error.OutOfMemory => true,
else => false, else => false,
}; };
} }
fn isBadHttpError(err: anyerror) bool { fn isBadHttpError(err: anyerror) bool {
return switch (err) { return switch (err) {
error.JetzigParseHeadError, error.JetzigParseHeadError,
error.UnknownHttpMethod, error.UnknownHttpMethod,
@ -556,63 +491,63 @@ pub fn RoutedServer(Routes: type) type {
=> true, => true,
else => false, else => false,
}; };
} }
fn renderInternalServerError( fn renderInternalServerError(
self: *Server, self: *Server,
request: *jetzig.http.Request, request: *jetzig.http.Request,
stack_trace: ?*std.builtin.StackTrace, stack_trace: ?*std.builtin.StackTrace,
err: anyerror, err: anyerror,
) !RenderedView { ) !RenderedView {
try self.logger.logError(stack_trace, err); try self.logger.logError(stack_trace, err);
const status = jetzig.http.StatusCode.internal_server_error; const status = jetzig.http.StatusCode.internal_server_error;
return try self.renderError(request, status, .{ .stack_trace = stack_trace, .err = err }); return try self.renderError(request, status, .{ .stack_trace = stack_trace, .err = err });
} }
fn renderNotFound(self: *Server, request: *jetzig.http.Request) !RenderedView { fn renderNotFound(self: *Server, request: *jetzig.http.Request) !RenderedView {
request.response_data.reset(); request.response_data.reset();
const status: jetzig.http.StatusCode = .not_found; const status: jetzig.http.StatusCode = .not_found;
return try self.renderError(request, status, .{}); return try self.renderError(request, status, .{});
} }
fn renderBadRequest(self: *Server, request: *jetzig.http.Request) !RenderedView { fn renderBadRequest(self: *Server, request: *jetzig.http.Request) !RenderedView {
request.response_data.reset(); request.response_data.reset();
const status: jetzig.http.StatusCode = .bad_request; const status: jetzig.http.StatusCode = .bad_request;
return try self.renderError(request, status, .{}); return try self.renderError(request, status, .{});
} }
fn renderError( fn renderError(
self: Server, self: Server,
request: *jetzig.http.Request, request: *jetzig.http.Request,
status_code: jetzig.http.StatusCode, status_code: jetzig.http.StatusCode,
error_info: jetzig.debug.ErrorInfo, error_info: jetzig.debug.ErrorInfo,
) !RenderedView { ) !RenderedView {
if (comptime jetzig.build_options.debug_console) { if (comptime jetzig.build_options.debug_console) {
return try self.renderDebugConsole(request, status_code, error_info); return try self.renderDebugConsole(request, status_code, error_info);
} else return try self.renderGeneralError(request, status_code); } else return try self.renderGeneralError(request, status_code);
} }
fn renderGeneralError( fn renderGeneralError(
self: Server, self: Server,
request: *jetzig.http.Request, request: *jetzig.http.Request,
status_code: jetzig.http.StatusCode, status_code: jetzig.http.StatusCode,
) !RenderedView { ) !RenderedView {
if (try self.renderErrorView(request, status_code)) |view| return view; if (try self.renderErrorView(request, status_code)) |view| return view;
if (try renderStaticErrorPage(request, status_code)) |view| return view; if (try renderStaticErrorPage(request, status_code)) |view| return view;
return try renderDefaultError(request, status_code); return try renderDefaultError(request, status_code);
} }
fn renderDebugConsole( fn renderDebugConsole(
self: Server, self: Server,
request: *jetzig.http.Request, request: *jetzig.http.Request,
status_code: jetzig.http.StatusCode, status_code: jetzig.http.StatusCode,
error_info: jetzig.debug.ErrorInfo, error_info: jetzig.debug.ErrorInfo,
) !RenderedView { ) !RenderedView {
if (comptime jetzig.build_options.debug_console) { if (comptime jetzig.build_options.debug_console) {
var buf = std.ArrayList(u8).init(request.allocator); var buf = std.ArrayList(u8).init(request.allocator);
const writer = buf.writer(); const writer = buf.writer();
@ -641,13 +576,13 @@ pub fn RoutedServer(Routes: type) type {
.content = if (content.len == 0) "" else content, .content = if (content.len == 0) "" else content,
}; };
} else unreachable; } else unreachable;
} }
fn renderErrorView( fn renderErrorView(
self: Server, self: Server,
request: *jetzig.http.Request, request: *jetzig.http.Request,
status_code: jetzig.http.StatusCode, status_code: jetzig.http.StatusCode,
) !?RenderedView { ) !?RenderedView {
for (self.routes) |route| { for (self.routes) |route| {
if (std.mem.eql(u8, route.view_name, "errors") and route.action == .index) { if (std.mem.eql(u8, route.view_name, "errors") and route.action == .index) {
request.response_data.reset(); request.response_data.reset();
@ -674,7 +609,6 @@ pub fn RoutedServer(Routes: type) type {
request.response_data, request.response_data,
jetzig.TemplateContext, jetzig.TemplateContext,
.{ .request = request }, .{ .request = request },
&.{},
.{}, .{},
), ),
}; };
@ -687,9 +621,9 @@ pub fn RoutedServer(Routes: type) type {
} }
return null; return null;
} }
fn renderStaticErrorPage(request: *jetzig.http.Request, status_code: jetzig.http.StatusCode) !?RenderedView { fn renderStaticErrorPage(request: *jetzig.http.Request, status_code: jetzig.http.StatusCode) !?RenderedView {
if (request.requestFormat() == .JSON) return null; if (request.requestFormat() == .JSON) return null;
var dir = std.fs.cwd().openDir( var dir = std.fs.cwd().openDir(
@ -719,40 +653,40 @@ pub fn RoutedServer(Routes: type) type {
.view = jetzig.views.View{ .data = request.response_data, .status_code = status_code }, .view = jetzig.views.View{ .data = request.response_data, .status_code = status_code },
.content = content, .content = content,
}; };
} }
fn renderDefaultError( fn renderDefaultError(
request: *const jetzig.http.Request, request: *const jetzig.http.Request,
status_code: jetzig.http.StatusCode, status_code: jetzig.http.StatusCode,
) !RenderedView { ) !RenderedView {
const content = try request.formatStatus(status_code); const content = try request.formatStatus(status_code);
return .{ return .{
.view = jetzig.views.View{ .data = request.response_data, .status_code = status_code }, .view = jetzig.views.View{ .data = request.response_data, .status_code = status_code },
.content = content, .content = content,
}; };
} }
fn logStackTrace( fn logStackTrace(
self: Server, self: Server,
stack: *std.builtin.StackTrace, stack: *std.builtin.StackTrace,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
) !void { ) !void {
var buf = std.ArrayList(u8).init(allocator); var buf = std.ArrayList(u8).init(allocator);
defer buf.deinit(); defer buf.deinit();
const writer = buf.writer(); const writer = buf.writer();
try stack.format("", .{}, writer); try stack.format("", .{}, writer);
if (buf.items.len > 0) try self.logger.ERROR("{s}\n", .{buf.items}); if (buf.items.len > 0) try self.logger.ERROR("{s}\n", .{buf.items});
} }
fn matchCustomRoute(self: Server, request: *const jetzig.http.Request) ?jetzig.views.Route { fn matchCustomRoute(self: Server, request: *const jetzig.http.Request) ?jetzig.views.Route {
for (self.custom_routes) |custom_route| { for (self.custom_routes) |custom_route| {
if (custom_route.match(request)) return custom_route; if (custom_route.match(request)) return custom_route;
} }
return null; return null;
} }
fn matchMiddlewareRoute(request: *const jetzig.http.Request) ?jetzig.middleware.MiddlewareRoute { fn matchMiddlewareRoute(request: *const jetzig.http.Request) ?jetzig.middleware.MiddlewareRoute {
const middlewares = jetzig.config.get([]const type, "middleware"); const middlewares = jetzig.config.get([]const type, "middleware");
inline for (middlewares) |middleware| { inline for (middlewares) |middleware| {
@ -764,9 +698,9 @@ pub fn RoutedServer(Routes: type) type {
} }
return null; return null;
} }
fn matchRoute(self: *Server, request: *jetzig.http.Request, static: bool) !?jetzig.views.Route { fn matchRoute(self: *Server, request: *jetzig.http.Request, static: bool) !?jetzig.views.Route {
for (self.routes) |route| { for (self.routes) |route| {
// .index routes always take precedence. // .index routes always take precedence.
if (route.action == .index and try request.match(route.*)) { if (route.action == .index and try request.match(route.*)) {
@ -783,14 +717,14 @@ pub fn RoutedServer(Routes: type) type {
} }
return null; return null;
} }
const StaticResource = struct { const StaticResource = struct {
content: []const u8, content: []const u8,
mime_type: []const u8 = "application/octet-stream", mime_type: []const u8 = "application/octet-stream",
}; };
fn matchStaticResource(self: *Server, request: *jetzig.http.Request) !?StaticResource { fn matchStaticResource(self: *Server, request: *jetzig.http.Request) !?StaticResource {
if (comptime jetzig.build_options.debug_console) { if (comptime jetzig.build_options.debug_console) {
if (std.mem.eql(u8, request.path.path, "/_jetzig_debug.js")) return .{ if (std.mem.eql(u8, request.path.path, "/_jetzig_debug.js")) return .{
.content = @embedFile("../../assets/debug.js"), .content = @embedFile("../../assets/debug.js"),
@ -813,9 +747,9 @@ pub fn RoutedServer(Routes: type) type {
}; };
return null; return null;
} }
fn matchPublicContent(self: *Server, request: *jetzig.http.Request) !?StaticResource { fn matchPublicContent(self: *Server, request: *jetzig.http.Request) !?StaticResource {
if (request.path.file_path.len <= 1) return null; if (request.path.file_path.len <= 1) return null;
if (request.method != .GET) return null; if (request.method != .GET) return null;
@ -855,9 +789,9 @@ pub fn RoutedServer(Routes: type) type {
} }
return null; return null;
} }
fn matchStaticContent(self: *Server, request: *jetzig.http.Request) !?[]const u8 { fn matchStaticContent(self: *Server, request: *jetzig.http.Request) !?[]const u8 {
const request_format = request.requestFormat(); const request_format = request.requestFormat();
const matched_route = try self.matchRoute(request, true); const matched_route = try self.matchRoute(request, true);
@ -889,9 +823,9 @@ pub fn RoutedServer(Routes: type) type {
} }
return null; return null;
} }
pub fn decodeStaticParams(self: *Server) !void { pub fn decodeStaticParams(self: *Server) !void {
if (comptime !@hasDecl(jetzig.root, "static")) return; if (comptime !@hasDecl(jetzig.root, "static")) return;
// Store decoded static params (i.e. declared in views) for faster comparison at request time. // Store decoded static params (i.e. declared in views) for faster comparison at request time.
@ -904,15 +838,15 @@ pub fn RoutedServer(Routes: type) type {
} }
self.decoded_static_route_params = try decoded.toOwnedSlice(); self.decoded_static_route_params = try decoded.toOwnedSlice();
} }
fn matchStaticOutput( fn matchStaticOutput(
maybe_expected_id: ?[]const u8, maybe_expected_id: ?[]const u8,
maybe_expected_params: ?*jetzig.data.Value, maybe_expected_params: ?*jetzig.data.Value,
route: jetzig.views.Route, route: jetzig.views.Route,
request: *const jetzig.http.Request, request: *const jetzig.http.Request,
params: jetzig.data.Value, params: jetzig.data.Value,
) bool { ) bool {
return if (maybe_expected_params) |expected_params| blk: { return if (maybe_expected_params) |expected_params| blk: {
const params_match = expected_params.count() == 0 or expected_params.eql(params); const params_match = expected_params.count() == 0 or expected_params.eql(params);
break :blk switch (route.action) { break :blk switch (route.action) {
@ -926,6 +860,4 @@ pub fn RoutedServer(Routes: type) type {
std.mem.eql(u8, id, request.path.resource_id) std.mem.eql(u8, id, request.path.resource_id)
else else
true; // We reached a params filter (possibly the default catch-all) with no params set. true; // We reached a params filter (possibly the default catch-all) with no params set.
}
};
} }

View File

@ -12,7 +12,6 @@ cookie_name: []const u8,
initialized: bool = false, initialized: bool = false,
data: jetzig.data.Data, data: jetzig.data.Data,
state: enum { parsed, pending } = .pending, state: enum { parsed, pending } = .pending,
id: [32]u8 = undefined,
const Self = @This(); const Self = @This();
@ -49,11 +48,7 @@ pub fn parse(self: *Self) !void {
/// Reset session to an empty state. /// Reset session to an empty state.
pub fn reset(self: *Self) !void { pub fn reset(self: *Self) !void {
self.data.reset(); self.data.reset();
var object = try self.data.object(); _ = try self.data.object();
_ = jetzig.util.generateRandomString(&self.id);
try object.put("_id", &self.id);
self.state = .parsed; self.state = .parsed;
try self.save(); try self.save();
} }
@ -75,7 +70,7 @@ pub fn get(self: *Self, key: []const u8) ?*jetzig.data.Value {
/// Get a typed value from the session. /// Get a typed value from the session.
pub fn getT( pub fn getT(
self: Self, self: *Self,
comptime T: jetzig.data.ValueType, comptime T: jetzig.data.ValueType,
key: []const u8, key: []const u8,
) @TypeOf(self.data.value.?.object.getT(T, key)) { ) @TypeOf(self.data.value.?.object.getT(T, key)) {

View File

@ -48,7 +48,7 @@ pub fn Type(comptime name: MiddlewareEnum()) type {
} }
} }
pub fn afterLaunch(server: *jetzig.http.Server.RoutedServer(@import("root").routes)) !void { pub fn afterLaunch(server: *jetzig.http.Server) !void {
inline for (middlewares) |middleware| { inline for (middlewares) |middleware| {
if (comptime @hasDecl(middleware, "afterLaunch")) { if (comptime @hasDecl(middleware, "afterLaunch")) {
try middleware.afterLaunch(server); try middleware.afterLaunch(server);
@ -94,38 +94,6 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
return middleware_data; return middleware_data;
} }
pub fn afterView(middleware_data: *MiddlewareData, request: *jetzig.http.Request, route: jetzig.views.Route) !void {
if (request.state != .failed) request.state = .after_view;
inline for (middlewares, 0..) |middleware, index| {
if (comptime !@hasDecl(middleware, "afterView")) continue;
if (request.state == .after_view) {
if (comptime @hasDecl(middleware, "init")) {
const data = middleware_data.get(index).?;
try @call(
.always_inline,
middleware.afterView,
.{ @as(*middleware, @ptrCast(@alignCast(data))), request, route },
);
} else {
try @call(
.always_inline,
middleware.afterView,
.{ request, route },
);
}
}
if (request.state != .after_view) {
request.middleware_rendered = .{
.name = @typeName(middleware),
.action = "afterView",
};
break;
}
}
}
pub fn beforeResponse( pub fn beforeResponse(
middleware_data: *MiddlewareData, middleware_data: *MiddlewareData,
request: *jetzig.http.Request, request: *jetzig.http.Request,

View File

@ -22,9 +22,6 @@ pub const Store = struct {
/// Store ephemeral data. /// Store ephemeral data.
pub const CacheStore = @import("kv/Store.zig").Store(config.get(Store.Options, "cache")); pub const CacheStore = @import("kv/Store.zig").Store(config.get(Store.Options, "cache"));
/// Store channel data.
pub const ChannelStore = @import("kv/Store.zig").Store(config.get(Store.Options, "channels"));
/// Background job storage. /// Background job storage.
pub const JobQueueStore = @import("kv/Store.zig").Store(config.get(Store.Options, "job_queue")); pub const JobQueueStore = @import("kv/Store.zig").Store(config.get(Store.Options, "job_queue"));

View File

@ -53,7 +53,7 @@ fn jetKVOptions(options: KVOptions) jetzig.jetkv.Options {
} }
/// Role a given store fills. Used in log outputs. /// Role a given store fills. Used in log outputs.
pub const Role = enum { jobs, cache, general, channels, custom }; pub const Role = enum { jobs, cache, general, custom };
pub fn Store(comptime options: KVOptions) type { pub fn Store(comptime options: KVOptions) type {
return struct { return struct {

View File

@ -148,7 +148,7 @@ fn defaultHtml(
try data.addConst("jetzig_view", data.string("")); try data.addConst("jetzig_view", data.string(""));
try data.addConst("jetzig_action", data.string("")); try data.addConst("jetzig_action", data.string(""));
return if (jetzig.zmpl.findPrefixed("mailers", mailer.html_template)) |template| return if (jetzig.zmpl.findPrefixed("mailers", mailer.html_template)) |template|
try template.render(&data, jetzig.TemplateContext, .{}, &.{}, .{}) try template.render(&data, jetzig.TemplateContext, .{}, .{})
else else
null; null;
} }
@ -166,7 +166,7 @@ fn defaultText(
try data.addConst("jetzig_view", data.string("")); try data.addConst("jetzig_view", data.string(""));
try data.addConst("jetzig_action", data.string("")); try data.addConst("jetzig_action", data.string(""));
return if (jetzig.zmpl.findPrefixed("mailers", mailer.text_template)) |template| return if (jetzig.zmpl.findPrefixed("mailers", mailer.text_template)) |template|
try template.render(&data, jetzig.TemplateContext, .{}, &.{}, .{}) try template.render(&data, jetzig.TemplateContext, .{}, .{})
else else
null; null;
} }

View File

@ -5,8 +5,6 @@ pub const HtmxMiddleware = @import("middleware/HtmxMiddleware.zig");
pub const CompressionMiddleware = @import("middleware/CompressionMiddleware.zig"); pub const CompressionMiddleware = @import("middleware/CompressionMiddleware.zig");
pub const AuthMiddleware = @import("middleware/AuthMiddleware.zig"); pub const AuthMiddleware = @import("middleware/AuthMiddleware.zig");
pub const AntiCsrfMiddleware = @import("middleware/AntiCsrfMiddleware.zig"); pub const AntiCsrfMiddleware = @import("middleware/AntiCsrfMiddleware.zig");
pub const InertiaMiddleware = @import("middleware/InertiaMiddleware.zig");
pub const ChannelsMiddleware = @import("middleware/ChannelsMiddleware.zig");
const RouteOptions = struct { const RouteOptions = struct {
content: ?[]const u8 = null, content: ?[]const u8 = null,

View File

@ -29,7 +29,7 @@ pub fn beforeRender(request: *jetzig.http.Request, route: jetzig.views.Route) !v
fn logFailure(request: *jetzig.http.Request) !void { fn logFailure(request: *jetzig.http.Request) !void {
_ = request.fail(.forbidden); _ = request.fail(.forbidden);
try request.logger.DEBUG("Anti-CSRF token validation failed. Request aborted.", .{}); try request.server.logger.DEBUG("Anti-CSRF token validation failed. Request aborted.", .{});
} }
fn verifyCsrfToken(request: *jetzig.http.Request) !void { fn verifyCsrfToken(request: *jetzig.http.Request) !void {

View File

@ -1,35 +0,0 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
const ChannelsMiddleware = @This();
pub fn setup(app: *jetzig.App) !void {
app.route(.GET, "/_channels.js", ChannelsMiddleware, .renderChannels);
}
pub const Blocks = struct {
pub fn header(_: jetzig.TemplateContext, writer: anytype) !void {
try writer.writeAll(
\\<script src="/_channels.js"></script>
);
}
pub fn footer(context: jetzig.TemplateContext, writer: anytype) !void {
const request = context.request orelse return;
const host = request.headers.getLower("host") orelse return;
try writer.print(
\\<script>
\\ (() => {{
\\ window.addEventListener('DOMContentLoaded', () => {{
\\ jetzig.channel.init("{s}", "{s}");
\\ }});
\\ }})();
\\</script>
\\
, .{ host, request.path.base_path });
}
};
pub fn renderChannels(request: *jetzig.Request) !jetzig.View {
return request.renderContent(.ok, @embedFile("channels/channels.js"));
}

View File

@ -9,7 +9,7 @@ const HtmxMiddleware = @This();
/// content rendered directly by the view function. /// content rendered directly by the view function.
pub fn afterRequest(request: *jetzig.http.Request) !void { pub fn afterRequest(request: *jetzig.http.Request) !void {
if (request.headers.get("HX-Request")) |_| { if (request.headers.get("HX-Request")) |_| {
try request.logger.DEBUG( try request.server.logger.DEBUG(
"[middleware-htmx] HX-Request header, disabling layout.", "[middleware-htmx] HX-Request header, disabling layout.",
.{}, .{},
); );

View File

@ -1,6 +0,0 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
// WIP
const InertiaMiddleware = @This();

View File

@ -1,176 +0,0 @@
window.jetzig = window.jetzig ? window.jetzig : {}
jetzig = window.jetzig;
(() => {
const transform = (value, state, element) => {
const id = element.getAttribute('jetzig-id');
const transformer = id && jetzig.channel.transformers[id];
if (transformer) {
return transformer(value, state, element);
} else {
return value === undefined || value == null ? '' : `${value}`
}
};
jetzig.channel = {
websocket: null,
actions: {},
action_specs: {},
stateChangedCallbacks: [],
messageCallbacks: [],
invokeCallbacks: {},
elementMap: {},
transformers: {},
onStateChanged: function(callback) { this.stateChangedCallbacks.push(callback); },
onMessage: function(callback) { this.messageCallbacks.push(callback); },
init: function(host, path) {
this.websocket = new WebSocket(`ws://${host}${path}`);
this.websocket.addEventListener("message", (event) => {
const state_tag = "__jetzig_channel_state__:";
const actions_tag = "__jetzig_actions__:";
const event_tag = "__jetzig_event__:";
if (event.data.startsWith(state_tag)) {
const state = JSON.parse(event.data.slice(state_tag.length));
Object.entries(this.elementMap).forEach(([ref, elements]) => {
const value = reduceState(ref, state);
elements.forEach(element => element.innerHTML = transform(value, state, element));
});
this.stateChangedCallbacks.forEach((callback) => {
callback(state);
});
} else if (event.data.startsWith(event_tag)) {
const data = JSON.parse(event.data.slice(event_tag.length));
if (Object.hasOwn(this.invokeCallbacks, data.method)) {
this.invokeCallbacks[data.method].forEach(callback => {
callback(data);
});
}
} else if (event.data.startsWith(actions_tag)) {
const data = JSON.parse(event.data.slice(actions_tag.length));
data.actions.forEach(action => {
this.action_specs[action.name] = {
callback: (...params) => {
if (action.params.length != params.length) {
throw new Error(`Invalid params for action '${action.name}'. Expected ${action.params.length} params, found ${params.length}`);
}
[...action.params].forEach((param, index) => {
if (param.type !== typeof params[index]) {
const err = `Incorrect argument type for argument ${index} in '${action.name}'. Expected: ${param.type}, found ${typeof params[index]}`;
switch (param.type) {
case "string":
params[index] = `${params[index]}`;
break;
case "integer":
try { params[index] = parseInt(params[index]) } catch {
throw new Error(err);
};
break;
case "float":
try { params[index] = parseFloat(params[index]) } catch {
throw new Error(err);
};
case "boolean":
params[index] = ["true", "y", "1", "yes", "t"].includes(params[index]);
break;
default:
throw new Error(err);
}
}
});
this.websocket.send(`_invoke:${action.name}:${JSON.stringify(params)}`);
},
spec: { ...action },
};
this.actions[action.name] = this.action_specs[action.name].callback;
});
document.querySelectorAll('[jetzig-click]').forEach(element => {
const ref = element.getAttribute('jetzig-click');
const action = this.action_specs[ref];
if (action) {
element.addEventListener('click', () => {
const args = [];
action.spec.params.forEach(param => {
const arg = element.dataset[param.name];
if (arg === undefined) {
throw new Error(`Expected 'data-${param.name}' attribute for '${action.name}' click handler.`);
} else {
args.push(element.dataset[param.name]);
}
});
action.callback(...args);
});
} else {
throw new Error(`Unknown click handler: '${ref}'`);
}
});
} else {
const data = JSON.parse(event.data);
this.messageCallbacks.forEach((callback) => {
callback(data);
});
}
});
const reduceState = (ref, state) => {
if (!ref.startsWith('$.')) throw new Error(`Unexpected ref format: ${ref}`);
const args = ref.split('.');
args.shift();
const isNumeric = (string) => [...string].every(char => '0123456789'.includes(char));
const isObject = (object) => object && typeof object === 'object';
return args.reduce((acc, arg) => {
if (isNumeric(arg)) {
if (acc && Array.isArray(acc) && acc.length > arg) return acc[parseInt(arg)];
return null;
} else {
if (acc && isObject(acc)) return acc[arg];
return null;
}
}, state);
};
document.querySelectorAll('[jetzig-connect]').forEach(element => {
const ref = element.getAttribute('jetzig-connect');
if (!this.elementMap[ref]) this.elementMap[ref] = [];
const id = `jetzig-${crypto.randomUUID()}`;
element.setAttribute('jetzig-id', id);
this.elementMap[ref].push(element);
const transformer = element.getAttribute('jetzig-transform');
if (transformer) {
this.transformers[id] = new Function("value", "$", "element", `return ${transformer};`);
}
});
const styled_elements = document.querySelectorAll('[jetzig-style]');
this.onStateChanged(state => {
styled_elements.forEach(element => {
const func = new Function("$", `return ${element.getAttribute('jetzig-style')};`)
const styles = func(state);
Object.entries(styles).forEach(([key, value]) => {
element.style.setProperty(key, value);
});
});
});
// this.websocket.addEventListener("open", (event) => {
// // TODO
// this.publish("websockets", {});
// });
},
receive: function(ref, callback) {
if (Object.hasOwn(this.invokeCallbacks, ref)) {
this.invokeCallbacks[ref].push(callback);
} else {
this.invokeCallbacks[ref] = [callback];
}
},
publish: function(data) {
if (this.websocket) {
const json = JSON.stringify(data);
this.websocket.send(json);
}
},
};
})();

View File

@ -1,3 +0,0 @@
@block head {
<script>console.log("hello");</script>
}

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head>
@partial views:inertia/head
</head>
<body>
<div
id="app"
data-page='{"component":"{{jetzig_view}}","props":{{zmpl.toJson()}},"url":"{{context.path()}}","version":"c32b8e4965f418ad16eaebba1d4e960f"}'
>
</div>
</body>
</html>

View File

@ -11,11 +11,10 @@ routes: []const jetzig.views.Route,
arena: *std.heap.ArenaAllocator, arena: *std.heap.ArenaAllocator,
store: *MemoryStore, store: *MemoryStore,
cache: *MemoryStore, cache: *MemoryStore,
channels: *MemoryStore,
job_queue: *MemoryStore, job_queue: *MemoryStore,
multipart_boundary: ?[]const u8 = null, multipart_boundary: ?[]const u8 = null,
logger: jetzig.loggers.Logger, logger: jetzig.loggers.Logger,
server: *jetzig.http.Server.RoutedServer(@import("root").routes), server: Server,
repo: *jetzig.database.Repo, repo: *jetzig.database.Repo,
cookies: *jetzig.http.Cookies, cookies: *jetzig.http.Cookies,
session: *jetzig.http.Session, session: *jetzig.http.Session,
@ -58,38 +57,15 @@ pub fn init(allocator: std.mem.Allocator, routes_module: type) !App {
const session = try alloc.create(jetzig.http.Session); const session = try alloc.create(jetzig.http.Session);
session.* = jetzig.http.Session.init(alloc, cookies, jetzig.testing.secret, .{}); session.* = jetzig.http.Session.init(alloc, cookies, jetzig.testing.secret, .{});
const server = try alloc.create(jetzig.http.Server.RoutedServer(@import("root").routes));
// This server is only used by database logging - we create a more lifelike server when we
// process the actual test request.
server.* = .{
.logger = logger,
.allocator = undefined,
.env = undefined,
.routes = undefined,
.channel_routes = undefined,
.custom_routes = undefined,
.job_definitions = undefined,
.mailer_definitions = undefined,
.mime_map = undefined,
.initialized = undefined,
.store = undefined,
.job_queue = undefined,
.cache = undefined,
.channels = undefined,
.repo = undefined,
.global = undefined,
};
app.* = App{ app.* = App{
.arena = arena, .arena = arena,
.allocator = allocator, .allocator = allocator,
.routes = &routes_module.routes, .routes = &routes_module.routes,
.store = try createStore(arena.allocator(), logger, .general), .store = try createStore(arena.allocator(), logger, .general),
.cache = try createStore(arena.allocator(), logger, .cache), .cache = try createStore(arena.allocator(), logger, .cache),
.channels = try createStore(arena.allocator(), logger, .channels),
.job_queue = try createStore(arena.allocator(), logger, .jobs), .job_queue = try createStore(arena.allocator(), logger, .jobs),
.logger = logger, .logger = logger,
.server = server, .server = .{ .logger = logger },
.repo = repo, .repo = repo,
.cookies = cookies, .cookies = cookies,
.session = session, .session = session,
@ -155,7 +131,7 @@ pub fn request(
.env_map = std.process.EnvMap.init(allocator), .env_map = std.process.EnvMap.init(allocator),
.env_file = null, .env_file = null,
}; };
var server = jetzig.http.Server.RoutedServer(@import("root").routes){ var server = jetzig.http.Server{
.allocator = allocator, .allocator = allocator,
.logger = self.logger, .logger = self.logger,
.env = .{ .env = .{
@ -172,14 +148,12 @@ pub fn request(
.secret = jetzig.testing.secret, .secret = jetzig.testing.secret,
}, },
.routes = routes, .routes = routes,
.channel_routes = std.StaticStringMap(jetzig.channels.Route).initComptime(.{}),
.custom_routes = &.{}, .custom_routes = &.{},
.mailer_definitions = &.{}, .mailer_definitions = &.{},
.job_definitions = &.{}, .job_definitions = &.{},
.mime_map = jetzig.testing.mime_map, .mime_map = jetzig.testing.mime_map,
.store = self.store, .store = self.store,
.cache = self.cache, .cache = self.cache,
.channels = self.channels,
.job_queue = self.job_queue, .job_queue = self.job_queue,
.global = undefined, .global = undefined,
.repo = self.repo, .repo = self.repo,

View File

@ -92,8 +92,6 @@ pub fn format(self: Route, _: []const u8, _: anytype, writer: anytype) !void {
pub fn match(self: Route, request: *const jetzig.http.Request) bool { pub fn match(self: Route, request: *const jetzig.http.Request) bool {
if (self.method != request.method) return false; if (self.method != request.method) return false;
if (std.mem.eql(u8, request.path.file_path, self.uri_path)) return true;
var request_path_it = std.mem.splitScalar(u8, request.path.base_path, '/'); var request_path_it = std.mem.splitScalar(u8, request.path.base_path, '/');
var uri_path_it = std.mem.splitScalar(u8, self.uri_path, '/'); var uri_path_it = std.mem.splitScalar(u8, self.uri_path, '/');

View File

@ -6,7 +6,6 @@ const jetzig = @import("../../jetzig.zig");
data: *jetzig.data.Data, data: *jetzig.data.Data,
status_code: jetzig.http.status_codes.StatusCode = .ok, status_code: jetzig.http.status_codes.StatusCode = .ok,
content: ?[]const u8 = null,
pub fn deinit(self: Self) void { pub fn deinit(self: Self) void {
_ = self; _ = self;

View File

@ -1,3 +0,0 @@
pub const RoutedWebsocket = @import("websockets/Websocket.zig").RoutedWebsocket;
pub const Websocket = RoutedWebsocket(@import("root").routes);
pub const Context = @import("websockets/Websocket.zig").Context;

View File

@ -1,136 +0,0 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
const httpz = @import("httpz");
pub const Context = struct {
allocator: std.mem.Allocator,
route: jetzig.channels.Route,
session_id: []const u8,
channels: *jetzig.kv.Store.ChannelStore,
logger: jetzig.loggers.Logger,
};
pub fn RoutedWebsocket(Routes: type) type {
return struct {
allocator: std.mem.Allocator,
connection: *httpz.websocket.Conn,
channels: *jetzig.kv.Store.ChannelStore,
route: jetzig.channels.Route,
data: *jetzig.Data,
session_id: []const u8,
logger: jetzig.loggers.Logger,
const Websocket = @This();
const router = jetzig.channels.ActionRouter.initComptime(Routes);
pub fn init(connection: *httpz.websocket.Conn, context: Context) !Websocket {
const data = try context.allocator.create(jetzig.Data);
data.* = jetzig.Data.init(context.allocator);
return Websocket{
.allocator = context.allocator,
.connection = connection,
.route = context.route,
.session_id = context.session_id,
.channels = context.channels,
.logger = context.logger,
.data = data,
};
}
pub fn afterInit(websocket: *Websocket, context: Context) !void {
_ = context;
if (router.encoded_params.get(websocket.route.path)) |params| {
var stack_fallback = std.heap.stackFallback(4096, websocket.allocator);
const allocator = stack_fallback.get();
var write_buffer = websocket.connection.writeBuffer(allocator, .text);
defer write_buffer.deinit();
const writer = write_buffer.writer();
try writer.print("__jetzig_actions__:{s}", .{params});
try write_buffer.flush();
}
const func = websocket.route.openConnectionFn orelse return;
const channel = jetzig.channels.Channel{
.allocator = websocket.allocator,
.websocket = websocket,
.state = try websocket.getState(),
.data = websocket.data,
};
try func(channel);
}
pub fn clientMessage(websocket: *Websocket, allocator: std.mem.Allocator, data: []const u8) !void {
const channel = jetzig.channels.RoutedChannel(Routes){
.allocator = allocator,
.websocket = websocket,
.state = try websocket.getState(),
.data = websocket.data,
};
if (websocket.invoke(channel, data)) |maybe_action| {
if (maybe_action) |action| {
websocket.logger.DEBUG(
"Invoked Channel Action `{s}:{?s}`",
.{ websocket.route.path, action },
) catch {};
return;
}
} else |err| {
websocket.logger.logError(@errorReturnTrace(), err) catch {};
return;
}
const message = jetzig.channels.Message.init(allocator, channel, data);
websocket.route.receiveMessage(message) catch |err| {
websocket.logger.logError(@errorReturnTrace(), err) catch {};
};
websocket.logger.DEBUG("Routed Channel message for `{s}`", .{websocket.route.path}) catch {};
}
pub fn syncState(websocket: *Websocket, channel: jetzig.channels.RoutedChannel(Routes)) !void {
var stack_fallback = std.heap.stackFallback(4096, channel.allocator);
const allocator = stack_fallback.get();
var write_buffer = channel.websocket.connection.writeBuffer(allocator, .text);
defer write_buffer.deinit();
const writer = write_buffer.writer();
// TODO: Make this really fast.
try websocket.channels.put(websocket.session_id, channel.state);
try writer.print("__jetzig_channel_state__:{s}", .{try websocket.data.toJson()});
try write_buffer.flush();
websocket.logger.DEBUG("Synchronized Channel state for `{s}`", .{websocket.route.path}) catch {};
}
pub fn getState(websocket: *Websocket) !*jetzig.data.Value {
return try websocket.channels.get(websocket.data, websocket.session_id) orelse blk: {
const root = try websocket.data.root(.object);
try websocket.channels.put(websocket.session_id, root);
break :blk try websocket.channels.get(websocket.data, websocket.session_id) orelse error.JetzigInvalidChannel;
};
}
fn invoke(
websocket: *Websocket,
channel: jetzig.channels.RoutedChannel(Routes),
data: []const u8,
) !?[]const u8 {
return router.invoke(
websocket.allocator,
websocket.route.path,
data,
@TypeOf(channel),
channel,
);
}
};
}

View File

@ -8,7 +8,6 @@ pub const std_options = std.Options{
}; };
pub const jetzig_options = @import("main").jetzig_options; pub const jetzig_options = @import("main").jetzig_options;
pub const routes = @import("main").routes;
pub fn log( pub fn log(
comptime message_level: std.log.Level, comptime message_level: std.log.Level,

View File

@ -11,7 +11,5 @@ test {
_ = @import("jetzig/http/Path.zig"); _ = @import("jetzig/http/Path.zig");
_ = @import("jetzig/jobs/Job.zig"); _ = @import("jetzig/jobs/Job.zig");
_ = @import("jetzig/mail/Mail.zig"); _ = @import("jetzig/mail/Mail.zig");
_ = @import("jetzig/channels/Channel.zig");
_ = @import("jetzig/channels/Message.zig");
_ = @import("jetzig/loggers/LogQueue.zig"); _ = @import("jetzig/loggers/LogQueue.zig");
} }