holochain_scaffolding_cli 0.601.1

CLI to easily generate and modify holochain apps
Documentation
use std::{
    path::{Path, PathBuf},
    process::Stdio,
    str::from_utf8,
};

use crate::file_tree::{file_content, insert_file, FileTree};
use cargo_metadata::{Metadata, MetadataCommand};

use crate::error::{ScaffoldError, ScaffoldResult};

pub fn workspace_cargo_toml() -> String {
    r#"[workspace]
members = [
  "dnas/*/zomes/coordinator/*",
  "dnas/*/zomes/integrity/*",
]
resolver = "2"

[profile.dev]
opt-level = "z"

[profile.release]
opt-level = "z"
"#
    .to_string()
}

pub fn add_workspace_external_dependency(
    app_file_tree: FileTree,
    crate_name: &str,
    crate_version: &str,
) -> ScaffoldResult<FileTree> {
    add_workspace_dependency(
        app_file_tree,
        crate_name,
        &toml::Value::String(crate_version.to_owned()),
    )
}

pub fn add_workspace_path_dependency(
    app_file_tree: FileTree,
    crate_name: &str,
    path_from_workspace_root: &Path,
) -> ScaffoldResult<FileTree> {
    let mut table = toml::map::Map::new();
    table.insert(
        "path".to_string(),
        toml::Value::String(
            path_from_workspace_root
                .as_os_str()
                .to_os_string()
                .to_str()
                .unwrap()
                .to_string(),
        ),
    );
    add_workspace_dependency(app_file_tree, crate_name, &toml::Value::Table(table))
}

pub fn add_workspace_dependency(
    mut app_file_tree: FileTree,
    crate_name: &str,
    crate_location: &toml::Value,
) -> ScaffoldResult<FileTree> {
    let mut workspace_cargo_toml = get_workspace_cargo_toml(&app_file_tree)?;
    let workspace_table = workspace_cargo_toml
        .as_table_mut()
        .ok_or(ScaffoldError::MalformedFile(
            workspace_cargo_toml_path(&app_file_tree),
            String::from("file does not conform to toml"),
        ))?
        .get_mut("workspace")
        .ok_or(ScaffoldError::MalformedFile(
            workspace_cargo_toml_path(&app_file_tree),
            String::from("no workspace table found in workspace root"),
        ))?
        .as_table_mut()
        .ok_or(ScaffoldError::MalformedFile(
            workspace_cargo_toml_path(&app_file_tree),
            String::from("workspace key is not a table"),
        ))?;

    let mut dependencies = match workspace_table.get("dependencies") {
        Some(d) => d
            .as_table()
            .ok_or(ScaffoldError::MalformedFile(
                workspace_cargo_toml_path(&app_file_tree),
                String::from("workspace.dependencies is not a table"),
            ))?
            .clone(),
        None => toml::map::Map::new(),
    };

    dependencies.insert(crate_name.to_owned(), crate_location.clone());
    workspace_table.insert(
        String::from("dependencies"),
        toml::Value::Table(dependencies),
    );

    let path = workspace_cargo_toml_path(&app_file_tree);

    let cargo_toml_str = toml::to_string(&workspace_cargo_toml)?;

    insert_file(&mut app_file_tree, &path, &cargo_toml_str)?;

    Ok(app_file_tree)
}

pub fn get_workspace_packages_locations(
    app_file_tree: &FileTree,
) -> ScaffoldResult<Option<Vec<PathBuf>>> {
    let current_dir = std::env::current_dir()?;

    let path = current_dir
        .join(workspace_cargo_toml_path(app_file_tree))
        .canonicalize()?;
    let command_result = MetadataCommand::new().manifest_path(path).exec();

    match command_result {
        Ok(metadata) => {
            let packages_paths: Vec<PathBuf> = metadata
                .workspace_packages()
                .into_iter()
                .map(|p| {
                    PathBuf::from(p.manifest_path.as_std_path())
                        .iter()
                        .skip(current_dir.components().count())
                        .collect()
                })
                .collect();
            Ok(Some(packages_paths))
        }
        Err(_) => Ok(None),
    }
}

pub fn workspace_package_path(
    app_file_tree: &FileTree,
    crate_name: &str,
) -> ScaffoldResult<Option<PathBuf>> {
    let current_dir = std::env::current_dir()?;

    let path = current_dir
        .join(workspace_cargo_toml_path(app_file_tree))
        .canonicalize()?;
    let metadata = MetadataCommand::new().manifest_path(path).exec()?;

    let package_path: Option<PathBuf> = metadata
        .workspace_packages()
        .into_iter()
        .find(|p| p.name.eq(crate_name))
        .map(|p| {
            PathBuf::from(p.manifest_path.as_std_path())
                .iter()
                .skip(current_dir.components().count())
                .collect()
        });
    Ok(package_path)
}

