1use std::{
2 fs,
3 path::{Path, PathBuf},
4};
5
6use anyhow::{Context, Result};
7
8pub fn pascal_to_snake(name: &str) -> String {
12 let mut out = String::new();
13 for (i, ch) in name.char_indices() {
14 if ch.is_uppercase() && i > 0 {
15 out.push('_');
16 }
17 out.extend(ch.to_lowercase());
18 }
19 out
20}
21
22fn register_in_mod(dir: &Path, module_name: &str) -> Result<()> {
27 fs::create_dir_all(dir)?;
28
29 let mod_path = dir.join("mod.rs");
30 let declaration = format!("pub mod {module_name};");
31
32 let existing = if mod_path.exists() {
33 fs::read_to_string(&mod_path).with_context(|| format!("Failed to read {mod_path:?}"))?
34 } else {
35 String::new()
36 };
37
38 if existing.contains(&declaration) {
39 return Ok(()); }
41
42 let mut updated = existing;
43 if !updated.is_empty() && !updated.ends_with('\n') {
44 updated.push('\n');
45 }
46 updated.push_str(&declaration);
47 updated.push('\n');
48
49 fs::write(&mod_path, &updated).with_context(|| format!("Failed to write {mod_path:?}"))?;
50
51 Ok(())
52}
53
54pub fn make_controller(name: &str) -> Result<()> {
58 let snake = pascal_to_snake(name);
59 let dir = Path::new("src/controllers");
60 fs::create_dir_all(dir)?;
61
62 let file = dir.join(format!("{snake}.rs"));
63 if file.exists() {
64 anyhow::bail!("Controller already exists: {file:?}");
65 }
66
67 let template = format!(
68 r#"use axum::{{extract::Path, Json}};
69use craken_http::{{CrakenError, Inject, RequestContext}};
70use craken_macros::{{controller, delete, get, post, put}};
71use serde::{{Deserialize, Serialize}};
72
73// ── Replace with your domain types ───────────────────────────────────────────
74
75#[derive(Serialize)]
76pub struct Item {{
77 pub id: u64,
78}}
79
80pub struct {name};
81
82// ── Route handlers ────────────────────────────────────────────────────────────
83//
84// Methods annotated with #[get], #[post], etc. must NOT take a `self`
85// receiver — use Inject<T> or RequestContext to access services.
86//
87// Mount this controller:
88// HttpServer::new().configure_routes(&controllers::{snake}::{name})
89
90#[controller]
91impl {name} {{
92 /// GET /
93 #[get("/")]
94 pub async fn index() -> Result<Json<Vec<Item>>, CrakenError> {{
95 Ok(Json(vec![]))
96 }}
97
98 /// GET /:id
99 #[get("/:id")]
100 pub async fn show(Path(id): Path<u64>) -> Result<Json<Item>, CrakenError> {{
101 Ok(Json(Item {{ id }}))
102 }}
103}}
104"#
105 );
106
107 fs::write(&file, &template).with_context(|| format!("Failed to write {file:?}"))?;
108 register_in_mod(dir, &snake)?;
109
110 println!("✓ src/controllers/{snake}.rs");
111 println!(" Register: HttpServer::new().configure_routes(&controllers::{snake}::{name})");
112 Ok(())
113}
114
115pub fn make_service(name: &str) -> Result<()> {
119 let snake = pascal_to_snake(name);
120 let dir = Path::new("src/services");
121 fs::create_dir_all(dir)?;
122
123 let file = dir.join(format!("{snake}.rs"));
124 if file.exists() {
125 anyhow::bail!("Service already exists: {file:?}");
126 }
127
128 let template = format!(
129 r#"/// {name} — application-layer service.
130///
131/// Register as a singleton:
132/// ```rust,ignore
133/// container.register({name}::new());
134/// ```
135///
136/// Register as scoped (one instance per request):
137/// ```rust,ignore
138/// container.register_scoped({name}::new);
139/// ```
140pub struct {name};
141
142impl {name} {{
143 pub fn new() -> Self {{
144 Self
145 }}
146}}
147
148impl Default for {name} {{
149 fn default() -> Self {{
150 Self::new()
151 }}
152}}
153"#
154 );
155
156 fs::write(&file, &template).with_context(|| format!("Failed to write {file:?}"))?;
157 register_in_mod(dir, &snake)?;
158
159 println!("✓ src/services/{snake}.rs");
160 println!(" Register: container.register(services::{snake}::{name}::new())");
161 Ok(())
162}
163
164pub fn make_module(name: &str) -> Result<()> {
169 let snake = pascal_to_snake(name);
170 let module_dir = PathBuf::from(format!("src/modules/{snake}"));
171
172 if module_dir.exists() {
173 anyhow::bail!("Module already exists: {module_dir:?}");
174 }
175
176 fs::create_dir_all(&module_dir)?;
177
178 let mod_template = format!(
180 r#"pub mod controller;
181pub mod service;
182
183pub use controller::{name}Controller;
184pub use service::{name}Service;
185"#
186 );
187 fs::write(module_dir.join("mod.rs"), &mod_template)?;
188
189 let controller_template = format!(
191 r#"use axum::Json;
192use craken_http::{{CrakenError, Inject}};
193use craken_macros::{{controller, get, post}};
194use super::service::{name}Service;
195
196pub struct {name}Controller;
197
198#[controller]
199impl {name}Controller {{
200 #[get("/{snake}")]
201 pub async fn index(
202 _svc: Inject<{name}Service>,
203 ) -> Result<Json<Vec<serde_json::Value>>, CrakenError> {{
204 Ok(Json(vec![]))
205 }}
206}}
207"#
208 );
209 fs::write(module_dir.join("controller.rs"), &controller_template)?;
210
211 let service_template = format!(
213 r#"pub struct {name}Service;
214
215impl {name}Service {{
216 pub fn new() -> Self {{
217 Self
218 }}
219}}
220
221impl Default for {name}Service {{
222 fn default() -> Self {{
223 Self::new()
224 }}
225}}
226"#
227 );
228 fs::write(module_dir.join("service.rs"), &service_template)?;
229
230 register_in_mod(Path::new("src/modules"), &snake)?;
232
233 println!("✓ src/modules/{snake}/mod.rs");
234 println!(" src/modules/{snake}/controller.rs");
235 println!(" src/modules/{snake}/service.rs");
236 println!(" Registered in src/modules/mod.rs");
237 Ok(())
238}
239
240pub fn make_migration(name: &str) -> Result<()> {
244 let snake = pascal_to_snake(name);
245 let dir = Path::new("src/migrations");
246 fs::create_dir_all(dir)?;
247
248 let timestamp = chrono::Local::now().format("%Y%m%d%H%M%S");
249 let version = format!("{}_{}", timestamp, snake);
250 let file = dir.join(format!("{}.rs", version));
251
252 if file.exists() {
253 anyhow::bail!("Migration already exists: {file:?}");
254 }
255
256 let template = format!(
257 r#"use craken_database::{{async_trait::async_trait, Database, migration::Migration}};
258
259pub struct {name};
260
261#[async_trait]
262impl Migration for {name} {{
263 fn name(&self) -> &'static str {{
264 "{version}"
265 }}
266
267 async fn up(&self, db: &Database) -> anyhow::Result<()> {{
268 // Write your migration UP logic here
269 // sqlx::query("CREATE TABLE IF NOT EXISTS ...").execute(db.pool()).await?;
270 Ok(())
271 }}
272
273 async fn down(&self, db: &Database) -> anyhow::Result<()> {{
274 // Write your migration DOWN logic here
275 // sqlx::query("DROP TABLE IF EXISTS ...").execute(db.pool()).await?;
276 Ok(())
277 }}
278}}
279"#
280 );
281
282 fs::write(&file, &template).with_context(|| format!("Failed to write {file:?}"))?;
283 register_in_mod(dir, &version)?;
284
285 println!("✓ src/migrations/{}.rs", version);
286 Ok(())
287}
288
289pub fn make_app(name: &str) -> Result<()> {
293 let root = Path::new(name);
294 if root.exists() {
295 anyhow::bail!("Directory already exists: {root:?}");
296 }
297
298 fs::create_dir_all(root.join("src/controllers"))?;
299 fs::create_dir_all(root.join("src/services"))?;
300 fs::create_dir_all(root.join("src/migrations"))?;
301 fs::create_dir_all(root.join("config"))?;
302
303 let cargo_toml = format!(
305 r#"[package]
306name = "{name}"
307version = "0.1.0"
308edition = "2021"
309
310[dependencies]
311craken-core = {{ git = "https://github.com/shayyz-code/craken.git" }}
312craken-http = {{ git = "https://github.com/shayyz-code/craken.git" }}
313craken-container = {{ git = "https://github.com/shayyz-code/craken.git" }}
314craken-database = {{ git = "https://github.com/shayyz-code/craken.git" }}
315craken-macros = {{ git = "https://github.com/shayyz-code/craken.git" }}
316craken-logging = {{ git = "https://github.com/shayyz-code/craken.git" }}
317tokio = {{ version = "1.0", features = ["full"] }}
318axum = "0.7"
319serde = {{ version = "1.0", features = ["derive"] }}
320anyhow = "1.0"
321"#
322 );
323 fs::write(root.join("Cargo.toml"), cargo_toml)?;
324
325 let main_rs = r#"use std::sync::Arc;
327use craken_core::{App, ServiceProvider};
328use craken_container::Container;
329use craken_http::{HttpServer, LoggingMiddleware};
330use craken_database::{Database, migration::MigrationRunner};
331use craken_macros::get;
332
333// ── App Modules ──────────────────────────────────────────────────────────────
334
335mod migrations;
336mod controllers;
337mod services;
338
339// ── Simple Health Route ──────────────────────────────────────────────────────
340
341#[get("/health")]
342pub async fn health() -> &'static str {
343 "OK"
344}
345
346// ── Service Registration ─────────────────────────────────────────────────────
347
348pub struct AppServiceProvider {
349 db: Arc<Database>,
350}
351
352impl ServiceProvider for AppServiceProvider {
353 fn register(&self, c: &mut Container) {
354 c.register_arc(self.db.clone());
355 // Register your application services here
356 }
357}
358
359// ── Application Entry Point ──────────────────────────────────────────────────
360
361#[tokio::main]
362async fn main() -> anyhow::Result<()> {
363 craken_logging::Logging::init();
364
365 // Simple argument handling without manual clap in main.rs
366 let args: Vec<String> = std::env::args().collect();
367 let command = args.get(1).map(|s| s.as_str()).unwrap_or("serve");
368
369 let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
370 let db = Arc::new(Database::connect(&db_url).await?);
371
372 match command {
373 "serve" => {
374 let mut app = App::new();
375 app.register_services(&AppServiceProvider { db: db.clone() });
376 app.boot().await?;
377
378 let addr = args.get(2).map(|s| s.as_str()).unwrap_or("127.0.0.1:8080");
379 println!("🦀 Craken application starting on http://{}", addr);
380
381 HttpServer::new()
382 .with_middleware(LoggingMiddleware)
383 .configure_routes(&HealthRoute)
384 .run(app.into_container(), addr)
385 .await?;
386 }
387 "migrate" => {
388 let mut runner = MigrationRunner::new();
389 // TODO: Register migrations here
390 // runner.add(Box::new(migrations::m20231012_create_users::CreateUsers));
391 runner.run_pending(&db).await?;
392 println!("✓ Migrations complete");
393 }
394 "rollback" => {
395 let mut runner = MigrationRunner::new();
396 // TODO: Register migrations here
397 runner.rollback_last(&db).await?;
398 println!("✓ Rollback complete");
399 }
400 _ => {
401 println!("Unknown command. Use: serve, migrate, rollback");
402 }
403 }
404 Ok(())
405}
406"#;
407 fs::write(root.join("src/main.rs"), main_rs)?;
408
409 fs::write(
411 root.join(".env"),
412 "DATABASE_URL=postgres://postgres:password@localhost/my_app\n",
413 )?;
414
415 fs::write(root.join("src/controllers/mod.rs"), "")?;
417 fs::write(root.join("src/services/mod.rs"), "")?;
418 fs::write(root.join("src/migrations/mod.rs"), "")?;
419
420 println!("✓ Created new Craken project in: {}", name);
421 println!(" Next steps:");
422 println!(" cd {}", name);
423 println!(" cargo run -- serve");
424
425 Ok(())
426}
427
428pub fn scaffold_project(name: &str) -> Result<()> {
442 let root = Path::new(name);
443 if root.exists() {
444 anyhow::bail!("Directory '{name}' already exists");
445 }
446
447 for sub in &[
449 "src/controllers",
450 "src/services",
451 "src/modules",
452 "src/middleware",
453 "src/models",
454 ] {
455 fs::create_dir_all(root.join(sub))?;
456 }
457
458 let mod_stub = "// Auto-generated by Craken. Add `pub mod` declarations here.\n";
460 for sub in &["controllers", "services", "modules", "middleware", "models"] {
461 fs::write(root.join(format!("src/{sub}/mod.rs")), mod_stub)?;
462 }
463
464 let cargo = format!(
466 r#"[package]
467name = "{name}"
468version = "0.1.0"
469edition = "2021"
470
471[dependencies]
472craken-core = "0.1"
473craken-http = "0.1"
474craken-macros = "0.1"
475craken-logging = "0.1"
476tokio = {{ version = "1.0", features = ["full"] }}
477axum = "0.7"
478serde = {{ version = "1.0", features = ["derive"] }}
479serde_json = "1.0"
480anyhow = "1.0"
481tracing = "0.1"
482"#
483 );
484 fs::write(root.join("Cargo.toml"), cargo)?;
485
486 let main_rs = r#"mod controllers;
488mod services;
489mod modules;
490mod middleware;
491mod models;
492
493use craken_core::App;
494use craken_http::{HttpServer, LoggingMiddleware};
495
496#[tokio::main]
497async fn main() -> anyhow::Result<()> {
498 craken_logging::Logging::init();
499
500 let mut app = App::new();
501 app.boot().await?;
502 // app.register_services(&services::AppServiceProvider);
503
504 let container = app.into_container();
505
506 HttpServer::new()
507 .with_middleware(LoggingMiddleware)
508 // .configure_routes(&controllers::YourController)
509 .run(container, "127.0.0.1:8080")
510 .await?;
511
512 Ok(())
513}
514"#;
515 fs::write(root.join("src/main.rs"), main_rs)?;
516
517 println!("✓ Created project '{name}'");
518 println!(" cd {name} && cargo run -- serve");
519 Ok(())
520}