use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
pub type CommandFuture = Pin<Box<dyn Future<Output = Result<(), CommandError>> + Send + 'static>>;
type ErasedCommand<S> = Arc<dyn Fn(&S) -> CommandFuture + Send + Sync + 'static>;
pub trait Command<S>: Send + Sync + 'static {
const NAME: &'static str;
fn run(state: &S) -> CommandFuture;
}
#[derive(Debug)]
pub enum CommandError {
NotFound(String),
Failed(String),
}
impl std::fmt::Display for CommandError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound(name) => write!(f, "command `{name}` is not registered"),
Self::Failed(msg) => write!(f, "command failed: {msg}"),
}
}
}
impl std::error::Error for CommandError {}
pub struct CommandRegistry<S: Send + Sync + 'static> {
handlers: Arc<HashMap<String, ErasedCommand<S>>>,
}
impl<S: Send + Sync + 'static> Default for CommandRegistry<S> {
fn default() -> Self {
Self::new()
}
}
impl<S: Send + Sync + 'static> CommandRegistry<S> {
#[must_use]
pub fn new() -> Self {
Self {
handlers: Arc::new(HashMap::new()),
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn register<F, Fut>(self, name: &str, handler: F) -> Self
where
F: Fn(&S) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<(), CommandError>> + Send + 'static,
{
let handler = Arc::new(handler);
let erased: ErasedCommand<S> = Arc::new(move |state: &S| Box::pin(handler(state)));
let mut map = (*self.handlers).clone();
map.insert(name.to_string(), erased);
Self {
handlers: Arc::new(map),
}
}
#[must_use]
pub fn register_command<C>(self) -> Self
where
C: Command<S>,
{
self.register(C::NAME, C::run)
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.handlers.contains_key(name)
}
#[must_use]
pub fn len(&self) -> usize {
self.handlers.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.handlers.is_empty()
}
#[must_use]
pub fn names(&self) -> Vec<&str> {
let mut names: Vec<&str> = self.handlers.keys().map(String::as_str).collect();
names.sort_unstable();
names
}
pub async fn run(&self, name: &str, state: &S) -> Result<(), CommandError> {
match self.handlers.get(name) {
Some(handler) => handler(state).await,
None => Err(CommandError::NotFound(name.to_string())),
}
}
}
impl<S: Send + Sync + 'static> std::fmt::Debug for CommandRegistry<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CommandRegistry")
.field("len", &self.handlers.len())
.field("names", &self.names())
.finish_non_exhaustive()
}
}
impl<S: Send + Sync + 'static> Clone for CommandRegistry<S> {
fn clone(&self) -> Self {
Self {
handlers: Arc::clone(&self.handlers),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone)]
struct DummyState;
#[tokio::test]
async fn register_and_run_command() {
let registry = CommandRegistry::new().register("greet", |_state: &DummyState| async {
Ok::<_, CommandError>(())
});
assert!(registry.contains("greet"));
let result = registry.run("greet", &DummyState).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn run_unknown_command_is_not_found() {
let registry = CommandRegistry::<DummyState>::new();
let err = registry.run("missing", &DummyState).await.unwrap_err();
assert!(matches!(err, CommandError::NotFound(_)));
}
struct DummyCommand;
impl Command<DummyState> for DummyCommand {
const NAME: &'static str = "dummy:run";
fn run(_state: &DummyState) -> CommandFuture {
Box::pin(async { Ok(()) })
}
}
struct FailingCommand;
impl Command<DummyState> for FailingCommand {
const NAME: &'static str = "dummy:fail";
fn run(_state: &DummyState) -> CommandFuture {
Box::pin(async { Err(CommandError::Failed("boom".to_string())) })
}
}
#[tokio::test]
async fn a_registered_command_type_runs_under_its_own_name() {
let registry = CommandRegistry::new().register_command::<DummyCommand>();
assert!(registry.contains("dummy:run"));
assert!(registry.run("dummy:run", &DummyState).await.is_ok());
}
#[tokio::test]
async fn a_command_type_that_fails_surfaces_its_message() {
let registry = CommandRegistry::new().register_command::<FailingCommand>();
let err = registry.run("dummy:fail", &DummyState).await.unwrap_err();
assert_eq!(err.to_string(), "command failed: boom");
}
#[test]
fn names_are_sorted() {
let registry = CommandRegistry::<DummyState>::new()
.register("zeta", |_| async { Ok(()) })
.register("alpha", |_| async { Ok(()) })
.register("middle", |_| async { Ok(()) });
assert_eq!(registry.names(), vec!["alpha", "middle", "zeta"]);
}
}