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 ensure_dir, is_json_output, print_info, print_success, print_warning, write_file,
11 TIDEWAY_VERSION,
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 needs_arc: false,
81 has_config: false,
82 };
83
84 let engine = BackendTemplateEngine::new(context)?;
86
87 generate_shared(&engine, output_path, &args)?;
89
90 generate_entities(&engine, output_path, &args)?;
92
93 generate_auth(&engine, output_path, &args)?;
95
96 generate_billing(&engine, output_path, &args)?;
98
99 if has_organizations {
101 generate_organizations(&engine, output_path, &args)?;
102 }
103
104 generate_admin(&engine, output_path, &args)?;
106
107 generate_migrations(&engine, migrations_path, &args)?;
109
110 if !is_json_output() {
111 println!(
112 "\n{} Backend scaffolding generated successfully!\n",
113 "✓".green().bold()
114 );
115
116 println!("{}", "Next steps:".yellow().bold());
118 println!(" 1. Add dependencies to Cargo.toml:");
119 println!(
120 " tideway = {{ version = \"{}\", features = [\"auth\", \"auth-mfa\", \"database\", \"billing\", \"billing-seaorm\", \"organizations\", \"admin\"] }}",
121 TIDEWAY_VERSION
122 );
123 println!(" axum = {{ version = \"0.8\", features = [\"macros\"] }}");
124 println!(" sea-orm = {{ version = \"1.1\", features = [\"sqlx-postgres\", \"runtime-tokio-rustls\"] }}");
125 println!(" tokio = {{ version = \"1\", features = [\"full\"] }}");
126 println!(" serde = {{ version = \"1\", features = [\"derive\"] }}");
127 println!(" serde_json = \"1\"");
128 println!(" tracing = \"0.1\"");
129 println!(" async-trait = \"0.1\"");
130 println!(" chrono = {{ version = \"0.4\", features = [\"serde\"] }}");
131 println!(" uuid = {{ version = \"1\", features = [\"v4\", \"serde\"] }}");
132 println!();
133 println!(" 2. Run migrations:");
134 println!(" sea-orm-cli migrate up");
135 println!();
136 println!(" 3. Start the server:");
137 println!(" cargo run");
138 println!();
139 }
140
141 Ok(())
142}
143
144fn generate_shared(
145 engine: &BackendTemplateEngine,
146 output_path: &Path,
147 args: &BackendArgs,
148) -> Result<()> {
149 if engine.has_template("shared/main") {
151 let content = engine.render("shared/main")?;
152 let file_path = output_path.join("main.rs");
153 write_file_with_force(&file_path, &content, args.force)?;
154 print_success("Generated main.rs");
155 }
156
157 if engine.has_template("shared/lib") {
159 let content = engine.render("shared/lib")?;
160 let file_path = output_path.join("lib.rs");
161 write_file_with_force(&file_path, &content, args.force)?;
162 print_success("Generated lib.rs");
163 }
164
165 if engine.has_template("shared/config") {
167 let content = engine.render("shared/config")?;
168 let file_path = output_path.join("config.rs");
169 write_file_with_force(&file_path, &content, args.force)?;
170 print_success("Generated config.rs");
171 }
172
173 if engine.has_template("shared/error") {
175 let content = engine.render("shared/error")?;
176 let file_path = output_path.join("error.rs");
177 write_file_with_force(&file_path, &content, args.force)?;
178 print_success("Generated error.rs");
179 }
180
181 Ok(())
182}
183
184fn generate_entities(
185 engine: &BackendTemplateEngine,
186 output_path: &Path,
187 args: &BackendArgs,
188) -> Result<()> {
189 let entities_path = output_path.join("entities");
190 ensure_dir(&entities_path)?;
191
192 if engine.has_template("entities/mod") {
194 let content = engine.render("entities/mod")?;
195 let file_path = entities_path.join("mod.rs");
196 write_file_with_force(&file_path, &content, args.force)?;
197 print_success("Generated entities/mod.rs");
198 }
199
200 if engine.has_template("entities/prelude") {
202 let content = engine.render("entities/prelude")?;
203 let file_path = entities_path.join("prelude.rs");
204 write_file_with_force(&file_path, &content, args.force)?;
205 print_success("Generated entities/prelude.rs");
206 }
207
208 let core_entities = [
210 ("user.rs", "entities/user"),
211 ("refresh_token_family.rs", "entities/refresh_token_family"),
212 ("verification_token.rs", "entities/verification_token"),
213 ];
214
215 for (filename, template_name) in core_entities {
216 if engine.has_template(template_name) {
217 let content = engine.render(template_name)?;
218 let file_path = entities_path.join(filename);
219 write_file_with_force(&file_path, &content, args.force)?;
220 print_success(&format!("Generated entities/{}", filename));
221 }
222 }
223
224 if args.preset == BackendPreset::B2b {
226 let org_entities = [
227 ("organization.rs", "entities/organization"),
228 ("membership.rs", "entities/membership"),
229 ];
230
231 for (filename, template_name) in org_entities {
232 if engine.has_template(template_name) {
233 let content = engine.render(template_name)?;
234 let file_path = entities_path.join(filename);
235 write_file_with_force(&file_path, &content, args.force)?;
236 print_success(&format!("Generated entities/{}", filename));
237 }
238 }
239 }
240
241 Ok(())
242}
243
244fn generate_auth(
245 engine: &BackendTemplateEngine,
246 output_path: &Path,
247 args: &BackendArgs,
248) -> Result<()> {
249 let auth_path = output_path.join("auth");
250 ensure_dir(&auth_path)?;
251
252 let templates = [
253 ("mod.rs", "auth/mod"),
254 ("routes.rs", "auth/routes"),
255 ("store.rs", "auth/store"),
256 ];
257
258 for (filename, template_name) in templates {
259 if engine.has_template(template_name) {
260 let content = engine.render(template_name)?;
261 let file_path = auth_path.join(filename);
262 write_file_with_force(&file_path, &content, args.force)?;
263 print_success(&format!("Generated auth/{}", filename));
264 }
265 }
266
267 Ok(())
268}
269
270fn generate_billing(
271 engine: &BackendTemplateEngine,
272 output_path: &Path,
273 args: &BackendArgs,
274) -> Result<()> {
275 let billing_path = output_path.join("billing");
276 ensure_dir(&billing_path)?;
277
278 let templates = [
279 ("mod.rs", "billing/mod"),
280 ("routes.rs", "billing/routes"),
281 ("store.rs", "billing/store"),
282 ];
283
284 for (filename, template_name) in templates {
285 if engine.has_template(template_name) {
286 let content = engine.render(template_name)?;
287 let file_path = billing_path.join(filename);
288 write_file_with_force(&file_path, &content, args.force)?;
289 print_success(&format!("Generated billing/{}", filename));
290 }
291 }
292
293 Ok(())
294}
295
296fn generate_organizations(
297 engine: &BackendTemplateEngine,
298 output_path: &Path,
299 args: &BackendArgs,
300) -> Result<()> {
301 let orgs_path = output_path.join("organizations");
302 ensure_dir(&orgs_path)?;
303
304 let templates = [
305 ("mod.rs", "organizations/mod"),
306 ("routes.rs", "organizations/routes"),
307 ("store.rs", "organizations/store"),
308 ];
309
310 for (filename, template_name) in templates {
311 if engine.has_template(template_name) {
312 let content = engine.render(template_name)?;
313 let file_path = orgs_path.join(filename);
314 write_file_with_force(&file_path, &content, args.force)?;
315 print_success(&format!("Generated organizations/{}", filename));
316 }
317 }
318
319 Ok(())
320}
321
322fn generate_admin(
323 engine: &BackendTemplateEngine,
324 output_path: &Path,
325 args: &BackendArgs,
326) -> Result<()> {
327 let admin_path = output_path.join("admin");
328 ensure_dir(&admin_path)?;
329
330 let templates = [
331 ("mod.rs", "admin/mod"),
332 ("routes.rs", "admin/routes"),
333 ("store.rs", "admin/store"),
334 ];
335
336 for (filename, template_name) in templates {
337 if engine.has_template(template_name) {
338 let content = engine.render(template_name)?;
339 let file_path = admin_path.join(filename);
340 write_file_with_force(&file_path, &content, args.force)?;
341 print_success(&format!("Generated admin/{}", filename));
342 }
343 }
344
345 Ok(())
346}
347
348fn generate_migrations(
349 engine: &BackendTemplateEngine,
350 migrations_path: &Path,
351 args: &BackendArgs,
352) -> Result<()> {
353 if engine.has_template("migrations/lib") {
355 let content = engine.render("migrations/lib")?;
356 let file_path = migrations_path.join("lib.rs");
357 write_file_with_force(&file_path, &content, args.force)?;
358 print_success("Generated migration/src/lib.rs");
359 }
360
361 let core_migrations = [
363 ("m001_create_users.rs", "migrations/m001_create_users"),
364 (
365 "m002_create_refresh_token_families.rs",
366 "migrations/m002_create_refresh_token_families",
367 ),
368 (
369 "m003_create_verification_tokens.rs",
370 "migrations/m003_create_verification_tokens",
371 ),
372 ("m004_create_billing.rs", "migrations/m004_create_billing"),
373 ];
374
375 for (filename, template_name) in core_migrations {
376 if engine.has_template(template_name) {
377 let content = engine.render(template_name)?;
378 let file_path = migrations_path.join(filename);
379 write_file_with_force(&file_path, &content, args.force)?;
380 print_success(&format!("Generated migration/src/{}", filename));
381 }
382 }
383
384 if args.preset == BackendPreset::B2b {
386 let b2b_migrations = [
387 (
388 "m005_create_organizations.rs",
389 "migrations/m005_create_organizations",
390 ),
391 (
392 "m006_create_memberships.rs",
393 "migrations/m006_create_memberships",
394 ),
395 ("m007_add_admin_flag.rs", "migrations/m007_add_admin_flag"),
396 ];
397
398 for (filename, template_name) in b2b_migrations {
399 if engine.has_template(template_name) {
400 let content = engine.render(template_name)?;
401 let file_path = migrations_path.join(filename);
402 write_file_with_force(&file_path, &content, args.force)?;
403 print_success(&format!("Generated migration/src/{}", filename));
404 }
405 }
406 } else {
407 if engine.has_template("migrations/m005_add_admin_flag") {
409 let content = engine.render("migrations/m005_add_admin_flag")?;
410 let file_path = migrations_path.join("m005_add_admin_flag.rs");
411 write_file_with_force(&file_path, &content, args.force)?;
412 print_success("Generated migration/src/m005_add_admin_flag.rs");
413 }
414 }
415
416 Ok(())
417}
418
419fn write_file_with_force(path: &Path, content: &str, force: bool) -> Result<()> {
420 if path.exists() && !force {
421 print_warning(&format!(
422 "Skipping {} (use --force to overwrite)",
423 path.display()
424 ));
425 return Ok(());
426 }
427 write_file(path, content).with_context(|| format!("Failed to write {}", path.display()))?;
428 Ok(())
429}