holochain_scaffolding_cli 0.0.2

CLI to easily generate and edit holochain apps
Documentation
use build_fs_tree::serde::Serialize;
use dialoguer::{theme::ColorfulTheme, Select};
use handlebars::Handlebars;
use include_dir::{include_dir, Dir};
use std::{ffi::OsString, path::PathBuf, str::FromStr};

use crate::{
    definitions::EntryDefinition,
    error::{ScaffoldError, ScaffoldResult},
    file_tree::{dir_to_file_tree, FileTree},
    generators::index::IndexType,
    templates::{
        register_all_partials_in_dir, register_case_helpers, register_concat_helper,
        render_template_file_tree_and_merge_with_existing,
    },
    versions::holochain_client_version,
};

static LIT_TEMPLATES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates/lit");
static SVELTE_TEMPLATES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates/svelte");
static VUE_TEMPLATES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates/vue");
static VANILLA_TEMPLATES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates/vanilla");

#[derive(Debug, Clone)]
pub enum UiFramework {
    Vanilla,
    Lit,
    Svelte,
    Vue,
}

impl FromStr for UiFramework {
    type Err = ScaffoldError;

    fn from_str(s: &str) -> ScaffoldResult<UiFramework> {
        match s {
            "vanilla" => Ok(UiFramework::Vanilla),
            "svelte" => Ok(UiFramework::Svelte),
            "vue" => Ok(UiFramework::Vue),
            "lit" => Ok(UiFramework::Lit),
            _ => Err(ScaffoldError::InvalidUiFramework(
                s.to_string(),
                "vanilla, lit, svelte, vue".to_string(),
            )),
        }
    }
}

pub fn choose_ui_framework() -> ScaffoldResult<UiFramework> {
    let frameworks = vec!["Vanilla", "Lit", "Svelte", "Vue"];
    let selection = Select::with_theme(&ColorfulTheme::default())
        .with_prompt("Choose UI framework:")
        .default(0)
        .items(&frameworks[..])
        .interact()?;

    UiFramework::from_str(frameworks[selection].to_lowercase().as_str())
}

#[derive(Serialize)]
pub struct ScaffoldWebAppData {
    app_name: String,
    holochain_client_version: String,
}

fn get_templates(framework: &UiFramework) -> ScaffoldResult<FileTree> {
    let dir = match framework {
        UiFramework::Lit => &LIT_TEMPLATES,
        UiFramework::Vanilla => &VANILLA_TEMPLATES,
        UiFramework::Svelte => &SVELTE_TEMPLATES,
        UiFramework::Vue => &VUE_TEMPLATES,
    };

    dir_to_file_tree(dir)
}

pub fn build_handlebars<'a>(templates_dir: &FileTree) -> ScaffoldResult<Handlebars<'a>> {
    let h = Handlebars::new();

    let h = register_concat_helper(h);
    let mut h = register_case_helpers(h);

    let field_types_path = PathBuf::from("field-types");
    let v: Vec<OsString> = field_types_path.iter().map(|s| s.to_os_string()).collect();

    if let Some(field_types_templates) = templates_dir.path(&mut v.iter()) {
        h = register_all_partials_in_dir(h, field_types_templates)?;
    }
    h.register_escape_fn(handlebars::no_escape);

    Ok(h)
}

pub fn scaffold_web_app_ui(
    mut app_file_tree: FileTree,
    framework: &UiFramework,
    app_name: &String,
) -> ScaffoldResult<FileTree> {
    let data = ScaffoldWebAppData {
        app_name: app_name.clone(),
        holochain_client_version: holochain_client_version(),
    };

    let templates = get_templates(framework)?;

    let h = build_handlebars(&templates)?;

    let field_types_path = PathBuf::from("web-app");
    let v: Vec<OsString> = field_types_path.iter().map(|s| s.to_os_string()).collect();

    if let Some(web_app_template) = templates.path(&mut v.iter()) {
        app_file_tree = render_template_file_tree_and_merge_with_existing(
            app_file_tree,
            &h,
            web_app_template,
            &data,
        )?;
    }

    Ok(app_file_tree)
}

