1use crate::seeder::SeederRegistry;
23use crate::{Config, Router, Server};
24use clap::{Parser, Subcommand};
25use sea_orm_migration::prelude::*;
26use std::env;
27use std::future::Future;
28use std::path::Path;
29use std::pin::Pin;
30
31type BootstrapFn = Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send>;
33
34#[derive(Parser)]
36#[command(name = "app")]
37#[command(about = "Ferro application server and utilities")]
38struct Cli {
39 #[command(subcommand)]
40 command: Option<Commands>,
41}
42
43#[derive(Subcommand)]
44enum Commands {
45 Serve {
47 #[arg(long)]
49 no_migrate: bool,
50 },
51 #[command(name = "db:migrate")]
53 DbMigrate,
54 #[command(name = "db:status")]
56 DbStatus,
57 #[command(name = "db:rollback")]
59 DbRollback {
60 #[arg(default_value = "1")]
62 steps: u32,
63 },
64 #[command(name = "db:fresh")]
66 DbFresh,
67 #[command(name = "schedule:work")]
69 ScheduleWork,
70 #[command(name = "schedule:run")]
72 ScheduleRun,
73 #[command(name = "schedule:list")]
75 ScheduleList,
76 #[command(name = "db:seed")]
78 DbSeed {
79 #[arg(long)]
81 class: Option<String>,
82 },
83}
84
85pub struct Application<M = NoMigrator>
89where
90 M: MigratorTrait,
91{
92 config_fn: Option<Box<dyn FnOnce()>>,
93 bootstrap_fn: Option<BootstrapFn>,
94 routes_fn: Option<Box<dyn FnOnce() -> Router + Send>>,
95 seeders_fn: Option<Box<dyn FnOnce() -> SeederRegistry + Send>>,
96 _migrator: std::marker::PhantomData<M>,
97}
98
99pub struct NoMigrator;
101
102impl MigratorTrait for NoMigrator {
103 fn migrations() -> Vec<Box<dyn MigrationTrait>> {
104 vec![]
105 }
106}
107
108impl Application<NoMigrator> {
109 pub fn new() -> Self {
111 Application {
112 config_fn: None,
113 bootstrap_fn: None,
114 routes_fn: None,
115 seeders_fn: None,
116 _migrator: std::marker::PhantomData,
117 }
118 }
119}
120
121impl Default for Application<NoMigrator> {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127impl<M> Application<M>
128where
129 M: MigratorTrait,
130{
131 pub fn config<F>(mut self, f: F) -> Self
143 where
144 F: FnOnce() + 'static,
145 {
146 self.config_fn = Some(Box::new(f));
147 self
148 }
149
150 pub fn bootstrap<F, Fut>(mut self, f: F) -> Self
162 where
163 F: FnOnce() -> Fut + Send + 'static,
164 Fut: Future<Output = ()> + Send + 'static,
165 {
166 self.bootstrap_fn = Some(Box::new(move || Box::pin(f())));
167 self
168 }
169
170 pub fn routes<F>(mut self, f: F) -> Self
181 where
182 F: FnOnce() -> Router + Send + 'static,
183 {
184 self.routes_fn = Some(Box::new(f));
185 self
186 }
187
188 pub fn migrations<NewM>(self) -> Application<NewM>
197 where
198 NewM: MigratorTrait,
199 {
200 Application {
201 config_fn: self.config_fn,
202 bootstrap_fn: self.bootstrap_fn,
203 routes_fn: self.routes_fn,
204 seeders_fn: self.seeders_fn,
205 _migrator: std::marker::PhantomData,
206 }
207 }
208
209 pub fn seeders<F>(mut self, f: F) -> Self
220 where
221 F: FnOnce() -> SeederRegistry + Send + 'static,
222 {
223 self.seeders_fn = Some(Box::new(f));
224 self
225 }
226
227 pub async fn run(self) {
237 let cli = Cli::parse();
238
239 Config::init(Path::new("."));
241
242 let Application {
244 config_fn,
245 bootstrap_fn,
246 routes_fn,
247 seeders_fn,
248 _migrator,
249 } = self;
250
251 if let Some(config_fn) = config_fn {
253 config_fn();
254 }
255
256 crate::lang::init::init();
258
259 match cli.command {
260 None | Some(Commands::Serve { no_migrate: false }) => {
261 Self::run_migrations_silent::<M>().await;
263 Self::run_server_internal(bootstrap_fn, routes_fn).await;
264 }
265 Some(Commands::Serve { no_migrate: true }) => {
266 Self::run_server_internal(bootstrap_fn, routes_fn).await;
268 }
269 Some(Commands::DbMigrate) => {
270 Self::run_migrations::<M>().await;
271 }
272 Some(Commands::DbStatus) => {
273 Self::show_migration_status::<M>().await;
274 }
275 Some(Commands::DbRollback { steps }) => {
276 Self::rollback_migrations::<M>(steps).await;
277 }
278 Some(Commands::DbFresh) => {
279 Self::fresh_migrations::<M>().await;
280 }
281 Some(Commands::ScheduleWork) => {
282 Self::run_scheduler_daemon_internal(bootstrap_fn).await;
283 }
284 Some(Commands::ScheduleRun) => {
285 Self::run_scheduled_tasks_internal(bootstrap_fn).await;
286 }
287 Some(Commands::ScheduleList) => {
288 Self::list_scheduled_tasks().await;
289 }
290 Some(Commands::DbSeed { class }) => {
291 Self::run_seeders(seeders_fn, class).await;
292 }
293 }
294 }
295
296 async fn run_seeders(
297 seeders_fn: Option<Box<dyn FnOnce() -> SeederRegistry + Send>>,
298 class: Option<String>,
299 ) {
300 let database_url = match std::env::var("DATABASE_URL") {
301 Ok(u) => u,
302 Err(_) => {
303 eprintln!("DATABASE_URL must be set");
304 std::process::exit(1);
305 }
306 };
307 let db = match sea_orm::Database::connect(&database_url).await {
308 Ok(c) => c,
309 Err(e) => {
310 eprintln!("Failed to connect to database: {e}");
311 std::process::exit(1);
312 }
313 };
314
315 let registry = match seeders_fn {
316 Some(f) => f(),
317 None => {
318 eprintln!("No seeders registered.");
319 eprintln!("Register seeders with .seeders(seeders::register) in main.rs");
320 return;
321 }
322 };
323
324 let result = match class {
325 Some(name) => registry.run_one(&name, &db).await,
326 None => registry.run_all(&db).await,
327 };
328
329 if let Err(e) = result {
330 eprintln!("Seeding failed: {e}");
331 std::process::exit(1);
332 }
333 }
334
335 async fn run_server_internal(
336 bootstrap_fn: Option<BootstrapFn>,
337 routes_fn: Option<Box<dyn FnOnce() -> Router + Send>>,
338 ) {
339 if let Some(bootstrap_fn) = bootstrap_fn {
341 bootstrap_fn().await;
342 }
343
344 let router = if let Some(routes_fn) = routes_fn {
346 routes_fn()
347 } else {
348 Router::new()
349 };
350
351 Server::from_config(router)
353 .run()
354 .await
355 .expect("Failed to start server");
356 }
357
358 async fn get_database_connection() -> sea_orm::DatabaseConnection {
359 let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
360
361 let database_url = if database_url.starts_with("sqlite://") {
363 let path = database_url.trim_start_matches("sqlite://");
364 let path = path.trim_start_matches("./");
365
366 if let Some(parent) = Path::new(path).parent() {
367 if !parent.as_os_str().is_empty() {
368 std::fs::create_dir_all(parent).ok();
369 }
370 }
371
372 if !Path::new(path).exists() {
373 std::fs::File::create(path).ok();
374 }
375
376 format!("sqlite:{path}?mode=rwc")
377 } else {
378 database_url
379 };
380
381 sea_orm::Database::connect(&database_url)
382 .await
383 .expect("Failed to connect to database")
384 }
385
386 async fn run_migrations_silent<Migrator: MigratorTrait>() {
387 let db = Self::get_database_connection().await;
388 if let Err(e) = Migrator::up(&db, None).await {
389 eprintln!("Warning: Migration failed: {e}");
390 }
391 }
392
393 async fn run_migrations<Migrator: MigratorTrait>() {
394 println!("Running migrations...");
395 let db = Self::get_database_connection().await;
396 Migrator::up(&db, None)
397 .await
398 .expect("Failed to run migrations");
399 println!("Migrations completed successfully!");
400 }
401
402 async fn show_migration_status<Migrator: MigratorTrait>() {
403 println!("Migration status:");
404 let db = Self::get_database_connection().await;
405 Migrator::status(&db)
406 .await
407 .expect("Failed to get migration status");
408 }
409
410 async fn rollback_migrations<Migrator: MigratorTrait>(steps: u32) {
411 println!("Rolling back {steps} migration(s)...");
412 let db = Self::get_database_connection().await;
413 Migrator::down(&db, Some(steps))
414 .await
415 .expect("Failed to rollback migrations");
416 println!("Rollback completed successfully!");
417 }
418
419 async fn fresh_migrations<Migrator: MigratorTrait>() {
420 println!("WARNING: Dropping all tables and re-running migrations...");
421 let db = Self::get_database_connection().await;
422 Migrator::fresh(&db)
423 .await
424 .expect("Failed to refresh database");
425 println!("Database refreshed successfully!");
426 }
427
428 async fn run_scheduler_daemon_internal(bootstrap_fn: Option<BootstrapFn>) {
429 if let Some(bootstrap_fn) = bootstrap_fn {
431 bootstrap_fn().await;
432 }
433
434 println!("==============================================");
435 println!(" Ferro Scheduler Daemon");
436 println!("==============================================");
437 println!();
438 println!(" Note: Create tasks with `ferro make:task <name>`");
439 println!(" Press Ctrl+C to stop");
440 println!();
441 println!("==============================================");
442
443 eprintln!("Scheduler daemon is not yet configured.");
444 eprintln!("Create a scheduled task with: ferro make:task <name>");
445 eprintln!("Then register it in src/schedule.rs");
446 }
447
448 async fn run_scheduled_tasks_internal(bootstrap_fn: Option<BootstrapFn>) {
449 if let Some(bootstrap_fn) = bootstrap_fn {
451 bootstrap_fn().await;
452 }
453
454 println!("Running scheduled tasks...");
455 eprintln!("Scheduler is not yet configured.");
456 eprintln!("Create a scheduled task with: ferro make:task <name>");
457 }
458
459 async fn list_scheduled_tasks() {
460 println!("Registered scheduled tasks:");
461 println!();
462 eprintln!("No scheduled tasks registered.");
463 eprintln!("Create a scheduled task with: ferro make:task <name>");
464 }
465}