Skip to main content

ferro_rs/
app.rs

1//! Application builder for Ferro framework
2//!
3//! Provides a fluent builder API to configure and run a Ferro application.
4//!
5//! # Example
6//!
7//! ```rust,ignore
8//! use ferro_rs::Application;
9//!
10//! #[tokio::main]
11//! async fn main() {
12//!     Application::new()
13//!         .config(config::register_all)
14//!         .bootstrap(bootstrap::register)
15//!         .routes(routes::register)
16//!         .migrations::<migrations::Migrator>()
17//!         .run()
18//!         .await;
19//! }
20//! ```
21
22use 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
31/// Type alias for async bootstrap function
32type BootstrapFn = Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send>;
33
34/// CLI structure for Ferro applications
35#[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    /// Run the web server (default command)
46    Serve {
47        /// Skip running migrations on startup
48        #[arg(long)]
49        no_migrate: bool,
50    },
51    /// Run pending database migrations
52    #[command(name = "db:migrate")]
53    DbMigrate,
54    /// Show migration status
55    #[command(name = "db:status")]
56    DbStatus,
57    /// Rollback the last migration(s)
58    #[command(name = "db:rollback")]
59    DbRollback {
60        /// Number of migrations to rollback
61        #[arg(default_value = "1")]
62        steps: u32,
63    },
64    /// Drop all tables and re-run all migrations
65    #[command(name = "db:fresh")]
66    DbFresh,
67    /// Run the scheduler daemon (checks every minute)
68    #[command(name = "schedule:work")]
69    ScheduleWork,
70    /// Run all due scheduled tasks once
71    #[command(name = "schedule:run")]
72    ScheduleRun,
73    /// List all registered scheduled tasks
74    #[command(name = "schedule:list")]
75    ScheduleList,
76    /// Run database seeders
77    #[command(name = "db:seed")]
78    DbSeed {
79        /// Run only a specific seeder
80        #[arg(long)]
81        class: Option<String>,
82    },
83}
84
85/// Application builder for Ferro framework
86///
87/// Use this to configure and run your Ferro application with a fluent API.
88pub 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
99/// Placeholder type for when no migrator is configured
100pub struct NoMigrator;
101
102impl MigratorTrait for NoMigrator {
103    fn migrations() -> Vec<Box<dyn MigrationTrait>> {
104        vec![]
105    }
106}
107
108impl Application<NoMigrator> {
109    /// Create a new application builder
110    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    /// Register a configuration function
132    ///
133    /// This function is called early during startup to register
134    /// application configuration.
135    ///
136    /// # Example
137    ///
138    /// ```rust,ignore
139    /// App::new()
140    ///     .config(config::register_all)
141    /// ```
142    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    /// Register a bootstrap function
151    ///
152    /// This async function is called to register services, middleware,
153    /// and other application components.
154    ///
155    /// # Example
156    ///
157    /// ```rust,ignore
158    /// App::new()
159    ///     .bootstrap(bootstrap::register)
160    /// ```
161    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    /// Register a routes function
171    ///
172    /// This function returns the application's router configuration.
173    ///
174    /// # Example
175    ///
176    /// ```rust,ignore
177    /// App::new()
178    ///     .routes(routes::register)
179    /// ```
180    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    /// Configure the migrator type for database migrations
189    ///
190    /// # Example
191    ///
192    /// ```rust,ignore
193    /// Application::new()
194    ///     .migrations::<migrations::Migrator>()
195    /// ```
196    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    /// Register a seeders function
210    ///
211    /// This function returns the application's seeder registry for database seeding.
212    ///
213    /// # Example
214    ///
215    /// ```rust,ignore
216    /// Application::new()
217    ///     .seeders(seeders::register)
218    /// ```
219    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    /// Run the application
228    ///
229    /// This parses CLI arguments and executes the appropriate command:
230    /// - `serve` (default): Run the web server
231    /// - `db:migrate`: Run pending migrations
232    /// - `db:status`: Show migration status
233    /// - `db:rollback`: Rollback migrations
234    /// - `db:fresh`: Drop and re-run all migrations
235    /// - `schedule:*`: Scheduler commands
236    pub async fn run(self) {
237        let cli = Cli::parse();
238
239        // Initialize framework configuration (loads .env files)
240        Config::init(Path::new("."));
241
242        // Destructure self to avoid partial move issues
243        let Application {
244            config_fn,
245            bootstrap_fn,
246            routes_fn,
247            seeders_fn,
248            _migrator,
249        } = self;
250
251        // Run user's config registration
252        if let Some(config_fn) = config_fn {
253            config_fn();
254        }
255
256        // Initialize translator (after config so user can override LangConfig)
257        crate::lang::init::init();
258
259        match cli.command {
260            None | Some(Commands::Serve { no_migrate: false }) => {
261                // Default: run server with auto-migrate
262                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                // Run server without migrations
267                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        // Run bootstrap
340        if let Some(bootstrap_fn) = bootstrap_fn {
341            bootstrap_fn().await;
342        }
343
344        // Get router
345        let router = if let Some(routes_fn) = routes_fn {
346            routes_fn()
347        } else {
348            Router::new()
349        };
350
351        // Create server with configuration from environment
352        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        // For SQLite, ensure the database file can be created
362        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        // Run bootstrap for scheduler context
430        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        // Run bootstrap for scheduler context
450        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}