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 config = crate::database::DatabaseConfig::from_env();
302 if let Err(e) = crate::database::DB::init_with(config).await {
303 eprintln!("Failed to connect to database: {e}");
304 std::process::exit(1);
305 }
306
307 let registry = match seeders_fn {
308 Some(f) => f(),
309 None => {
310 eprintln!("No seeders registered.");
311 eprintln!("Register seeders with .seeders(seeders::register) in main.rs");
312 return;
313 }
314 };
315
316 let result = match class {
317 Some(name) => registry.run_one(&name).await,
318 None => registry.run_all().await,
319 };
320
321 if let Err(e) = result {
322 eprintln!("Seeding failed: {e}");
323 std::process::exit(1);
324 }
325 }
326
327 async fn run_server_internal(
328 bootstrap_fn: Option<BootstrapFn>,
329 routes_fn: Option<Box<dyn FnOnce() -> Router + Send>>,
330 ) {
331 if let Some(bootstrap_fn) = bootstrap_fn {
333 bootstrap_fn().await;
334 }
335
336 let router = if let Some(routes_fn) = routes_fn {
338 routes_fn()
339 } else {
340 Router::new()
341 };
342
343 Server::from_config(router)
345 .run()
346 .await
347 .expect("Failed to start server");
348 }
349
350 async fn get_database_connection() -> sea_orm::DatabaseConnection {
351 let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
352
353 let database_url = if database_url.starts_with("sqlite://") {
355 let path = database_url.trim_start_matches("sqlite://");
356 let path = path.trim_start_matches("./");
357
358 if let Some(parent) = Path::new(path).parent() {
359 if !parent.as_os_str().is_empty() {
360 std::fs::create_dir_all(parent).ok();
361 }
362 }
363
364 if !Path::new(path).exists() {
365 std::fs::File::create(path).ok();
366 }
367
368 format!("sqlite:{path}?mode=rwc")
369 } else {
370 database_url
371 };
372
373 sea_orm::Database::connect(&database_url)
374 .await
375 .expect("Failed to connect to database")
376 }
377
378 async fn run_migrations_silent<Migrator: MigratorTrait>() {
379 let db = Self::get_database_connection().await;
380 if let Err(e) = Migrator::up(&db, None).await {
381 eprintln!("Warning: Migration failed: {e}");
382 }
383 }
384
385 async fn run_migrations<Migrator: MigratorTrait>() {
386 println!("Running migrations...");
387 let db = Self::get_database_connection().await;
388 Migrator::up(&db, None)
389 .await
390 .expect("Failed to run migrations");
391 println!("Migrations completed successfully!");
392 }
393
394 async fn show_migration_status<Migrator: MigratorTrait>() {
395 println!("Migration status:");
396 let db = Self::get_database_connection().await;
397 Migrator::status(&db)
398 .await
399 .expect("Failed to get migration status");
400 }
401
402 async fn rollback_migrations<Migrator: MigratorTrait>(steps: u32) {
403 println!("Rolling back {steps} migration(s)...");
404 let db = Self::get_database_connection().await;
405 Migrator::down(&db, Some(steps))
406 .await
407 .expect("Failed to rollback migrations");
408 println!("Rollback completed successfully!");
409 }
410
411 async fn fresh_migrations<Migrator: MigratorTrait>() {
412 println!("WARNING: Dropping all tables and re-running migrations...");
413 let db = Self::get_database_connection().await;
414 Migrator::fresh(&db)
415 .await
416 .expect("Failed to refresh database");
417 println!("Database refreshed successfully!");
418 }
419
420 async fn run_scheduler_daemon_internal(bootstrap_fn: Option<BootstrapFn>) {
421 if let Some(bootstrap_fn) = bootstrap_fn {
423 bootstrap_fn().await;
424 }
425
426 println!("==============================================");
427 println!(" Ferro Scheduler Daemon");
428 println!("==============================================");
429 println!();
430 println!(" Note: Create tasks with `ferro make:task <name>`");
431 println!(" Press Ctrl+C to stop");
432 println!();
433 println!("==============================================");
434
435 eprintln!("Scheduler daemon is not yet configured.");
436 eprintln!("Create a scheduled task with: ferro make:task <name>");
437 eprintln!("Then register it in src/schedule.rs");
438 }
439
440 async fn run_scheduled_tasks_internal(bootstrap_fn: Option<BootstrapFn>) {
441 if let Some(bootstrap_fn) = bootstrap_fn {
443 bootstrap_fn().await;
444 }
445
446 println!("Running scheduled tasks...");
447 eprintln!("Scheduler is not yet configured.");
448 eprintln!("Create a scheduled task with: ferro make:task <name>");
449 }
450
451 async fn list_scheduled_tasks() {
452 println!("Registered scheduled tasks:");
453 println!();
454 eprintln!("No scheduled tasks registered.");
455 eprintln!("Create a scheduled task with: ferro make:task <name>");
456 }
457}