fn guess_or_choose_framework(app_file_tree: &FileTree) -> ScaffoldResult<UiFramework> {
    let ui_package_json_path = PathBuf::from("ui/package.json");
    let v: Vec<OsString> = ui_package_json_path
        .iter()
        .map(|s| s.to_os_string())
        .collect();
    let ui_package_json = app_file_tree
        .path(&mut v.iter())
        .ok_or(ScaffoldError::PathNotFound(ui_package_json_path.clone()))?
        .file_content()
        .ok_or(ScaffoldError::PathNotFound(ui_package_json_path.clone()))?
        .clone();

    if ui_package_json.contains("lit") {
        return Ok(UiFramework::Lit);
    } else if ui_package_json.contains("svelte") {
        return Ok(UiFramework::Svelte);
    } else if ui_package_json.contains("vue") {
        return Ok(UiFramework::Vue);
    }

    choose_ui_framework()
}

#[derive(Serialize)]
pub struct ScaffoldEntryTypeData {
    dna_role_id: String,
    coordinator_zome_name: String,
    entry_type: EntryDefinition,
    depends_on: Vec<String>,
}
pub fn scaffold_entry_type_templates(
    mut app_file_tree: FileTree,
    dna_role_id: &String,
    coordinator_zome_name: &String,
    entry_def: &EntryDefinition,
    depends_on: &Vec<String>,
) -> ScaffoldResult<FileTree> {
    let framework = guess_or_choose_framework(&app_file_tree)?;

    let data = ScaffoldEntryTypeData {
        dna_role_id: dna_role_id.clone(),
        coordinator_zome_name: coordinator_zome_name.clone(),
        entry_type: entry_def.clone(),
        depends_on: depends_on.clone(),
    };

    let templates = get_templates(&framework)?;

    let h = build_handlebars(&templates)?;

    let field_types_path = PathBuf::from("entry-type");
    let v: Vec<OsString> = field_types_path.iter().map(|s| s.to_os_string()).collect();

    if let Some(web_app_template) = templates.path(&mut v.iter()) {
        app_file_tree = render_template_file_tree_and_merge_with_existing(
            app_file_tree,
            &h,
            web_app_template,
            &data,
        )?;
    }

    Ok(app_file_tree)
}

#[derive(Serialize)]
pub struct ScaffoldLinkTypeData {
    dna_role_id: String,
    coordinator_zome_name: String,
    from_entry_type: String,
    to_entry_type: String,
}
pub fn scaffold_link_type_templates(
    mut app_file_tree: FileTree,
    dna_role_id: &String,
    coordinator_zome_name: &String,
    from_entry_type: &String,
    to_entry_type: &String,
) -> ScaffoldResult<FileTree> {
    let framework = guess_or_choose_framework(&app_file_tree)?;

    let data = ScaffoldLinkTypeData {
        dna_role_id: dna_role_id.clone(),
        coordinator_zome_name: coordinator_zome_name.clone(),
        from_entry_type: from_entry_type.clone(),
        to_entry_type: to_entry_type.clone(),
    };

    let templates = get_templates(&framework)?;

    let h = build_handlebars(&templates)?;

    let field_types_path = PathBuf::from("link-type");
    let v: Vec<OsString> = field_types_path.iter().map(|s| s.to_os_string()).collect();

    if let Some(web_app_template) = templates.path(&mut v.iter()) {
        app_file_tree = render_template_file_tree_and_merge_with_existing(
            app_file_tree,
            &h,
            web_app_template,
            &data,
        )?;
    }

    Ok(app_file_tree)
}

#[derive(Serialize)]
pub struct ScaffoldIndexData {
    dna_role_id: String,
    coordinator_zome_name: String,
    index_type: IndexType,
    index_name: String,
    entry_types: Vec<String>,
}
pub fn scaffold_index_templates(
    mut app_file_tree: FileTree,
    dna_role_id: &String,
    coordinator_zome_name: &String,
    index_type: &IndexType,
    index_name: &String,
    entry_types: &Vec<String>,
) -> ScaffoldResult<FileTree> {
    let framework = guess_or_choose_framework(&app_file_tree)?;

    let data = ScaffoldIndexData {
        entry_types: entry_types.clone(),
        dna_role_id: dna_role_id.clone(),
        coordinator_zome_name: coordinator_zome_name.clone(),
        index_name: index_name.clone(),
        index_type: index_type.clone(),
    };

    let templates = get_templates(&framework)?;

    let h = build_handlebars(&templates)?;

    let field_types_path = PathBuf::from("index");
    let v: Vec<OsString> = field_types_path.iter().map(|s| s.to_os_string()).collect();

    if let Some(web_app_template) = templates.path(&mut v.iter()) {
        app_file_tree = render_template_file_tree_and_merge_with_existing(
            app_file_tree,
            &h,
            web_app_template,
            &data,
        )?;
    }

    Ok(app_file_tree)
}