1use clap::ArgMatches;
32use serde::Serialize;
33
34#[derive(Debug, Clone, Default)]
44pub struct CommandContext {
45 pub command_path: Vec<String>,
47}
48
49#[derive(Debug)]
53pub enum Output<T: Serialize> {
54 Render(T),
56 Silent,
58 Binary {
60 data: Vec<u8>,
62 filename: String,
64 },
65}
66
67impl<T: Serialize> Output<T> {
68 pub fn is_render(&self) -> bool {
70 matches!(self, Output::Render(_))
71 }
72
73 pub fn is_silent(&self) -> bool {
75 matches!(self, Output::Silent)
76 }
77
78 pub fn is_binary(&self) -> bool {
80 matches!(self, Output::Binary { .. })
81 }
82}
83
84pub type HandlerResult<T> = Result<Output<T>, anyhow::Error>;
88
89#[derive(Debug)]
94pub enum RunResult {
95 Handled(String),
97 Binary(Vec<u8>, String),
99 Silent,
101 NoMatch(ArgMatches),
103}
104
105impl RunResult {
106 pub fn is_handled(&self) -> bool {
108 matches!(self, RunResult::Handled(_))
109 }
110
111 pub fn is_binary(&self) -> bool {
113 matches!(self, RunResult::Binary(_, _))
114 }
115
116 pub fn is_silent(&self) -> bool {
118 matches!(self, RunResult::Silent)
119 }
120
121 pub fn output(&self) -> Option<&str> {
123 match self {
124 RunResult::Handled(s) => Some(s),
125 _ => None,
126 }
127 }
128
129 pub fn binary(&self) -> Option<(&[u8], &str)> {
131 match self {
132 RunResult::Binary(bytes, filename) => Some((bytes, filename)),
133 _ => None,
134 }
135 }
136
137 pub fn matches(&self) -> Option<&ArgMatches> {
139 match self {
140 RunResult::NoMatch(m) => Some(m),
141 _ => None,
142 }
143 }
144}
145
146pub trait Handler: Send + Sync {
150 type Output: Serialize;
152
153 fn handle(&self, matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<Self::Output>;
155}
156
157pub struct FnHandler<F, T>
159where
160 F: Fn(&ArgMatches, &CommandContext) -> HandlerResult<T> + Send + Sync,
161 T: Serialize + Send + Sync,
162{
163 f: F,
164 _phantom: std::marker::PhantomData<fn() -> T>,
165}
166
167impl<F, T> FnHandler<F, T>
168where
169 F: Fn(&ArgMatches, &CommandContext) -> HandlerResult<T> + Send + Sync,
170 T: Serialize + Send + Sync,
171{
172 pub fn new(f: F) -> Self {
174 Self {
175 f,
176 _phantom: std::marker::PhantomData,
177 }
178 }
179}
180
181impl<F, T> Handler for FnHandler<F, T>
182where
183 F: Fn(&ArgMatches, &CommandContext) -> HandlerResult<T> + Send + Sync,
184 T: Serialize + Send + Sync,
185{
186 type Output = T;
187
188 fn handle(&self, matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<T> {
189 (self.f)(matches, ctx)
190 }
191}
192
193pub trait LocalHandler {
200 type Output: Serialize;
202
203 fn handle(&mut self, matches: &ArgMatches, ctx: &CommandContext)
205 -> HandlerResult<Self::Output>;
206}
207
208pub struct LocalFnHandler<F, T>
210where
211 F: FnMut(&ArgMatches, &CommandContext) -> HandlerResult<T>,
212 T: Serialize,
213{
214 f: F,
215 _phantom: std::marker::PhantomData<fn() -> T>,
216}
217
218impl<F, T> LocalFnHandler<F, T>
219where
220 F: FnMut(&ArgMatches, &CommandContext) -> HandlerResult<T>,
221 T: Serialize,
222{
223 pub fn new(f: F) -> Self {
225 Self {
226 f,
227 _phantom: std::marker::PhantomData,
228 }
229 }
230}
231
232impl<F, T> LocalHandler for LocalFnHandler<F, T>
233where
234 F: FnMut(&ArgMatches, &CommandContext) -> HandlerResult<T>,
235 T: Serialize,
236{
237 type Output = T;
238
239 fn handle(&mut self, matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<T> {
240 (self.f)(matches, ctx)
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use serde_json::json;
248
249 #[test]
250 fn test_command_context_creation() {
251 let ctx = CommandContext {
252 command_path: vec!["config".into(), "get".into()],
253 };
254 assert_eq!(ctx.command_path, vec!["config", "get"]);
255 }
256
257 #[test]
258 fn test_command_context_default() {
259 let ctx = CommandContext::default();
260 assert!(ctx.command_path.is_empty());
261 }
262
263 #[test]
264 fn test_output_render() {
265 let output: Output<String> = Output::Render("success".into());
266 assert!(output.is_render());
267 assert!(!output.is_silent());
268 assert!(!output.is_binary());
269 }
270
271 #[test]
272 fn test_output_silent() {
273 let output: Output<String> = Output::Silent;
274 assert!(!output.is_render());
275 assert!(output.is_silent());
276 assert!(!output.is_binary());
277 }
278
279 #[test]
280 fn test_output_binary() {
281 let output: Output<String> = Output::Binary {
282 data: vec![0x25, 0x50, 0x44, 0x46],
283 filename: "report.pdf".into(),
284 };
285 assert!(!output.is_render());
286 assert!(!output.is_silent());
287 assert!(output.is_binary());
288 }
289
290 #[test]
291 fn test_run_result_handled() {
292 let result = RunResult::Handled("output".into());
293 assert!(result.is_handled());
294 assert!(!result.is_binary());
295 assert!(!result.is_silent());
296 assert_eq!(result.output(), Some("output"));
297 assert!(result.matches().is_none());
298 }
299
300 #[test]
301 fn test_run_result_silent() {
302 let result = RunResult::Silent;
303 assert!(!result.is_handled());
304 assert!(!result.is_binary());
305 assert!(result.is_silent());
306 }
307
308 #[test]
309 fn test_run_result_binary() {
310 let bytes = vec![0x25, 0x50, 0x44, 0x46];
311 let result = RunResult::Binary(bytes.clone(), "report.pdf".into());
312 assert!(!result.is_handled());
313 assert!(result.is_binary());
314 assert!(!result.is_silent());
315
316 let (data, filename) = result.binary().unwrap();
317 assert_eq!(data, &bytes);
318 assert_eq!(filename, "report.pdf");
319 }
320
321 #[test]
322 fn test_run_result_no_match() {
323 let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
324 let result = RunResult::NoMatch(matches);
325 assert!(!result.is_handled());
326 assert!(!result.is_binary());
327 assert!(result.matches().is_some());
328 }
329
330 #[test]
331 fn test_fn_handler() {
332 let handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
333 Ok(Output::Render(json!({"status": "ok"})))
334 });
335
336 let ctx = CommandContext::default();
337 let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
338
339 let result = handler.handle(&matches, &ctx);
340 assert!(result.is_ok());
341 }
342
343 #[test]
344 fn test_local_fn_handler_mutation() {
345 let mut counter = 0u32;
346
347 let mut handler = LocalFnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
348 counter += 1;
349 Ok(Output::Render(counter))
350 });
351
352 let ctx = CommandContext::default();
353 let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
354
355 let _ = handler.handle(&matches, &ctx);
356 let _ = handler.handle(&matches, &ctx);
357 let result = handler.handle(&matches, &ctx);
358
359 assert!(result.is_ok());
360 if let Ok(Output::Render(count)) = result {
361 assert_eq!(count, 3);
362 }
363 }
364}