clap_noun_verb/
router.rs

1//! Command routing logic for noun-verb CLI
2
3use crate::error::{NounVerbError, Result};
4use crate::noun::NounCommand;
5use crate::verb::{VerbArgs, VerbContext};
6use clap::{ArgMatches, Command};
7use std::collections::HashMap;
8
9/// Router for dispatching noun-verb commands
10pub struct CommandRouter {
11    nouns: HashMap<String, Box<dyn NounCommand>>,
12}
13
14impl CommandRouter {
15    /// Create a new command router
16    pub fn new() -> Self {
17        Self { nouns: HashMap::new() }
18    }
19
20    /// Register a noun command
21    pub fn register_noun(&mut self, noun: Box<dyn NounCommand>) {
22        self.nouns.insert(noun.name().to_string(), noun);
23    }
24
25    /// Route a command based on clap matches
26    pub fn route(&self, matches: &ArgMatches) -> Result<()> {
27        // Get the top-level subcommand (noun)
28        let (noun_name, noun_matches) = matches
29            .subcommand()
30            .ok_or_else(|| NounVerbError::invalid_structure("No subcommand found"))?;
31
32        // Find the noun command
33        let noun =
34            self.nouns.get(noun_name).ok_or_else(|| NounVerbError::command_not_found(noun_name))?;
35
36        // Route the command recursively with root matches for global args
37        self.route_recursive(noun.as_ref(), noun_name, noun_matches, matches)
38    }
39
40    /// Recursively route commands through nested noun-verb structure
41    #[allow(clippy::only_used_in_recursion)]
42    fn route_recursive(
43        &self,
44        noun: &dyn NounCommand,
45        noun_name: &str,
46        matches: &ArgMatches,
47        root_matches: &ArgMatches,
48    ) -> Result<()> {
49        // Check if there's a subcommand (either verb or sub-noun)
50        if let Some((sub_name, sub_matches)) = matches.subcommand() {
51            // First check if it's a verb
52            if let Some(verb) = noun.verbs().iter().find(|v| v.name() == sub_name) {
53                // Execute the verb with root matches for global args access
54                let context = VerbContext::new(sub_name).with_noun(noun_name);
55                let args = VerbArgs::new(sub_matches.clone())
56                    .with_parent(root_matches.clone())
57                    .with_context(context);
58
59                verb.run(&args)
60            } else if let Some(sub_noun) = noun.sub_nouns().iter().find(|n| n.name() == sub_name) {
61                // Recursively route to sub-noun, passing root matches for global args
62                self.route_recursive(sub_noun.as_ref(), sub_name, sub_matches, root_matches)
63            } else {
64                // Neither verb nor sub-noun found
65                Err(NounVerbError::verb_not_found(noun_name, sub_name))
66            }
67        } else {
68            // No subcommand, try direct noun execution
69            let context = VerbContext::new("").with_noun(noun_name);
70            let args = VerbArgs::new(matches.clone()).with_context(context);
71
72            noun.handle_direct(&args)
73        }
74    }
75
76    /// Build the complete clap command structure
77    pub fn build_command(&self, app_name: &'static str, about: &'static str) -> Command {
78        let mut cmd = Command::new(app_name).about(about);
79
80        for noun in self.nouns.values() {
81            cmd = cmd.subcommand(noun.build_command());
82        }
83
84        cmd
85    }
86
87    /// Get all registered noun names
88    pub fn noun_names(&self) -> Vec<&str> {
89        self.nouns.keys().map(|s| s.as_str()).collect()
90    }
91
92    /// Get verbs for a specific noun
93    pub fn get_verbs(&self, noun_name: &str) -> Result<Vec<String>> {
94        let noun =
95            self.nouns.get(noun_name).ok_or_else(|| NounVerbError::command_not_found(noun_name))?;
96
97        Ok(noun.verbs().iter().map(|v| v.name().to_string()).collect())
98    }
99}
100
101impl Default for CommandRouter {
102    fn default() -> Self {
103        Self::new()
104    }
105}