argot-cmd 0.2.0

An agent-first command interface framework for Rust
Documentation
//! Middleware hooks for the [`crate::cli::Cli`] dispatch loop.
//!
//! Implement [`Middleware`] and register it with [`crate::Cli::with_middleware`]
//! to intercept the parse-dispatch lifecycle.

use crate::model::ParsedCommand;
use crate::parser::ParseError;

/// Hook into the [`crate::cli::Cli`] parse-and-dispatch lifecycle.
///
/// All methods have default no-op implementations so you only need to
/// override the hooks you care about.
///
/// # Examples
///
/// ```
/// use argot_cmd::middleware::Middleware;
/// use argot_cmd::ParsedCommand;
///
/// struct Logger;
///
/// impl Middleware for Logger {
///     fn before_dispatch(&self, parsed: &ParsedCommand<'_>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
///         eprintln!("[log] dispatching: {}", parsed.command.canonical);
///         Ok(())
///     }
/// }
/// ```
pub trait Middleware: Send + Sync {
    /// Called after a successful parse, before the handler is invoked.
    ///
    /// Return `Err(...)` to abort dispatch with a [`crate::cli::CliError::Handler`].
    ///
    /// # Examples
    ///
    /// ```
    /// use argot_cmd::middleware::Middleware;
    /// use argot_cmd::ParsedCommand;
    ///
    /// struct RateLimiter { max: usize }
    ///
    /// impl Middleware for RateLimiter {
    ///     fn before_dispatch(
    ///         &self,
    ///         parsed: &ParsedCommand<'_>,
    ///     ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    ///         // Allow all commands in this example.
    ///         // A real implementation would check a counter.
    ///         println!("dispatching: {}", parsed.command.canonical);
    ///         Ok(())
    ///     }
    /// }
    /// ```
    fn before_dispatch(
        &self,
        _parsed: &ParsedCommand<'_>,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        Ok(())
    }

    /// Called after the handler returns (whether `Ok` or `Err`).
    ///
    /// # Examples
    ///
    /// ```
    /// use argot_cmd::middleware::Middleware;
    /// use argot_cmd::ParsedCommand;
    ///
    /// struct AuditLog;
    ///
    /// impl Middleware for AuditLog {
    ///     fn after_dispatch(
    ///         &self,
    ///         parsed: &ParsedCommand<'_>,
    ///         result: &Result<(), Box<dyn std::error::Error + Send + Sync>>,
    ///     ) {
    ///         match result {
    ///             Ok(()) => println!("✓ {}", parsed.command.canonical),
    ///             Err(e) => eprintln!("✗ {}: {}", parsed.command.canonical, e),
    ///         }
    ///     }
    /// }
    /// ```
    fn after_dispatch(
        &self,
        _parsed: &ParsedCommand<'_>,
        _result: &Result<(), Box<dyn std::error::Error + Send + Sync>>,
    ) {
    }

    /// Called when `Parser::parse` returns an error, before it is surfaced to the caller.
    ///
    /// # Examples
    ///
    /// ```
    /// use argot_cmd::middleware::Middleware;
    /// use argot_cmd::parser::ParseError;
    ///
    /// struct ErrorLogger;
    ///
    /// impl Middleware for ErrorLogger {
    ///     fn on_parse_error(&self, err: &ParseError) {
    ///         eprintln!("parse failed: {}", err);
    ///     }
    /// }
    /// ```
    fn on_parse_error(&self, _error: &ParseError) {}
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::Command;

    /// A no-op middleware that uses all default implementations.
    struct NoOpMiddleware;
    impl Middleware for NoOpMiddleware {}

    #[test]
    fn test_default_before_dispatch_returns_ok() {
        let cmd = Command::builder("test").build().unwrap();
        let cmds = vec![cmd];
        let parser = crate::parser::Parser::new(&cmds);
        let parsed = parser.parse(&["test"]).unwrap();
        let mw = NoOpMiddleware;
        let result = mw.before_dispatch(&parsed);
        assert!(result.is_ok());
    }

    #[test]
    fn test_default_after_dispatch_is_noop() {
        let cmd = Command::builder("test").build().unwrap();
        let cmds = vec![cmd];
        let parser = crate::parser::Parser::new(&cmds);
        let parsed = parser.parse(&["test"]).unwrap();
        let mw = NoOpMiddleware;
        // Should not panic
        let ok_result: Result<(), Box<dyn std::error::Error + Send + Sync>> = Ok(());
        mw.after_dispatch(&parsed, &ok_result);
        let err_result: Result<(), Box<dyn std::error::Error + Send + Sync>> =
            Err("some error".into());
        mw.after_dispatch(&parsed, &err_result);
    }

    #[test]
    fn test_default_on_parse_error_is_noop() {
        let mw = NoOpMiddleware;
        let err = ParseError::NoCommand;
        // Should not panic
        mw.on_parse_error(&err);
    }
}