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        // Initialize database connection for seeders
301        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        // Run bootstrap
332        if let Some(bootstrap_fn) = bootstrap_fn {
333            bootstrap_fn().await;
334        }
335
336        // Get router
337        let router = if let Some(routes_fn) = routes_fn {
338            routes_fn()
339        } else {
340            Router::new()
341        };
342
343        // Create server with configuration from environment
344        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        // For SQLite, ensure the database file can be created
354        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        // Run bootstrap for scheduler context
422        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        // Run bootstrap for scheduler context
442        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}