1use std::path::Path;
2
3use heck::ToPascalCase;
4use rand::rngs::StdRng;
5use rand::{RngCore, 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); 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 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 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 = \"{}\"", cot::__private::COT_VERSION)
152 );
153 }
154}