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