diff --git a/crates/cuddle-actions/Cargo.toml b/crates/cuddle-actions/Cargo.toml index 2f51434..f82bdad 100644 --- a/crates/cuddle-actions/Cargo.toml +++ b/crates/cuddle-actions/Cargo.toml @@ -5,7 +5,7 @@ version.workspace = true [dependencies] anyhow.workspace = true -clap.workspace = true +clap = { workspace = true, features = ["string"] } serde.workspace = true serde_json.workspace = true diff --git a/crates/cuddle-actions/src/lib.rs b/crates/cuddle-actions/src/lib.rs index 4ebb3e6..5e1b77e 100644 --- a/crates/cuddle-actions/src/lib.rs +++ b/crates/cuddle-actions/src/lib.rs @@ -6,7 +6,7 @@ use serde::Serialize; // Fix design make it so that it works like axum! -type ActionFn = dyn Fn() + 'static; +type ActionFn = dyn Fn() -> anyhow::Result<()> + 'static; struct Action { name: String, @@ -38,7 +38,7 @@ impl CuddleActions { options: &AddActionOptions, ) -> &mut Self where - F: Fn() + 'static, + F: Fn() -> anyhow::Result<()> + 'static, { self.actions.insert( name.into(), @@ -53,41 +53,68 @@ impl CuddleActions { } pub fn execute(&mut self) -> anyhow::Result<()> { - let output = self.execute_from(std::env::args())?; - - // Write all stdout to buffer - std::io::stdout().write_all(output.as_bytes())?; - std::io::stdout().write_all("\n".as_bytes())?; + self.execute_from(std::env::args())?; Ok(()) } - pub fn execute_from(&mut self, items: I) -> anyhow::Result + pub fn execute_from(&mut self, items: I) -> anyhow::Result<()> where I: IntoIterator, T: Into + Clone, { + let mut do_cmd = clap::Command::new("do").subcommand_required(true); + + for action in self.actions.values() { + let mut do_action_cmd = clap::Command::new(action.name.clone()); + if let Some(description) = &action.description { + do_action_cmd = do_action_cmd.about(description.clone()); + } + + do_cmd = do_cmd.subcommand(do_action_cmd); + } + let root = clap::Command::new("cuddle-action") .subcommand_required(true) - .subcommand(clap::Command::new("schema")); + .subcommand(clap::Command::new("schema")) + .subcommand(do_cmd); - let matches = root.get_matches_from(items); + let matches = root.try_get_matches_from(items)?; match matches.subcommand().expect("subcommand to be required") { ("schema", _args) => { - let schema = self - .actions - .values() - .map(|a| ActionSchema { - name: &a.name, - description: a.description.as_ref().map(|d| d.as_str()), - }) - .collect::>(); + let output = self.get_pretty_actions()?; - let output = serde_json::to_string_pretty(&schema)?; + // Write all stdout to buffer + std::io::stdout().write_all(output.as_bytes())?; + std::io::stdout().write_all("\n".as_bytes())?; - Ok(output) + Ok(()) } - _ => Ok("".into()), + ("do", args) => { + let (command_name, _args) = args.subcommand().unwrap(); + match self.actions.get_mut(command_name) { + Some(action) => (*action.f)(), + None => { + anyhow::bail!("command not found: {}", command_name); + } + } + } + _ => anyhow::bail!("no command found"), } } + + pub fn get_pretty_actions(&self) -> anyhow::Result { + let schema = self + .actions + .values() + .map(|a| ActionSchema { + name: &a.name, + description: a.description.as_deref(), + }) + .collect::>(); + + let output = serde_json::to_string_pretty(&schema)?; + + Ok(output) + } } diff --git a/crates/cuddle-actions/tests/mod.rs b/crates/cuddle-actions/tests/mod.rs index 7940278..155ba1a 100644 --- a/crates/cuddle-actions/tests/mod.rs +++ b/crates/cuddle-actions/tests/mod.rs @@ -3,8 +3,7 @@ use pretty_assertions::assert_eq; #[test] fn test_can_schema_no_actions() -> anyhow::Result<()> { - let output = - cuddle_actions::CuddleActions::default().execute_from(vec!["cuddle-actions", "schema"])?; + let output = cuddle_actions::CuddleActions::default().get_pretty_actions()?; assert_eq!("[]", &output); @@ -14,8 +13,8 @@ fn test_can_schema_no_actions() -> anyhow::Result<()> { #[test] fn test_can_schema_simple_action() -> anyhow::Result<()> { let output = cuddle_actions::CuddleActions::default() - .add_action("something", || {}, &AddActionOptions::default()) - .execute_from(vec!["cuddle-actions", "schema"])?; + .add_action("something", || Ok(()), &AddActionOptions::default()) + .get_pretty_actions()?; assert_eq!( r#"[ @@ -29,3 +28,40 @@ fn test_can_schema_simple_action() -> anyhow::Result<()> { Ok(()) } + +#[test] +fn test_can_call_simple_action() -> anyhow::Result<()> { + cuddle_actions::CuddleActions::default() + .add_action("something", || Ok(()), &AddActionOptions::default()) + .execute_from(vec!["cuddle-actions", "do", "something"])?; + + Ok(()) +} + +#[test] +fn test_can_fail_on_unknown_command() -> anyhow::Result<()> { + let res = cuddle_actions::CuddleActions::default().execute_from(vec![ + "cuddle-actions", + "do", + "something", + ]); + + assert!(res.is_err()); + + Ok(()) +} + +#[test] +fn test_can_cmd_can_fail() -> anyhow::Result<()> { + let res = cuddle_actions::CuddleActions::default() + .add_action( + "something", + || anyhow::bail!("failed to run cmd"), + &AddActionOptions::default(), + ) + .execute_from(vec!["cuddle-actions", "do", "something"]); + + assert!(res.is_err()); + + Ok(()) +}