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 #[cfg(feature = "json-ui")]
85 #[command(name = "json-ui:schema")]
86 JsonUiSchema {
87 #[arg(long, short = 'o')]
89 output: Option<String>,
90
91 #[arg(long)]
93 pretty: bool,
94
95 #[arg(long)]
97 component: Option<String>,
98 },
99}
100
101pub struct Application<M = NoMigrator>
105where
106 M: MigratorTrait,
107{
108 config_fn: Option<Box<dyn FnOnce()>>,
109 bootstrap_fn: Option<BootstrapFn>,
110 routes_fn: Option<Box<dyn FnOnce() -> Router + Send>>,
111 seeders_fn: Option<Box<dyn FnOnce() -> SeederRegistry + Send>>,
112 _migrator: std::marker::PhantomData<M>,
113}
114
115pub struct NoMigrator;
117
118impl MigratorTrait for NoMigrator {
119 fn migrations() -> Vec<Box<dyn MigrationTrait>> {
120 vec![]
121 }
122}
123
124impl Application<NoMigrator> {
125 pub fn new() -> Self {
127 Application {
128 config_fn: None,
129 bootstrap_fn: None,
130 routes_fn: None,
131 seeders_fn: None,
132 _migrator: std::marker::PhantomData,
133 }
134 }
135}
136
137impl Default for Application<NoMigrator> {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143impl<M> Application<M>
144where
145 M: MigratorTrait,
146{
147 pub fn config<F>(mut self, f: F) -> Self
159 where
160 F: FnOnce() + 'static,
161 {
162 self.config_fn = Some(Box::new(f));
163 self
164 }
165
166 pub fn bootstrap<F, Fut>(mut self, f: F) -> Self
178 where
179 F: FnOnce() -> Fut + Send + 'static,
180 Fut: Future<Output = ()> + Send + 'static,
181 {
182 self.bootstrap_fn = Some(Box::new(move || Box::pin(f())));
183 self
184 }
185
186 pub fn routes<F>(mut self, f: F) -> Self
197 where
198 F: FnOnce() -> Router + Send + 'static,
199 {
200 self.routes_fn = Some(Box::new(f));
201 self
202 }
203
204 pub fn migrations<NewM>(self) -> Application<NewM>
213 where
214 NewM: MigratorTrait,
215 {
216 Application {
217 config_fn: self.config_fn,
218 bootstrap_fn: self.bootstrap_fn,
219 routes_fn: self.routes_fn,
220 seeders_fn: self.seeders_fn,
221 _migrator: std::marker::PhantomData,
222 }
223 }
224
225 pub fn seeders<F>(mut self, f: F) -> Self
236 where
237 F: FnOnce() -> SeederRegistry + Send + 'static,
238 {
239 self.seeders_fn = Some(Box::new(f));
240 self
241 }
242
243 pub async fn run(self) {
253 let cli = Cli::parse();
254
255 Config::init(Path::new("."));
257
258 let Application {
260 config_fn,
261 bootstrap_fn,
262 routes_fn,
263 seeders_fn,
264 _migrator,
265 } = self;
266
267 if let Some(config_fn) = config_fn {
269 config_fn();
270 }
271
272 crate::lang::init::init();
274
275 match cli.command {
276 None | Some(Commands::Serve { no_migrate: false }) => {
277 Self::run_migrations_silent::<M>().await;
279 Self::run_server_internal(bootstrap_fn, routes_fn).await;
280 }
281 Some(Commands::Serve { no_migrate: true }) => {
282 Self::run_server_internal(bootstrap_fn, routes_fn).await;
284 }
285 Some(Commands::DbMigrate) => {
286 Self::run_migrations::<M>().await;
287 }
288 Some(Commands::DbStatus) => {
289 Self::show_migration_status::<M>().await;
290 }
291 Some(Commands::DbRollback { steps }) => {
292 Self::rollback_migrations::<M>(steps).await;
293 }
294 Some(Commands::DbFresh) => {
295 Self::fresh_migrations::<M>().await;
296 }
297 Some(Commands::ScheduleWork) => {
298 Self::run_scheduler_daemon_internal(bootstrap_fn).await;
299 }
300 Some(Commands::ScheduleRun) => {
301 Self::run_scheduled_tasks_internal(bootstrap_fn).await;
302 }
303 Some(Commands::ScheduleList) => {
304 Self::list_scheduled_tasks().await;
305 }
306 Some(Commands::DbSeed { class }) => {
307 Self::run_seeders(seeders_fn, class).await;
308 }
309 #[cfg(feature = "json-ui")]
310 Some(Commands::JsonUiSchema {
311 output,
312 pretty,
313 component,
314 }) => {
315 Self::run_json_ui_schema(output, pretty, component).await;
316 }
317 }
318 }
319
320 #[cfg(feature = "json-ui")]
321 async fn run_json_ui_schema(output: Option<String>, pretty: bool, component: Option<String>) {
322 let catalog = match ferro_json_ui::Catalog::build() {
325 Ok(c) => c,
326 Err(e) => {
327 eprintln!("error building catalog: {e}");
328 std::process::exit(1);
329 }
330 };
331
332 let value: &serde_json::Value = match &component {
333 Some(name) => match catalog.component_schema(name) {
334 Some(v) => v,
335 None => {
336 eprintln!("error: unknown component '{name}'");
337 std::process::exit(1);
338 }
339 },
340 None => catalog.json_schema(),
341 };
342
343 let _ = pretty;
347 let serialized = serde_json::to_string_pretty(value).expect("schema serializes");
348
349 match output {
350 Some(path) => {
351 if let Err(e) = std::fs::write(&path, serialized) {
352 eprintln!("error writing to {path}: {e}");
353 std::process::exit(1);
354 }
355 }
356 None => println!("{serialized}"),
357 }
358 }
359
360 async fn run_seeders(
361 seeders_fn: Option<Box<dyn FnOnce() -> SeederRegistry + Send>>,
362 class: Option<String>,
363 ) {
364 let database_url = match std::env::var("DATABASE_URL") {
365 Ok(u) => u,
366 Err(_) => {
367 eprintln!("DATABASE_URL must be set");
368 std::process::exit(1);
369 }
370 };
371 let db = match sea_orm::Database::connect(&database_url).await {
372 Ok(c) => c,
373 Err(e) => {
374 eprintln!("Failed to connect to database: {e}");
375 std::process::exit(1);
376 }
377 };
378
379 let registry = match seeders_fn {
380 Some(f) => f(),
381 None => {
382 eprintln!("No seeders registered.");
383 eprintln!("Register seeders with .seeders(seeders::register) in main.rs");
384 return;
385 }
386 };
387
388 let result = match class {
389 Some(name) => registry.run_one(&name, &db).await,
390 None => registry.run_all(&db).await,
391 };
392
393 if let Err(e) = result {
394 eprintln!("Seeding failed: {e}");
395 std::process::exit(1);
396 }
397 }
398
399 async fn run_server_internal(
400 bootstrap_fn: Option<BootstrapFn>,
401 routes_fn: Option<Box<dyn FnOnce() -> Router + Send>>,
402 ) {
403 if let Some(bootstrap_fn) = bootstrap_fn {
405 bootstrap_fn().await;
406 }
407
408 let router = if let Some(routes_fn) = routes_fn {
410 routes_fn()
411 } else {
412 Router::new()
413 };
414
415 if let Err(e) = Server::from_config(router).run().await {
417 eprintln!("Failed to start server: {e}");
418 std::process::exit(1);
419 }
420 }
421
422 async fn get_database_connection() -> sea_orm::DatabaseConnection {
423 let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
424
425 let database_url = if database_url.starts_with("sqlite://") {
427 let path = database_url.trim_start_matches("sqlite://");
428 let path = path.trim_start_matches("./");
429
430 if let Some(parent) = Path::new(path).parent() {
431 if !parent.as_os_str().is_empty() {
432 std::fs::create_dir_all(parent).ok();
433 }
434 }
435
436 if !Path::new(path).exists() {
437 std::fs::File::create(path).ok();
438 }
439
440 format!("sqlite:{path}?mode=rwc")
441 } else {
442 database_url
443 };
444
445 sea_orm::Database::connect(&database_url)
446 .await
447 .expect("Failed to connect to database")
448 }
449
450 async fn run_migrations_silent<Migrator: MigratorTrait>() {
457 let db = Self::get_database_connection().await;
458 if let Err(e) = Migrator::up(&db, None).await {
459 eprintln!("Migration failed: {e}");
460 std::process::exit(1);
461 }
462 }
463
464 async fn run_migrations<Migrator: MigratorTrait>() {
465 println!("Running migrations...");
466 let db = Self::get_database_connection().await;
467 Migrator::up(&db, None)
468 .await
469 .expect("Failed to run migrations");
470 println!("Migrations completed successfully!");
471 }
472
473 async fn show_migration_status<Migrator: MigratorTrait>() {
474 println!("Migration status:");
475 let db = Self::get_database_connection().await;
476 Migrator::status(&db)
477 .await
478 .expect("Failed to get migration status");
479 }
480
481 async fn rollback_migrations<Migrator: MigratorTrait>(steps: u32) {
482 println!("Rolling back {steps} migration(s)...");
483 let db = Self::get_database_connection().await;
484 Migrator::down(&db, Some(steps))
485 .await
486 .expect("Failed to rollback migrations");
487 println!("Rollback completed successfully!");
488 }
489
490 async fn fresh_migrations<Migrator: MigratorTrait>() {
491 println!("WARNING: Dropping all tables and re-running migrations...");
492 let db = Self::get_database_connection().await;
493 Migrator::fresh(&db)
494 .await
495 .expect("Failed to refresh database");
496 println!("Database refreshed successfully!");
497 }
498
499 async fn run_scheduler_daemon_internal(bootstrap_fn: Option<BootstrapFn>) {
500 if let Some(bootstrap_fn) = bootstrap_fn {
502 bootstrap_fn().await;
503 }
504
505 println!("==============================================");
506 println!(" Ferro Scheduler Daemon");
507 println!("==============================================");
508 println!();
509 println!(" Note: Create tasks with `ferro make:task <name>`");
510 println!(" Press Ctrl+C to stop");
511 println!();
512 println!("==============================================");
513
514 eprintln!("Scheduler daemon is not yet configured.");
515 eprintln!("Create a scheduled task with: ferro make:task <name>");
516 eprintln!("Then register it in src/schedule.rs");
517 }
518
519 async fn run_scheduled_tasks_internal(bootstrap_fn: Option<BootstrapFn>) {
520 if let Some(bootstrap_fn) = bootstrap_fn {
522 bootstrap_fn().await;
523 }
524
525 println!("Running scheduled tasks...");
526 eprintln!("Scheduler is not yet configured.");
527 eprintln!("Create a scheduled task with: ferro make:task <name>");
528 }
529
530 async fn list_scheduled_tasks() {
531 println!("Registered scheduled tasks:");
532 println!();
533 eprintln!("No scheduled tasks registered.");
534 eprintln!("Create a scheduled task with: ferro make:task <name>");
535 }
536}