mirror of
https://github.com/Xevion/banner.git
synced 2026-01-30 22:23:32 -06:00
refactor: modernize Justfile commands and simplify service management
This commit is contained in:
+2
-2
@@ -115,5 +115,5 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|||||||
ENV HOSTS=0.0.0.0,[::]
|
ENV HOSTS=0.0.0.0,[::]
|
||||||
|
|
||||||
# Implicitly uses PORT environment variable
|
# Implicitly uses PORT environment variable
|
||||||
# temporary: running without 'scraper' service
|
# Runs all services: web, bot, and scraper
|
||||||
CMD ["sh", "-c", "exec ./banner --services web,bot"]
|
CMD ["sh", "-c", "exec ./banner"]
|
||||||
|
|||||||
@@ -1,51 +1,280 @@
|
|||||||
set dotenv-load
|
set dotenv-load
|
||||||
default_services := "bot,web,scraper"
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
just --list
|
just --list
|
||||||
|
|
||||||
# Run all checks (format, clippy, tests, lint)
|
# Run all checks in parallel. Pass -f/--fix to auto-format and fix first.
|
||||||
check:
|
[script("bun")]
|
||||||
cargo fmt --all -- --check
|
check *flags:
|
||||||
cargo clippy --all-features -- --deny warnings
|
const args = "{{flags}}".split(/\s+/).filter(Boolean);
|
||||||
cargo nextest run -E 'not test(export_bindings)'
|
let fix = false;
|
||||||
bun run --cwd web check
|
for (const arg of args) {
|
||||||
bun run --cwd web test
|
if (arg === "-f" || arg === "--fix") fix = true;
|
||||||
|
else { console.error(`Unknown flag: ${arg}`); process.exit(1); }
|
||||||
|
}
|
||||||
|
|
||||||
# Generate TypeScript bindings from Rust types (ts-rs)
|
const run = (cmd) => {
|
||||||
bindings:
|
const proc = Bun.spawnSync(cmd, { stdio: ["inherit", "inherit", "inherit"] });
|
||||||
cargo test export_bindings
|
if (proc.exitCode !== 0) process.exit(proc.exitCode);
|
||||||
|
};
|
||||||
|
|
||||||
# Run all tests (Rust + frontend)
|
if (fix) {
|
||||||
test: test-rust test-web
|
console.log("\x1b[1;36m→ Fixing...\x1b[0m");
|
||||||
|
run(["cargo", "fmt", "--all"]);
|
||||||
|
run(["bun", "run", "--cwd", "web", "format"]);
|
||||||
|
run(["cargo", "clippy", "--all-features", "--fix", "--allow-dirty", "--allow-staged",
|
||||||
|
"--", "--deny", "warnings"]);
|
||||||
|
console.log("\x1b[1;36m→ Verifying...\x1b[0m");
|
||||||
|
}
|
||||||
|
|
||||||
# Run only Rust tests (excludes ts-rs bindings generation)
|
const checks = [
|
||||||
test-rust *ARGS:
|
{ name: "rustfmt", cmd: ["cargo", "fmt", "--all", "--", "--check"] },
|
||||||
cargo nextest run -E 'not test(export_bindings)' {{ARGS}}
|
{ name: "clippy", cmd: ["cargo", "clippy", "--all-features", "--", "--deny", "warnings"] },
|
||||||
|
{ name: "rust-test", cmd: ["cargo", "nextest", "run", "-E", "not test(export_bindings)"] },
|
||||||
|
{ name: "svelte-check", cmd: ["bun", "run", "--cwd", "web", "check"] },
|
||||||
|
{ name: "biome", cmd: ["bun", "run", "--cwd", "web", "format:check"] },
|
||||||
|
{ name: "web-test", cmd: ["bun", "run", "--cwd", "web", "test"] },
|
||||||
|
// { name: "sqlx-prepare", cmd: ["cargo", "sqlx", "prepare", "--check"] },
|
||||||
|
];
|
||||||
|
|
||||||
# Run only frontend tests
|
const isTTY = process.stderr.isTTY;
|
||||||
test-web:
|
const start = Date.now();
|
||||||
bun run --cwd web test
|
const remaining = new Set(checks.map(c => c.name));
|
||||||
|
|
||||||
# Quick check: clippy + tests + typecheck (skips formatting)
|
const promises = checks.map(async (check) => {
|
||||||
check-quick:
|
const proc = Bun.spawn(check.cmd, {
|
||||||
cargo clippy --all-features -- --deny warnings
|
env: { ...process.env, FORCE_COLOR: "1" },
|
||||||
cargo nextest run -E 'not test(export_bindings)'
|
stdout: "pipe", stderr: "pipe",
|
||||||
bun run --cwd web check
|
});
|
||||||
|
const [stdout, stderr] = await Promise.all([
|
||||||
|
new Response(proc.stdout).text(),
|
||||||
|
new Response(proc.stderr).text(),
|
||||||
|
]);
|
||||||
|
await proc.exited;
|
||||||
|
return { ...check, stdout, stderr, exitCode: proc.exitCode,
|
||||||
|
elapsed: ((Date.now() - start) / 1000).toFixed(1) };
|
||||||
|
});
|
||||||
|
|
||||||
# Run the Banner API search demo (hits live UTSA API, ~20s)
|
const interval = isTTY ? setInterval(() => {
|
||||||
search *ARGS:
|
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||||
cargo run -q --bin search -- {{ARGS}}
|
process.stderr.write(`\r\x1b[K${elapsed}s [${Array.from(remaining).join(", ")}]`);
|
||||||
|
}, 100) : null;
|
||||||
|
|
||||||
|
let anyFailed = false;
|
||||||
|
for (const promise of promises) {
|
||||||
|
const r = await promise;
|
||||||
|
remaining.delete(r.name);
|
||||||
|
if (isTTY) process.stderr.write(`\r\x1b[K`);
|
||||||
|
if (r.exitCode !== 0) {
|
||||||
|
anyFailed = true;
|
||||||
|
process.stdout.write(`\x1b[31m✗ ${r.name}\x1b[0m (${r.elapsed}s)\n`);
|
||||||
|
if (r.stdout) process.stdout.write(r.stdout);
|
||||||
|
if (r.stderr) process.stderr.write(r.stderr);
|
||||||
|
} else {
|
||||||
|
process.stdout.write(`\x1b[32m✓ ${r.name}\x1b[0m (${r.elapsed}s)\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interval) clearInterval(interval);
|
||||||
|
if (isTTY) process.stderr.write(`\r\x1b[K`);
|
||||||
|
process.exit(anyFailed ? 1 : 0);
|
||||||
|
|
||||||
# Format all Rust and TypeScript code
|
# Format all Rust and TypeScript code
|
||||||
format:
|
format:
|
||||||
cargo fmt --all
|
cargo fmt --all
|
||||||
bun run --cwd web format
|
bun run --cwd web format
|
||||||
|
|
||||||
# Check formatting without modifying (CI-friendly)
|
# Run tests. Usage: just test [rust|web|<nextest filter args>]
|
||||||
format-check:
|
[script("bun")]
|
||||||
cargo fmt --all -- --check
|
test *args:
|
||||||
bun run --cwd web format:check
|
const input = "{{args}}".trim();
|
||||||
|
const run = (cmd) => {
|
||||||
|
const proc = Bun.spawnSync(cmd, { stdio: ["inherit", "inherit", "inherit"] });
|
||||||
|
if (proc.exitCode !== 0) process.exit(proc.exitCode);
|
||||||
|
};
|
||||||
|
if (input === "web") {
|
||||||
|
run(["bun", "run", "--cwd", "web", "test"]);
|
||||||
|
} else if (input === "rust") {
|
||||||
|
run(["cargo", "nextest", "run", "-E", "not test(export_bindings)"]);
|
||||||
|
} else if (input === "") {
|
||||||
|
run(["cargo", "nextest", "run", "-E", "not test(export_bindings)"]);
|
||||||
|
run(["bun", "run", "--cwd", "web", "test"]);
|
||||||
|
} else {
|
||||||
|
run(["cargo", "nextest", "run", ...input.split(/\s+/)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate TypeScript bindings from Rust types (ts-rs)
|
||||||
|
bindings:
|
||||||
|
cargo test export_bindings
|
||||||
|
|
||||||
|
# Run the Banner API search demo (hits live UTSA API, ~20s)
|
||||||
|
search *ARGS:
|
||||||
|
cargo run -q --bin search -- {{ARGS}}
|
||||||
|
|
||||||
|
# Pass args to binary after --: just dev -n -- --some-flag
|
||||||
|
# Dev server. Flags: -f(rontend) -b(ackend) -W(no-watch) -n(o-build) -r(elease) -e(mbed) --tracing <fmt>
|
||||||
|
[script("bun")]
|
||||||
|
dev *flags:
|
||||||
|
const argv = "{{flags}}".split(/\s+/).filter(Boolean);
|
||||||
|
|
||||||
|
let frontendOnly = false, backendOnly = false;
|
||||||
|
let noWatch = false, noBuild = false, release = false, embed = false;
|
||||||
|
let tracing = "pretty";
|
||||||
|
const passthrough = [];
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
let seenDashDash = false;
|
||||||
|
while (i < argv.length) {
|
||||||
|
const arg = argv[i];
|
||||||
|
if (seenDashDash) { passthrough.push(arg); i++; continue; }
|
||||||
|
if (arg === "--") { seenDashDash = true; i++; continue; }
|
||||||
|
if (arg.startsWith("--")) {
|
||||||
|
if (arg === "--frontend-only") frontendOnly = true;
|
||||||
|
else if (arg === "--backend-only") backendOnly = true;
|
||||||
|
else if (arg === "--no-watch") noWatch = true;
|
||||||
|
else if (arg === "--no-build") noBuild = true;
|
||||||
|
else if (arg === "--release") release = true;
|
||||||
|
else if (arg === "--embed") embed = true;
|
||||||
|
else if (arg === "--tracing") { tracing = argv[++i] || "pretty"; }
|
||||||
|
else { console.error(`Unknown flag: ${arg}`); process.exit(1); }
|
||||||
|
} else if (arg.startsWith("-") && arg.length > 1) {
|
||||||
|
for (const c of arg.slice(1)) {
|
||||||
|
if (c === "f") frontendOnly = true;
|
||||||
|
else if (c === "b") backendOnly = true;
|
||||||
|
else if (c === "W") noWatch = true;
|
||||||
|
else if (c === "n") noBuild = true;
|
||||||
|
else if (c === "r") release = true;
|
||||||
|
else if (c === "e") embed = true;
|
||||||
|
else { console.error(`Unknown flag: -${c}`); process.exit(1); }
|
||||||
|
}
|
||||||
|
} else { console.error(`Unknown argument: ${arg}`); process.exit(1); }
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -e implies -b (no point running Vite if assets are embedded)
|
||||||
|
if (embed) backendOnly = true;
|
||||||
|
// -n implies -W (no build means no watch)
|
||||||
|
if (noBuild) noWatch = true;
|
||||||
|
|
||||||
|
// Validate conflicting flags
|
||||||
|
if (frontendOnly && backendOnly) {
|
||||||
|
console.error("Cannot use -f and -b together (or -e implies -b)");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const runFrontend = !backendOnly;
|
||||||
|
const runBackend = !frontendOnly;
|
||||||
|
const profile = release ? "release" : "dev";
|
||||||
|
const profileDir = release ? "release" : "debug";
|
||||||
|
|
||||||
|
const procs = [];
|
||||||
|
const cleanup = () => { for (const p of procs) p.kill(); };
|
||||||
|
process.on("SIGINT", () => { cleanup(); process.exit(130); });
|
||||||
|
process.on("SIGTERM", () => { cleanup(); process.exit(143); });
|
||||||
|
|
||||||
|
// Frontend: Vite dev server
|
||||||
|
if (runFrontend) {
|
||||||
|
const proc = Bun.spawn(["bun", "run", "--cwd", "web", "dev"], {
|
||||||
|
stdio: ["inherit", "inherit", "inherit"],
|
||||||
|
});
|
||||||
|
procs.push(proc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend
|
||||||
|
if (runBackend) {
|
||||||
|
const backendArgs = [`--tracing`, tracing, ...passthrough];
|
||||||
|
const bin = `target/${profileDir}/banner`;
|
||||||
|
|
||||||
|
if (noWatch) {
|
||||||
|
// Build first unless -n (skip build)
|
||||||
|
if (!noBuild) {
|
||||||
|
console.log(`\x1b[1;36m→ Building backend (${profile})...\x1b[0m`);
|
||||||
|
const cargoArgs = ["cargo", "build", "--bin", "banner"];
|
||||||
|
if (!embed) cargoArgs.push("--no-default-features");
|
||||||
|
if (release) cargoArgs.push("--release");
|
||||||
|
const build = Bun.spawnSync(cargoArgs, { stdio: ["inherit", "inherit", "inherit"] });
|
||||||
|
if (build.exitCode !== 0) { cleanup(); process.exit(build.exitCode); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the binary directly (no watch)
|
||||||
|
const { existsSync } = await import("fs");
|
||||||
|
if (!existsSync(bin)) {
|
||||||
|
console.error(`Binary not found: ${bin}`);
|
||||||
|
console.error(`Run 'just build${release ? "" : " -d"}' first, or remove -n to use bacon.`);
|
||||||
|
cleanup();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(`\x1b[1;36m→ Running ${bin} (no watch)\x1b[0m`);
|
||||||
|
const proc = Bun.spawn([bin, ...backendArgs], {
|
||||||
|
stdio: ["inherit", "inherit", "inherit"],
|
||||||
|
});
|
||||||
|
procs.push(proc);
|
||||||
|
} else {
|
||||||
|
// Bacon watch mode
|
||||||
|
const baconArgs = ["bacon", "--headless", "run", "--"];
|
||||||
|
if (!embed) baconArgs.push("--no-default-features");
|
||||||
|
if (release) baconArgs.push("--profile", "release");
|
||||||
|
baconArgs.push("--", ...backendArgs);
|
||||||
|
const proc = Bun.spawn(baconArgs, {
|
||||||
|
stdio: ["inherit", "inherit", "inherit"],
|
||||||
|
});
|
||||||
|
procs.push(proc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for any process to exit, then kill the rest
|
||||||
|
const results = procs.map((p, i) => p.exited.then(code => ({ i, code })));
|
||||||
|
const first = await Promise.race(results);
|
||||||
|
cleanup();
|
||||||
|
process.exit(first.code);
|
||||||
|
|
||||||
|
# Production build. Flags: -d(ebug) -f(rontend-only) -b(ackend-only)
|
||||||
|
[script("bun")]
|
||||||
|
build *flags:
|
||||||
|
const argv = "{{flags}}".split(/\s+/).filter(Boolean);
|
||||||
|
|
||||||
|
let debug = false, frontendOnly = false, backendOnly = false;
|
||||||
|
for (const arg of argv) {
|
||||||
|
if (arg.startsWith("--")) {
|
||||||
|
if (arg === "--debug") debug = true;
|
||||||
|
else if (arg === "--frontend-only") frontendOnly = true;
|
||||||
|
else if (arg === "--backend-only") backendOnly = true;
|
||||||
|
else { console.error(`Unknown flag: ${arg}`); process.exit(1); }
|
||||||
|
} else if (arg.startsWith("-") && arg.length > 1) {
|
||||||
|
for (const c of arg.slice(1)) {
|
||||||
|
if (c === "d") debug = true;
|
||||||
|
else if (c === "f") frontendOnly = true;
|
||||||
|
else if (c === "b") backendOnly = true;
|
||||||
|
else { console.error(`Unknown flag: -${c}`); process.exit(1); }
|
||||||
|
}
|
||||||
|
} else { console.error(`Unknown argument: ${arg}`); process.exit(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frontendOnly && backendOnly) {
|
||||||
|
console.error("Cannot use -f and -b together");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const run = (cmd) => {
|
||||||
|
const proc = Bun.spawnSync(cmd, { stdio: ["inherit", "inherit", "inherit"] });
|
||||||
|
if (proc.exitCode !== 0) process.exit(proc.exitCode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildFrontend = !backendOnly;
|
||||||
|
const buildBackend = !frontendOnly;
|
||||||
|
const profile = debug ? "debug" : "release";
|
||||||
|
|
||||||
|
if (buildFrontend) {
|
||||||
|
console.log("\x1b[1;36m→ Building frontend...\x1b[0m");
|
||||||
|
run(["bun", "run", "--cwd", "web", "build"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildBackend) {
|
||||||
|
console.log(`\x1b[1;36m→ Building backend (${profile})...\x1b[0m`);
|
||||||
|
const cmd = ["cargo", "build", "--bin", "banner"];
|
||||||
|
if (!debug) cmd.push("--release");
|
||||||
|
run(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
# Start PostgreSQL in Docker and update .env with connection string
|
# Start PostgreSQL in Docker and update .env with connection string
|
||||||
# Commands: start (default), reset, rm
|
# Commands: start (default), reset, rm
|
||||||
@@ -115,86 +344,6 @@ db cmd="start":
|
|||||||
await updateEnv();
|
await updateEnv();
|
||||||
}
|
}
|
||||||
|
|
||||||
# Auto-reloading frontend server
|
|
||||||
frontend:
|
|
||||||
bun run --cwd web dev
|
|
||||||
|
|
||||||
# Production build of frontend
|
|
||||||
build-frontend:
|
|
||||||
bun run --cwd web build
|
|
||||||
|
|
||||||
# Auto-reloading backend server (with embedded assets)
|
|
||||||
backend *ARGS:
|
|
||||||
bacon --headless run -- -- {{ARGS}}
|
|
||||||
|
|
||||||
# Auto-reloading backend server (no embedded assets, for dev proxy mode)
|
|
||||||
backend-dev *ARGS:
|
|
||||||
bacon --headless run -- --no-default-features -- {{ARGS}}
|
|
||||||
|
|
||||||
# Production build
|
|
||||||
build:
|
|
||||||
bun run --cwd web build
|
|
||||||
cargo build --release --bin banner
|
|
||||||
|
|
||||||
# Run auto-reloading development build with release characteristics
|
|
||||||
dev-build *ARGS='--services web --tracing pretty': build-frontend
|
|
||||||
bacon --headless run -- --profile dev-release -- {{ARGS}}
|
|
||||||
|
|
||||||
# Auto-reloading development build: Vite frontend + backend (no embedded assets, proxies to Vite)
|
|
||||||
[parallel]
|
|
||||||
dev *ARGS='--services web,bot': frontend (backend-dev ARGS)
|
|
||||||
|
|
||||||
# Smoke test: start web server, hit API endpoints, verify responses
|
|
||||||
[script("bash")]
|
|
||||||
test-smoke port="18080":
|
|
||||||
set -euo pipefail
|
|
||||||
PORT={{port}}
|
|
||||||
|
|
||||||
cleanup() { kill "$SERVER_PID" 2>/dev/null; wait "$SERVER_PID" 2>/dev/null; }
|
|
||||||
|
|
||||||
# Start server in background
|
|
||||||
PORT=$PORT cargo run -q --no-default-features -- --services web --tracing json &
|
|
||||||
SERVER_PID=$!
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
# Wait for server to be ready (up to 15s)
|
|
||||||
for i in $(seq 1 30); do
|
|
||||||
if curl -sf "http://localhost:$PORT/api/health" >/dev/null 2>&1; then break; fi
|
|
||||||
if ! kill -0 "$SERVER_PID" 2>/dev/null; then echo "FAIL: server exited early"; exit 1; fi
|
|
||||||
sleep 0.5
|
|
||||||
done
|
|
||||||
|
|
||||||
PASS=0; FAIL=0
|
|
||||||
check() {
|
|
||||||
local label="$1" url="$2" expected="$3"
|
|
||||||
body=$(curl -sf "$url") || { echo "FAIL: $label - request failed"; FAIL=$((FAIL+1)); return; }
|
|
||||||
if echo "$body" | grep -q "$expected"; then
|
|
||||||
echo "PASS: $label"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo "FAIL: $label - expected '$expected' in: $body"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check "GET /api/health" "http://localhost:$PORT/api/health" '"status":"healthy"'
|
|
||||||
check "GET /api/status" "http://localhost:$PORT/api/status" '"version"'
|
|
||||||
check "GET /api/metrics" "http://localhost:$PORT/api/metrics" '"banner_api"'
|
|
||||||
|
|
||||||
# Test 404
|
|
||||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$PORT/api/nonexistent")
|
|
||||||
if [ "$STATUS" = "404" ]; then
|
|
||||||
echo "PASS: 404 on unknown route"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo "FAIL: expected 404, got $STATUS"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Results: $PASS passed, $FAIL failed"
|
|
||||||
[ "$FAIL" -eq 0 ]
|
|
||||||
|
|
||||||
alias b := bun
|
alias b := bun
|
||||||
bun *ARGS:
|
bun *ARGS:
|
||||||
cd web && bun {{ ARGS }}
|
cd web && bun {{ ARGS }}
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ The application consists of three modular services that can be run independently
|
|||||||
bun install --cwd web # Install frontend dependencies
|
bun install --cwd web # Install frontend dependencies
|
||||||
cargo build # Build the backend
|
cargo build # Build the backend
|
||||||
|
|
||||||
just dev # Runs auto-reloading dev build
|
just dev # Runs auto-reloading dev build with all services
|
||||||
just dev --services bot,web # Runs auto-reloading dev build, running only the bot and web services
|
|
||||||
just dev-build # Development build with release characteristics (frontend is embedded, non-auto-reloading)
|
just dev-build # Development build with release characteristics (frontend is embedded, non-auto-reloading)
|
||||||
|
|
||||||
just build # Production build that embeds assets
|
just build # Production build that embeds assets
|
||||||
|
|||||||
+1
-105
@@ -2,34 +2,16 @@ use clap::Parser;
|
|||||||
|
|
||||||
/// Banner Discord Bot - Course availability monitoring
|
/// Banner Discord Bot - Course availability monitoring
|
||||||
///
|
///
|
||||||
/// This application runs multiple services that can be controlled via CLI arguments:
|
/// This application runs all services:
|
||||||
/// - bot: Discord bot for course monitoring commands
|
/// - bot: Discord bot for course monitoring commands
|
||||||
/// - web: HTTP server for web interface and API
|
/// - web: HTTP server for web interface and API
|
||||||
/// - scraper: Background service for scraping course data
|
/// - scraper: Background service for scraping course data
|
||||||
///
|
|
||||||
/// Use --services to specify which services to run, or --disable-services to exclude specific services.
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
/// Log formatter to use
|
/// Log formatter to use
|
||||||
#[arg(long, value_enum, default_value_t = default_tracing_format())]
|
#[arg(long, value_enum, default_value_t = default_tracing_format())]
|
||||||
pub tracing: TracingFormat,
|
pub tracing: TracingFormat,
|
||||||
|
|
||||||
/// Services to run (comma-separated). Default: all services
|
|
||||||
///
|
|
||||||
/// Examples:
|
|
||||||
/// --services bot,web # Run only bot and web services
|
|
||||||
/// --services scraper # Run only the scraper service
|
|
||||||
#[arg(long, value_delimiter = ',', conflicts_with = "disable_services")]
|
|
||||||
pub services: Option<Vec<ServiceName>>,
|
|
||||||
|
|
||||||
/// Services to disable (comma-separated)
|
|
||||||
///
|
|
||||||
/// Examples:
|
|
||||||
/// --disable-services bot # Run web and scraper only
|
|
||||||
/// --disable-services bot,web # Run only the scraper service
|
|
||||||
#[arg(long, value_delimiter = ',', conflicts_with = "services")]
|
|
||||||
pub disable_services: Option<Vec<ServiceName>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::ValueEnum, Clone, Debug)]
|
#[derive(clap::ValueEnum, Clone, Debug)]
|
||||||
@@ -66,34 +48,6 @@ impl ServiceName {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine which services should be enabled based on CLI arguments
|
|
||||||
pub fn determine_enabled_services(args: &Args) -> Result<Vec<ServiceName>, anyhow::Error> {
|
|
||||||
match (&args.services, &args.disable_services) {
|
|
||||||
(Some(services), None) => {
|
|
||||||
// User specified which services to run
|
|
||||||
Ok(services.clone())
|
|
||||||
}
|
|
||||||
(None, Some(disabled)) => {
|
|
||||||
// User specified which services to disable
|
|
||||||
let enabled: Vec<ServiceName> = ServiceName::all()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|s| !disabled.contains(s))
|
|
||||||
.collect();
|
|
||||||
Ok(enabled)
|
|
||||||
}
|
|
||||||
(None, None) => {
|
|
||||||
// Default: run all services
|
|
||||||
Ok(ServiceName::all())
|
|
||||||
}
|
|
||||||
(Some(_), Some(_)) => {
|
|
||||||
// This should be prevented by clap's conflicts_with, but just in case
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"Cannot specify both --services and --disable-services"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
const DEFAULT_TRACING_FORMAT: TracingFormat = TracingFormat::Pretty;
|
const DEFAULT_TRACING_FORMAT: TracingFormat = TracingFormat::Pretty;
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
@@ -107,64 +61,6 @@ fn default_tracing_format() -> TracingFormat {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn args_with_services(
|
|
||||||
services: Option<Vec<ServiceName>>,
|
|
||||||
disable: Option<Vec<ServiceName>>,
|
|
||||||
) -> Args {
|
|
||||||
Args {
|
|
||||||
tracing: TracingFormat::Pretty,
|
|
||||||
services,
|
|
||||||
disable_services: disable,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_default_enables_all_services() {
|
|
||||||
let result = determine_enabled_services(&args_with_services(None, None)).unwrap();
|
|
||||||
assert_eq!(result.len(), 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_explicit_services_only_those() {
|
|
||||||
let result =
|
|
||||||
determine_enabled_services(&args_with_services(Some(vec![ServiceName::Web]), None))
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
assert_eq!(result[0].as_str(), "web");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_disable_bot_leaves_web_and_scraper() {
|
|
||||||
let result =
|
|
||||||
determine_enabled_services(&args_with_services(None, Some(vec![ServiceName::Bot])))
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(result.len(), 2);
|
|
||||||
assert!(result.iter().all(|s| s.as_str() != "bot"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_disable_all_leaves_empty() {
|
|
||||||
let result = determine_enabled_services(&args_with_services(
|
|
||||||
None,
|
|
||||||
Some(vec![
|
|
||||||
ServiceName::Bot,
|
|
||||||
ServiceName::Web,
|
|
||||||
ServiceName::Scraper,
|
|
||||||
]),
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
assert!(result.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_both_specified_returns_error() {
|
|
||||||
let result = determine_enabled_services(&args_with_services(
|
|
||||||
Some(vec![ServiceName::Web]),
|
|
||||||
Some(vec![ServiceName::Bot]),
|
|
||||||
));
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_service_name_as_str() {
|
fn test_service_name_as_str() {
|
||||||
assert_eq!(ServiceName::Bot.as_str(), "bot");
|
assert_eq!(ServiceName::Bot.as_str(), "bot");
|
||||||
|
|||||||
+3
-4
@@ -1,5 +1,5 @@
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::cli::{Args, ServiceName, determine_enabled_services};
|
use crate::cli::{Args, ServiceName};
|
||||||
use crate::logging::setup_logging;
|
use crate::logging::setup_logging;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
@@ -29,9 +29,8 @@ async fn main() -> ExitCode {
|
|||||||
// Parse CLI arguments
|
// Parse CLI arguments
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
// Determine which services should be enabled
|
// Always run all services
|
||||||
let enabled_services: Vec<ServiceName> =
|
let enabled_services = ServiceName::all();
|
||||||
determine_enabled_services(&args).expect("Failed to determine enabled services");
|
|
||||||
|
|
||||||
// Create and initialize the application
|
// Create and initialize the application
|
||||||
let mut app = App::new().await.expect("Failed to initialize application");
|
let mut app = App::new().await.expect("Failed to initialize application");
|
||||||
|
|||||||
Reference in New Issue
Block a user