huskies: merge 607_story_extract_bot_command_service
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
//! 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."
|
||||
))),
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user