holochain_scaffolding_cli 0.2000.0

CLI to easily generate and modify holochain apps
Documentation
use build_fs_tree::serde::Serialize;
use dialoguer::theme::ColorfulTheme;
use dialoguer::Select;
use handlebars::Handlebars;
use regex::Regex;
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::path::PathBuf;

use crate::error::{ScaffoldError, ScaffoldResult};
use crate::file_tree::{
    dir_content, dir_exists, file_content, find_files, flatten_file_tree, unflatten_file_tree,
    FileTree,
};
use crate::scaffold::web_app::uis::{guess_or_choose_framework, template_for_ui_framework};

pub mod get;
pub mod helpers;

pub mod collection;
pub mod coordinator;
pub mod dna;
pub mod entry_type;
pub mod example;
pub mod integrity;
pub mod link_type;
pub mod web_app;

pub struct ScaffoldedTemplate {
    pub file_tree: FileTree,
    pub next_instructions: Option<String>,
}

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

    let mut h = helpers::register_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 register_all_partials_in_dir<'a>(
    mut h: Handlebars<'a>,
    file_tree: &FileTree,
) -> ScaffoldResult<Handlebars<'a>> {
    let partials = find_files(file_tree, &|path, _contents| {
        if let Some(e) = PathBuf::from(path).extension() {
            if e == "hbs" {
                return true;
            }
        }
        return false;
    });

    for (path, content) in partials {
        h.register_partial(
            path.with_extension("").as_os_str().to_str().unwrap(),
            content.trim(),
        )?;
    }

    Ok(h)
}

pub fn render_template_file<'a>(
    h: &Handlebars<'a>,
    existing_app_file_tree: &FileTree,
    target_path: &PathBuf,
    template_str: &String,
    value: &serde_json::Value,
) -> ScaffoldResult<String> {
    let mut value = value.clone();

    if let Ok(previous_content) = file_content(existing_app_file_tree, &target_path) {
        value
            .as_object_mut()
            .unwrap()
            .insert("previous_file_content".into(), previous_content.into());
    }

    let mut h = h.clone();
    h.register_template_string(target_path.to_str().unwrap(), template_str)?;
    let new_contents = h.render(target_path.to_str().unwrap(), &value)?;

    Ok(new_contents)
}

pub fn render_template_file_tree<'a, T: Serialize>(
    existing_app_file_tree: &FileTree,
    h: &Handlebars<'a>,
    templates_file_tree: &FileTree,
    data: &T,
) -> ScaffoldResult<FileTree> {
    let flattened_templates = flatten_file_tree(templates_file_tree);

    let mut transformed_templates: BTreeMap<PathBuf, Option<String>> = BTreeMap::new();

    let new_data = serde_json::to_string(data)?;
    let value: serde_json::Value = serde_json::from_str(new_data.as_str())?;

    for (path, maybe_contents) in flattened_templates {
        let path = PathBuf::from(path.to_str().unwrap().replace('¡', "/"));
        let path = PathBuf::from(path.to_str().unwrap().replace('\'', "\""));
        if let Some(contents) = maybe_contents {
            let re = Regex::new(
                r"(?P<c>(.)*)/\{\{#each (?P<b>([^\{\}])*)\}\}(?P<a>(.)*)\{\{/each\}\}.hbs\z",
            )
            .unwrap();
            let if_regex = Regex::new(
                r"(?P<c>(.)*)/\{\{#if (?P<b>([^\{\}])*)\}\}(?P<a>(.)*)\{\{/if\}\}.hbs\z",
            )
            .unwrap();

            if re.is_match(path.to_str().unwrap()) {
                let path_prefix = re.replace(path.to_str().unwrap(), "${c}");
                let path_prefix = h.render_template(path_prefix.to_string().as_str(), data)?;

                let new_path_suffix =
                    re.replace(path.to_str().unwrap(), "{{#each ${b} }}${a}.hbs{{/each}}");

                let all_paths = h.render_template(new_path_suffix.to_string().as_str(), data)?;

                let files_to_create: Vec<String> = all_paths
                    .split(".hbs")
                    .map(|s| s.to_string())
                    .filter(|s| !s.is_empty())
                    .collect();

                if files_to_create.len() > 0 {
                    let delimiter = "\n----END_OF_FILE_DELIMITER----\n";

                    let each_if_re = Regex::new(
                    r"(?P<c>(.)*)/\{\{#each (?P<b>([^\{\}])*)\}\}\{\{#if (?P<d>([^\{\}])*)\}\}(?P<a>(.)*)\{\{/if\}\}\{\{/each\}\}.hbs\z",
                )
                .unwrap();
                    let b = re.replace(path.to_str().unwrap(), "${b}");
                    let new_all_contents = match each_if_re.is_match(path.to_str().unwrap()) {
                        true => {
                            let d = each_if_re.replace(path.to_str().unwrap(), "${d}");
                            format!(
                                "{{{{#each {} }}}}{{{{#if {} }}}}\n{}{}{{{{/if}}}}{{{{/each}}}}",
                                b, d, contents, delimiter
                            )
                        }

                        false => format!(
                            "{{{{#each {} }}}}\n{}{}{{{{/each}}}}",
                            b, contents, delimiter
                        ),
                    };
                    let new_contents = render_template_file(
                        &h,
                        existing_app_file_tree,
                        &path,
                        &new_all_contents,
                        &value,
                    )?;
                    let new_contents_split: Vec<String> = new_contents
                        .split(delimiter)
                        .into_iter()
                        .map(|s| s.to_string())
                        .collect();

                    for (i, f) in files_to_create.into_iter().enumerate() {
                        let target_path = PathBuf::from(path_prefix.clone()).join(f);

                        transformed_templates
                            .insert(target_path, Some(new_contents_split[i].clone()));
                    }
                }
            } else if if_regex.is_match(path.to_str().unwrap()) {
                let path_prefix = if_regex.replace(path.to_str().unwrap(), "${c}");
                let path_prefix = h.render_template(path_prefix.to_string().as_str(), data)?;

                let new_path_suffix =
                    if_regex.replace(path.to_str().unwrap(), "{{#if ${b} }}${a}.hbs{{/if}}");

                let new_template = h.render_template(new_path_suffix.to_string().as_str(), data)?;

                if let Some(file_name) = new_template.strip_suffix(".hbs") {
                    let target_path = PathBuf::from(path_prefix.clone()).join(file_name);

                    let new_contents = render_template_file(
                        &h,
                        existing_app_file_tree,
                        &target_path,
                        &contents,
                        &value,
                    )?;
                    transformed_templates.insert(target_path, Some(new_contents));
                }
            } else if let Some(e) = path.extension() {
                if e == "hbs" {
                    let new_path = h.render_template(path.as_os_str().to_str().unwrap(), data)?;
                    let target_path = PathBuf::from(new_path).with_extension("");

                    let new_contents = render_template_file(
                        &h,
                        existing_app_file_tree,
                        &target_path,
                        &contents,
                        &value,
                    )?;

                    transformed_templates.insert(target_path, Some(new_contents));
                }
            }
        } else {
            let new_path = h.render_template(path.as_os_str().to_str().unwrap(), data)?;
            transformed_templates.insert(PathBuf::from(new_path), None);
        }
    }

    unflatten_file_tree(&transformed_templates)
}

