This commit is contained in:
Bob Farrell 2024-01-15 20:57:53 +00:00
parent 7748b25e64
commit 65bb626bbc
33 changed files with 750 additions and 189 deletions

View File

@ -16,7 +16,7 @@ If you are interested in _Jetzig_ you will probably find these tools interesting
* :white_check_mark: File system-based routing with [slug] matching. * :white_check_mark: File system-based routing with [slug] matching.
* :white_check_mark: _HTML_ and _JSON_ response (inferred from extension and/or `Accept` header). * :white_check_mark: _HTML_ and _JSON_ response (inferred from extension and/or `Accept` header).
* :white_check_mark: _JSON_-compatible response data builder. * :white_check_mark: _JSON_-compatible response data builder.
* :white_check_mark: _HTML_ templating (see [Zmpl](https://github.com/bobf/zmpl). * :white_check_mark: _HTML_ templating (see [Zmpl](https://github.com/bobf/zmpl)).
* :white_check_mark: Per-request arena allocator. * :white_check_mark: Per-request arena allocator.
* :x: Sessions. * :x: Sessions.
* :x: Cookies. * :x: Cookies.
@ -27,6 +27,8 @@ If you are interested in _Jetzig_ you will probably find these tools interesting
* :x: Custom/dynamic routes. * :x: Custom/dynamic routes.
* :x: General-purpose cache. * :x: General-purpose cache.
* :x: Background jobs. * :x: Background jobs.
* :x: Testing helpers for testing HTTP requests/responses.
* :x: Development server auto-reload.
* :x: Database integration. * :x: Database integration.
* :x: Email receipt (via SendGrid/AWS SES/etc.) * :x: Email receipt (via SendGrid/AWS SES/etc.)

View File

@ -23,12 +23,20 @@ pub fn build(b: *std.Build) !void {
b.installArtifact(exe); b.installArtifact(exe);
const jetzig_module = b.createModule(.{ .source_file = .{ .path = "src/jetzig.zig" } }); const jetzig_module = b.createModule(.{ .source_file = .{ .path = "src/jetzig.zig" } });
exe.addModule("jetzig", jetzig_module); // exe.addModule("jetzig", jetzig_module);
lib.addModule("jetzig", jetzig_module); // lib.addModule("jetzig", jetzig_module);
try b.modules.put("jetzig", jetzig_module); try b.modules.put("jetzig", jetzig_module);
try b.env_map.put("ZMPL_TEMPLATES_PATH", "src/app/views"); const zmpl_module = b.dependency(
const zmpl_module = b.dependency("zmpl", .{ .target = target, .optimize = optimize }); "zmpl",
.{
.target = target,
.optimize = optimize,
.zmpl_templates_path = @as([]const u8, "src/app/views/"),
.zmpl_manifest_path = @as([]const u8, "src/app/views/zmpl.manifest.zig"),
},
);
lib.addModule("zmpl", zmpl_module.module("zmpl")); lib.addModule("zmpl", zmpl_module.module("zmpl"));
exe.addModule("zmpl", zmpl_module.module("zmpl")); exe.addModule("zmpl", zmpl_module.module("zmpl"));
@ -53,11 +61,12 @@ pub fn build(b: *std.Build) !void {
run_step.dependOn(&run_cmd.step); run_step.dependOn(&run_cmd.step);
const main_tests = b.addTest(.{ const main_tests = b.addTest(.{
.root_source_file = .{ .path = "src/main.zig" }, .root_source_file = .{ .path = "src/tests.zig" },
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
main_tests.addModule("zmpl", zmpl_module.module("zmpl"));
const run_main_tests = b.addRunArtifact(main_tests); const run_main_tests = b.addRunArtifact(main_tests);
const test_step = b.step("test", "Run library tests"); const test_step = b.step("test", "Run library tests");
@ -79,7 +88,7 @@ fn findViews(allocator: std.mem.Allocator) !std.ArrayList(*ViewItem) {
const extension = std.fs.path.extension(entry.path); const extension = std.fs.path.extension(entry.path);
const basename = std.fs.path.basename(entry.path); const basename = std.fs.path.basename(entry.path);
if (std.mem.eql(u8, basename, "routes.zig")) continue; if (std.mem.eql(u8, basename, "routes.zig")) continue;
if (std.mem.eql(u8, basename, "manifest.zig")) continue; if (std.mem.eql(u8, basename, "zmpl.manifest.zig")) continue;
if (std.mem.startsWith(u8, basename, ".")) continue; if (std.mem.startsWith(u8, basename, ".")) continue;
if (!std.mem.eql(u8, extension, ".zig")) continue; if (!std.mem.eql(u8, extension, ".zig")) continue;

View File

@ -20,8 +20,9 @@
// When updating this field to a new URL, be sure to delete the corresponding // When updating this field to a new URL, be sure to delete the corresponding
// `hash`, otherwise you are communicating that you expect to find the old hash at // `hash`, otherwise you are communicating that you expect to find the old hash at
// the new URL. // the new URL.
//
.url = "https://github.com/bobf/zmpl/archive/refs/tags/0.0.1.tar.gz", .url = "https://github.com/bobf/zmpl/archive/refs/tags/0.0.1.tar.gz",
.hash = "1220eedaec5c45067b0158251fd49f3ecc9c7c4676f7e3e0567ca2b2c465fa6cd983", .hash = "12205f95df0bf7a66c9d00ed76f8c3b1548eb958f9210425045c77f88ea165f20fef",
// This is computed from the file contents of the directory of files that is // This is computed from the file contents of the directory of files that is
// obtained after fetching `url` and applying the inclusion rules given by // obtained after fetching `url` and applying the inclusion rules given by

View File

@ -1 +0,0 @@
hellhelloo

View File

@ -1,48 +1,53 @@
const std = @import("std"); const std = @import("std");
const root = @import("root"); const root = @import("root");
const Request = root.jetzig.http.Request;
const Data = root.jetzig.data.Data;
const View = root.jetzig.views.View;
pub fn index(request: *root.jetzig.http.Request) anyerror!root.jetzig.views.View { pub fn index(request: *Request, data: *Data) anyerror!View {
var data = request.data();
var object = try data.object(); var object = try data.object();
try object.add("message", data.string("hello there")); try request.session.put("foo", data.string("bar"));
try object.add("foo", data.string("foo lookup")); try object.put("message", data.string("hello there"));
try object.add("bar", data.string("bar lookup")); try object.put("foo", data.string("foo lookup"));
try object.put("bar", data.string("bar lookup"));
try object.put("session_value", (try request.session.get("foo")).?);
return request.render(.ok); return request.render(.ok);
} }
pub fn get(id: []const u8, request: *root.jetzig.http.Request) anyerror!root.jetzig.views.View { pub fn get(id: []const u8, request: *Request, data: *Data) anyerror!View {
var data = request.data();
var object = try data.object(); var object = try data.object();
try object.add("message", data.string("hello there")); try request.session.put("foo", data.string("bar"));
try object.add("other_message", data.string("hello again")); try object.put("session_value", (try request.session.get("foo")).?);
try object.add("an_integer", data.integer(10));
try object.add("a_float", data.float(1.345)); try object.put("message", data.string("hello there"));
try object.add("a_boolean", data.boolean(true)); try object.put("other_message", data.string("hello again"));
try object.add("Null", data.Null); try object.put("an_integer", data.integer(10));
try object.add("a_random_integer", data.integer(std.crypto.random.int(u8))); try object.put("a_float", data.float(1.345));
try object.add("resource_id", data.string(id)); try object.put("a_boolean", data.boolean(true));
try object.put("Null", data.Null);
try object.put("a_random_integer", data.integer(std.crypto.random.int(u8)));
try object.put("resource_id", data.string(id));
var nested_object = try data.object(); var nested_object = try data.object();
try nested_object.add("nested key", data.string("nested value")); try nested_object.put("nested key", data.string("nested value"));
try object.add("other_message", nested_object.*); try object.put("other_message", nested_object.*);
return request.render(.ok); return request.render(.ok);
} }
pub fn patch(id: []const u8, request: *root.jetzig.http.Request) anyerror!root.jetzig.views.View { pub fn patch(id: []const u8, request: *Request, data: *Data) anyerror!View {
var data = request.data();
var object = try data.object(); var object = try data.object();
const integer = std.crypto.random.int(u8); const integer = std.crypto.random.int(u8);
try object.add("message", data.string("hello there")); try object.put("message", data.string("hello there"));
try object.add("other_message", data.string("hello again")); try object.put("other_message", data.string("hello again"));
try object.add("other_message", data.integer(integer)); try object.put("other_message", data.integer(integer));
try object.add("id", data.string(id)); try object.put("id", data.string(id));
return request.render(.ok); return request.render(.ok);
} }

View File

@ -1,14 +1,2 @@
if (std.mem.eql(u8, try zmpl.getValueString("message"), "hello there")) { <span>{.resource_id}</span>
const foo = "foo const"; <span>{.session_value}</span>
const bar = "bar const";
inline for (1..4) |index| {
<div>Hello {:foo}!</div>
<div>Hello {:bar}!</div>
<div>Hello {:index}!</div>
<div>Hello {.foo}!</div>
<div>Hello {.bar}!</div>
}
} else {
<div>Unexpected message</div>
}

View File

@ -1 +0,0 @@
<span>{.resource_id}</span>

View File

@ -0,0 +1,14 @@
if (std.mem.eql(u8, try zmpl.getValueString("message"), "hello there")) {
const foo = "foo const";
const bar = "bar const";
inline for (1..4) |index| {
<div>Hello {:foo}!</div>
<div>Hello {:bar}!</div>
<div>Hello {:index}!</div>
<div>Hello {.foo}!</div>
<div>Hello {.bar}!</div>
}
} else {
<div>Unexpected message</div>
}

12
src/app/views/index.zig Normal file
View File

@ -0,0 +1,12 @@
const std = @import("std");
const jetzig = @import("root").jetzig;
const Request = jetzig.http.Request;
const Data = jetzig.data.Data;
const View = jetzig.views.View;
pub fn index(request: *Request, data: *Data) anyerror!View {
var object = try data.object();
try object.put("foo", data.string("hello"));
return request.render(.ok);
}

11
src/app/views/index.zmpl Normal file
View File

@ -0,0 +1,11 @@
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<div hx-get="/options/latest" hx-trigger="every 1s"></div>
inline for (0..10) |index| {
<div>
<button hx-trigger="click" hx-put="/options/{:index}" hx-swap="innerHTML" hx-target="#option">Option #{:index}</option>
</div>
}
<div id="option">Select an option.</div>

36
src/app/views/options.zig Normal file
View File

@ -0,0 +1,36 @@
const std = @import("std");
const root = @import("root");
const Request = root.jetzig.http.Request;
const Data = root.jetzig.data.Data;
const View = root.jetzig.views.View;
pub fn put(id: []const u8, request: *Request, data: *Data) anyerror!View {
try request.session.put("option", data.string(id));
var object = try data.object();
try object.put("option", data.string(id));
const count = try request.session.get("count");
if (count) |value| {
try request.session.put("count", data.integer(value.integer.value + 1));
try object.put("count", data.integer(value.integer.value + 1));
} else {
try request.session.put("count", data.integer(0));
try object.put("count", data.integer(0));
}
return request.render(.ok);
}
pub fn get(id: []const u8, request: *Request, data: *Data) anyerror!View {
if (std.mem.eql(u8, id, "latest")) {
var object = try data.object();
const count = try request.session.get("count");
if (count) |value| {
try object.put("count", data.integer(value.integer.value + 1));
} else {
try object.put("count", data.integer(0));
}
}
return request.render(.ok);
}

View File

@ -0,0 +1 @@
<div>Number of times clicked: {.count}</div>

View File

@ -0,0 +1 @@
<div>You chose option {.option}</div>

View File

@ -1,3 +1,5 @@
pub const routes = .{ pub const routes = .{
@import("foo/bar/baz.zig"), @import("foo/bar/baz.zig"),
@import("index.zig"),
@import("options.zig"),
}; };

View File

@ -2,6 +2,9 @@
// This file is automatically generated at build time. Manual edits will be discarded. // This file is automatically generated at build time. Manual edits will be discarded.
// This file should _not_ be stored in version control. // This file should _not_ be stored in version control.
pub const templates = struct { pub const templates = struct {
pub const index = @import(".index.zmpl.compiled.zig");
pub const foo_bar_baz_index = @import("foo/bar/baz/.index.zmpl.compiled.zig");
pub const foo_bar_baz_get = @import("foo/bar/baz/.get.zmpl.compiled.zig"); pub const foo_bar_baz_get = @import("foo/bar/baz/.get.zmpl.compiled.zig");
pub const foo_bar_baz_get_id = @import("foo/bar/baz/.get[id].zmpl.compiled.zig"); pub const options_put = @import("options/.put.zmpl.compiled.zig");
pub const options_get = @import("options/.get.zmpl.compiled.zig");
}; };

View File

@ -4,11 +4,11 @@ pub const zmpl = @import("zmpl");
pub const http = @import("jetzig/http.zig"); pub const http = @import("jetzig/http.zig");
pub const loggers = @import("jetzig/loggers.zig"); pub const loggers = @import("jetzig/loggers.zig");
pub const data = @import("jetzig/data.zig");
pub const caches = @import("jetzig/caches.zig"); pub const caches = @import("jetzig/caches.zig");
pub const views = @import("jetzig/views.zig"); pub const views = @import("jetzig/views.zig");
pub const colors = @import("jetzig/colors.zig"); pub const colors = @import("jetzig/colors.zig");
pub const App = @import("jetzig/App.zig"); pub const App = @import("jetzig/App.zig");
pub const DefaultAllocator = std.heap.GeneralPurposeAllocator(.{});
pub fn init(allocator: std.mem.Allocator) !App { pub fn init(allocator: std.mem.Allocator) !App {
const args = try std.process.argsAlloc(allocator); const args = try std.process.argsAlloc(allocator);
@ -22,8 +22,6 @@ pub fn init(allocator: std.mem.Allocator) !App {
// TODO: Fix this up with proper arg parsing // TODO: Fix this up with proper arg parsing
const port: u16 = if (args.len > 2) try std.fmt.parseInt(u16, args[2], 10) else 8080; const port: u16 = if (args.len > 2) try std.fmt.parseInt(u16, args[2], 10) else 8080;
const use_cache: bool = args.len > 3 and std.mem.eql(u8, args[3], "--cache"); const use_cache: bool = args.len > 3 and std.mem.eql(u8, args[3], "--cache");
var debug: bool = args.len > 3 and std.mem.eql(u8, args[3], "--debug");
debug = debug or (args.len > 4 and std.mem.eql(u8, args[4], "--debug"));
const server_cache = switch (use_cache) { const server_cache = switch (use_cache) {
true => caches.Cache{ .memory_cache = caches.MemoryCache.init(allocator) }, true => caches.Cache{ .memory_cache = caches.MemoryCache.init(allocator) },
false => caches.Cache{ .null_cache = caches.NullCache.init(allocator) }, false => caches.Cache{ .null_cache = caches.NullCache.init(allocator) },
@ -38,11 +36,14 @@ pub fn init(allocator: std.mem.Allocator) !App {
} }
}; };
const logger = loggers.Logger{ .development_logger = loggers.DevelopmentLogger.init(allocator, debug) }; const logger = loggers.Logger{ .development_logger = loggers.DevelopmentLogger.init(allocator) };
const secret = try generateSecret(allocator);
const server_options = http.Server.ServerOptions{ const server_options = http.Server.ServerOptions{
.cache = server_cache, .cache = server_cache,
.logger = logger, .logger = logger,
.root_path = root_path, .root_path = root_path,
.secret = secret,
}; };
return .{ return .{
@ -58,27 +59,35 @@ pub fn init(allocator: std.mem.Allocator) !App {
// Each detected function is stored as a Route which can be accessed at runtime to route requests // Each detected function is stored as a Route which can be accessed at runtime to route requests
// to the appropriate View. // to the appropriate View.
pub fn route(comptime modules: anytype) []views.Route { pub fn route(comptime modules: anytype) []views.Route {
var size: u16 = 0; var size: usize = 0;
for (modules) |module| { for (modules) |module| {
const decls = @typeInfo(module).Struct.decls; const decls = @typeInfo(module).Struct.decls;
for (decls) |_| size += 1; for (decls) |decl| {
if (@hasField(views.Route.ViewType, decl.name)) size += 1;
}
} }
var detected: [size]views.Route = undefined; var detected: [size]views.Route = undefined;
var index: usize = 0;
for (modules, 1..) |module, module_index| { for (modules) |module| {
const decls = @typeInfo(module).Struct.decls; const decls = @typeInfo(module).Struct.decls;
for (decls, 1..) |decl, decl_index| { for (decls) |decl| {
if (!@hasField(views.Route.ViewType, decl.name)) {
// TODO: Figure out how to log a warning here (comptime issues).
continue;
}
const view = @unionInit(views.Route.ViewType, decl.name, @field(module, decl.name)); const view = @unionInit(views.Route.ViewType, decl.name, @field(module, decl.name));
detected[module_index * decl_index - 1] = .{ detected[index] = .{
.name = @typeName(module), .name = @typeName(module),
.action = @field(views.Route.Action, decl.name), .action = @field(views.Route.Action, decl.name),
.view = view, .view = view,
}; };
index += 1;
} }
} }
@ -109,3 +118,40 @@ pub const TemplateFn = struct {
name: []const u8, name: []const u8,
render: *const fn (*zmpl.Data) anyerror![]const u8, render: *const fn (*zmpl.Data) anyerror![]const u8,
}; };
pub fn generateSecret(allocator: std.mem.Allocator) ![]const u8 {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var secret: [64]u8 = undefined;
for (0..64) |index| {
secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len)];
}
return try allocator.dupe(u8, &secret);
}
pub fn base64Encode(allocator: std.mem.Allocator, string: []const u8) ![]const u8 {
const encoder = std.base64.Base64Encoder.init(
std.base64.url_safe_no_pad.alphabet_chars,
std.base64.url_safe_no_pad.pad_char,
);
const size = encoder.calcSize(string.len);
const ptr = try allocator.alloc(u8, size);
_ = encoder.encode(ptr, string);
return ptr;
}
pub fn base64Decode(allocator: std.mem.Allocator, string: []const u8) ![]const u8 {
const decoder = std.base64.Base64Decoder.init(
std.base64.url_safe_no_pad.alphabet_chars,
std.base64.url_safe_no_pad.pad_char,
);
const size = try decoder.calcSizeForSlice(string);
const ptr = try allocator.alloc(u8, size);
try decoder.decode(ptr, string);
return ptr;
}
test {
@import("std").testing.refAllDeclsRecursive(@This());
}

View File

@ -1,26 +1,21 @@
const std = @import("std"); const std = @import("std");
const root = @import("root"); const jetzig = @import("../jetzig.zig");
const Self = @This(); const Self = @This();
server_options: root.jetzig.http.Server.ServerOptions, server_options: jetzig.http.Server.ServerOptions,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
host: []const u8, host: []const u8,
port: u16, port: u16,
root_path: []const u8, root_path: []const u8,
pub fn render(self: *const Self, data: anytype) root.views.View {
_ = self;
return .{ .data = data };
}
pub fn deinit(self: Self) void { pub fn deinit(self: Self) void {
_ = self; _ = self;
} }
pub fn start(self: Self, views: []root.jetzig.views.Route, templates: []root.jetzig.TemplateFn) !void { pub fn start(self: Self, views: []jetzig.views.Route, templates: []jetzig.TemplateFn) !void {
var server = root.jetzig.http.Server.init( var server = jetzig.http.Server.init(
self.allocator, self.allocator,
self.host, self.host,
self.port, self.port,
@ -32,6 +27,7 @@ pub fn start(self: Self, views: []root.jetzig.views.Route, templates: []root.jet
defer server.deinit(); defer server.deinit();
defer self.allocator.free(self.root_path); defer self.allocator.free(self.root_path);
defer self.allocator.free(self.host); defer self.allocator.free(self.host);
defer self.allocator.free(server.options.secret);
server.listen() catch |err| { server.listen() catch |err| {
switch (err) { switch (err) {

View File

@ -12,7 +12,7 @@ pub const Cache = union(enum) {
pub fn deinit(self: *Cache) void { pub fn deinit(self: *Cache) void {
switch (self.*) { switch (self.*) {
inline else => |*case| try case.deinit(), inline else => |*case| case.deinit(),
} }
} }

View File

@ -2,13 +2,13 @@ const std = @import("std");
const Self = @This(); const Self = @This();
const root = @import("root"); const jetzig = @import("../../jetzig.zig");
value: root.jetzig.http.Response, value: jetzig.http.Response,
cached: bool, cached: bool,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator, value: root.jetzig.http.Response, cached: bool) Self { pub fn init(allocator: std.mem.Allocator, value: jetzig.http.Response, cached: bool) Self {
return .{ .allocator = allocator, .cached = cached, .value = value }; return .{ .allocator = allocator, .cached = cached, .value = value };
} }

View File

@ -5,4 +5,6 @@ const colors = @import("colors.zig");
pub const Server = @import("http/Server.zig"); pub const Server = @import("http/Server.zig");
pub const Request = @import("http/Request.zig"); pub const Request = @import("http/Request.zig");
pub const Response = @import("http/Response.zig"); pub const Response = @import("http/Response.zig");
pub const Session = @import("http/Session.zig");
pub const Cookies = @import("http/Cookies.zig");
pub const status_codes = @import("http/status_codes.zig"); pub const status_codes = @import("http/status_codes.zig");

180
src/jetzig/http/Cookies.zig Normal file
View File

@ -0,0 +1,180 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
allocator: std.mem.Allocator,
cookie_string: []const u8,
buf: std.ArrayList(u8),
cookies: std.StringArrayHashMap(*Cookie),
const Self = @This();
pub const Cookie = struct {
value: []const u8,
};
pub fn init(allocator: std.mem.Allocator, cookie_string: []const u8) Self {
return .{
.allocator = allocator,
.cookie_string = cookie_string,
.cookies = std.StringArrayHashMap(*Cookie).init(allocator),
.buf = std.ArrayList(u8).init(allocator),
};
}
pub fn deinit(self: *Self) void {
var it = self.cookies.iterator();
while (it.next()) |item| {
self.allocator.free(item.key_ptr.*);
self.allocator.free(item.value_ptr.*.value);
self.allocator.destroy(item.value_ptr.*);
}
self.cookies.deinit();
self.buf.deinit();
}
pub fn get(self: *Self, key: []const u8) ?*Cookie {
return self.cookies.get(key);
}
pub fn put(self: *Self, key: []const u8, value: Cookie) !void {
if (self.cookies.fetchSwapRemove(key)) |entry| {
self.allocator.free(entry.key);
self.allocator.free(entry.value.value);
self.allocator.destroy(entry.value);
}
const ptr = try self.allocator.create(Cookie);
ptr.* = value;
ptr.*.value = try self.allocator.dupe(u8, value.value);
try self.cookies.put(try self.allocator.dupe(u8, key), ptr);
}
pub const HeaderIterator = struct {
allocator: std.mem.Allocator,
cookies_iterator: std.StringArrayHashMap(*Cookie).Iterator,
pub fn init(allocator: std.mem.Allocator, cookies: *Self) HeaderIterator {
return .{ .allocator = allocator, .cookies_iterator = cookies.cookies.iterator() };
}
pub fn next(self: *HeaderIterator) !?[]const u8 {
if (self.cookies_iterator.next()) |*item| {
return try std.fmt.allocPrint(
self.allocator,
"{s}={s}; path=/; domain=localhost", // TODO: Add all options, remove hardcoded domain
.{ item.key_ptr.*, item.value_ptr.*.value },
);
} else {
return null;
}
}
};
pub fn headerIterator(self: *Self) HeaderIterator {
var buf = std.ArrayList([]const u8).init(self.allocator);
defer buf.deinit();
defer for (buf.items) |item| self.allocator.free(item);
return HeaderIterator.init(self.allocator, self);
}
// https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.1
// cookie-header = "Cookie:" OWS cookie-string OWS
// cookie-string = cookie-pair *( ";" SP cookie-pair )
pub fn parse(self: *Self) !void {
var key_buf = std.ArrayList(u8).init(self.allocator);
var value_buf = std.ArrayList(u8).init(self.allocator);
var key_terminated = false;
var value_started = false;
defer key_buf.deinit();
defer value_buf.deinit();
for (self.cookie_string, 0..) |char, index| {
if (char == '=') {
key_terminated = true;
continue;
}
if (char == ';' or index == self.cookie_string.len - 1) {
if (char != ';') try value_buf.append(char);
try self.put(
key_buf.items,
Cookie{ .value = value_buf.items },
);
key_buf.clearAndFree();
value_buf.clearAndFree();
value_started = false;
key_terminated = false;
continue;
}
if (!key_terminated and char == ' ') continue;
if (!key_terminated) {
try key_buf.append(char);
continue;
}
if (char == ' ' and !value_started) continue;
if (char != ' ' and !value_started) value_started = true;
if (key_terminated and value_started) {
try value_buf.append(char);
continue;
}
unreachable;
}
}
test "basic cookie string" {
const allocator = std.testing.allocator;
var cookies = Self.init(allocator, "foo=bar; baz=qux;");
defer cookies.deinit();
try cookies.parse();
try std.testing.expectEqualStrings("bar", cookies.get("foo").?.value);
try std.testing.expectEqualStrings("qux", cookies.get("baz").?.value);
}
test "empty cookie string" {
const allocator = std.testing.allocator;
var cookies = Self.init(allocator, "");
defer cookies.deinit();
try cookies.parse();
}
test "cookie string with irregular spaces" {
const allocator = std.testing.allocator;
var cookies = Self.init(allocator, "foo= bar; baz= qux;");
defer cookies.deinit();
try cookies.parse();
try std.testing.expectEqualStrings("bar", cookies.get("foo").?.value);
try std.testing.expectEqualStrings("qux", cookies.get("baz").?.value);
}
test "headerIterator" {
const allocator = std.testing.allocator;
var buf = std.ArrayList(u8).init(allocator);
defer buf.deinit();
const writer = buf.writer();
var cookies = Self.init(allocator, "foo=bar; baz=qux;");
defer cookies.deinit();
try cookies.parse();
var it = cookies.headerIterator();
while (try it.next()) |*header| {
try writer.writeAll(header.*);
try writer.writeAll("\n");
allocator.free(header.*);
}
try std.testing.expectEqualStrings(
\\foo=bar; path=/; domain=localhost
\\baz=qux; path=/; domain=localhost
\\
, buf.items);
}

View File

@ -1,6 +1,6 @@
const std = @import("std"); const std = @import("std");
const root = @import("root"); const jetzig = @import("../../jetzig.zig");
const Self = @This(); const Self = @This();
const default_content_type = "text/html"; const default_content_type = "text/html";
@ -14,16 +14,18 @@ path: []const u8,
method: Method, method: Method,
headers: std.http.Headers, headers: std.http.Headers,
segments: std.ArrayList([]const u8), segments: std.ArrayList([]const u8),
server: *root.jetzig.http.Server, server: *jetzig.http.Server,
status_code: root.jetzig.http.status_codes.StatusCode = undefined, session: *jetzig.http.Session,
response_data: root.jetzig.views.data.Data = undefined, status_code: jetzig.http.status_codes.StatusCode = undefined,
response_data: *jetzig.data.Data,
cookies: *jetzig.http.Cookies,
pub fn init( pub fn init(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
server: *root.jetzig.http.Server, server: *jetzig.http.Server,
request: std.http.Server.Request, response: *std.http.Server.Response,
) !Self { ) !Self {
const method = switch (request.method) { const method = switch (response.request.method) {
.DELETE => Method.DELETE, .DELETE => Method.DELETE,
.GET => Method.GET, .GET => Method.GET,
.PATCH => Method.PATCH, .PATCH => Method.PATCH,
@ -35,29 +37,57 @@ pub fn init(
.TRACE => Method.TRACE, .TRACE => Method.TRACE,
}; };
var it = std.mem.splitScalar(u8, request.target, '/'); var it = std.mem.splitScalar(u8, response.request.target, '/');
var segments = std.ArrayList([]const u8).init(allocator); var segments = std.ArrayList([]const u8).init(allocator);
while (it.next()) |segment| try segments.append(segment); while (it.next()) |segment| try segments.append(segment);
var cookies = try allocator.create(jetzig.http.Cookies);
cookies.* = jetzig.http.Cookies.init(
allocator,
response.request.headers.getFirstValue("Cookie") orelse "",
);
try cookies.parse();
var session = try allocator.create(jetzig.http.Session);
session.* = jetzig.http.Session.init(allocator, cookies, server.options.secret);
session.parse() catch |err| {
switch (err) {
error.JetzigInvalidSessionCookie => {
server.logger.debug("Invalid session cookie detected. Resetting session.", .{});
try session.reset();
},
else => return err,
}
};
const response_data = try allocator.create(jetzig.data.Data);
response_data.* = jetzig.data.Data.init(allocator);
return .{ return .{
.allocator = allocator, .allocator = allocator,
.path = request.target, .path = response.request.target,
.method = method, .method = method,
.headers = request.headers, .headers = response.request.headers,
.server = server, .server = server,
.segments = segments, .segments = segments,
.cookies = cookies,
.session = session,
.response_data = response_data,
}; };
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
defer self.segments.deinit(); self.session.deinit();
self.segments.deinit();
self.allocator.destroy(self.cookies);
self.allocator.destroy(self.session);
} }
pub fn render(self: *Self, status_code: root.jetzig.http.status_codes.StatusCode) root.jetzig.views.View { pub fn render(self: *Self, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
return .{ .data = &self.response_data, .status_code = status_code }; return .{ .data = self.response_data, .status_code = status_code };
} }
pub fn requestFormat(self: *Self) root.jetzig.http.Request.Format { pub fn requestFormat(self: *Self) jetzig.http.Request.Format {
return self.extensionFormat() orelse self.acceptHeaderFormat() orelse .UNKNOWN; return self.extensionFormat() orelse self.acceptHeaderFormat() orelse .UNKNOWN;
} }
@ -65,7 +95,7 @@ pub fn getHeader(self: *Self, key: []const u8) ?[]const u8 {
return self.headers.getFirstValue(key); return self.headers.getFirstValue(key);
} }
fn extensionFormat(self: *Self) ?root.jetzig.http.Request.Format { fn extensionFormat(self: *Self) ?jetzig.http.Request.Format {
const extension = std.fs.path.extension(self.path); const extension = std.fs.path.extension(self.path);
if (std.mem.eql(u8, extension, ".html")) { if (std.mem.eql(u8, extension, ".html")) {
@ -77,7 +107,7 @@ fn extensionFormat(self: *Self) ?root.jetzig.http.Request.Format {
} }
} }
pub fn acceptHeaderFormat(self: *Self) ?root.jetzig.http.Request.Format { pub fn acceptHeaderFormat(self: *Self) ?jetzig.http.Request.Format {
const acceptHeader = self.getHeader("Accept"); const acceptHeader = self.getHeader("Accept");
if (acceptHeader) |item| { if (acceptHeader) |item| {
@ -96,25 +126,6 @@ pub fn hash(self: *Self) ![]const u8 {
); );
} }
pub fn fullPath(self: *Self) ![]const u8 {
const base_path = try std.fs.path.join(self.allocator, &[_][]const u8{
self.server.options.root_path,
"views",
});
defer self.allocator.free(base_path);
const resource_path = try self.resourcePath();
defer self.allocator.free(resource_path);
const full_path = try std.fs.path.join(self.allocator, &[_][]const u8{
base_path,
resource_path,
self.resourceName(),
self.templateName(),
});
defer self.allocator.free(full_path);
return self.allocator.dupe(u8, full_path);
}
pub fn resourceModifier(self: *Self) ?Modifier { pub fn resourceModifier(self: *Self) ?Modifier {
const basename = std.fs.path.basename(self.segments.items[self.segments.items.len - 1]); const basename = std.fs.path.basename(self.segments.items[self.segments.items.len - 1]);
const extension = std.fs.path.extension(basename); const extension = std.fs.path.extension(basename);
@ -144,12 +155,18 @@ pub fn resourceId(self: *Self) []const u8 {
return self.resourceName(); return self.resourceName();
} }
pub fn match(self: *Self, route: root.jetzig.views.Route) !bool { pub fn match(self: *Self, route: jetzig.views.Route) !bool {
switch (self.method) { switch (self.method) {
.GET => { .GET => {
return switch (route.action) { return switch (route.action) {
.index => std.mem.eql(u8, try self.nameWithResourceId(), route.name), .index => blk: {
.get => std.mem.eql(u8, try self.nameWithoutResourceId(), route.name), if (std.mem.eql(u8, self.path, "/") and std.mem.eql(u8, route.name, "app.views.index")) {
break :blk true;
} else {
break :blk std.mem.eql(u8, try self.fullName(), route.name);
}
},
.get => std.mem.eql(u8, try self.fullNameWithStrippedResourceId(), route.name),
else => false, else => false,
}; };
}, },
@ -163,19 +180,6 @@ pub fn match(self: *Self, route: root.jetzig.views.Route) !bool {
return false; return false;
} }
pub fn data(self: *Self) *root.jetzig.views.data.Data {
self.response_data = root.jetzig.views.data.Data.init(self.allocator);
return &self.response_data;
}
fn templateName(self: *Self) []const u8 {
switch (self.method) {
.GET => return "index.html.zmpl",
.SHOW => return "[id].html.zmpl",
else => unreachable, // TODO: Missing HTTP verbs.
}
}
fn isEditAction(self: *Self) bool { fn isEditAction(self: *Self) bool {
if (self.resourceModifier()) |modifier| { if (self.resourceModifier()) |modifier| {
return modifier == .edit; return modifier == .edit;
@ -188,11 +192,11 @@ fn isNewAction(self: *Self) bool {
} else return false; } else return false;
} }
fn nameWithResourceId(self: *Self) ![]const u8 { fn fullName(self: *Self) ![]const u8 {
return try self.name(true); return try self.name(true);
} }
fn nameWithoutResourceId(self: *Self) ![]const u8 { fn fullNameWithStrippedResourceId(self: *Self) ![]const u8 {
return try self.name(false); return try self.name(false);
} }

View File

@ -8,6 +8,18 @@ allocator: std.mem.Allocator,
content: []const u8, content: []const u8,
status_code: http.status_codes.StatusCode, status_code: http.status_codes.StatusCode,
pub fn init(
allocator: std.mem.Allocator,
content: []const u8,
status_code: http.status_codes.StatusCode,
) Self {
return .{
.status_code = status_code,
.content = content,
.allocator = allocator,
};
}
pub fn deinit(self: *const Self) void { pub fn deinit(self: *const Self) void {
_ = self; _ = self;
// self.allocator.free(self.content); // self.allocator.free(self.content);

View File

@ -1,23 +1,24 @@
const std = @import("std"); const std = @import("std");
const root = @import("root"); const jetzig = @import("../../jetzig.zig");
pub const ServerOptions = struct { pub const ServerOptions = struct {
cache: root.jetzig.caches.Cache, cache: jetzig.caches.Cache,
logger: root.jetzig.loggers.Logger, logger: jetzig.loggers.Logger,
root_path: []const u8, root_path: []const u8,
secret: []const u8,
}; };
server: std.http.Server, server: std.http.Server,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
port: u16, port: u16,
host: []const u8, host: []const u8,
cache: root.jetzig.caches.Cache, cache: jetzig.caches.Cache,
logger: root.jetzig.loggers.Logger, logger: jetzig.loggers.Logger,
options: ServerOptions, options: ServerOptions,
start_time: i128 = undefined, start_time: i128 = undefined,
routes: []root.jetzig.views.Route, routes: []jetzig.views.Route,
templates: []root.jetzig.TemplateFn, templates: []jetzig.TemplateFn,
const Self = @This(); const Self = @This();
@ -26,8 +27,8 @@ pub fn init(
host: []const u8, host: []const u8,
port: u16, port: u16,
options: ServerOptions, options: ServerOptions,
routes: []root.jetzig.views.Route, routes: []jetzig.views.Route,
templates: []root.jetzig.TemplateFn, templates: []jetzig.TemplateFn,
) Self { ) Self {
const server = std.http.Server.init(allocator, .{ .reuse_address = true }); const server = std.http.Server.init(allocator, .{ .reuse_address = true });
@ -59,7 +60,13 @@ pub fn listen(self: *Self) !void {
fn processRequests(self: *Self) !void { fn processRequests(self: *Self) !void {
while (true) { while (true) {
self.processNextRequest() catch |err| { var response = try self.server.accept(.{ .allocator = self.allocator });
defer response.deinit();
try response.headers.append("Connection", "close");
while (response.reset() != .closing) {
self.processNextRequest(&response) catch |err| {
switch (err) { switch (err) {
error.EndOfStream => continue, error.EndOfStream => continue,
error.ConnectionResetByPeer => continue, error.ConnectionResetByPeer => continue,
@ -67,24 +74,31 @@ fn processRequests(self: *Self) !void {
} }
}; };
} }
}
} }
fn processNextRequest(self: *Self) !void { fn processNextRequest(self: *Self, response: *std.http.Server.Response) !void {
var response = try self.server.accept(.{ .allocator = self.allocator });
defer response.deinit();
try response.wait(); try response.wait();
self.start_time = std.time.nanoTimestamp(); self.start_time = std.time.nanoTimestamp();
const body = try response.reader().readAllAlloc(self.allocator, 8192); // FIXME: Configurable max body size
defer self.allocator.free(body);
var arena = std.heap.ArenaAllocator.init(self.allocator); var arena = std.heap.ArenaAllocator.init(self.allocator);
defer arena.deinit(); defer arena.deinit();
var request = try root.jetzig.http.Request.init(arena.allocator(), self, response.request); var request = try jetzig.http.Request.init(arena.allocator(), self, response);
defer request.deinit(); defer request.deinit();
const result = try self.pageContent(&request); const result = try self.pageContent(&request);
defer result.deinit(); defer result.deinit();
response.transfer_encoding = .{ .content_length = result.value.content.len }; response.transfer_encoding = .{ .content_length = result.value.content.len };
var cookie_it = request.cookies.headerIterator();
while (try cookie_it.next()) |header| {
try response.headers.append("Set-Cookie", header);
}
response.status = switch (result.value.status_code) { response.status = switch (result.value.status_code) {
.ok => .ok, .ok => .ok,
.not_found => .not_found, .not_found => .not_found,
@ -92,6 +106,7 @@ fn processNextRequest(self: *Self) !void {
try response.do(); try response.do();
try response.writeAll(result.value.content); try response.writeAll(result.value.content);
try response.finish(); try response.finish();
const log_message = try self.requestLogMessage(&request, result); const log_message = try self.requestLogMessage(&request, result);
@ -99,7 +114,7 @@ fn processNextRequest(self: *Self) !void {
self.logger.debug("{s}", .{log_message}); self.logger.debug("{s}", .{log_message});
} }
fn pageContent(self: *Self, request: *root.jetzig.http.Request) !root.jetzig.caches.Result { fn pageContent(self: *Self, request: *jetzig.http.Request) !jetzig.caches.Result {
const cache_key = try request.hash(); const cache_key = try request.hash();
if (self.cache.get(cache_key)) |item| { if (self.cache.get(cache_key)) |item| {
@ -110,7 +125,7 @@ fn pageContent(self: *Self, request: *root.jetzig.http.Request) !root.jetzig.cac
} }
} }
fn renderResponse(self: *Self, request: *root.jetzig.http.Request) !root.jetzig.http.Response { fn renderResponse(self: *Self, request: *jetzig.http.Request) !jetzig.http.Response {
const view = try self.matchView(request); const view = try self.matchView(request);
switch (request.requestFormat()) { switch (request.requestFormat()) {
@ -122,15 +137,16 @@ fn renderResponse(self: *Self, request: *root.jetzig.http.Request) !root.jetzig.
fn renderHTML( fn renderHTML(
self: *Self, self: *Self,
request: *root.jetzig.http.Request, request: *jetzig.http.Request,
route: ?root.jetzig.views.Route, route: ?jetzig.views.Route,
) !root.jetzig.http.Response { ) !jetzig.http.Response {
if (route) |matched_route| { if (route) |matched_route| {
const expected_name = try matched_route.templateName(self.allocator); const expected_name = try matched_route.templateName(self.allocator);
defer self.allocator.free(expected_name); defer self.allocator.free(expected_name);
for (self.templates) |template| { for (self.templates) |template| {
// FIXME: Tidy this up and use a hashmap for templates instead of an array. // FIXME: Tidy this up and use a hashmap for templates (or a more comprehensive
// matching system) instead of an array.
if (std.mem.eql(u8, expected_name, template.name)) { if (std.mem.eql(u8, expected_name, template.name)) {
const view = try matched_route.render(matched_route, request); const view = try matched_route.render(matched_route, request);
const content = try template.render(view.data); const content = try template.render(view.data);
@ -154,9 +170,9 @@ fn renderHTML(
fn renderJSON( fn renderJSON(
self: *Self, self: *Self,
request: *root.jetzig.http.Request, request: *jetzig.http.Request,
route: ?root.jetzig.views.Route, route: ?jetzig.views.Route,
) !root.jetzig.http.Response { ) !jetzig.http.Response {
if (route) |matched_route| { if (route) |matched_route| {
const view = try matched_route.render(matched_route, request); const view = try matched_route.render(matched_route, request);
var data = view.data; var data = view.data;
@ -172,13 +188,13 @@ fn renderJSON(
}; };
} }
fn requestLogMessage(self: *Self, request: *root.jetzig.http.Request, result: root.jetzig.caches.Result) ![]const u8 { fn requestLogMessage(self: *Self, request: *jetzig.http.Request, result: jetzig.caches.Result) ![]const u8 {
const status: root.jetzig.http.status_codes.TaggedStatusCode = switch (result.value.status_code) { const status: jetzig.http.status_codes.TaggedStatusCode = switch (result.value.status_code) {
.ok => .{ .ok = .{} }, .ok => .{ .ok = .{} },
.not_found => .{ .not_found = .{} }, .not_found => .{ .not_found = .{} },
}; };
const formatted_duration = try root.jetzig.colors.duration(self.allocator, self.duration()); const formatted_duration = try jetzig.colors.duration(self.allocator, self.duration());
defer self.allocator.free(formatted_duration); defer self.allocator.free(formatted_duration);
return try std.fmt.allocPrint(self.allocator, "[{s} {s}] {s} {s}", .{ return try std.fmt.allocPrint(self.allocator, "[{s} {s}] {s} {s}", .{
@ -193,16 +209,14 @@ fn duration(self: *Self) i64 {
return @intCast(std.time.nanoTimestamp() - self.start_time); return @intCast(std.time.nanoTimestamp() - self.start_time);
} }
fn matchView(self: *Self, request: *root.jetzig.http.Request) !?root.jetzig.views.Route { fn matchView(self: *Self, request: *jetzig.http.Request) !?jetzig.views.Route {
for (self.routes) |route| { for (self.routes) |route| {
if (route.action == .index and try request.match(route)) return route; if (route.action == .index and try request.match(route)) return route;
} }
for (self.routes) |route| { for (self.routes) |route| {
if (route.action == .get and try request.match(route)) return route; if (try request.match(route)) return route;
} }
// TODO: edit, new, update, delete
return null; return null;
} }

221
src/jetzig/http/Session.zig Normal file
View File

@ -0,0 +1,221 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
pub const cookie_name = "_jetzig-session";
const Cipher = std.crypto.aead.aes_gcm.Aes256Gcm;
allocator: std.mem.Allocator,
encryption_key: ?[]const u8,
cookies: *jetzig.http.Cookies,
hashmap: std.StringHashMap(jetzig.data.Value),
cookie: ?jetzig.http.Cookies.Cookie = null,
initialized: bool = false,
data: jetzig.data.Data = undefined,
state: enum { parsed, pending } = .pending,
encrypted: ?[]const u8 = null,
decrypted: ?[]const u8 = null,
encoded: ?[]const u8 = null,
const Self = @This();
pub fn init(
allocator: std.mem.Allocator,
cookies: *jetzig.http.Cookies,
encryption_key: ?[]const u8,
) Self {
return .{
.allocator = allocator,
.hashmap = std.StringHashMap(jetzig.data.Value).init(allocator),
.cookies = cookies,
.encryption_key = encryption_key,
};
}
pub fn parse(self: *Self) !void {
if (self.cookies.get(cookie_name)) |cookie| {
try self.parseSessionCookie(cookie.value);
} else {
try self.reset();
}
}
pub fn reset(self: *Self) !void {
self.data = jetzig.data.Data.init(self.allocator);
_ = try self.data.object();
self.state = .parsed;
try self.save();
}
pub fn deinit(self: *Self) void {
if (self.state != .parsed) return;
var it = self.hashmap.iterator();
while (it.next()) |item| {
self.allocator.destroy(item.key_ptr);
self.allocator.destroy(item.value_ptr);
}
self.hashmap.deinit();
if (self.encoded) |*ptr| self.allocator.free(ptr.*);
if (self.decrypted) |*ptr| self.allocator.free(ptr.*);
if (self.encrypted) |*ptr| self.allocator.free(ptr.*);
if (self.cookie) |*ptr| self.allocator.free(ptr.*.value);
}
pub fn get(self: *Self, key: []const u8) !?jetzig.data.Value {
if (self.state != .parsed) return error.UnparsedSessionCookie;
return switch (self.data.value.?) {
.object => self.data.value.?.object.get(key),
else => unreachable,
};
}
pub fn put(self: *Self, key: []const u8, value: jetzig.data.Value) !void {
if (self.state != .parsed) return error.UnparsedSessionCookie;
switch (self.data.value.?) {
.object => |*object| {
try object.put(key, value);
},
else => unreachable,
}
try self.save();
}
fn save(self: *Self) !void {
if (self.state != .parsed) return error.UnparsedSessionCookie;
const json = try self.data.toJson();
defer self.allocator.free(json);
if (self.encrypted) |*ptr| {
self.allocator.free(ptr.*);
self.encrypted = null;
}
self.encrypted = try self.encrypt(json);
const encoded = try jetzig.base64Encode(self.allocator, self.encrypted.?);
defer self.allocator.free(encoded);
if (self.cookie) |*ptr| self.allocator.free(ptr.*.value);
self.cookie = .{ .value = try self.allocator.dupe(u8, encoded) };
try self.cookies.put(
cookie_name,
self.cookie.?,
);
}
fn parseSessionCookie(self: *Self, cookie_value: []const u8) !void {
self.data = jetzig.data.Data.init(self.allocator);
const decoded = try jetzig.base64Decode(self.allocator, cookie_value);
defer self.allocator.free(decoded);
const buf = self.decrypt(decoded) catch |err| {
switch (err) {
error.AuthenticationFailed => return error.JetzigInvalidSessionCookie,
else => return err,
}
};
defer self.allocator.free(buf);
if (self.decrypted) |*ptr| self.allocator.free(ptr.*);
self.decrypted = try self.allocator.dupe(u8, buf);
try self.data.fromJson(self.decrypted.?);
self.state = .parsed;
}
fn decrypt(self: *Self, data: []const u8) ![]const u8 {
if (self.encryption_key) |secret| {
const encrypted = data[0 .. data.len - Cipher.tag_length];
const secret_bytes = std.mem.sliceAsBytes(secret);
const key: [Cipher.key_length]u8 = secret_bytes[0..Cipher.key_length].*;
const nonce: [Cipher.nonce_length]u8 = secret_bytes[Cipher.key_length .. Cipher.key_length + Cipher.nonce_length].*;
const buf = try self.allocator.alloc(u8, data.len - Cipher.tag_length);
const additional_data = "";
var tag: [Cipher.tag_length]u8 = undefined;
std.mem.copyForwards(u8, &tag, data[data.len - Cipher.tag_length ..]);
try Cipher.decrypt(
buf,
encrypted,
tag,
additional_data,
nonce,
key,
);
return buf[0..];
} else {
return self.allocator.dupe(u8, "hello");
}
}
fn encrypt(self: *Self, value: []const u8) ![]const u8 {
if (self.encryption_key) |secret| {
const secret_bytes = std.mem.sliceAsBytes(secret);
const key: [Cipher.key_length]u8 = secret_bytes[0..Cipher.key_length].*;
const nonce: [Cipher.nonce_length]u8 = secret_bytes[Cipher.key_length .. Cipher.key_length + Cipher.nonce_length].*;
const associated_data = "";
if (self.encrypted) |*val| {
self.allocator.free(val.*);
self.encrypted = null;
}
const buf = try self.allocator.alloc(u8, value.len);
defer self.allocator.free(buf);
var tag: [Cipher.tag_length]u8 = undefined;
Cipher.encrypt(buf, &tag, value, associated_data, nonce, key);
if (self.encrypted) |*ptr| self.allocator.free(ptr.*);
self.encrypted = try std.mem.concat(
self.allocator,
u8,
&[_][]const u8{ buf, tag[0..] },
);
return self.encrypted.?;
} else {
return value;
}
}
test "put and get session key/value" {
const allocator = std.testing.allocator;
var cookies = jetzig.http.Cookies.init(allocator, "");
defer cookies.deinit();
try cookies.parse();
const secret: [Cipher.key_length + Cipher.nonce_length]u8 = [_]u8{0x69} ** (Cipher.key_length + Cipher.nonce_length);
var session = Self.init(allocator, &cookies, &secret);
defer session.deinit();
defer session.data.deinit();
var data = jetzig.data.Data.init(allocator);
defer data.deinit();
try session.parse();
try session.put("foo", data.string("bar"));
var value = (try session.get("foo")).?;
try std.testing.expectEqualStrings(try value.toString(), "bar");
}
test "get value from parsed/decrypted cookie" {
const allocator = std.testing.allocator;
var cookies = jetzig.http.Cookies.init(allocator, "_jetzig-session=GIRI22v4C9EwU_mY02_obbnX2QkdnEZenlQz2xs");
defer cookies.deinit();
try cookies.parse();
const secret: [Cipher.key_length + Cipher.nonce_length]u8 = [_]u8{0x69} ** (Cipher.key_length + Cipher.nonce_length);
var session = Self.init(allocator, &cookies, &secret);
defer session.deinit();
defer session.data.deinit();
try session.parse();
var value = (try session.get("foo")).?;
try std.testing.expectEqualStrings("bar", try value.toString());
}

View File

@ -1,6 +1,6 @@
const std = @import("std"); const std = @import("std");
const root = @import("root"); const jetzig = @import("../../jetzig.zig");
pub const StatusCode = enum { pub const StatusCode = enum {
ok, ok,
@ -20,13 +20,13 @@ pub fn StatusCodeType(comptime code: []const u8, comptime message: []const u8) t
const full_message = code ++ " " ++ message; const full_message = code ++ " " ++ message;
if (std.mem.startsWith(u8, code, "2")) { if (std.mem.startsWith(u8, code, "2")) {
return root.jetzig.colors.green(full_message); return jetzig.colors.green(full_message);
} else if (std.mem.startsWith(u8, code, "3")) { } else if (std.mem.startsWith(u8, code, "3")) {
return root.jetzig.colors.blue(full_message); return jetzig.colors.blue(full_message);
} else if (std.mem.startsWith(u8, code, "4")) { } else if (std.mem.startsWith(u8, code, "4")) {
return root.jetzig.colors.yellow(full_message); return jetzig.colors.yellow(full_message);
} else if (std.mem.startsWith(u8, code, "5")) { } else if (std.mem.startsWith(u8, code, "5")) {
return root.jetzig.colors.red(full_message); return jetzig.colors.red(full_message);
} else { } else {
return full_message; return full_message;
} }

View File

@ -4,15 +4,12 @@ const Self = @This();
const Timestamp = @import("../types/Timestamp.zig"); const Timestamp = @import("../types/Timestamp.zig");
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
enabled: bool,
pub fn init(allocator: std.mem.Allocator, enabled: bool) Self { pub fn init(allocator: std.mem.Allocator) Self {
return .{ .allocator = allocator, .enabled = enabled }; return .{ .allocator = allocator };
} }
pub fn debug(self: *Self, comptime message: []const u8, args: anytype) !void { pub fn debug(self: *Self, comptime message: []const u8, args: anytype) !void {
if (!self.enabled) return;
const output = try std.fmt.allocPrint(self.allocator, message, args); const output = try std.fmt.allocPrint(self.allocator, message, args);
defer self.allocator.free(output); defer self.allocator.free(output);
const timestamp = Timestamp.init(std.time.timestamp(), self.allocator); const timestamp = Timestamp.init(std.time.timestamp(), self.allocator);

View File

@ -1,5 +1,4 @@
const root = @import("root"); const root = @import("root");
pub const data = @import("views/data.zig");
pub const Route = @import("views/Route.zig"); pub const Route = @import("views/Route.zig");
pub const View = @import("views/View.zig"); pub const View = @import("views/View.zig");

View File

@ -1,14 +1,14 @@
const std = @import("std"); const std = @import("std");
const root = @import("root"); const jetzig = @import("../../jetzig.zig");
const Self = @This(); const Self = @This();
pub const Action = enum { index, get, post, put, patch, delete }; pub const Action = enum { index, get, post, put, patch, delete };
pub const RenderFn = *const fn (Self, *root.jetzig.http.Request) anyerror!root.jetzig.views.View; pub const RenderFn = *const fn (Self, *jetzig.http.Request) anyerror!jetzig.views.View;
const ViewWithoutId = *const fn (*root.jetzig.http.Request) anyerror!root.jetzig.views.View; const ViewWithoutId = *const fn (*jetzig.http.Request, *jetzig.data.Data) anyerror!jetzig.views.View;
const ViewWithId = *const fn (id: []const u8, *root.jetzig.http.Request) anyerror!root.jetzig.views.View; const ViewWithId = *const fn (id: []const u8, *jetzig.http.Request, *jetzig.data.Data) anyerror!jetzig.views.View;
pub const ViewType = union(Action) { pub const ViewType = union(Action) {
index: ViewWithoutId, index: ViewWithoutId,
@ -25,6 +25,9 @@ view: ViewType,
render: RenderFn = renderFn, render: RenderFn = renderFn,
pub fn templateName(self: Self, allocator: std.mem.Allocator) ![]const u8 { pub fn templateName(self: Self, allocator: std.mem.Allocator) ![]const u8 {
if (std.mem.eql(u8, self.name, "app.views.index") and self.action == .index)
return try allocator.dupe(u8, "index");
const underscored_name = try std.mem.replaceOwned(u8, allocator, self.name, ".", "_"); const underscored_name = try std.mem.replaceOwned(u8, allocator, self.name, ".", "_");
defer allocator.free(underscored_name); defer allocator.free(underscored_name);
@ -35,22 +38,19 @@ pub fn templateName(self: Self, allocator: std.mem.Allocator) ![]const u8 {
const suffixed = try std.mem.concat(allocator, u8, &[_][]const u8{ const suffixed = try std.mem.concat(allocator, u8, &[_][]const u8{
unprefixed, unprefixed,
"_", "_",
switch (self.action) { @tagName(self.action),
.get => "get_id",
else => @tagName(self.action),
},
}); });
return suffixed; return suffixed;
} }
fn renderFn(self: Self, request: *root.jetzig.http.Request) anyerror!root.jetzig.views.View { fn renderFn(self: Self, request: *jetzig.http.Request) anyerror!jetzig.views.View {
switch (self.view) { switch (self.view) {
.index => |view| return try view(request), .index => |view| return try view(request, request.response_data),
.get => |view| return try view(request.resourceId(), request), .get => |view| return try view(request.resourceId(), request, request.response_data),
.post => |view| return try view(request), .post => |view| return try view(request, request.response_data),
.patch => |view| return try view(request.resourceId(), request), .patch => |view| return try view(request.resourceId(), request, request.response_data),
.put => |view| return try view(request.resourceId(), request), .put => |view| return try view(request.resourceId(), request, request.response_data),
.delete => |view| return try view(request.resourceId(), request), .delete => |view| return try view(request.resourceId(), request, request.response_data),
} }
} }

View File

@ -1,6 +1,6 @@
const std = @import("std"); const std = @import("std");
const root = @import("root"); const jetzig = @import("../../jetzig.zig");
data: *root.jetzig.views.data.Data, data: *jetzig.data.Data,
status_code: root.jetzig.http.status_codes.StatusCode, status_code: jetzig.http.status_codes.StatusCode,

View File

@ -1,7 +1,7 @@
const std = @import("std"); const std = @import("std");
pub const jetzig = @import("jetzig.zig"); pub const jetzig = @import("jetzig.zig");
pub const templates = @import("app/views/manifest.zig").templates; pub const templates = @import("app/views/zmpl.manifest.zig").templates;
pub const routes = @import("app/views/routes.zig").routes; pub const routes = @import("app/views/routes.zig").routes;
pub fn main() !void { pub fn main() !void {

7
src/tests.zig Normal file
View File

@ -0,0 +1,7 @@
// const Cookies = @import("http/Cookies.zig");
test {
_ = @import("jetzig.zig");
_ = @import("zmpl");
@import("std").testing.refAllDeclsRecursive(@This());
}