pub fn get_workspace_members(app_file_tree: &FileTree) -> ScaffoldResult<Vec<String>> {
    let cargo_toml = get_workspace_cargo_toml(app_file_tree)?;

    let members: Vec<String> = cargo_toml
        .as_table()
        .ok_or(ScaffoldError::MalformedFile(
            workspace_cargo_toml_path(app_file_tree),
            String::from("file does not conform to toml"),
        ))?
        .get("workspace")
        .ok_or(ScaffoldError::MalformedFile(
            workspace_cargo_toml_path(app_file_tree),
            String::from("should have a workspace table"),
        ))?
        .as_table()
        .ok_or(ScaffoldError::MalformedFile(
            workspace_cargo_toml_path(app_file_tree),
            String::from("should have a workspace table"),
        ))?
        .get("members")
        .ok_or(ScaffoldError::MalformedFile(
            workspace_cargo_toml_path(app_file_tree),
            String::from("should have a members field in the workspace table"),
        ))?
        .as_array()
        .ok_or(ScaffoldError::MalformedFile(
            workspace_cargo_toml_path(app_file_tree),
            String::from("the members field in the workspace table should be an array"),
        ))?
        .iter()
        .filter_map(|s| s.as_str())
        .map(|s| s.to_string())
        .collect();

    Ok(members)
}

pub fn get_workspace_cargo_toml(app_file_tree: &FileTree) -> ScaffoldResult<toml::Value> {
    let path = workspace_cargo_toml_path(app_file_tree);

    let cargo_toml_str = file_content(app_file_tree, &path)?;
    let v = toml::from_str(cargo_toml_str.as_str())?;

    Ok(v)
}

pub fn exec_metadata(app_file_tree: &FileTree) -> Result<Metadata, cargo_metadata::Error> {
    let current_dir = std::env::current_dir()?;
    let path = current_dir
        .join(workspace_cargo_toml_path(app_file_tree))
        .canonicalize()?;
    let output = MetadataCommand::new()
        .manifest_path(path)
        .cargo_command()
        .stderr(Stdio::inherit())
        .output()?;
    if !output.status.success() {
        return Err(cargo_metadata::Error::CargoMetadata {
            stderr: String::from_utf8(output.stderr)?,
        });
    }
    let stdout = from_utf8(&output.stdout)?
        .lines()
        .find(|line| line.starts_with('{'))
        .ok_or(cargo_metadata::Error::NoJson)?;
    MetadataCommand::parse(stdout)
}

#[inline]
fn workspace_cargo_toml_path(_app_file_tree: &FileTree) -> PathBuf {
    PathBuf::new().join("Cargo.toml")
}

#[cfg(test)]
mod tests {
    use super::*;
    use build_fs_tree::{dir, file};

    #[test]
    fn get_workspace_cargo_toml_from_app_file_tree() {
        let workspace_cargo_toml_string = workspace_cargo_toml();
        // Create a basic app file tree with workspace Cargo.toml
        let app_file_tree = dir! {
            "Cargo.toml" => file!(workspace_cargo_toml_string.clone())
        };
        let cargo_toml = get_workspace_cargo_toml(&app_file_tree).unwrap();
        assert_eq!(
            cargo_toml,
            toml::from_str(&workspace_cargo_toml_string).unwrap()
        );
    }

    #[test]
    fn workspace_cargo_toml_contains_integrity_and_coordinator_zomes_as_members() {
        let cargo_toml = toml::from_str::<toml::Value>(&workspace_cargo_toml()).unwrap();

        let members = cargo_toml
            .get("workspace")
            .unwrap()
            .get("members")
            .unwrap()
            .as_array()
            .unwrap();

        assert!(members.contains(&toml::Value::String("dnas/*/zomes/integrity/*".to_string())));
        assert!(members.contains(&toml::Value::String(
            "dnas/*/zomes/coordinator/*".to_string()
        )));
    }

    #[test]
    fn add_dependency_to_workspace() {
        let app_file_tree = dir! {
            "Cargo.toml" => file!(workspace_cargo_toml())
        };

        let crate_name = "test-crate";
        let crate_version = toml::Value::String("1.0.0".to_string());
        let updated_tree =
            add_workspace_dependency(app_file_tree, crate_name, &crate_version).unwrap();

        // Verify the dependency was added
        let cargo_toml = get_workspace_cargo_toml(&updated_tree).unwrap();
        let dependencies = cargo_toml
            .get("workspace")
            .unwrap()
            .get("dependencies")
            .unwrap()
            .as_table()
            .unwrap();

        assert_eq!(dependencies.get(crate_name).unwrap(), &crate_version);
    }
}