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()); +}