doido_generators/generators/
new.rs1use crate::generator::{GeneratedFile, Generator};
11use doido_core::{anyhow, Result};
12use include_dir::{include_dir, Dir, DirEntry};
13
14static APP_TEMPLATE_DIR: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates/new");
16
17struct TemplateContext<'a> {
18 name: &'a str,
19 db_url: String,
20 db_url_test: String,
21 db_url_production: String,
22 sqlx_feature: &'a str,
23}
24
25fn substitute_template(template: &str, ctx: &TemplateContext<'_>) -> String {
26 template
27 .replace("{doido_name}", ctx.name)
28 .replace("{doido_db_url_test}", &ctx.db_url_test)
29 .replace("{doido_db_url_production}", &ctx.db_url_production)
30 .replace("{doido_db_url}", &ctx.db_url)
31 .replace("{doido_sqlx_feature}", ctx.sqlx_feature)
32 .replace("{doido_path}", crate::TEMPLATE_WORKSPACE_PATH)
33}
34
35fn collect_from_dir(
36 dir: &Dir<'_>,
37 ctx: &TemplateContext<'_>,
38 app_name: &str,
39 out: &mut Vec<GeneratedFile>,
40) -> Result<()> {
41 for entry in dir.entries() {
42 match entry {
43 DirEntry::Dir(sub) => collect_from_dir(sub, ctx, app_name, out)?,
44 DirEntry::File(f) => {
45 let relative = f.path();
48 let raw = f.contents_utf8().ok_or_else(|| {
49 anyhow::anyhow!("template file '{}' is not valid UTF-8", relative.display())
50 })?;
51 let rendered = substitute_template(raw, ctx);
52 let relative = relative.to_string_lossy().replace('\\', "/");
57 let relative = relative.strip_suffix(".template").unwrap_or(&relative);
58 let disk_path = format!("{app_name}/{relative}");
59 out.push(GeneratedFile {
60 path: disk_path,
61 content: rendered,
62 });
63 }
64 }
65 }
66 Ok(())
67}
68
69struct DbDefaults {
71 scheme: &'static str,
73 user: &'static str,
75 password: &'static str,
77 port: u16,
79}
80
81fn db_defaults(backend: &str) -> Option<DbDefaults> {
84 match backend {
85 "postgres" => Some(DbDefaults {
86 scheme: "postgres",
87 user: "postgres",
88 password: "postgres",
89 port: 5432,
90 }),
91 "mysql" => Some(DbDefaults {
92 scheme: "mysql",
93 user: "root",
94 password: "password",
95 port: 3306,
96 }),
97 _ => None,
98 }
99}
100
101fn default_database_url(backend: &str, name: &str, env: &str) -> String {
114 match db_defaults(backend) {
115 Some(d) => {
116 let password = if env == "production" {
117 "CHANGE_ME"
118 } else {
119 d.password
120 };
121 format!(
122 "{}://{}:{}@localhost:{}/{}_{}",
123 d.scheme, d.user, password, d.port, name, env
124 )
125 }
126 None => format!("sqlite://db/{env}.db"),
127 }
128}
129
130pub struct ProjectGenerator;
131
132impl Generator for ProjectGenerator {
133 fn name(&self) -> &str {
134 "new"
135 }
136
137 fn generate(&self, args: &[&str]) -> Result<Vec<GeneratedFile>> {
138 let name = args
139 .first()
140 .copied()
141 .ok_or_else(|| anyhow::anyhow!("new generator requires a name argument"))?;
142
143 let database = args
144 .iter()
145 .find(|a| a.starts_with("--database="))
146 .and_then(|a| a.split_once('=').map(|(_, v)| v))
147 .unwrap_or("sqlite");
148
149 match database {
150 "sqlite" | "postgres" | "mysql" => {}
151 other => {
152 return Err(anyhow::anyhow!(
153 "Unknown database: {}. Use sqlite, postgres, or mysql.",
154 other
155 ));
156 }
157 }
158
159 let db_url = default_database_url(database, name, "development");
160 let db_url_test = default_database_url(database, name, "test");
161 let db_url_production = default_database_url(database, name, "production");
162
163 let sqlx_feature = match database {
164 "postgres" => "postgres",
165 "mysql" => "mysql",
166 _ => "sqlite",
167 };
168
169 let ctx = TemplateContext {
170 name,
171 db_url,
172 db_url_test,
173 db_url_production,
174 sqlx_feature,
175 };
176
177 let mut files = Vec::new();
178 collect_from_dir(&APP_TEMPLATE_DIR, &ctx, name, &mut files)?;
179 files.sort_by(|a, b| a.path.cmp(&b.path));
180 Ok(files)
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::default_database_url;
187
188 #[test]
189 fn postgres_url_has_default_user_password_and_port() {
190 assert_eq!(
191 default_database_url("postgres", "blog", "development"),
192 "postgres://postgres:postgres@localhost:5432/blog_development"
193 );
194 }
195
196 #[test]
197 fn mysql_url_has_default_user_password_and_port() {
198 assert_eq!(
199 default_database_url("mysql", "store", "test"),
200 "mysql://root:password@localhost:3306/store_test"
201 );
202 }
203
204 #[test]
205 fn production_password_is_a_placeholder() {
206 assert_eq!(
207 default_database_url("postgres", "blog", "production"),
208 "postgres://postgres:CHANGE_ME@localhost:5432/blog_production"
209 );
210 assert_eq!(
211 default_database_url("mysql", "store", "production"),
212 "mysql://root:CHANGE_ME@localhost:3306/store_production"
213 );
214 }
215
216 #[test]
217 fn sqlite_stays_a_bare_file_path() {
218 assert_eq!(
219 default_database_url("sqlite", "blog", "development"),
220 "sqlite://db/development.db"
221 );
222 }
223}