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 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 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}