milestone 8: web server, rest api, sse, auth, metrics and static assets

This commit is contained in:
2026-08-02 00:54:13 +02:00
parent a8092bb1b9
commit 5253c47303
59 changed files with 19640 additions and 150 deletions
+48 -10
View File
@@ -57,8 +57,12 @@ pub const CheckArgs = struct { paths: Paths = .{}, config_explicit: bool = false
pub const ExportArgs = struct { paths: Paths = .{}, out: ?[]const u8 = null };
pub const ImportArgs = struct { paths: Paths = .{}, file: []const u8, force: bool = false };
/// `web_dev` is milestone-8 ruling 24's `--web-dev <dir>`: serve the web
/// interface from that directory instead of the embedded assets.
pub const RunArgs = struct { paths: Paths = .{}, web_dev: ?[]const u8 = null };
pub const Command = union(enum) {
run: Paths,
run: RunArgs,
check: CheckArgs,
export_: ExportArgs,
import_: ImportArgs,
@@ -89,7 +93,7 @@ pub fn parseArgs(argv: []const []const u8) ParseError!Command {
if (rest.len != 0) return error.TooManyArguments;
return .help;
}
if (eql(command, "run")) return .{ .run = (try parseCheckArgs(rest)).paths };
if (eql(command, "run")) return .{ .run = try parseRunArgs(rest) };
if (eql(command, "check")) return .{ .check = try parseCheckArgs(rest) };
if (eql(command, "export")) return .{ .export_ = try parseExportArgs(rest) };
if (eql(command, "import")) return .{ .import_ = try parseImportArgs(rest) };
@@ -124,8 +128,24 @@ fn flagValue(flag: Flag, argv: []const []const u8, i: *usize) ParseError![]const
return argv[i.*];
}
/// `run` and `check` take the same two flags. `run` throws away
/// `config_explicit`; bootstrap reads the path either way.
/// `run` takes `check`'s two flags plus `--web-dev`, which only a process that
/// serves has any use for; `check` deliberately rejects it.
fn parseRunArgs(argv: []const []const u8) ParseError!RunArgs {
var args: RunArgs = .{};
var i: usize = 0;
while (i < argv.len) : (i += 1) {
const flag = splitFlag(argv[i]) orelse return error.TooManyArguments;
if (eql(flag.name, "data-dir")) {
args.paths.data_dir = try flagValue(flag, argv, &i);
} else if (eql(flag.name, "config")) {
args.paths.config = try flagValue(flag, argv, &i);
} else if (eql(flag.name, "web-dev")) {
args.web_dev = try flagValue(flag, argv, &i);
} else return error.UnknownFlag;
}
return args;
}
fn parseCheckArgs(argv: []const []const u8) ParseError!CheckArgs {
var args: CheckArgs = .{};
var i: usize = 0;
@@ -324,6 +344,8 @@ const usage_text =
\\ --config FILE configuration file (default /etc/nxdns/config.zon)
\\ --out FILE write the export to FILE instead of stdout
\\ --force let import replace a database that already has content
\\ --web-dev DIR run only: serve the web interface from DIR instead of
\\ the embedded assets
\\
;
@@ -375,8 +397,8 @@ pub fn runVersion(r: Runner) u8 {
/// Serves DNS until SIGINT or SIGTERM. The whole of it lives in `app.zig`,
/// which is where the composition root belongs; this stays the entry point so
/// that `main` dispatches every command the same way.
pub fn runRun(r: Runner, paths: Paths) u8 {
return app.run(r, paths);
pub fn runRun(r: Runner, args: RunArgs) u8 {
return app.run(r, args);
}
pub fn runExport(r: Runner, args: ExportArgs) u8 {
@@ -742,14 +764,30 @@ const testing = std.testing;
test "parseArgs accepts run with no flags" {
const command = try parseArgs(&.{"run"});
try testing.expectEqualStrings("/var/lib/nxdns", command.run.data_dir);
try testing.expectEqualStrings("/etc/nxdns/config.zon", command.run.config);
try testing.expectEqualStrings("/var/lib/nxdns", command.run.paths.data_dir);
try testing.expectEqualStrings("/etc/nxdns/config.zon", command.run.paths.config);
try testing.expectEqual(@as(?[]const u8, null), command.run.web_dev);
}
test "parseArgs accepts run with --data-dir and --config" {
const command = try parseArgs(&.{ "run", "--data-dir", "/srv/nx", "--config", "/tmp/c.zon" });
try testing.expectEqualStrings("/srv/nx", command.run.data_dir);
try testing.expectEqualStrings("/tmp/c.zon", command.run.config);
try testing.expectEqualStrings("/srv/nx", command.run.paths.data_dir);
try testing.expectEqualStrings("/tmp/c.zon", command.run.paths.config);
}
test "parseArgs accepts run with --web-dev in both spellings" {
const attached = try parseArgs(&.{ "run", "--web-dev=web/dist" });
try testing.expectEqualStrings("web/dist", attached.run.web_dev.?);
const separate = try parseArgs(&.{ "run", "--web-dev", "web/dist" });
try testing.expectEqualStrings("web/dist", separate.run.web_dev.?);
}
test "parseArgs rejects --web-dev without a value and outside run" {
try testing.expectError(error.MissingValue, parseArgs(&.{ "run", "--web-dev" }));
try testing.expectError(error.MissingValue, parseArgs(&.{ "run", "--web-dev=" }));
try testing.expectError(error.UnknownFlag, parseArgs(&.{ "check", "--web-dev", "web/dist" }));
try testing.expectError(error.UnknownFlag, parseArgs(&.{ "export", "--web-dev", "web/dist" }));
}
test "parseArgs accepts --data-dir with and without an equals sign" {