Skip to main content

argot_cmd/middleware/
mod.rs

1//! Middleware hooks for the [`crate::cli::Cli`] dispatch loop.
2//!
3//! Implement [`Middleware`] and register it with [`crate::Cli::with_middleware`]
4//! to intercept the parse-dispatch lifecycle.
5
6use crate::model::ParsedCommand;
7use crate::parser::ParseError;
8
9/// Hook into the [`crate::cli::Cli`] parse-and-dispatch lifecycle.
10///
11/// All methods have default no-op implementations so you only need to
12/// override the hooks you care about.
13///
14/// # Examples
15///
16/// ```
17/// use argot_cmd::middleware::Middleware;
18/// use argot_cmd::ParsedCommand;
19///
20/// struct Logger;
21///
22/// impl Middleware for Logger {
23///     fn before_dispatch(&self, parsed: &ParsedCommand<'_>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
24///         eprintln!("[log] dispatching: {}", parsed.command.canonical);
25///         Ok(())
26///     }
27/// }
28/// ```
29pub trait Middleware: Send + Sync {
30    /// Called after a successful parse, before the handler is invoked.
31    ///
32    /// Return `Err(...)` to abort dispatch with a [`crate::cli::CliError::Handler`].
33    ///
34    /// # Examples
35    ///
36    /// ```
37    /// use argot_cmd::middleware::Middleware;
38    /// use argot_cmd::ParsedCommand;
39    ///
40    /// struct RateLimiter { max: usize }
41    ///
42    /// impl Middleware for RateLimiter {
43    ///     fn before_dispatch(
44    ///         &self,
45    ///         parsed: &ParsedCommand<'_>,
46    ///     ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
47    ///         // Allow all commands in this example.
48    ///         // A real implementation would check a counter.
49    ///         println!("dispatching: {}", parsed.command.canonical);
50    ///         Ok(())
51    ///     }
52    /// }
53    /// ```
54    fn before_dispatch(
55        &self,
56        _parsed: &ParsedCommand<'_>,
57    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
58        Ok(())
59    }
60
61    /// Called after the handler returns (whether `Ok` or `Err`).
62    ///
63    /// # Examples
64    ///
65    /// ```
66    /// use argot_cmd::middleware::Middleware;
67    /// use argot_cmd::ParsedCommand;
68    ///
69    /// struct AuditLog;
70    ///
71    /// impl Middleware for AuditLog {
72    ///     fn after_dispatch(
73    ///         &self,
74    ///         parsed: &ParsedCommand<'_>,
75    ///         result: &Result<(), Box<dyn std::error::Error + Send + Sync>>,
76    ///     ) {
77    ///         match result {
78    ///             Ok(()) => println!("✓ {}", parsed.command.canonical),
79    ///             Err(e) => eprintln!("✗ {}: {}", parsed.command.canonical, e),
80    ///         }
81    ///     }
82    /// }
83    /// ```
84    fn after_dispatch(
85        &self,
86        _parsed: &ParsedCommand<'_>,
87        _result: &Result<(), Box<dyn std::error::Error + Send + Sync>>,
88    ) {
89    }
90
91    /// Called when `Parser::parse` returns an error, before it is surfaced to the caller.
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// use argot_cmd::middleware::Middleware;
97    /// use argot_cmd::parser::ParseError;
98    ///
99    /// struct ErrorLogger;
100    ///
101    /// impl Middleware for ErrorLogger {
102    ///     fn on_parse_error(&self, err: &ParseError) {
103    ///         eprintln!("parse failed: {}", err);
104    ///     }
105    /// }
106    /// ```
107    fn on_parse_error(&self, _error: &ParseError) {}
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::model::Command;
114
115    /// A no-op middleware that uses all default implementations.
116    struct NoOpMiddleware;
117    impl Middleware for NoOpMiddleware {}
118
119    #[test]
120    fn test_default_before_dispatch_returns_ok() {
121        let cmd = Command::builder("test").build().unwrap();
122        let cmds = vec![cmd];
123        let parser = crate::parser::Parser::new(&cmds);
124        let parsed = parser.parse(&["test"]).unwrap();
125        let mw = NoOpMiddleware;
126        let result = mw.before_dispatch(&parsed);
127        assert!(result.is_ok());
128    }
129
130    #[test]
131    fn test_default_after_dispatch_is_noop() {
132        let cmd = Command::builder("test").build().unwrap();
133        let cmds = vec![cmd];
134        let parser = crate::parser::Parser::new(&cmds);
135        let parsed = parser.parse(&["test"]).unwrap();
136        let mw = NoOpMiddleware;
137        // Should not panic
138        let ok_result: Result<(), Box<dyn std::error::Error + Send + Sync>> = Ok(());
139        mw.after_dispatch(&parsed, &ok_result);
140        let err_result: Result<(), Box<dyn std::error::Error + Send + Sync>> =
141            Err("some error".into());
142        mw.after_dispatch(&parsed, &err_result);
143    }
144
145    #[test]
146    fn test_default_on_parse_error_is_noop() {
147        let mw = NoOpMiddleware;
148        let err = ParseError::NoCommand;
149        // Should not panic
150        mw.on_parse_error(&err);
151    }
152}