diff --git a/README.md b/README.md
index b077585..d463809 100644
--- a/README.md
+++ b/README.md
@@ -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: _HTML_ and _JSON_ response (inferred from extension and/or `Accept` header).
* :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.
* :x: Sessions.
* :x: Cookies.
@@ -27,6 +27,8 @@ If you are interested in _Jetzig_ you will probably find these tools interesting
* :x: Custom/dynamic routes.
* :x: General-purpose cache.
* :x: Background jobs.
+* :x: Testing helpers for testing HTTP requests/responses.
+* :x: Development server auto-reload.
* :x: Database integration.
* :x: Email receipt (via SendGrid/AWS SES/etc.)
diff --git a/build.zig b/build.zig
index 92bdd43..0895aec 100644
--- a/build.zig
+++ b/build.zig
@@ -23,12 +23,20 @@ pub fn build(b: *std.Build) !void {
b.installArtifact(exe);
const jetzig_module = b.createModule(.{ .source_file = .{ .path = "src/jetzig.zig" } });
- exe.addModule("jetzig", jetzig_module);
- lib.addModule("jetzig", jetzig_module);
+ // exe.addModule("jetzig", jetzig_module);
+ // lib.addModule("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("zmpl", .{ .target = target, .optimize = optimize });
+ const zmpl_module = b.dependency(
+ "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"));
exe.addModule("zmpl", zmpl_module.module("zmpl"));
@@ -53,11 +61,12 @@ pub fn build(b: *std.Build) !void {
run_step.dependOn(&run_cmd.step);
const main_tests = b.addTest(.{
- .root_source_file = .{ .path = "src/main.zig" },
+ .root_source_file = .{ .path = "src/tests.zig" },
.target = target,
.optimize = optimize,
});
+ main_tests.addModule("zmpl", zmpl_module.module("zmpl"));
const run_main_tests = b.addRunArtifact(main_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 basename = std.fs.path.basename(entry.path);
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.eql(u8, extension, ".zig")) continue;
diff --git a/build.zig.zon b/build.zig.zon
index 1206c8c..e91e8a8 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -20,8 +20,9 @@
// 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
// the new URL.
+ //
.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
// obtained after fetching `url` and applying the inclusion rules given by
diff --git a/get/foo b/get/foo
deleted file mode 100644
index 65ab103..0000000
--- a/get/foo
+++ /dev/null
@@ -1 +0,0 @@
-hellhelloo
diff --git a/src/app/views/foo/bar/baz.zig b/src/app/views/foo/bar/baz.zig
index ad1f37c..f936b47 100644
--- a/src/app/views/foo/bar/baz.zig
+++ b/src/app/views/foo/bar/baz.zig
@@ -1,48 +1,53 @@
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 index(request: *root.jetzig.http.Request) anyerror!root.jetzig.views.View {
- var data = request.data();
+pub fn index(request: *Request, data: *Data) anyerror!View {
var object = try data.object();
- try object.add("message", data.string("hello there"));
- try object.add("foo", data.string("foo lookup"));
- try object.add("bar", data.string("bar lookup"));
+ try request.session.put("foo", data.string("bar"));
+ try object.put("message", data.string("hello there"));
+ 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);
}
-pub fn get(id: []const u8, request: *root.jetzig.http.Request) anyerror!root.jetzig.views.View {
- var data = request.data();
+pub fn get(id: []const u8, request: *Request, data: *Data) anyerror!View {
var object = try data.object();
- try object.add("message", data.string("hello there"));
- try object.add("other_message", data.string("hello again"));
- try object.add("an_integer", data.integer(10));
- try object.add("a_float", data.float(1.345));
- try object.add("a_boolean", data.boolean(true));
- try object.add("Null", data.Null);
- try object.add("a_random_integer", data.integer(std.crypto.random.int(u8)));
- try object.add("resource_id", data.string(id));
+ try request.session.put("foo", data.string("bar"));
+ try object.put("session_value", (try request.session.get("foo")).?);
+
+ try object.put("message", data.string("hello there"));
+ try object.put("other_message", data.string("hello again"));
+ try object.put("an_integer", data.integer(10));
+ try object.put("a_float", data.float(1.345));
+ 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();
- try nested_object.add("nested key", data.string("nested value"));
- try object.add("other_message", nested_object.*);
+ try nested_object.put("nested key", data.string("nested value"));
+ try object.put("other_message", nested_object.*);
return request.render(.ok);
}
-pub fn patch(id: []const u8, request: *root.jetzig.http.Request) anyerror!root.jetzig.views.View {
- var data = request.data();
+pub fn patch(id: []const u8, request: *Request, data: *Data) anyerror!View {
var object = try data.object();
const integer = std.crypto.random.int(u8);
- try object.add("message", data.string("hello there"));
- try object.add("other_message", data.string("hello again"));
- try object.add("other_message", data.integer(integer));
- try object.add("id", data.string(id));
+ try object.put("message", data.string("hello there"));
+ try object.put("other_message", data.string("hello again"));
+ try object.put("other_message", data.integer(integer));
+ try object.put("id", data.string(id));
return request.render(.ok);
}
diff --git a/src/app/views/foo/bar/baz/get.zmpl b/src/app/views/foo/bar/baz/get.zmpl
index 0c41920..871ecfc 100644
--- a/src/app/views/foo/bar/baz/get.zmpl
+++ b/src/app/views/foo/bar/baz/get.zmpl
@@ -1,14 +1,2 @@
-if (std.mem.eql(u8, try zmpl.getValueString("message"), "hello there")) {
- const foo = "foo const";
- const bar = "bar const";
-
- inline for (1..4) |index| {
-
Hello {:foo}!
- Hello {:bar}!
- Hello {:index}!
- Hello {.foo}!
- Hello {.bar}!
- }
-} else {
- Unexpected message
-}
+{.resource_id}
+{.session_value}
diff --git a/src/app/views/foo/bar/baz/get[id].zmpl b/src/app/views/foo/bar/baz/get[id].zmpl
deleted file mode 100644
index 3346d04..0000000
--- a/src/app/views/foo/bar/baz/get[id].zmpl
+++ /dev/null
@@ -1 +0,0 @@
-{.resource_id}
diff --git a/src/app/views/foo/bar/baz/index.zmpl b/src/app/views/foo/bar/baz/index.zmpl
new file mode 100644
index 0000000..0c41920
--- /dev/null
+++ b/src/app/views/foo/bar/baz/index.zmpl
@@ -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| {
+ Hello {:foo}!
+ Hello {:bar}!
+ Hello {:index}!
+ Hello {.foo}!
+ Hello {.bar}!
+ }
+} else {
+ Unexpected message
+}
diff --git a/src/app/views/index.zig b/src/app/views/index.zig
new file mode 100644
index 0000000..ad1eb80
--- /dev/null
+++ b/src/app/views/index.zig
@@ -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);
+}
diff --git a/src/app/views/index.zmpl b/src/app/views/index.zmpl
new file mode 100644
index 0000000..f9d6505
--- /dev/null
+++ b/src/app/views/index.zmpl
@@ -0,0 +1,11 @@
+
+
+
+
+inline for (0..10) |index| {
+
+
+}
+
+Select an option.
diff --git a/src/app/views/options.zig b/src/app/views/options.zig
new file mode 100644
index 0000000..013d570
--- /dev/null
+++ b/src/app/views/options.zig
@@ -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);
+}
diff --git a/src/app/views/options/get.zmpl b/src/app/views/options/get.zmpl
new file mode 100644
index 0000000..f2163ad
--- /dev/null
+++ b/src/app/views/options/get.zmpl
@@ -0,0 +1 @@
+Number of times clicked: {.count}
diff --git a/src/app/views/options/put.zmpl b/src/app/views/options/put.zmpl
new file mode 100644
index 0000000..b4ee168
--- /dev/null
+++ b/src/app/views/options/put.zmpl
@@ -0,0 +1 @@
+You chose option {.option}
diff --git a/src/app/views/routes.zig b/src/app/views/routes.zig
index 1fc3613..8ccc5dd 100644
--- a/src/app/views/routes.zig
+++ b/src/app/views/routes.zig
@@ -1,3 +1,5 @@
pub const routes = .{
@import("foo/bar/baz.zig"),
+ @import("index.zig"),
+ @import("options.zig"),
};
diff --git a/src/app/views/manifest.zig b/src/app/views/zmpl.manifest.zig
similarity index 50%
rename from src/app/views/manifest.zig
rename to src/app/views/zmpl.manifest.zig
index ba1eecf..e917ec4 100644
--- a/src/app/views/manifest.zig
+++ b/src/app/views/zmpl.manifest.zig
@@ -2,6 +2,9 @@
// This file is automatically generated at build time. Manual edits will be discarded.
// This file should _not_ be stored in version control.
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_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");
};
diff --git a/src/jetzig.zig b/src/jetzig.zig
index 046df51..0ca86e3 100644
--- a/src/jetzig.zig
+++ b/src/jetzig.zig
@@ -4,11 +4,11 @@ pub const zmpl = @import("zmpl");
pub const http = @import("jetzig/http.zig");
pub const loggers = @import("jetzig/loggers.zig");
+pub const data = @import("jetzig/data.zig");
pub const caches = @import("jetzig/caches.zig");
pub const views = @import("jetzig/views.zig");
pub const colors = @import("jetzig/colors.zig");
pub const App = @import("jetzig/App.zig");
-pub const DefaultAllocator = std.heap.GeneralPurposeAllocator(.{});
pub fn init(allocator: std.mem.Allocator) !App {
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
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");
- 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) {
true => caches.Cache{ .memory_cache = caches.MemoryCache.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{
.cache = server_cache,
.logger = logger,
.root_path = root_path,
+ .secret = secret,
};
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
// to the appropriate View.
pub fn route(comptime modules: anytype) []views.Route {
- var size: u16 = 0;
+ var size: usize = 0;
for (modules) |module| {
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 index: usize = 0;
- for (modules, 1..) |module, module_index| {
+ for (modules) |module| {
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));
- detected[module_index * decl_index - 1] = .{
+ detected[index] = .{
.name = @typeName(module),
.action = @field(views.Route.Action, decl.name),
.view = view,
};
+ index += 1;
}
}
@@ -109,3 +118,40 @@ pub const TemplateFn = struct {
name: []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());
+}
diff --git a/src/jetzig/App.zig b/src/jetzig/App.zig
index 5ecffae..0076024 100644
--- a/src/jetzig/App.zig
+++ b/src/jetzig/App.zig
@@ -1,26 +1,21 @@
const std = @import("std");
-const root = @import("root");
+const jetzig = @import("../jetzig.zig");
const Self = @This();
-server_options: root.jetzig.http.Server.ServerOptions,
+server_options: jetzig.http.Server.ServerOptions,
allocator: std.mem.Allocator,
host: []const u8,
port: u16,
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 {
_ = self;
}
-pub fn start(self: Self, views: []root.jetzig.views.Route, templates: []root.jetzig.TemplateFn) !void {
- var server = root.jetzig.http.Server.init(
+pub fn start(self: Self, views: []jetzig.views.Route, templates: []jetzig.TemplateFn) !void {
+ var server = jetzig.http.Server.init(
self.allocator,
self.host,
self.port,
@@ -32,6 +27,7 @@ pub fn start(self: Self, views: []root.jetzig.views.Route, templates: []root.jet
defer server.deinit();
defer self.allocator.free(self.root_path);
defer self.allocator.free(self.host);
+ defer self.allocator.free(server.options.secret);
server.listen() catch |err| {
switch (err) {
diff --git a/src/jetzig/caches.zig b/src/jetzig/caches.zig
index 875a053..94a2dc4 100644
--- a/src/jetzig/caches.zig
+++ b/src/jetzig/caches.zig
@@ -12,7 +12,7 @@ pub const Cache = union(enum) {
pub fn deinit(self: *Cache) void {
switch (self.*) {
- inline else => |*case| try case.deinit(),
+ inline else => |*case| case.deinit(),
}
}
diff --git a/src/jetzig/caches/Result.zig b/src/jetzig/caches/Result.zig
index a9a1786..628645b 100644
--- a/src/jetzig/caches/Result.zig
+++ b/src/jetzig/caches/Result.zig
@@ -2,13 +2,13 @@ const std = @import("std");
const Self = @This();
-const root = @import("root");
+const jetzig = @import("../../jetzig.zig");
-value: root.jetzig.http.Response,
+value: jetzig.http.Response,
cached: bool,
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 };
}
diff --git a/src/jetzig/views/data.zig b/src/jetzig/data.zig
similarity index 100%
rename from src/jetzig/views/data.zig
rename to src/jetzig/data.zig
diff --git a/src/jetzig/http.zig b/src/jetzig/http.zig
index d5019be..f13c758 100644
--- a/src/jetzig/http.zig
+++ b/src/jetzig/http.zig
@@ -5,4 +5,6 @@ const colors = @import("colors.zig");
pub const Server = @import("http/Server.zig");
pub const Request = @import("http/Request.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");
diff --git a/src/jetzig/http/Cookies.zig b/src/jetzig/http/Cookies.zig
new file mode 100644
index 0000000..b5bd3a0
--- /dev/null
+++ b/src/jetzig/http/Cookies.zig
@@ -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);
+}
diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig
index ae80634..d99aea1 100644
--- a/src/jetzig/http/Request.zig
+++ b/src/jetzig/http/Request.zig
@@ -1,6 +1,6 @@
const std = @import("std");
-const root = @import("root");
+const jetzig = @import("../../jetzig.zig");
const Self = @This();
const default_content_type = "text/html";
@@ -14,16 +14,18 @@ path: []const u8,
method: Method,
headers: std.http.Headers,
segments: std.ArrayList([]const u8),
-server: *root.jetzig.http.Server,
-status_code: root.jetzig.http.status_codes.StatusCode = undefined,
-response_data: root.jetzig.views.data.Data = undefined,
+server: *jetzig.http.Server,
+session: *jetzig.http.Session,
+status_code: jetzig.http.status_codes.StatusCode = undefined,
+response_data: *jetzig.data.Data,
+cookies: *jetzig.http.Cookies,
pub fn init(
allocator: std.mem.Allocator,
- server: *root.jetzig.http.Server,
- request: std.http.Server.Request,
+ server: *jetzig.http.Server,
+ response: *std.http.Server.Response,
) !Self {
- const method = switch (request.method) {
+ const method = switch (response.request.method) {
.DELETE => Method.DELETE,
.GET => Method.GET,
.PATCH => Method.PATCH,
@@ -35,29 +37,57 @@ pub fn init(
.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);
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 .{
.allocator = allocator,
- .path = request.target,
+ .path = response.request.target,
.method = method,
- .headers = request.headers,
+ .headers = response.request.headers,
.server = server,
.segments = segments,
+ .cookies = cookies,
+ .session = session,
+ .response_data = response_data,
};
}
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 {
- return .{ .data = &self.response_data, .status_code = status_code };
+pub fn render(self: *Self, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
+ 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;
}
@@ -65,7 +95,7 @@ pub fn getHeader(self: *Self, key: []const u8) ?[]const u8 {
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);
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");
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 {
const basename = std.fs.path.basename(self.segments.items[self.segments.items.len - 1]);
const extension = std.fs.path.extension(basename);
@@ -144,12 +155,18 @@ pub fn resourceId(self: *Self) []const u8 {
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) {
.GET => {
return switch (route.action) {
- .index => std.mem.eql(u8, try self.nameWithResourceId(), route.name),
- .get => std.mem.eql(u8, try self.nameWithoutResourceId(), route.name),
+ .index => blk: {
+ 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,
};
},
@@ -163,19 +180,6 @@ pub fn match(self: *Self, route: root.jetzig.views.Route) !bool {
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 {
if (self.resourceModifier()) |modifier| {
return modifier == .edit;
@@ -188,11 +192,11 @@ fn isNewAction(self: *Self) bool {
} else return false;
}
-fn nameWithResourceId(self: *Self) ![]const u8 {
+fn fullName(self: *Self) ![]const u8 {
return try self.name(true);
}
-fn nameWithoutResourceId(self: *Self) ![]const u8 {
+fn fullNameWithStrippedResourceId(self: *Self) ![]const u8 {
return try self.name(false);
}
diff --git a/src/jetzig/http/Response.zig b/src/jetzig/http/Response.zig
index cff76af..37e6add 100644
--- a/src/jetzig/http/Response.zig
+++ b/src/jetzig/http/Response.zig
@@ -8,6 +8,18 @@ allocator: std.mem.Allocator,
content: []const u8,
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 {
_ = self;
// self.allocator.free(self.content);
diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig
index 56cafdd..6fdafcc 100644
--- a/src/jetzig/http/Server.zig
+++ b/src/jetzig/http/Server.zig
@@ -1,23 +1,24 @@
const std = @import("std");
-const root = @import("root");
+const jetzig = @import("../../jetzig.zig");
pub const ServerOptions = struct {
- cache: root.jetzig.caches.Cache,
- logger: root.jetzig.loggers.Logger,
+ cache: jetzig.caches.Cache,
+ logger: jetzig.loggers.Logger,
root_path: []const u8,
+ secret: []const u8,
};
server: std.http.Server,
allocator: std.mem.Allocator,
port: u16,
host: []const u8,
-cache: root.jetzig.caches.Cache,
-logger: root.jetzig.loggers.Logger,
+cache: jetzig.caches.Cache,
+logger: jetzig.loggers.Logger,
options: ServerOptions,
start_time: i128 = undefined,
-routes: []root.jetzig.views.Route,
-templates: []root.jetzig.TemplateFn,
+routes: []jetzig.views.Route,
+templates: []jetzig.TemplateFn,
const Self = @This();
@@ -26,8 +27,8 @@ pub fn init(
host: []const u8,
port: u16,
options: ServerOptions,
- routes: []root.jetzig.views.Route,
- templates: []root.jetzig.TemplateFn,
+ routes: []jetzig.views.Route,
+ templates: []jetzig.TemplateFn,
) Self {
const server = std.http.Server.init(allocator, .{ .reuse_address = true });
@@ -59,32 +60,45 @@ pub fn listen(self: *Self) !void {
fn processRequests(self: *Self) !void {
while (true) {
- self.processNextRequest() catch |err| {
- switch (err) {
- error.EndOfStream => continue,
- error.ConnectionResetByPeer => continue,
- else => return 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) {
+ error.EndOfStream => continue,
+ error.ConnectionResetByPeer => continue,
+ else => return err,
+ }
+ };
+ }
}
}
-fn processNextRequest(self: *Self) !void {
- var response = try self.server.accept(.{ .allocator = self.allocator });
- defer response.deinit();
+fn processNextRequest(self: *Self, response: *std.http.Server.Response) !void {
try response.wait();
+
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);
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();
const result = try self.pageContent(&request);
defer result.deinit();
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) {
.ok => .ok,
.not_found => .not_found,
@@ -92,6 +106,7 @@ fn processNextRequest(self: *Self) !void {
try response.do();
try response.writeAll(result.value.content);
+
try response.finish();
const log_message = try self.requestLogMessage(&request, result);
@@ -99,7 +114,7 @@ fn processNextRequest(self: *Self) !void {
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();
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);
switch (request.requestFormat()) {
@@ -122,15 +137,16 @@ fn renderResponse(self: *Self, request: *root.jetzig.http.Request) !root.jetzig.
fn renderHTML(
self: *Self,
- request: *root.jetzig.http.Request,
- route: ?root.jetzig.views.Route,
-) !root.jetzig.http.Response {
+ request: *jetzig.http.Request,
+ route: ?jetzig.views.Route,
+) !jetzig.http.Response {
if (route) |matched_route| {
const expected_name = try matched_route.templateName(self.allocator);
defer self.allocator.free(expected_name);
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)) {
const view = try matched_route.render(matched_route, request);
const content = try template.render(view.data);
@@ -154,9 +170,9 @@ fn renderHTML(
fn renderJSON(
self: *Self,
- request: *root.jetzig.http.Request,
- route: ?root.jetzig.views.Route,
-) !root.jetzig.http.Response {
+ request: *jetzig.http.Request,
+ route: ?jetzig.views.Route,
+) !jetzig.http.Response {
if (route) |matched_route| {
const view = try matched_route.render(matched_route, request);
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 {
- const status: root.jetzig.http.status_codes.TaggedStatusCode = switch (result.value.status_code) {
+fn requestLogMessage(self: *Self, request: *jetzig.http.Request, result: jetzig.caches.Result) ![]const u8 {
+ const status: jetzig.http.status_codes.TaggedStatusCode = switch (result.value.status_code) {
.ok => .{ .ok = .{} },
.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);
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);
}
-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| {
if (route.action == .index and try request.match(route)) return 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;
}
diff --git a/src/jetzig/http/Session.zig b/src/jetzig/http/Session.zig
new file mode 100644
index 0000000..777e1ed
--- /dev/null
+++ b/src/jetzig/http/Session.zig
@@ -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());
+}
diff --git a/src/jetzig/http/status_codes.zig b/src/jetzig/http/status_codes.zig
index af6c05e..2f70e28 100644
--- a/src/jetzig/http/status_codes.zig
+++ b/src/jetzig/http/status_codes.zig
@@ -1,6 +1,6 @@
const std = @import("std");
-const root = @import("root");
+const jetzig = @import("../../jetzig.zig");
pub const StatusCode = enum {
ok,
@@ -20,13 +20,13 @@ pub fn StatusCodeType(comptime code: []const u8, comptime message: []const u8) t
const full_message = code ++ " " ++ message;
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")) {
- return root.jetzig.colors.blue(full_message);
+ return jetzig.colors.blue(full_message);
} 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")) {
- return root.jetzig.colors.red(full_message);
+ return jetzig.colors.red(full_message);
} else {
return full_message;
}
diff --git a/src/jetzig/loggers/DevelopmentLogger.zig b/src/jetzig/loggers/DevelopmentLogger.zig
index 7172bbe..90415e0 100644
--- a/src/jetzig/loggers/DevelopmentLogger.zig
+++ b/src/jetzig/loggers/DevelopmentLogger.zig
@@ -4,15 +4,12 @@ const Self = @This();
const Timestamp = @import("../types/Timestamp.zig");
allocator: std.mem.Allocator,
-enabled: bool,
-pub fn init(allocator: std.mem.Allocator, enabled: bool) Self {
- return .{ .allocator = allocator, .enabled = enabled };
+pub fn init(allocator: std.mem.Allocator) Self {
+ return .{ .allocator = allocator };
}
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);
defer self.allocator.free(output);
const timestamp = Timestamp.init(std.time.timestamp(), self.allocator);
diff --git a/src/jetzig/views.zig b/src/jetzig/views.zig
index 15a1c03..469390a 100644
--- a/src/jetzig/views.zig
+++ b/src/jetzig/views.zig
@@ -1,5 +1,4 @@
const root = @import("root");
-pub const data = @import("views/data.zig");
pub const Route = @import("views/Route.zig");
pub const View = @import("views/View.zig");
diff --git a/src/jetzig/views/Route.zig b/src/jetzig/views/Route.zig
index c54dbe9..8c3ed29 100644
--- a/src/jetzig/views/Route.zig
+++ b/src/jetzig/views/Route.zig
@@ -1,14 +1,14 @@
const std = @import("std");
-const root = @import("root");
+const jetzig = @import("../../jetzig.zig");
const Self = @This();
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 ViewWithId = *const fn (id: []const u8, *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, *jetzig.http.Request, *jetzig.data.Data) anyerror!jetzig.views.View;
pub const ViewType = union(Action) {
index: ViewWithoutId,
@@ -25,6 +25,9 @@ view: ViewType,
render: RenderFn = renderFn,
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, ".", "_");
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{
unprefixed,
"_",
- switch (self.action) {
- .get => "get_id",
- else => @tagName(self.action),
- },
+ @tagName(self.action),
});
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) {
- .index => |view| return try view(request),
- .get => |view| return try view(request.resourceId(), request),
- .post => |view| return try view(request),
- .patch => |view| return try view(request.resourceId(), request),
- .put => |view| return try view(request.resourceId(), request),
- .delete => |view| return try view(request.resourceId(), request),
+ .index => |view| return try view(request, request.response_data),
+ .get => |view| return try view(request.resourceId(), request, request.response_data),
+ .post => |view| return try view(request, request.response_data),
+ .patch => |view| return try view(request.resourceId(), request, request.response_data),
+ .put => |view| return try view(request.resourceId(), request, request.response_data),
+ .delete => |view| return try view(request.resourceId(), request, request.response_data),
}
}
diff --git a/src/jetzig/views/View.zig b/src/jetzig/views/View.zig
index d7e2bc0..5ef1859 100644
--- a/src/jetzig/views/View.zig
+++ b/src/jetzig/views/View.zig
@@ -1,6 +1,6 @@
const std = @import("std");
-const root = @import("root");
+const jetzig = @import("../../jetzig.zig");
-data: *root.jetzig.views.data.Data,
-status_code: root.jetzig.http.status_codes.StatusCode,
+data: *jetzig.data.Data,
+status_code: jetzig.http.status_codes.StatusCode,
diff --git a/src/main.zig b/src/main.zig
index c8940c3..74482fb 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,7 +1,7 @@
const std = @import("std");
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 fn main() !void {
diff --git a/src/tests.zig b/src/tests.zig
new file mode 100644
index 0000000..7ee440f
--- /dev/null
+++ b/src/tests.zig
@@ -0,0 +1,7 @@
+// const Cookies = @import("http/Cookies.zig");
+
+test {
+ _ = @import("jetzig.zig");
+ _ = @import("zmpl");
+ @import("std").testing.refAllDeclsRecursive(@This());
+}