Skip to main content

cot_cli/
new_project.rs

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