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}