diff --git a/build.zig b/build.zig index 24cd2fe..cfe4839 100644 --- a/build.zig +++ b/build.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const htzx = @import("build/htzx_build.zig"); +const htzx = @import("src/htzx/htzx_build.zig"); pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); diff --git a/src/lib/users/user_routes.zig b/build.zig.zon similarity index 100% rename from src/lib/users/user_routes.zig rename to build.zig.zon diff --git a/src/htzx/html_parser/scanner.zig b/src/htzx/html_parser/scanner.zig new file mode 100644 index 0000000..e4eb5c4 --- /dev/null +++ b/src/htzx/html_parser/scanner.zig @@ -0,0 +1,21 @@ +const std = @import("std"); + +const Token = @import("token.zig").Token; + +pub fn Scanner(comptime Source: type) type { + return struct { + const Self = @This(); + const SourceType = Source; + line: u32, + col: u32, + current: u32, + + // fn scanArrayList(self: Self, source: SourceType, token_array: std.ArrayListAligned(Token, null)) !std.ArrayListAligned(Token, null) { + // _ = self; + // var eof = false; + // while (!eof) { + // // switch( + // } + // } + }; +} diff --git a/src/htzx/html_parser/token.zig b/src/htzx/html_parser/token.zig new file mode 100644 index 0000000..a9edf68 --- /dev/null +++ b/src/htzx/html_parser/token.zig @@ -0,0 +1,82 @@ +const std = @import("std"); + +pub const Token = struct { + lexeme: Lexeme, + raw: []const u8, + line: u32, + col: u32, +}; + +pub const Lexeme = enum { + NEWLINE, + LT, + GT, + CLOSING_LT, + EQUALS, + TAG, + SINGLE_QUOTE, + DOUBLE_QOUTE, + TEXT, + DOUBLE_OPEN_BRACE, + DOUBLE_CLOSE_BRACE, +}; + +pub const TagTypes = enum { + a, + abbr, + blockquote, + body, + br, + button, + canvas, + caption, + code, + div, + em, + embed, + form, + frame, + h1, + h2, + h3, + h4, + h5, + h6, + head, + header, + html, + iframe, + img, + input, + label, + main, + nav, + object, + option, + p, + picture, + script, + section, + source, + span, + strong, + style, + table, + template, + title, + video, +}; + +pub const TagAttributes = enum { + href, + alt, + border, + name, + src, + style, + class, + maxlength, + name, + onblur +}; +//

text

diff --git a/build/htzx_build.zig b/src/htzx/htzx_build.zig similarity index 83% rename from build/htzx_build.zig rename to src/htzx/htzx_build.zig index ec32ee9..fd7fd64 100644 --- a/build/htzx_build.zig +++ b/src/htzx/htzx_build.zig @@ -4,8 +4,10 @@ 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 markup_files_content = std.ArrayList([]const u8).init(b.allocator); // var zig_files = std.ArrayList([]const u8).init(b.allocator); defer markup_files.deinit(); + defer markup_files_content.deinit(); // defer zig_files.deinit(); var options = b.addOptions(); @@ -27,7 +29,7 @@ pub fn build_htzx(b: *std.Build, exe: *std.Build.Step.Compile) !void { } 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}); + 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")) { @@ -49,8 +51,11 @@ pub fn build_htzx(b: *std.Build, exe: *std.Build.Step.Compile) !void { if (std.mem.eql(u8, extension, "html")) { log.info("Adding html file {s}...", .{file_path}); try markup_files.append(b.dupe(file_path)); + var f = try std.fs.cwd().openFile(file_path, .{}); + var content = try f.readToEndAlloc(b.allocator, 1024 * 1024); + try markup_files_content.append(content); } else { - log.info("Unrecognized file extension '.{s}'\t{s}", .{ extension, file_path }); + 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 }); @@ -60,5 +65,6 @@ pub fn build_htzx(b: *std.Build, exe: *std.Build.Step.Compile) !void { // 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); + options.addOption([]const []const u8, "markup_files_content", markup_files_content.items); exe.addOptions("options", options); } diff --git a/src/htzx/method_route.zig b/src/htzx/method_route.zig new file mode 100644 index 0000000..c6658ec --- /dev/null +++ b/src/htzx/method_route.zig @@ -0,0 +1,66 @@ +const std = @import("std"); +const string = []const u8; + +/// Tagged union `std.http.Method` with string route +pub const MethodRoute = union(std.http.Method) { + GET: string, + HEAD: string, + POST: string, + PUT: string, + DELETE: string, + CONNECT: string, + OPTIONS: string, + TRACE: string, + PATCH: string, + + pub fn create(method: std.http.Method, target: string) MethodRoute { + var method_route = switch (method) { + .GET => MethodRoute{ .GET = target }, + .POST => MethodRoute{ .POST = target }, + .PUT => MethodRoute{ .PUT = target }, + .DELETE => MethodRoute{ .DELETE = target }, + .PATCH => MethodRoute{ .PATCH = target }, + else => unreachable, + }; + return method_route; + } + + pub fn getRoute(method_route: MethodRoute) string { + var route = switch (method_route) { + .GET => method_route.GET, + .POST => method_route.POST, + .PUT => method_route.PUT, + .DELETE => method_route.DELETE, + .PATCH => method_route.PATCH, + else => unreachable, + }; + return route; + } + + pub fn getMethod(method_route: MethodRoute) std.http.Method { + var http_method = switch (method_route) { + .GET => std.http.Method.GET, + .POST => std.http.Method.POST, + .PUT => std.http.Method.PUT, + .DELETE => std.http.Method.DELETE, + .PATCH => std.http.Method.PATCH, + else => unreachable, + }; + return http_method; + } +}; + +/// Route Context for creating a std.ArrayHashMap with MethodRoute keys +pub const RouteContext = struct { + pub fn hash(self: @This(), key: MethodRoute) u32 { + _ = self; + var route = key.getRoute(); + return std.array_hash_map.hashString(route); + } + + pub fn eql(self: @This(), a: MethodRoute, b: MethodRoute, b_index: usize) bool { + _ = b_index; + _ = self; + return std.mem.eql(u8, a.getRoute(), b.getRoute()) and a.getMethod() == b.getMethod(); + } +}; diff --git a/src/htzx/utils.zig b/src/htzx/utils.zig index 5733615..04f5a17 100644 --- a/src/htzx/utils.zig +++ b/src/htzx/utils.zig @@ -1,17 +1,66 @@ const std = @import("std"); const one_megabyte: usize = 1024 * 1024; const string = []const u8; +const log = std.log.scoped(.server); +const HtmlValidator = @import("validators.zig").HtmlValidator; +const markup_files_content = @import("options").markup_files_content; + +pub const MarkupContent = struct { + path: string, + html: string, +}; // Caller owns the returned StringArrayHashMap -pub fn open_html_load(allocator: std.mem.Allocator, filenames: []const []const u8) !std.StringArrayHashMap(string) { +pub fn open_validate_html_load(allocator: std.mem.Allocator, comptime 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| { + comptime var index = 0; + inline for (filenames) |file_name| { + comptime { + // Validate comptime html file content + var html_content = markup_files_content[index]; + var valid = HtmlValidator.comp_validate(html_content); + if (!valid) { + @compileError("Invalid html in file " ++ file_name); + } + index += 1; + } + // Get rid of "src/lib" + var file_base = file_name[7..]; + var iter = std.mem.splitBackwardsSequence(u8, file_base, "/"); + var basename = iter.first(); + var route_len = file_base.len - basename.len; + var arr: [100]u8 = undefined; + var buf = arr[0..]; + var route_name = try std.fmt.bufPrint(buf, "{s}", .{file_base[0..route_len]}); 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); + // log.info("Adding html get route to {s}\tGot file_base {s}, basename {s}, route_len {d}...", .{ + // route_name, + // file_base, + // basename, + // route_len, + // }); + try html_file_map.putNoClobber(try allocator.dupe(u8, route_name), html_content); } return html_file_map; } + +// pub fn nameToEnumTag(name: []const u8, comptime Enum: type) enumError!Enum { +// comptime { +// if (@typeInfo(Enum) != .Enum) { +// @compileError("Non enum type passed into function: " ++ @typeName(Enum)); +// } +// } + +// inline for (@typeInfo(Enum).Enum.fields) |field| { +// if (std.mem.eql(u8, name, field.name)) { +// const tag: Enum = @enumFromInt(field.value); +// return tag; +// } +// } +// return enumError.EnumFieldNotFound; +// } + diff --git a/src/htzx/validators.zig b/src/htzx/validators.zig new file mode 100644 index 0000000..9fa3382 --- /dev/null +++ b/src/htzx/validators.zig @@ -0,0 +1,17 @@ +const std = @import("std"); + + +pub const HtmlValidator = struct { + + pub fn comp_validate(comptime html: []const u8) bool { + _ = html; + // TODO this needs to be done + return true; + } + + pub fn validate(html: []const u8) bool { + _ = html; + + return true; + } +}; diff --git a/src/lib/index.html b/src/lib/index.html index e69de29..bcfa5fb 100644 --- a/src/lib/index.html +++ b/src/lib/index.html @@ -0,0 +1,22 @@ + + + + My Simple HTML Page + + + +

Welcome to My Web Page

+

This is a simple HTML document.

+

Thank you for visiting.

+
+

Want Zig Facts?

+ +
+ + diff --git a/src/lib/routes.zig b/src/lib/routes.zig deleted file mode 100644 index 9be76f1..0000000 --- a/src/lib/routes.zig +++ /dev/null @@ -1,6 +0,0 @@ -const std = @import("std"); - -pub fn post() i32 { - std.debug.print("Running route get!\n", .{}); - return 13; -} diff --git a/src/main.zig b/src/main.zig index 2bcaedf..9d4d8c2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -14,7 +14,8 @@ pub fn main() !void { var server = Zx.init(allocator); defer server.deinit(); - try server.GET("/zig", index.getIndex); + try server.GET("/zig", index.getZig); + try server.POST("/zig", index.postZig); server.runServer() catch |err| { log.err("Server exited with err: {any}\nDumping trace:\n", .{err}); diff --git a/src/routes/index.zig b/src/routes/index.zig index 2a0d621..909eeae 100644 --- a/src/routes/index.zig +++ b/src/routes/index.zig @@ -1,11 +1,44 @@ const std = @import("std"); const http = std.http; const zx = @import("../zx.zig"); +const log = std.log.scoped(.index); -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}); +pub fn getZig(ctz: *zx.ZxContext) zx.ZxError!void { + ctz.html( + \\ + , .ok) catch |err| { + log.err("Got error {any}\n", .{err}); return zx.ZxError.Unexpected; }; return; } + +const ZigPost = struct { + id: u32, + name: []const u8, +}; + +pub fn postZig(ctz: *zx.ZxContext) zx.ZxError!void { + if (ctz.req_body.len > 0) { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + + var json_body = std.json.parseFromSliceLeaky(ZigPost, arena.allocator(), ctz.req_body, .{}) catch |err| { + log.err("Error while parsing json: {any}", .{err}); + return zx.ZxError.Unexpected; + }; + log.info("Got id {d} name {s}", .{ json_body.id, json_body.name }); + try ctz.json(.{ .success = true }, .ok); + } else { + ctz.json(.{ .success = false, .message = "Invalid request" }, .bad_request) catch |err| { + log.err("Got error {any}\n", .{err}); + return zx.ZxError.Unexpected; + }; + } + + return; +} diff --git a/src/zx.zig b/src/zx.zig index 769a395..732f96e 100644 --- a/src/zx.zig +++ b/src/zx.zig @@ -1,11 +1,20 @@ 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 zx_utils = @import("htzx/utils.zig"); + +const MethodRoute = @import("htzx/method_route.zig").MethodRoute; +const RouteContext = @import("htzx/method_route.zig").RouteContext; +const validators = @import("htzx/validators.zig"); +const HtmlValidator = validators.HtmlValidator; + +const markup_files = options.markup_files; + +// Types const string = []const u8; +pub const ZxRouteFn = *const fn (*ZxContext) ZxError!void; const server_addr = "127.0.0.1"; const server_port = 8000; @@ -18,19 +27,17 @@ pub const ZxError = error{ Unexpected, }; -pub const ZxRouteFn = *const fn (*ZxContext) ZxError!void; - pub const Zx = struct { server: http.Server, allocator: std.mem.Allocator, - routes: std.StringArrayHashMap(ZxRouteFn), + routes: std.ArrayHashMap(MethodRoute, ZxRouteFn, RouteContext, true), html_files_map: std.StringArrayHashMap(string), pub fn init( allocator: std.mem.Allocator, ) Zx { var server = http.Server.init(allocator, .{ .reuse_address = true }); - var routes = std.StringArrayHashMap(ZxRouteFn).init(allocator); + var routes = std.ArrayHashMap(MethodRoute, ZxRouteFn, RouteContext, true).init(allocator); var html_files_map = std.StringArrayHashMap(string).init(allocator); return .{ .server = server, .allocator = allocator, .routes = routes, .html_files_map = html_files_map }; } @@ -40,20 +47,19 @@ pub const Zx = struct { 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.allocator.free(zx.html_files_map.keys()); + zx.allocator.free(zx.html_files_map.values()); + // for (zx.html_files_map.keys()) |key| { + // var entry = zx.html_files_map.getEntry(key); + // zx.allocator.destroy(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); + zx.html_files_map = try zx_utils.open_validate_html_load(zx.allocator, markup_files); // Parse the server address. const address = try std.net.Address.parseIp(server_addr, server_port); @@ -80,13 +86,15 @@ pub const Zx = struct { } } - pub fn POST(comptime path: string, handler: ZxRouteFn) void { - _ = handler; - _ = path; + pub fn POST(zx: *Zx, path: string, handler: ZxRouteFn) ZxError!void { + zx.routes.putNoClobber(MethodRoute.create(.POST, path), handler) catch |err| { + std.debug.print("{any}\nCant add another route at {s}\n", .{ err, path }); + return ZxError.RedefinedRoute; + }; } - pub fn GET(zx: *Zx, comptime path: string, handler: ZxRouteFn) ZxError!void { - zx.routes.putNoClobber(path, handler) catch |err| { + pub fn GET(zx: *Zx, path: string, handler: ZxRouteFn) ZxError!void { + zx.routes.putNoClobber(MethodRoute.create(.GET, path), handler) catch |err| { std.debug.print("{any}\nCant add another route at {s}\n", .{ err, path }); return ZxError.RedefinedRoute; }; @@ -124,22 +132,27 @@ pub const Zx = struct { if (response.request.method != .HEAD) { var ctz = ZxContext.init(allocator, response, body); + // Check to return html from lib/ 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); + var handler = zx.routes.get(ctz.method_route); if (handler) |h| { h(&ctz) catch |err| { - errorHandler(response, err); + log.err("Caught error handling route {any}", .{err}); + errorHandler(response, .internal_server_error); }; } else { - std.debug.print("No route defined for {s}\n", .{response.request.target}); + log.warn("No route defined for {s}\n", .{response.request.target}); response.status = .not_found; response.do() catch return ZxError.DoError; } } + // + // if (response.state != .responded) { + // errorHandler(response, ); + // } response.finish() catch |err| { log.err("Got finish error {any}\n", .{err}); return ZxError.FinishError; @@ -151,17 +164,28 @@ pub const Zx = struct { } fn has_html_route(zx: *Zx, route_target: string) bool { + // for (zx.html_files_map.keys()) |key| { + // log.info("html route at {s}", .{key}); + // } 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); + try ctz.html_dynamic(html, .ok); } - fn errorHandler(response: *http.Server.Response, err: ZxError) void { - log.err("Got error while handling route {any}\n", .{err}); - response.status = .internal_server_error; + fn errorHandler(response: *http.Server.Response, status: ?http.Status) void { + if (status) |stat| { + response.status = stat; + } else { + response.status = .internal_server_error; + } + if (response.state == .waited) { + response.do() catch { + log.err("Already responded to client, cannot send error {any}", .{response.status}); + }; + } } }; @@ -169,45 +193,77 @@ pub const ZxContext = struct { allocator: std.mem.Allocator, response: *http.Server.Response, req_body: string, + method_route: MethodRoute, pub fn init(alloc: std.mem.Allocator, res: *http.Server.Response, body: string) ZxContext { + var method_route = MethodRoute.create(res.request.method, res.request.target); return ZxContext{ .allocator = alloc, .response = res, .req_body = body, + .method_route = method_route, }; } - /// Helper function to return JSON to the client - pub fn json(ctz: *ZxContext, content: anytype) ZxError!void { + /// Wrapper on ZxContext.respond() to return JSON to the client + pub fn json(ctz: *ZxContext, content: anytype, status: ?std.http.Status) 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); + try ctz.respond(json_content, status); } test "json helper function" {} - pub fn html(ctz: *ZxContext, html_content: string) ZxError!void { + /// Wrapper on ZxContext.respond() to return html + pub fn html_dynamic(ctz: *ZxContext, html_content: string, status: std.http.Status) ZxError!void { + var valid = HtmlValidator.validate(html_content); + if (!valid) { + log.err("Invalid html returned from route", .{}); + } + // Add html header ctz.response.headers.append("Content-Type", "text/html") catch return ZxError.Unexpected; - try ctz.respond(html_content); + try ctz.respond(html_content, status); } - // Helper function to return body to the client - pub fn respond(ctz: *ZxContext, body: string) ZxError!void { + /// Wrapper on ZxContext.respond() to return html, compile-time checked html + pub fn html(ctz: *ZxContext, comptime html_content: string, status: std.http.Status) ZxError!void { + comptime { + var valid = HtmlValidator.comp_validate(html_content); + if (!valid) { + @compileError("Trying to return invalid html\n" ++ html_content); + } + } + // Add html header + ctz.response.headers.append("Content-Type", "text/html") catch |err| { + log.err("{any} while appending headers to html response", .{err}); + return ZxError.Unexpected; + }; + + try ctz.respond(html_content, status); + } + + /// Helper function to return `body` and optional `status` to the client + pub fn respond(ctz: *ZxContext, body: string, status: ?std.http.Status) ZxError!void { if (!ctz.response.headers.contains("Content-Type")) { ctz.response.headers.append("Content-Type", "text/html") catch return ZxError.Unexpected; } + if (status) |stat| { + ctz.response.status = stat; + } 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; + ctz.response.writeAll(body) catch |err| { + log.err("{any} while writing to response body", .{err}); + return ZxError.Unexpected; + }; } };