geam-cli 0.2.3

Standalone command implementation for Geam
Documentation
use crate::error::CliError;
use crate::process::run_checked_with_progress;
use crate::progress::Progress;
use camino::{Utf8Path, Utf8PathBuf};
use cargo_metadata::{Metadata, MetadataCommand};
use std::process::{Command, Stdio};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum CargoMetadataMode {
    Workspace,
    Resolve,
    Locked,
}

pub(super) trait CargoMetadataLoader {
    fn load(
        &self,
        current_directory: &Utf8Path,
        manifest: &Utf8Path,
        mode: CargoMetadataMode,
        progress: &mut Progress<'_>,
    ) -> Result<Metadata, CliError>;
}

pub(super) struct SystemCargoMetadata;

impl CargoMetadataLoader for SystemCargoMetadata {
    fn load(
        &self,
        current_directory: &Utf8Path,
        manifest: &Utf8Path,
        mode: CargoMetadataMode,
        progress: &mut Progress<'_>,
    ) -> Result<Metadata, CliError> {
        let mut command = Command::new("cargo");
        command
            .arg("metadata")
            .arg("--format-version")
            .arg("1")
            .arg("--manifest-path")
            .arg(manifest)
            .current_dir(current_directory);
        match mode {
            CargoMetadataMode::Workspace => {
                command.arg("--no-deps");
            }
            CargoMetadataMode::Resolve => {}
            CargoMetadataMode::Locked => {
                command.arg("--locked");
            }
        }
        let output = run_checked_with_progress(&mut command, progress, Stdio::piped())?;
        parse_metadata_output(manifest, &output.stdout)
    }
}

pub(super) fn parse_metadata_output(
    manifest: &Utf8Path,
    output: &[u8],
) -> Result<Metadata, CliError> {
    MetadataCommand::parse(String::from_utf8_lossy(output)).map_err(|error| {
        CliError::InvalidCargoMetadata {
            manifest: manifest.to_path_buf(),
            reason: error.to_string(),
        }
    })
}

pub(super) fn canonical_manifest(path: Utf8PathBuf) -> Result<Utf8PathBuf, CliError> {
    std::fs::canonicalize(&path)
        .map_err(|error| CliError::FileRead {
            path: path.clone(),
            error,
        })
        .and_then(crate::project::into_utf8_path)
}

#[cfg(test)]
mod tests {
    use super::{
        CargoMetadataLoader, CargoMetadataMode, SystemCargoMetadata, canonical_manifest,
        parse_metadata_output,
    };
    use crate::error::CliError;
    use crate::progress::Progress;
    use camino::Utf8PathBuf;

    #[test]
    fn rejects_invalid_cargo_metadata_with_manifest_context() {
        let manifest = Utf8PathBuf::from("/workspace/Cargo.toml");
        let expected = cargo_metadata::MetadataCommand::parse("not JSON")
            .expect_err("invalid Cargo output fixture should be rejected")
            .to_string();
        let error = parse_metadata_output(&manifest, b"not JSON")
            .expect_err("invalid Cargo metadata should fail");

        assert!(matches!(
            error,
            CliError::InvalidCargoMetadata { manifest: path, reason }
                if path == manifest && reason == expected
        ));
    }

    #[test]
    fn preserves_missing_manifest_process_failure() {
        let root = tempfile::tempdir().expect("temporary directory should be created");
        let root = Utf8PathBuf::from_path_buf(root.path().to_path_buf())
            .expect("temporary path should be valid UTF-8");
        let manifest = root.join("missing/Cargo.toml");
        let error = SystemCargoMetadata
            .load(
                &root,
                &manifest,
                CargoMetadataMode::Locked,
                &mut Progress::Hidden,
            )
            .expect_err("missing manifest should fail");
        let expected_command =
            format!("cargo metadata --format-version 1 --manifest-path {manifest} --locked");

        assert!(matches!(
            error,
            CliError::ProcessFailure { command, status: Some(101), stderr }
                if command == expected_command && stderr.contains("manifest path")
        ));
    }

    #[test]
    fn preserves_canonical_manifest_filesystem_failures() {
        let directory = tempfile::tempdir().expect("temporary directory should be created");
        let path = Utf8PathBuf::from_path_buf(directory.path().join("Cargo.toml"))
            .expect("temporary path should be UTF-8");
        let error =
            canonical_manifest(path.clone()).expect_err("an absent canonical manifest should fail");
        assert!(matches!(error, CliError::FileRead { path: actual, error }
            if actual == path && error.kind() == std::io::ErrorKind::NotFound));
    }
}