cot_cli/
new_project.rs

1use std::path::Path;
2
3use heck::ToPascalCase;
4use rand::rngs::StdRng;
5use rand::{RngCore, SeedableRng};
6use tracing::trace;
7
8use crate::utils::{print_status_msg, StatusType};
9
10macro_rules! project_file {
11    ($name:literal) => {
12        ($name, include_str!(concat!("project_template/", $name)))
13    };
14}
15
16const PROJECT_FILES: [(&str, &str); 10] = [
17    project_file!("Cargo.toml.template"),
18    project_file!("Cargo.lock.template"),
19    project_file!("bacon.toml"),
20    project_file!(".gitignore"),
21    project_file!("src/main.rs"),
22    project_file!("src/migrations.rs"),
23    project_file!("static/css/main.css"),
24    project_file!("templates/index.html"),
25    project_file!("config/dev.toml"),
26    project_file!("config/prod.toml.example"),
27];
28
29#[derive(Debug, Clone, PartialEq, Eq, Hash)]
30pub enum CotSource<'a> {
31    Git,
32    Path(&'a Path),
33    PublishedCrate,
34}
35
36impl CotSource<'_> {
37    fn as_cargo_toml_source(&self) -> String {
38        match self {
39            CotSource::Git => {
40                "package = \"cot\", git = \"https://github.com/cot-rs/cot.git\"".to_owned()
41            }
42            CotSource::Path(path) => {
43                format!(
44                    "path = \"{}\"",
45                    path.display().to_string().replace('\\', "\\\\")
46                )
47            }
48            CotSource::PublishedCrate => format!("version = \"{}\"", cot::__private::COT_VERSION),
49        }
50    }
51}
52
53pub fn new_project(
54    path: &Path,
55    project_name: &str,
56    cot_source: &CotSource<'_>,
57) -> anyhow::Result<()> {
58    print_status_msg(
59        StatusType::Creating,
60        &format!("Cot project `{project_name}`"),
61    );
62
63    if path.exists() {
64        anyhow::bail!("destination `{}` already exists", path.display());
65    }
66
67    let project_struct_name = format!("{}Project", project_name.to_pascal_case());
68    let app_name = format!("{}App", project_name.to_pascal_case());
69    let cot_source = cot_source.as_cargo_toml_source();
70    let dev_secret_key = generate_secret_key();
71
72    for (file_name, content) in PROJECT_FILES {
73        // Cargo reads and parses all files that are named "Cargo.toml" in a repository,
74        // so we need a different name so that it doesn't fail on build.
75        let file_name = file_name.replace(".template", "");
76
77        let file_path = path.join(file_name);
78        trace!("Writing file: {:?}", file_path);
79
80        std::fs::create_dir_all(
81            file_path
82                .parent()
83                .expect("joined path should always have a parent"),
84        )?;
85
86        std::fs::write(
87            file_path,
88            content
89                .replace("{{ project_name }}", project_name)
90                .replace("{{ project_struct_name }}", &project_struct_name)
91                .replace("{{ app_name }}", &app_name)
92                .replace("{{ cot_source }}", &cot_source)
93                .replace("{{ dev_secret_key }}", &dev_secret_key),
94        )?;
95    }
96    print_status_msg(
97        StatusType::Created,
98        &format!("Cot project `{project_name}`"),
99    );
100
101    Ok(())
102}
103
104fn generate_secret_key() -> String {
105    // Cryptographically secure random number generator:
106    // https://rust-random.github.io/book/guide-rngs.html#cryptographically-secure-pseudo-random-number-generators-csprngs
107    // https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#secure-random-number-generation
108    let mut rng = StdRng::from_os_rng();
109    let mut key = [0u8; 32];
110    rng.fill_bytes(&mut key);
111    hex::encode(key)
112}
113
114#[cfg(test)]
115mod tests {
116    use std::path::Path;
117
118    use super::*;
119
120    #[test]
121    fn as_cargo_toml_source_git() {
122        let source = CotSource::Git;
123        assert_eq!(
124            source.as_cargo_toml_source(),
125            "package = \"cot\", git = \"https://github.com/cot-rs/cot.git\""
126        );
127    }
128
129    #[test]
130    fn as_cargo_toml_source_path() {
131        let path = Path::new("/some/local/path");
132        let source = CotSource::Path(path);
133        assert_eq!(source.as_cargo_toml_source(), "path = \"/some/local/path\"");
134    }
135
136    #[test]
137    fn as_cargo_toml_source_path_windows() {
138        let path = Path::new("C:\\some\\local\\path");
139        let source = CotSource::Path(path);
140        assert_eq!(
141            source.as_cargo_toml_source(),
142            "path = \"C:\\\\some\\\\local\\\\path\""
143        );
144    }
145
146    #[test]
147    fn as_cargo_toml_source_published_crate() {
148        let source = CotSource::PublishedCrate;
149        assert_eq!(
150            source.as_cargo_toml_source(),
151            format!("version = \"{}\"", env!("CARGO_PKG_VERSION"))
152        );
153    }
154}