use std::{ collections::BTreeMap, ops::{Deref, DerefMut}, }; use async_trait::async_trait; pub struct CuddleCI { pr_action: Vec>, main_action: Vec>, release_action: Vec>, } #[derive(Default, Debug)] pub struct Context { store: BTreeMap, } impl Deref for Context { type Target = BTreeMap; fn deref(&self) -> &Self::Target { &self.store } } impl DerefMut for Context { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.store } } impl CuddleCI { pub fn new( pr: Box, main: Box, release: Box, ) -> Self { Self { pr_action: vec![pr], main_action: vec![main], release_action: vec![release], } } pub fn with_pull_request(&mut self, pr: T) -> &mut Self where T: PullRequestAction + ToOwned + Send + Sync + 'static, T: ToOwned, { self.pr_action.push(Box::new(pr.to_owned())); self } pub fn with_main(&mut self, main: T) -> &mut Self where T: MainAction + Send + Sync + 'static, T: ToOwned, { self.main_action.push(Box::new(main)); self } pub fn with_release(&mut self, release: Box) -> &mut Self { self.release_action.push(release); self } pub async fn execute( &mut self, args: impl IntoIterator>, ) -> eyre::Result<()> { let matches = clap::Command::new("cuddle-ci") .about("is a wrapper around common CI actions") .subcommand(clap::Command::new("pr")) .subcommand(clap::Command::new("main")) .subcommand(clap::Command::new("release")) .subcommand_required(true) .try_get_matches_from(args.into_iter().map(|a| a.into()).collect::>())?; let mut context = Context::default(); match matches.subcommand() { Some((name, args)) => match (name, args) { ("pr", _args) => { eprintln!("starting pr validate"); for pr_action in self.pr_action.iter() { pr_action.execute_pull_request(&mut context).await?; } eprintln!("finished pr validate"); } ("main", _args) => { eprintln!("starting main validate"); for main_action in self.main_action.iter() { main_action.execute_main(&mut context).await?; } eprintln!("finished main validate"); } ("release", _args) => { eprintln!("starting release validate"); for release_action in self.release_action.iter() { release_action.execute_release(&mut context).await?; } eprintln!("finished release validate"); } (command_name, _) => { eyre::bail!("command is not recognized: {}", command_name) } }, None => eyre::bail!("command required a subcommand [pr, main, release] etc."), } Ok(()) } } impl Default for CuddleCI { fn default() -> Self { Self::new( Box::new(DefaultPullRequestAction {}), Box::new(DefaultMainAction {}), Box::new(DefaultReleaseAction {}), ) } } #[async_trait] pub trait PullRequestAction { async fn execute_pull_request(&self, _ctx: &mut Context) -> eyre::Result<()> { eprintln!("validate pull request: noop"); Ok(()) } } pub struct DefaultPullRequestAction {} #[async_trait] impl PullRequestAction for DefaultPullRequestAction {} #[async_trait] pub trait MainAction { async fn execute_main(&self, _ctx: &mut Context) -> eyre::Result<()>; } pub struct DefaultMainAction {} #[async_trait] impl MainAction for DefaultMainAction { async fn execute_main(&self, _ctx: &mut Context) -> eyre::Result<()> { Ok(()) } } #[async_trait] pub trait ReleaseAction { async fn execute_release(&self, _ctx: &mut Context) -> eyre::Result<()> { eprintln!("validate release: noop"); Ok(()) } } pub struct DefaultReleaseAction {} #[async_trait] impl ReleaseAction for DefaultReleaseAction {} #[cfg(test)] mod test { use super::*; #[tokio::test] async fn test_can_call_default() -> eyre::Result<()> { CuddleCI::default().execute(["cuddle-ci", "pr"]).await?; Ok(()) } #[tokio::test] async fn test_fails_on_no_command() -> eyre::Result<()> { let res = CuddleCI::default().execute(["cuddle-ci"]).await; assert!(res.is_err()); Ok(()) } #[tokio::test] async fn test_fails_on_wrong_command() -> eyre::Result<()> { let res = CuddleCI::default() .execute(["cuddle-ci", "something"]) .await; assert!(res.is_err()); Ok(()) } }