diff --git a/build.zig b/build.zig index 945523e..24cd2fe 100644 --- a/build.zig +++ b/build.zig @@ -1,5 +1,5 @@ const std = @import("std"); - +const htzx = @import("build/htzx_build.zig"); pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); @@ -37,63 +37,6 @@ pub fn build(b: *std.Build) !void { const test_step = b.step("test", "Run library tests"); test_step.dependOn(&run_main_tests.step); - // // Setup comptime parsing for /lib files - // var markup_files = std.ArrayList([]const u8).init(b.allocator); - // var zig_files = std.ArrayList([]const u8).init(b.allocator); - // defer markup_files.deinit(); - // defer zig_files.deinit(); + try htzx.build_htzx(b, exe); - // var options = b.addOptions(); - - // // Add all files names in the src folder to `files` - // var iter_dir = try std.fs.cwd().openIterableDir("lib", .{}); - // var src_dir = try std.fs.cwd().openDir("src", .{}); - // var lib_walker = try iter_dir.walk(b.allocator); - // var walking = true; - // while (walking) blk: { - // var file = lib_walker.next() catch { - // walking = false; - // break :blk; - // }; - // if (file == null) { - // walking = false; - // break :blk; - // } - // if (file.?.kind == .file) { - // const path = file.?.path; - // var split_iter = std.mem.splitBackwardsAny(u8, path, "."); - // var extension = split_iter.first(); - // if (std.mem.eql(u8, extension, "zig")) { - // std.debug.print("Adding zig file {s}...\n", .{path}); - // try zig_files.append(b.dupe(path)); - // var arr: [50]u8 = undefined; - // var buf = arr[0..]; - // var z_file_path = try std.fmt.bufPrint(buf, "lib/{s}", .{path}); - // var z_file = try std.fs.cwd().openFile(z_file_path, .{}); - // const zig_contents = try z_file.readToEndAlloc(b.allocator, 64000); - // const index = std.mem.indexOf(u8, zig_contents, "pub fn post"); - // if (index) |i| { - // std.debug.print("Found post endpoint in {s}\n", .{z_file_path}); - // var routes_file = try src_dir.createFile("routes.zig", .{}); - // _ = try routes_file.write(zig_contents[i..]); - // std.debug.print("Wrote {s}... to routes.zig\n", .{zig_contents[0..10]}); - // } - - // } - // else if (std.mem.eql(u8, extension, "html")) { - // std.debug.print("Adding markup file {s}...\n", .{path}); - // try markup_files.append(b.dupe(path)); - // } else { - // std.debug.print("Unrecognized file extension {s}\t{s}\n", .{extension, path}); - // } - // } - // // std.debug.print("Got entry in `versions:` base: {s}, path: {s}, kind: {any}\n", .{ ver.?.basename, ver.?.path, ver.?.kind }); - // } - - - // // Add the file names as an option to the exe, making it available - // // as a string array at comptime in main.zig - // options.addOption([]const []const u8, "zig_files", zig_files.items); - // options.addOption([]const []const u8, "markup_files", markup_files.items); - // exe.addOptions("options", options); } diff --git a/build/htzx_build.zig b/build/htzx_build.zig new file mode 100644 index 0000000..ec32ee9 --- /dev/null +++ b/build/htzx_build.zig @@ -0,0 +1,64 @@ +const std = @import("std"); +const log = std.log.scoped(.htzx_comp); + +pub fn build_htzx(b: *std.Build, exe: *std.Build.Step.Compile) !void { + // Setup comptime parsing for /lib files + var markup_files = std.ArrayList([]const u8).init(b.allocator); + // var zig_files = std.ArrayList([]const u8).init(b.allocator); + defer markup_files.deinit(); + // defer zig_files.deinit(); + + var options = b.addOptions(); + + // Add all files names in the src folder to `files` + // var iter_dir = try std.fs.cwd().openIterableDir("lib", .{}); + const lib_dir_path = "src/lib/"; + var lib_dir = try std.fs.cwd().openIterableDir(lib_dir_path, .{}); + var lib_walker = try lib_dir.walk(b.allocator); + var walking = true; + while (walking) blk: { + var file = lib_walker.next() catch { + walking = false; + break :blk; + }; + if (file == null) { + walking = false; + break :blk; + } + if (file.?.kind == .file) { + const relative_path = file.?.path; + const file_path = try std.mem.concat(b.allocator, u8, &[_][]const u8{lib_dir_path, relative_path}); + var split_iter = std.mem.splitBackwardsAny(u8, relative_path, "."); + var extension = split_iter.first(); + // if (std.mem.eql(u8, extension, "zig")) { + // std.debug.print("Adding zig file {s}...\n", .{path}); + // try zig_files.append(b.dupe(path)); + // var arr: [50]u8 = undefined; + // var buf = arr[0..]; + // var z_file_path = try std.fmt.bufPrint(buf, "lib/{s}", .{path}); + // var z_file = try std.fs.cwd().openFile(z_file_path, .{}); + // const zig_contents = try z_file.readToEndAlloc(b.allocator, 64000); + // const index = std.mem.indexOf(u8, zig_contents, "pub fn post"); + // if (index) |i| { + // std.debug.print("Found post endpoint in {s}\n", .{z_file_path}); + // var routes_file = try src_dir.createFile("routes.zig", .{}); + // _ = try routes_file.write(zig_contents[i..]); + // std.debug.print("Wrote {s}... to routes.zig\n", .{zig_contents[0..10]}); + // } + // } else + if (std.mem.eql(u8, extension, "html")) { + log.info("Adding html file {s}...", .{file_path}); + try markup_files.append(b.dupe(file_path)); + } else { + log.info("Unrecognized file extension '.{s}'\t{s}", .{ extension, file_path }); + } + } + // std.debug.print("Got entry in `versions:` base: {s}, path: {s}, kind: {any}\n", .{ ver.?.basename, ver.?.path, ver.?.kind }); + } + + // Add the file names as an option to the exe, making it available + // as a string array at comptime in main.zig + // options.addOption([]const []const u8, "zig_files", zig_files.items); + options.addOption([]const []const u8, "markup_files", markup_files.items); + exe.addOptions("options", options); +} diff --git a/src/htzx/utils.zig b/src/htzx/utils.zig new file mode 100644 index 0000000..5733615 --- /dev/null +++ b/src/htzx/utils.zig @@ -0,0 +1,17 @@ +const std = @import("std"); +const one_megabyte: usize = 1024 * 1024; +const string = []const u8; + +// Caller owns the returned StringArrayHashMap +pub fn open_html_load(allocator: std.mem.Allocator, filenames: []const []const u8) !std.StringArrayHashMap(string) { + var dir = std.fs.cwd(); + var html_file_map = std.StringArrayHashMap(string).init(allocator); + + for (filenames) |file_name| { + var f = try dir.openFile(file_name, .{}); + var html_content = try f.readToEndAlloc(allocator, one_megabyte); + try html_file_map.putNoClobber(file_name, html_content); + } + + return html_file_map; +} diff --git a/src/htzx/index.html b/src/lib/index.html similarity index 100% rename from src/htzx/index.html rename to src/lib/index.html diff --git a/src/htzx/routes.zig b/src/lib/routes.zig similarity index 100% rename from src/htzx/routes.zig rename to src/lib/routes.zig diff --git a/src/htzx/users/user_routes.zig b/src/lib/users/index.html similarity index 100% rename from src/htzx/users/user_routes.zig rename to src/lib/users/index.html diff --git a/src/lib/users/user_routes.zig b/src/lib/users/user_routes.zig new file mode 100644 index 0000000..e69de29 diff --git a/src/main.zig b/src/main.zig index b232957..2bcaedf 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,72 +1,8 @@ const std = @import("std"); const http = std.http; const log = std.log.scoped(.server); - -const server_addr = "127.0.0.1"; -const server_port = 8000; - -// Run the server and handle incoming requests. -fn runServer(server: *http.Server, allocator: std.mem.Allocator) !void { - outer: while (true) { - // Accept incoming connection. - var response = try server.accept(.{ - .allocator = allocator, - }); - defer response.deinit(); - - while (response.reset() != .closing) { - // Handle errors during request processing. - response.wait() catch |err| switch (err) { - error.HttpHeadersInvalid => continue :outer, - error.EndOfStream => continue, - else => return err, - }; - - // Process the request. - try handleRequest(&response, allocator); - } - } -} - -// Handle an individual request. -fn handleRequest(response: *http.Server.Response, allocator: std.mem.Allocator) !void { - // Log the request details. - log.info("{s} {s} {s}", .{ @tagName(response.request.method), @tagName(response.request.version), response.request.target }); - - // Read the request body. - const body = try response.reader().readAllAlloc(allocator, 8192); - defer allocator.free(body); - - // Set "connection" header to "keep-alive" if present in request headers. - if (response.request.headers.contains("connection")) { - try response.headers.append("connection", "keep-alive"); - } - - // Check if the request target starts with "/get". - if (std.mem.startsWith(u8, response.request.target, "/get")) { - // Check if the request target contains "?chunked". - if (std.mem.indexOf(u8, response.request.target, "?chunked") != null) { - response.transfer_encoding = .chunked; - } else { - response.transfer_encoding = .{ .content_length = 10 }; - } - - // Set "content-type" header to "text/plain". - try response.headers.append("content-type", "text/plain"); - - // Write the response body. - try response.do(); - if (response.request.method != .HEAD) { - try response.writeAll("Zig "); - try response.writeAll("Bits!\n"); - try response.finish(); - } - } else { - // Set the response status to 404 (not found). - response.status = .not_found; - try response.do(); - } -} +const Zx = @import("zx.zig").Zx; +const index = @import("routes/index.zig"); pub fn main() !void { // Create an allocator. @@ -75,23 +11,13 @@ pub fn main() !void { const allocator = gpa.allocator(); // Initialize the server. - var server = http.Server.init(allocator, .{ .reuse_address = true }); + var server = Zx.init(allocator); defer server.deinit(); - // Log the server address and port. - log.info("Server is running at {s}:{d}", .{ server_addr, server_port }); + try server.GET("/zig", index.getIndex); - // Parse the server address. - const address = try std.net.Address.parseIp(server_addr, server_port); - try server.listen(address); - - // Run the server. - runServer(&server, allocator) catch |err| { - // Handle server errors. - log.err("server error: {}\n", .{err}); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - std.os.exit(1); + server.runServer() catch |err| { + log.err("Server exited with err: {any}\nDumping trace:\n", .{err}); + std.debug.dumpCurrentStackTrace(null); }; } diff --git a/src/routes/index.zig b/src/routes/index.zig new file mode 100644 index 0000000..2a0d621 --- /dev/null +++ b/src/routes/index.zig @@ -0,0 +1,11 @@ +const std = @import("std"); +const http = std.http; +const zx = @import("../zx.zig"); + +pub fn getIndex(ctz: *zx.ZxContext) zx.ZxError!void { + ctz.json(.{ .success = true, .message = "hello did you get this?", .age = 1 }) catch |err| { + std.debug.print("Got error {any}\n", .{err}); + return zx.ZxError.Unexpected; + }; + return; +} diff --git a/src/zx.zig b/src/zx.zig index 1289be5..769a395 100644 --- a/src/zx.zig +++ b/src/zx.zig @@ -1,32 +1,213 @@ const std = @import("std"); +const options = @import("options"); +const zx_utils = @import("htzx/utils.zig"); +const markup_files = options.markup_files; const http = std.http; const log = std.log.scoped(.server); +const string = []const u8; + const server_addr = "127.0.0.1"; const server_port = 8000; +pub const ZxError = error{ + DoError, + FinishError, + OutOfMemory, + RedefinedRoute, + Unexpected, +}; + +pub const ZxRouteFn = *const fn (*ZxContext) ZxError!void; + pub const Zx = struct { + server: http.Server, + allocator: std.mem.Allocator, + routes: std.StringArrayHashMap(ZxRouteFn), + html_files_map: std.StringArrayHashMap(string), - pub fn init( allocator: std.mem.Allocator,) Zx { + pub fn init( + allocator: std.mem.Allocator, + ) Zx { var server = http.Server.init(allocator, .{ .reuse_address = true }); - defer server.deinit(); + var routes = std.StringArrayHashMap(ZxRouteFn).init(allocator); + var html_files_map = std.StringArrayHashMap(string).init(allocator); + return .{ .server = server, .allocator = allocator, .routes = routes, .html_files_map = html_files_map }; + } - // Log the server address and port. + pub fn deinit(zx: *Zx) void { + zx.server.deinit(); + zx.routes.deinit(); + + // free html file content values + for (zx.html_files_map.keys()) |key| { + var entry = zx.html_files_map.getEntry(key); + zx.allocator.free(entry.?.value_ptr); + } + zx.html_files_map.deinit(); + } + + pub fn runServer(zx: *Zx) !void { log.info("Server is running at {s}:{d}", .{ server_addr, server_port }); + for (markup_files) |file| { + log.info("{s}", .{file}); + } + zx.html_files_map = try zx_utils.open_html_load(zx.allocator, markup_files); + // Parse the server address. const address = try std.net.Address.parseIp(server_addr, server_port); - try server.listen(address); + zx.server.listen(address) catch return ZxError.Unexpected; - // Run the server. - runServer(&server, allocator) catch |err| { - // Handle server errors. - log.err("server error: {}\n", .{err}); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); + outer: while (true) { + // Accept incoming connection. + var response = zx.server.accept(.{ + .allocator = zx.allocator, + }) catch return ZxError.Unexpected; + defer response.deinit(); + + while (response.reset() != .closing) { + // Handle errors during request processing. + response.wait() catch |err| switch (err) { + error.HttpHeadersInvalid => continue :outer, + error.EndOfStream => continue, + else => return err, + }; + + // Process the request. + try handleRequest(zx, &response, zx.allocator); } - std.os.exit(1); - }; + } + } + pub fn POST(comptime path: string, handler: ZxRouteFn) void { + _ = handler; + _ = path; + } + + pub fn GET(zx: *Zx, comptime path: string, handler: ZxRouteFn) ZxError!void { + zx.routes.putNoClobber(path, handler) catch |err| { + std.debug.print("{any}\nCant add another route at {s}\n", .{ err, path }); + return ZxError.RedefinedRoute; + }; + } + + /// [/ <----------------------------------- \] + /// Order of operations: accept -> wait -> do [ -> write -> finish][ -> reset /] + /// \ -> read / + fn handleRequest(zx: *Zx, response: *http.Server.Response, allocator: std.mem.Allocator) ZxError!void { + + // Read the request body. + const body = response.reader().readAllAlloc(allocator, 8192) catch return ZxError.Unexpected; + defer allocator.free(body); + + // Log the request details. + log.info("{s} {s}", .{ @tagName(response.request.method), response.request.target }); + + // Set "connection" header to "keep-alive" if present in request headers. + if (response.request.headers.contains("connection")) { + response.headers.append("connection", "keep-alive") catch return ZxError.Unexpected; + } + + response.headers.append("Server", "Htzx") catch return ZxError.Unexpected; + + // if (std.mem.indexOf(u8, response.request.target, "?chunked") != null) { + // response.transfer_encoding = .chunked; + // } else { + // response.transfer_encoding = .{ .content_length = 10 }; + // } + + // Set "content-type" header to "text/plain". + // response.headers.append("content-type", "text/plain") catch return ZxError.Unexpected; + + // Write the response body. + + if (response.request.method != .HEAD) { + var ctz = ZxContext.init(allocator, response, body); + if (response.request.method == .GET and zx.has_html_route(response.request.target)) { + log.info("Returning html file at {s}...", .{response.request.target}); + try zx.return_html(&ctz); + } else { + var handler = zx.routes.get(response.request.target); + if (handler) |h| { + h(&ctz) catch |err| { + errorHandler(response, err); + }; + } else { + std.debug.print("No route defined for {s}\n", .{response.request.target}); + + response.status = .not_found; + response.do() catch return ZxError.DoError; + } + } + response.finish() catch |err| { + log.err("Got finish error {any}\n", .{err}); + return ZxError.FinishError; + }; + } else { + response.do() catch return ZxError.Unexpected; + response.finish() catch return ZxError.FinishError; + } + } + + fn has_html_route(zx: *Zx, route_target: string) bool { + return zx.html_files_map.contains(route_target); + } + + fn return_html(zx: *Zx, ctz: *ZxContext) ZxError!void { + var html = zx.html_files_map.get(ctz.response.request.target).?; + try ctz.html(html); + } + + fn errorHandler(response: *http.Server.Response, err: ZxError) void { + log.err("Got error while handling route {any}\n", .{err}); + response.status = .internal_server_error; + } +}; + +pub const ZxContext = struct { + allocator: std.mem.Allocator, + response: *http.Server.Response, + req_body: string, + + pub fn init(alloc: std.mem.Allocator, res: *http.Server.Response, body: string) ZxContext { + return ZxContext{ + .allocator = alloc, + .response = res, + .req_body = body, + }; + } + + /// Helper function to return JSON to the client + pub fn json(ctz: *ZxContext, content: anytype) ZxError!void { + // Add json header + ctz.response.headers.append("Content-Type", "application/json") catch return ZxError.Unexpected; + + const json_content = std.json.stringifyAlloc(ctz.allocator, content, .{}) catch return ZxError.OutOfMemory; + defer ctz.allocator.free(json_content); + + try ctz.respond(json_content); + } + + test "json helper function" {} + + pub fn html(ctz: *ZxContext, html_content: string) ZxError!void { + // Add html header + ctz.response.headers.append("Content-Type", "text/html") catch return ZxError.Unexpected; + + try ctz.respond(html_content); + } + + // Helper function to return body to the client + pub fn respond(ctz: *ZxContext, body: string) ZxError!void { + if (!ctz.response.headers.contains("Content-Type")) { + ctz.response.headers.append("Content-Type", "text/html") catch return ZxError.Unexpected; + } + + ctz.response.transfer_encoding = .{ .content_length = body.len }; + // Send headers + ctz.response.do() catch return ZxError.DoError; + // Send html body + ctz.response.writeAll(body) catch return ZxError.Unexpected; } };