1use anyhow::{Context, Result};
4use colored::Colorize;
5use std::path::Path;
6
7use crate::cli::{BackendArgs, BackendPreset};
8use crate::templates::{BackendTemplateContext, BackendTemplateEngine};
9use crate::{
10 TIDEWAY_VERSION, ensure_dir, is_json_output, print_info, print_success, print_warning,
11 write_file,
12};
13
14fn to_pascal_case(s: &str) -> String {
16 s.split('_')
17 .map(|word| {
18 let mut chars = word.chars();
19 match chars.next() {
20 None => String::new(),
21 Some(first) => first.to_uppercase().chain(chars).collect(),
22 }
23 })
24 .collect()
25}
26
27pub fn run(args: BackendArgs) -> Result<()> {
29 let has_organizations = args.preset == BackendPreset::B2b;
30 let preset_name = match args.preset {
31 BackendPreset::B2c => "B2C (Auth + Billing + Admin)",
32 BackendPreset::B2b => "B2B (Auth + Billing + Organizations + Admin)",
33 };
34
35 if !is_json_output() {
36 println!(
37 "\n{} Generating {} backend scaffolding\n",
38 "tideway".cyan().bold(),
39 preset_name.green()
40 );
41 println!(
42 " Project: {}\n Database: {}\n Output: {}\n",
43 args.name.yellow(),
44 args.database.yellow(),
45 args.output.yellow()
46 );
47 }
48
49 let output_path = Path::new(&args.output);
51 let migrations_path = Path::new(&args.migrations_output);
52
53 if !output_path.exists() {
54 ensure_dir(output_path)
55 .with_context(|| format!("Failed to create output directory: {}", args.output))?;
56 print_info(&format!("Created directory: {}", args.output));
57 }
58
59 if !migrations_path.exists() {
60 ensure_dir(migrations_path).with_context(|| {
61 format!(
62 "Failed to create migrations directory: {}",
63 args.migrations_output
64 )
65 })?;
66 print_info(&format!("Created directory: {}", args.migrations_output));
67 }
68
69 let context = BackendTemplateContext {
71 project_name: args.name.clone(),
72 project_name_pascal: to_pascal_case(&args.name),
73 has_organizations,
74 database: args.database.clone(),
75 tideway_version: TIDEWAY_VERSION.to_string(),
76 tideway_features: Vec::new(),
77 has_tideway_features: false,
78 has_auth_feature: false,
79 has_database_feature: false,
80 has_openapi_feature: false,
81 needs_arc: false,
82 has_config: false,
83 };
84
85 let engine = BackendTemplateEngine::new(context)?;
87
88 generate_shared(&engine, output_path, &args)?;
90
91 generate_entities(&engine, output_path, &args)?;
93
94 generate_auth(&engine, output_path, &args)?;
96
97 generate_billing(&engine, output_path, &args)?;
99
100 if has_organizations {
102 generate_organizations(&engine, output_path, &args)?;
103 }
104
105 generate_admin(&engine, output_path, &args)?;
107
108 generate_migrations(&engine, migrations_path, &args)?;
110
111 if !is_json_output() {
112 println!(
113 "\n{} Backend scaffolding generated successfully!\n",
114 "✓".green().bold()
115 );
116
117 println!("{}", "Next steps:".yellow().bold());
119 println!(" 1. Add dependencies to Cargo.toml:");
120 println!(
121 " tideway = {{ version = \"{}\", features = [\"auth\", \"auth-mfa\", \"database\", \"billing\", \"billing-seaorm\", \"organizations\", \"admin\"] }}",
122 TIDEWAY_VERSION
123 );
124 println!(" axum = {{ version = \"0.8\", features = [\"macros\"] }}");
125 println!(
126 " sea-orm = {{ version = \"1.1\", features = [\"sqlx-postgres\", \"runtime-tokio-rustls\"] }}"
127 );
128 println!(" tokio = {{ version = \"1\", features = [\"full\"] }}");
129 println!(" serde = {{ version = \"1\", features = [\"derive\"] }}");
130 println!(" serde_json = \"1\"");
131 println!(" tracing = \"0.1\"");
132 println!(" async-trait = \"0.1\"");
133 println!(" chrono = {{ version = \"0.4\", features = [\"serde\"] }}");
134 println!(" uuid = {{ version = \"1\", features = [\"v4\", \"serde\"] }}");
135 println!();
136 println!(" 2. Run migrations:");
137 println!(" sea-orm-cli migrate up");
138 println!();
139 println!(" 3. Start the server:");
140 println!(" cargo run");
141 println!();
142 }
143
144 Ok(())
145}
146
147fn generate_shared(
148 engine: &BackendTemplateEngine,
149 output_path: &Path,
150 args: &BackendArgs,
151) -> Result<()> {
152 if engine.has_template("shared/main") {
154 let content = engine.render("shared/main")?;
155 let file_path = output_path.join("main.rs");
156 write_file_with_force(&file_path, &content, args.force)?;
157 print_success("Generated main.rs");
158 }
159
160 if engine.has_template("shared/lib") {
162 let content = engine.render("shared/lib")?;
163 let file_path = output_path.join("lib.rs");
164 write_file_with_force(&file_path, &content, args.force)?;
165 print_success("Generated lib.rs");
166 }
167
168 if engine.has_template("shared/config") {
170 let content = engine.render("shared/config")?;
171 let file_path = output_path.join("config.rs");
172 write_file_with_force(&file_path, &content, args.force)?;
173 print_success("Generated config.rs");
174 }
175
176 if engine.has_template("shared/error") {
178 let content = engine.render("shared/error")?;
179 let file_path = output_path.join("error.rs");
180 write_file_with_force(&file_path, &content, args.force)?;
181 print_success("Generated error.rs");
182 }
183
184 Ok(())
185}
186
187fn generate_entities(
188 engine: &BackendTemplateEngine,
189 output_path: &Path,
190 args: &BackendArgs,
191) -> Result<()> {
192 let entities_path = output_path.join("entities");
193 ensure_dir(&entities_path)?;
194
195 if engine.has_template("entities/mod") {
197 let content = engine.render("entities/mod")?;
198 let file_path = entities_path.join("mod.rs");
199 write_file_with_force(&file_path, &content, args.force)?;
200 print_success("Generated entities/mod.rs");
201 }
202
203 if engine.has_template("entities/prelude") {
205 let content = engine.render("entities/prelude")?;
206 let file_path = entities_path.join("prelude.rs");
207 write_file_with_force(&file_path, &content, args.force)?;
208 print_success("Generated entities/prelude.rs");
209 }
210
211 let core_entities = [
213 ("user.rs", "entities/user"),
214 ("refresh_token_family.rs", "entities/refresh_token_family"),
215 ("verification_token.rs", "entities/verification_token"),
216 ];
217
218 for (filename, template_name) in core_entities {
219 if engine.has_template(template_name) {
220 let content = engine.render(template_name)?;
221 let file_path = entities_path.join(filename);
222 write_file_with_force(&file_path, &content, args.force)?;
223 print_success(&format!("Generated entities/{}", filename));
224 }
225 }
226
227 if args.preset == BackendPreset::B2b {
229 let org_entities = [
230 ("organization.rs", "entities/organization"),
231 ("organization_member.rs", "entities/organization_member"),
232 ];
233
234 for (filename, template_name) in org_entities {
235 if engine.has_template(template_name) {
236 let content = engine.render(template_name)?;
237 let file_path = entities_path.join(filename);
238 write_file_with_force(&file_path, &content, args.force)?;
239 print_success(&format!("Generated entities/{}", filename));
240 }
241 }
242 }
243
244 Ok(())
245}
246
247fn generate_auth(
248 engine: &BackendTemplateEngine,
249 output_path: &Path,
250 args: &BackendArgs,
251) -> Result<()> {
252 let auth_path = output_path.join("auth");
253 ensure_dir(&auth_path)?;
254
255 let templates = [
256 ("mod.rs", "auth/mod"),
257 ("routes.rs", "auth/routes"),
258 ("store.rs", "auth/store"),
259 ];
260
261 for (filename, template_name) in templates {
262 if engine.has_template(template_name) {
263 let content = engine.render(template_name)?;
264 let file_path = auth_path.join(filename);
265 write_file_with_force(&file_path, &content, args.force)?;
266 print_success(&format!("Generated auth/{}", filename));
267 }
268 }
269
270 Ok(())
271}
272
273fn generate_billing(
274 engine: &BackendTemplateEngine,
275 output_path: &Path,
276 args: &BackendArgs,
277) -> Result<()> {
278 let billing_path = output_path.join("billing");
279 ensure_dir(&billing_path)?;
280
281 let templates = [
282 ("mod.rs", "billing/mod"),
283 ("routes.rs", "billing/routes"),
284 ("store.rs", "billing/store"),
285 ];
286
287 for (filename, template_name) in templates {
288 if engine.has_template(template_name) {
289 let content = engine.render(template_name)?;
290 let file_path = billing_path.join(filename);
291 write_file_with_force(&file_path, &content, args.force)?;
292 print_success(&format!("Generated billing/{}", filename));
293 }
294 }
295
296 Ok(())
297}
298
299fn generate_organizations(
300 engine: &BackendTemplateEngine,
301 output_path: &Path,
302 args: &BackendArgs,
303) -> Result<()> {
304 let orgs_path = output_path.join("organizations");
305 ensure_dir(&orgs_path)?;
306
307 let templates = [
308 ("mod.rs", "organizations/mod"),
309 ("routes.rs", "organizations/routes"),
310 ("store.rs", "organizations/store"),
311 ];
312
313 for (filename, template_name) in templates {
314 if engine.has_template(template_name) {
315 let content = engine.render(template_name)?;
316 let file_path = orgs_path.join(filename);
317 write_file_with_force(&file_path, &content, args.force)?;
318 print_success(&format!("Generated organizations/{}", filename));
319 }
320 }
321
322 Ok(())
323}
324
325fn generate_admin(
326 engine: &BackendTemplateEngine,
327 output_path: &Path,
328 args: &BackendArgs,
329) -> Result<()> {
330 let admin_path = output_path.join("admin");
331 ensure_dir(&admin_path)?;
332
333 let templates = [
334 ("mod.rs", "admin/mod"),
335 ("routes.rs", "admin/routes"),
336 ("store.rs", "admin/store"),
337 ];
338
339 for (filename, template_name) in templates {
340 if engine.has_template(template_name) {
341 let content = engine.render(template_name)?;
342 let file_path = admin_path.join(filename);
343 write_file_with_force(&file_path, &content, args.force)?;
344 print_success(&format!("Generated admin/{}", filename));
345 }
346 }
347
348 Ok(())
349}
350
351fn generate_migrations(
352 engine: &BackendTemplateEngine,
353 migrations_path: &Path,
354 args: &BackendArgs,
355) -> Result<()> {
356 if engine.has_template("migrations/lib") {
358 let content = engine.render("migrations/lib")?;
359 let file_path = migrations_path.join("lib.rs");
360 write_file_with_force(&file_path, &content, args.force)?;
361 print_success("Generated migration/src/lib.rs");
362 }
363
364 let core_migrations = [
366 ("m001_create_users.rs", "migrations/m001_create_users"),
367 (
368 "m002_create_refresh_token_families.rs",
369 "migrations/m002_create_refresh_token_families",
370 ),
371 (
372 "m003_create_verification_tokens.rs",
373 "migrations/m003_create_verification_tokens",
374 ),
375 ("m004_create_billing.rs", "migrations/m004_create_billing"),
376 ];
377
378 for (filename, template_name) in core_migrations {
379 if engine.has_template(template_name) {
380 let content = engine.render(template_name)?;
381 let file_path = migrations_path.join(filename);
382 write_file_with_force(&file_path, &content, args.force)?;
383 print_success(&format!("Generated migration/src/{}", filename));
384 }
385 }
386
387 if args.preset == BackendPreset::B2b {
389 let b2b_migrations = [
390 (
391 "m005_create_organizations.rs",
392 "migrations/m005_create_organizations",
393 ),
394 (
395 "m006_create_organization_members.rs",
396 "migrations/m006_create_organization_members",
397 ),
398 ("m007_add_admin_flag.rs", "migrations/m007_add_admin_flag"),
399 ];
400
401 for (filename, template_name) in b2b_migrations {
402 if engine.has_template(template_name) {
403 let content = engine.render(template_name)?;
404 let file_path = migrations_path.join(filename);
405 write_file_with_force(&file_path, &content, args.force)?;
406 print_success(&format!("Generated migration/src/{}", filename));
407 }
408 }
409 } else {
410 if engine.has_template("migrations/m005_add_admin_flag") {
412 let content = engine.render("migrations/m005_add_admin_flag")?;
413 let file_path = migrations_path.join("m005_add_admin_flag.rs");
414 write_file_with_force(&file_path, &content, args.force)?;
415 print_success("Generated migration/src/m005_add_admin_flag.rs");
416 }
417 }
418
419 Ok(())
420}
421
422fn write_file_with_force(path: &Path, content: &str, force: bool) -> Result<()> {
423 if path.exists() && !force {
424 print_warning(&format!(
425 "Skipping {} (use --force to overwrite)",
426 path.display()
427 ));
428 return Ok(());
429 }
430 write_file(path, content).with_context(|| format!("Failed to write {}", path.display()))?;
431 Ok(())
432}