repobin 0.1.0-alpha.1

Experimental repo-local Bazel command dispatcher; API and behavior may change without notice
Documentation
use std::ffi::OsString;
use std::path::{Path, PathBuf};

use crate::app::RepobinError;
use crate::bazel::BazelAdapter;
use crate::config::{RepoConfig, load_repo_config};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DispatchPlan {
    pub repo_root: PathBuf,
    pub tool_name: String,
    pub target: String,
    pub executable_path: PathBuf,
    pub original_cwd: PathBuf,
    pub forwarded_args: Vec<OsString>,
}

pub fn prepare_dispatch<B: BazelAdapter>(
    bazel: &B,
    cwd: &Path,
    tool_name: &str,
    forwarded_args: &[OsString],
) -> Result<DispatchPlan, RepobinError> {
    let repo_config = load_repo_config(cwd)?;
    prepare_dispatch_from_repo_config(bazel, repo_config, cwd, tool_name, forwarded_args)
}

pub fn prepare_dispatch_from_repo_config<B: BazelAdapter>(
    bazel: &B,
    repo_config: RepoConfig,
    cwd: &Path,
    tool_name: &str,
    forwarded_args: &[OsString],
) -> Result<DispatchPlan, RepobinError> {
    let tool =
        repo_config
            .config
            .tools
            .get(tool_name)
            .ok_or_else(|| RepobinError::ToolNotConfigured {
                tool: tool_name.to_string(),
                config_path: repo_config.config_path.clone(),
            })?;

    bazel.build(&repo_config.repo_root, &tool.target)?;
    let executable_path = bazel.resolve_executable(&repo_config.repo_root, &tool.target)?;

    Ok(DispatchPlan {
        repo_root: repo_config.repo_root,
        tool_name: tool_name.to_string(),
        target: tool.target.clone(),
        executable_path,
        original_cwd: cwd.to_path_buf(),
        forwarded_args: forwarded_args.to_vec(),
    })
}

#[cfg(test)]
mod tests {
    use std::cell::RefCell;
    use std::collections::BTreeMap;
    use std::path::{Path, PathBuf};

    use crate::bazel::BazelAdapter;
    use crate::config::{Config, RepoConfig, ToolConfig};

    use super::prepare_dispatch_from_repo_config;

    #[derive(Default)]
    struct FakeBazel {
        builds: RefCell<Vec<(PathBuf, String)>>,
        queries: RefCell<Vec<(PathBuf, String)>>,
        executable: PathBuf,
    }

    impl BazelAdapter for FakeBazel {
        fn build(&self, repo_root: &Path, target: &str) -> Result<(), crate::app::RepobinError> {
            self.builds
                .borrow_mut()
                .push((repo_root.to_path_buf(), target.to_string()));
            Ok(())
        }

        fn resolve_executable(
            &self,
            repo_root: &Path,
            target: &str,
        ) -> Result<PathBuf, crate::app::RepobinError> {
            self.queries
                .borrow_mut()
                .push((repo_root.to_path_buf(), target.to_string()));
            Ok(self.executable.clone())
        }
    }

    fn sample_repo_config() -> RepoConfig {
        RepoConfig {
            repo_root: PathBuf::from("/repo"),
            config_path: PathBuf::from("/repo/REPOBIN.toml"),
            config: Config {
                version: 1,
                tools: BTreeMap::from([(
                    "boss".to_string(),
                    ToolConfig {
                        target: "//tools/boss/cli:boss".to_string(),
                    },
                )]),
            },
        }
    }

    #[test]
    fn prepare_dispatch_builds_and_resolves_target() {
        let bazel = FakeBazel {
            executable: PathBuf::from("/repo/bazel-bin/tools/boss/cli/boss"),
            ..FakeBazel::default()
        };

        let plan = prepare_dispatch_from_repo_config(
            &bazel,
            sample_repo_config(),
            Path::new("/repo/subdir"),
            "boss",
            &[
                std::ffi::OsString::from("task"),
                std::ffi::OsString::from("list"),
            ],
        )
        .expect("dispatch plan");

        assert_eq!(plan.tool_name, "boss");
        assert_eq!(plan.target, "//tools/boss/cli:boss");
        assert_eq!(plan.original_cwd, Path::new("/repo/subdir"));
        assert_eq!(
            bazel.builds.borrow().as_slice(),
            &[(PathBuf::from("/repo"), "//tools/boss/cli:boss".to_string())]
        );
        assert_eq!(
            bazel.queries.borrow().as_slice(),
            &[(PathBuf::from("/repo"), "//tools/boss/cli:boss".to_string())]
        );
    }
}