pub fn render_template_file_tree_and_merge_with_existing<'a, T: Serialize>(
    app_file_tree: FileTree,
    h: &Handlebars<'a>,
    template_file_tree: &FileTree,
    data: &T,
) -> ScaffoldResult<FileTree> {
    let rendered_templates =
        render_template_file_tree(&app_file_tree, h, template_file_tree, data)?;

    let mut flattened_app_file_tree = flatten_file_tree(&app_file_tree);
    let flattened_templates = flatten_file_tree(&rendered_templates);

    flattened_app_file_tree.extend(flattened_templates);

    unflatten_file_tree(&flattened_app_file_tree)
}

pub fn templates_path() -> PathBuf {
    PathBuf::from(".templates")
}

pub fn choose_or_get_template_file_tree(
    file_tree: &FileTree,
    template: &Option<String>,
) -> ScaffoldResult<FileTree> {
    if dir_exists(file_tree, &templates_path()) {
        let template_name = choose_or_get_template(file_tree, template)?;

        Ok(FileTree::Directory(dir_content(
            &file_tree,
            &templates_path().join(template_name),
        )?))
    } else {
        let ui_framework = guess_or_choose_framework(file_tree)?;

        template_for_ui_framework(&ui_framework)
    }
}

pub fn choose_or_get_template(
    file_tree: &FileTree,
    template: &Option<String>,
) -> ScaffoldResult<String> {
    let templates_path = PathBuf::new().join(templates_path());

    let templates_dir_content =
        dir_content(file_tree, &templates_path).map_err(|_e| ScaffoldError::NoTemplatesFound)?;

    let templates: Vec<String> = templates_dir_content
        .into_keys()
        .map(|k| k.to_str().unwrap().to_string())
        .collect();

    let chosen_template_name = match (template, templates.len()) {
        (_, 0) => Err(ScaffoldError::NoTemplatesFound),
        (None, 1) => Ok(templates[0].clone()),
        (None, _) => {
            let option = Select::with_theme(&ColorfulTheme::default())
                .with_prompt("Which template should we use?")
                .default(0)
                .items(&templates[..])
                .interact()?;

            Ok(templates[option].clone())
        }
        (Some(t), _) => match templates.contains(&t) {
            true => Ok(t.clone()),
            false => Err(ScaffoldError::TemplateNotFound(t.clone())),
        },
    }?;

    Ok(chosen_template_name)
}