Files
huskies/server/src/service/bot_command/mod.rs
T

98 lines
3.8 KiB
Rust
Raw Normal View History

//! Bot command service — domain logic for dispatching slash commands.
//!
//! Extracted from `http/bot_command.rs` so that argument parsing and dispatch
//! are independently testable without an HTTP layer.
//!
//! Conventions: `docs/architecture/service-modules.md`
//!
//! # Structure
//! - `mod.rs` (this file) — public API and typed `Error` type
//! - `parse.rs` — pure argument parsing, no I/O
//! - `io.rs` — all side-effectful calls (transport handlers, stores, agent pool)
pub(super) mod io;
pub mod parse;
use crate::agents::AgentPool;
use std::path::Path;
use std::sync::Arc;
// ── Error type ────────────────────────────────────────────────────────────────
/// Typed errors returned by `service::bot_command::execute`.
///
/// HTTP handlers map these to specific status codes:
/// - [`Error::UnknownCommand`] → 404 Not Found
/// - [`Error::BadArgs`] → 400 Bad Request
/// - [`Error::CommandFailed`] → 500 Internal Server Error
#[derive(Debug)]
#[allow(dead_code)] // CommandFailed is part of the public API contract; not yet reachable
pub enum Error {
/// The command keyword does not match any registered command.
UnknownCommand(String),
/// The command exists but the provided arguments are invalid.
BadArgs(String),
/// The command ran but failed with an internal error.
CommandFailed(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownCommand(msg) | Self::BadArgs(msg) | Self::CommandFailed(msg) => {
write!(f, "{msg}")
}
}
}
}
// ── Public API ────────────────────────────────────────────────────────────────
/// Execute a bot command and return the markdown response.
///
/// Dispatches to the same handlers used by the Matrix and Slack bots. The
/// `cmd` argument is the lower-cased command keyword (e.g. `"status"`,
/// `"start"`). The `args` argument is any text after the keyword, already
/// trimmed.
///
/// # Errors
/// - [`Error::UnknownCommand`] if the command keyword is not registered.
/// - [`Error::BadArgs`] if the arguments fail validation.
/// - [`Error::CommandFailed`] if command execution raises an internal error.
pub async fn execute(
cmd: &str,
args: &str,
project_root: &Path,
agents: &Arc<AgentPool>,
) -> Result<String, Error> {
match cmd {
"assign" => {
let parsed = parse::parse_assign(args).map_err(Error::BadArgs)?;
Ok(io::call_assign(&parsed, project_root, agents).await)
}
"start" => {
let parsed = parse::parse_start(args).map_err(Error::BadArgs)?;
Ok(io::call_start(&parsed, project_root, agents).await)
}
"delete" => {
let number = parse::parse_number("delete", args).map_err(Error::BadArgs)?;
Ok(io::call_delete(&number, project_root, agents).await)
}
"rmtree" => {
let number = parse::parse_number("rmtree", args).map_err(Error::BadArgs)?;
Ok(io::call_rmtree(&number, project_root, agents).await)
}
"rebuild" => Ok(io::call_rebuild(project_root, agents).await),
"timer" => io::call_timer(args, project_root)
.await
.map_err(Error::BadArgs),
"htop" => Ok(io::call_htop(args, agents)),
_ => match io::call_sync(cmd, args, project_root, agents) {
Some(response) => Ok(response),
None => Err(Error::UnknownCommand(format!(
"Unknown command: `/{cmd}`. Type `/help` to see available commands."
))),
},
}
}