cufflink-cli 0.23.0

CLI for the Cufflink CRUD microservice platform — deploy, init, and manage services
use crate::bundle::{Bundle, File};
use std::path::Path;

fn made_ready(path: &Path) -> eyre::Result<()> {
    let unwritable = |why: String| eyre::eyre!("Cannot write the bundle {}: {why}", path.display());
    if path.is_dir() {
        return Err(unwritable("it is a directory".into()));
    }
    path.parent()
        .filter(|parent| !parent.as_os_str().is_empty())
        .map_or(Ok(()), std::fs::create_dir_all)
        .map_err(|e| unwritable(format!("its directory cannot be made ({e})")))
}

fn report(path: &Path, bundle: &Bundle, file: &File) -> Vec<String> {
    let or_none = |value: &Option<String>| value.clone().unwrap_or_else(|| "none".into());
    vec![
        format!("Wrote {} ({} bytes)", path.display(), file.size),
        format!(
            "  Service: {} (mode: {})",
            bundle.service,
            bundle.mode.as_str()
        ),
        format!("  Commit:  {}", or_none(&bundle.commit_hash)),
        format!("  Module:  {}", or_none(&bundle.wasm_hash)),
        format!("  SHA-256: {}", file.sha256),
    ]
}

const NO_COMMIT: &str = "warning: the bundle names no commit (this is not a git checkout, and \
                         no --commit-hash was given): a deploy of it reports none, and \
                         `cufflink rollback --commit` cannot find it";

pub fn bundle(path: &Path, commit_hash: Option<String>) -> eyre::Result<()> {
    made_ready(path)?;
    let bundle = super::deploy::bundle_of_sources(commit_hash)?;
    let file = bundle.write(path)?;
    report(path, &bundle, &file)
        .iter()
        .for_each(|line| println!("{line}"));
    if bundle.commit_hash.is_none() {
        eprintln!("{NO_COMMIT}");
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use cufflink_types::ServiceMode;

    fn bundle() -> Bundle {
        Bundle {
            service: "notes".into(),
            mode: ServiceMode::Wasm,
            manifest: serde_json::json!({"name": "notes"}),
            wasm_base64: Some("AGFzbQ==".into()),
            wasm_hash: Some("30602f71".into()),
            features: Vec::new(),
            commit_hash: Some("4f2a9c1".into()),
            cli_version: "0.21.0".into(),
        }
    }

    #[test]
    fn a_build_says_what_it_wrote_and_the_digest_that_names_it() {
        let file = File {
            size: 48213,
            sha256: "9e8d7c".into(),
        };
        assert_eq!(
            report(Path::new("out/notes.bundle"), &bundle(), &file),
            [
                "Wrote out/notes.bundle (48213 bytes)",
                "  Service: notes (mode: wasm)",
                "  Commit:  4f2a9c1",
                "  Module:  30602f71",
                "  SHA-256: 9e8d7c",
            ]
        );
        let bare = Bundle {
            mode: ServiceMode::Crud,
            wasm_base64: None,
            wasm_hash: None,
            commit_hash: None,
            ..bundle()
        };
        assert_eq!(
            report(Path::new("todos.bundle"), &bare, &file)[1..4],
            [
                "  Service: notes (mode: crud)",
                "  Commit:  none",
                "  Module:  none"
            ]
        );
    }

    #[test]
    fn where_a_bundle_goes_is_made_or_refused_before_anything_is_built() {
        let dir = tempfile::tempdir().expect("dir");
        let nested = dir.path().join("out").join("ci").join("notes.bundle");
        made_ready(&nested).expect("made");
        assert!(dir.path().join("out").join("ci").is_dir());
        assert!(!nested.exists(), "the directory, not the file");
        made_ready(Path::new("notes.bundle")).expect("the working directory is there");

        assert_eq!(
            made_ready(dir.path()).expect_err("a directory").to_string(),
            format!(
                "Cannot write the bundle {}: it is a directory",
                dir.path().display()
            )
        );
        let file = dir.path().join("file");
        std::fs::write(&file, b"x").expect("file");
        let under_a_file = file.join("notes.bundle");
        assert!(made_ready(&under_a_file)
            .expect_err("a file is no directory")
            .to_string()
            .starts_with(&format!(
                "Cannot write the bundle {}: its directory cannot be made (",
                under_a_file.display()
            )));
    }
}