diff --git a/src/config.zig b/src/config.zig index e124be4..b541bff 100644 --- a/src/config.zig +++ b/src/config.zig @@ -6,6 +6,7 @@ pub const home_env_var = "NULLTICKETS_HOME"; pub const home_dir_name = ".nulltickets"; pub const Config = struct { + host: []const u8 = "127.0.0.1", port: u16 = 7700, db: []const u8 = "nulltickets.db", api_token: ?[]const u8 = null, @@ -73,6 +74,7 @@ fn getHomeDirOwned(allocator: std.mem.Allocator) ![]u8 { test "loadFromFile returns defaults when missing" { const cfg = try loadFromFile(std.testing.allocator, "nonexistent-config-file-12345.json"); try std.testing.expectEqual(@as(u16, 7700), cfg.port); + try std.testing.expectEqualStrings("127.0.0.1", cfg.host); try std.testing.expectEqualStrings("nulltickets.db", cfg.db); try std.testing.expectEqual(@as(?[]const u8, null), cfg.api_token); } @@ -85,6 +87,7 @@ test "loadFromFile reads config values" { .sub_path = "config.json", .data = \\{ + \\ "host": "0.0.0.0", \\ "port": 7788, \\ "db": "tickets.db", \\ "api_token": "secret" @@ -99,11 +102,36 @@ test "loadFromFile reads config values" { defer arena.deinit(); const cfg = try loadFromFile(arena.allocator(), cfg_path); + try std.testing.expectEqualStrings("0.0.0.0", cfg.host); try std.testing.expectEqual(@as(u16, 7788), cfg.port); try std.testing.expectEqualStrings("tickets.db", cfg.db); try std.testing.expectEqualStrings("secret", cfg.api_token.?); } +test "loadFromFile keeps default host when unset" { + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + try tmp.dir.writeFile(.{ + .sub_path = "config.json", + .data = + \\{ + \\ "port": 7799 + \\} + , + }); + + const cfg_path = try tmp.dir.realpathAlloc(std.testing.allocator, "config.json"); + defer std.testing.allocator.free(cfg_path); + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const cfg = try loadFromFile(arena.allocator(), cfg_path); + try std.testing.expectEqualStrings("127.0.0.1", cfg.host); + try std.testing.expectEqual(@as(u16, 7799), cfg.port); +} + test "resolveRelativePaths anchors db to config directory" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); diff --git a/src/main.zig b/src/main.zig index 50a84f6..71c0601 100644 --- a/src/main.zig +++ b/src/main.zig @@ -33,6 +33,7 @@ pub fn main(init: std.process.Init) !void { } var port_override: ?u16 = null; + var host_override: ?[]const u8 = null; var db_override: ?[:0]const u8 = null; var token_override: ?[]const u8 = null; var config_path_override: ?[]const u8 = null; @@ -49,6 +50,11 @@ pub fn main(init: std.process.Init) !void { return; }; } + } else if (std.mem.eql(u8, arg, "--host")) { + if (arg_index + 1 < args.len) { + arg_index += 1; + host_override = args[arg_index]; + } } else if (std.mem.eql(u8, arg, "--db")) { if (arg_index + 1 < args.len) { arg_index += 1; @@ -115,17 +121,18 @@ pub fn main(init: std.process.Init) !void { var store = try Store.init(allocator, db_path); defer store.deinit(); - const addr = std.Io.net.IpAddress.resolve(std_compat.io(), "127.0.0.1", port) catch |err| { + const bind_host = host_override orelse (if (cfg.host.len > 0) cfg.host else "127.0.0.1"); + const addr = std.Io.net.IpAddress.resolve(std_compat.io(), bind_host, port) catch |err| { std.debug.print("failed to resolve address: {}\n", .{err}); return; }; var server = addr.listen(std_compat.io(), .{ .reuse_address = true }) catch |err| { - std.debug.print("failed to listen on port {d}: {}\n", .{ port, err }); + std.debug.print("failed to listen on {s}:{d}: {}\n", .{ bind_host, port, err }); return; }; defer server.deinit(std_compat.io()); - std.debug.print("listening on http://127.0.0.1:{d}\n", .{port}); + std.debug.print("listening on http://{s}:{d}\n", .{ bind_host, port }); while (true) { var conn = server.accept(std_compat.io()) catch |err| {