vertigo_cli/new/
mod.rs

1use clap::Args;
2use derive_more::Display;
3use include_dir::{Dir, include_dir};
4use std::{fs, path::Path};
5use walkdir::WalkDir;
6
7use crate::commons::ErrorCode;
8
9#[derive(clap::ValueEnum, Clone, Default, Display)]
10pub enum Template {
11    #[display("fullstack")]
12    Fullstack,
13    #[display("frontend")]
14    #[default]
15    Frontend,
16}
17
18impl Template {
19    pub fn get_dir(&self) -> Dir<'_> {
20        match self {
21            Template::Fullstack => include_dir!("$CARGO_MANIFEST_DIR/src/new/fs_template"),
22            Template::Frontend => include_dir!("$CARGO_MANIFEST_DIR/src/new/fe_template"),
23        }
24    }
25}
26
27#[derive(Args)]
28pub struct NewOpts {
29    pub project_name: String,
30    #[arg(short, long, default_value_t = {Template::default()})]
31    pub template: Template,
32    #[arg(long, default_value_t = {"./".to_string()})]
33    pub dest_dir: String,
34}
35
36pub fn run(opts: NewOpts) -> Result<(), ErrorCode> {
37    log::info!("Creating {}", opts.project_name);
38
39    let target_path = Path::new(&opts.dest_dir).join(&opts.project_name);
40
41    // Check if dir is empty or non-existent
42    if let Ok(mut dir) = target_path.read_dir()
43        && dir.next().is_some()
44    {
45        log::error!(
46            "Destination dir ({}) is not empty",
47            target_path.to_string_lossy()
48        );
49        return Err(ErrorCode::NewProjectDirNotEmpty);
50    }
51
52    // Create directory
53    if let Err(err) = fs::create_dir_all(&target_path) {
54        log::error!(
55            "Can't create directory {}: {}",
56            target_path.to_string_lossy(),
57            err
58        );
59        return Err(ErrorCode::NewProjectCantCreateDir);
60    };
61
62    // Paste files into it
63    if let Err(err) = opts
64        .template
65        .get_dir()
66        .extract(Path::new(&opts.dest_dir).join(&opts.project_name))
67    {
68        log::error!(
69            "Can't unpack vertigo stub to {}: {}",
70            target_path.to_string_lossy(),
71            err
72        );
73        return Err(ErrorCode::NewProjectCantUnpackStub);
74    };
75
76    // Process all Cargo.toml_ files recursively
77    // (cargo packaging does not permit adding second Cargo.toml file)
78    process_cargo_toml_files(&target_path, &opts.project_name)?;
79
80    Ok(())
81}
82
83/// Find all Cargo.toml_ files, replace "my_app" with package_name, and save as Cargo.toml
84fn process_cargo_toml_files(dir: &Path, package_name: &str) -> Result<(), ErrorCode> {
85    for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
86        if entry.file_name() == "Cargo.toml_" {
87            let path = entry.path();
88            // Read the content
89            let content = match fs::read_to_string(path) {
90                Ok(content) => content,
91                Err(err) => {
92                    log::error!("Can't read {}: {}", path.display(), err);
93                    return Err(ErrorCode::NewProjectCantCreateCargoToml);
94                }
95            };
96
97            // Replace my_app with package_name
98            let new_content = content.replace("my_app", package_name);
99
100            // Write to Cargo.toml in the same directory
101            if let Some(parent) = path.parent() {
102                let cargo_toml_path = parent.join("Cargo.toml");
103                if let Err(err) = fs::write(&cargo_toml_path, new_content) {
104                    log::error!("Can't write to {}: {}", cargo_toml_path.display(), err);
105                    return Err(ErrorCode::NewProjectCanWriteToCargoToml);
106                }
107            }
108
109            // Remove the original Cargo.toml_ file
110            if let Err(err) = fs::remove_file(path) {
111                log::error!("Can't remove {}: {}", path.display(), err);
112                return Err(ErrorCode::NewProjectCantCreateCargoToml);
113            }
114        }
115    }
116
117    Ok(())
118}