use crate::seeder::SeederRegistry;
use crate::{Config, Router, Server};
use clap::{Parser, Subcommand};
use sea_orm_migration::prelude::*;
use std::env;
use std::future::Future;
use std::path::Path;
use std::pin::Pin;
type BootstrapFn = Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send>;
#[derive(Parser)]
#[command(name = "app")]
#[command(about = "Ferro application server and utilities")]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
Serve {
#[arg(long)]
no_migrate: bool,
},
#[command(name = "db:migrate")]
DbMigrate,
#[command(name = "db:status")]
DbStatus,
#[command(name = "db:rollback")]
DbRollback {
#[arg(default_value = "1")]
steps: u32,
},
#[command(name = "db:fresh")]
DbFresh,
#[command(name = "schedule:work")]
ScheduleWork,
#[command(name = "schedule:run")]
ScheduleRun,
#[command(name = "schedule:list")]
ScheduleList,
#[command(name = "db:seed")]
DbSeed {
#[arg(long)]
class: Option<String>,
},
}
pub struct Application<M = NoMigrator>
where
M: MigratorTrait,
{
config_fn: Option<Box<dyn FnOnce()>>,
bootstrap_fn: Option<BootstrapFn>,
routes_fn: Option<Box<dyn FnOnce() -> Router + Send>>,
seeders_fn: Option<Box<dyn FnOnce() -> SeederRegistry + Send>>,
_migrator: std::marker::PhantomData<M>,
}
pub struct NoMigrator;
impl MigratorTrait for NoMigrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![]
}
}
impl Application<NoMigrator> {
pub fn new() -> Self {
Application {
config_fn: None,
bootstrap_fn: None,
routes_fn: None,
seeders_fn: None,
_migrator: std::marker::PhantomData,
}
}
}
impl Default for Application<NoMigrator> {
fn default() -> Self {
Self::new()
}
}
impl<M> Application<M>
where
M: MigratorTrait,
{
pub fn config<F>(mut self, f: F) -> Self
where
F: FnOnce() + 'static,
{
self.config_fn = Some(Box::new(f));
self
}
pub fn bootstrap<F, Fut>(mut self, f: F) -> Self
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
self.bootstrap_fn = Some(Box::new(move || Box::pin(f())));
self
}
pub fn routes<F>(mut self, f: F) -> Self
where
F: FnOnce() -> Router + Send + 'static,
{
self.routes_fn = Some(Box::new(f));
self
}
pub fn migrations<NewM>(self) -> Application<NewM>
where
NewM: MigratorTrait,
{
Application {
config_fn: self.config_fn,
bootstrap_fn: self.bootstrap_fn,
routes_fn: self.routes_fn,
seeders_fn: self.seeders_fn,
_migrator: std::marker::PhantomData,
}
}
pub fn seeders<F>(mut self, f: F) -> Self
where
F: FnOnce() -> SeederRegistry + Send + 'static,
{
self.seeders_fn = Some(Box::new(f));
self
}
pub async fn run(self) {
let cli = Cli::parse();
Config::init(Path::new("."));
let Application {
config_fn,
bootstrap_fn,
routes_fn,
seeders_fn,
_migrator,
} = self;
if let Some(config_fn) = config_fn {
config_fn();
}
crate::lang::init::init();
match cli.command {
None | Some(Commands::Serve { no_migrate: false }) => {
Self::run_migrations_silent::<M>().await;
Self::run_server_internal(bootstrap_fn, routes_fn).await;
}
Some(Commands::Serve { no_migrate: true }) => {
Self::run_server_internal(bootstrap_fn, routes_fn).await;
}
Some(Commands::DbMigrate) => {
Self::run_migrations::<M>().await;
}
Some(Commands::DbStatus) => {
Self::show_migration_status::<M>().await;
}
Some(Commands::DbRollback { steps }) => {
Self::rollback_migrations::<M>(steps).await;
}
Some(Commands::DbFresh) => {
Self::fresh_migrations::<M>().await;
}
Some(Commands::ScheduleWork) => {
Self::run_scheduler_daemon_internal(bootstrap_fn).await;
}
Some(Commands::ScheduleRun) => {
Self::run_scheduled_tasks_internal(bootstrap_fn).await;
}
Some(Commands::ScheduleList) => {
Self::list_scheduled_tasks().await;
}
Some(Commands::DbSeed { class }) => {
Self::run_seeders(seeders_fn, class).await;
}
}
}
async fn run_seeders(
seeders_fn: Option<Box<dyn FnOnce() -> SeederRegistry + Send>>,
class: Option<String>,
) {
let config = crate::database::DatabaseConfig::from_env();
if let Err(e) = crate::database::DB::init_with(config).await {
eprintln!("Failed to connect to database: {e}");
std::process::exit(1);
}
let registry = match seeders_fn {
Some(f) => f(),
None => {
eprintln!("No seeders registered.");
eprintln!("Register seeders with .seeders(seeders::register) in main.rs");
return;
}
};
let result = match class {
Some(name) => registry.run_one(&name).await,
None => registry.run_all().await,
};
if let Err(e) = result {
eprintln!("Seeding failed: {e}");
std::process::exit(1);
}
}
async fn run_server_internal(
bootstrap_fn: Option<BootstrapFn>,
routes_fn: Option<Box<dyn FnOnce() -> Router + Send>>,
) {
if let Some(bootstrap_fn) = bootstrap_fn {
bootstrap_fn().await;
}
let router = if let Some(routes_fn) = routes_fn {
routes_fn()
} else {
Router::new()
};
Server::from_config(router)
.run()
.await
.expect("Failed to start server");
}
async fn get_database_connection() -> sea_orm::DatabaseConnection {
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let database_url = if database_url.starts_with("sqlite://") {
let path = database_url.trim_start_matches("sqlite://");
let path = path.trim_start_matches("./");
if let Some(parent) = Path::new(path).parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent).ok();
}
}
if !Path::new(path).exists() {
std::fs::File::create(path).ok();
}
format!("sqlite:{path}?mode=rwc")
} else {
database_url
};
sea_orm::Database::connect(&database_url)
.await
.expect("Failed to connect to database")
}
async fn run_migrations_silent<Migrator: MigratorTrait>() {
let db = Self::get_database_connection().await;
if let Err(e) = Migrator::up(&db, None).await {
eprintln!("Warning: Migration failed: {e}");
}
}
async fn run_migrations<Migrator: MigratorTrait>() {
println!("Running migrations...");
let db = Self::get_database_connection().await;
Migrator::up(&db, None)
.await
.expect("Failed to run migrations");
println!("Migrations completed successfully!");
}
async fn show_migration_status<Migrator: MigratorTrait>() {
println!("Migration status:");
let db = Self::get_database_connection().await;
Migrator::status(&db)
.await
.expect("Failed to get migration status");
}
async fn rollback_migrations<Migrator: MigratorTrait>(steps: u32) {
println!("Rolling back {steps} migration(s)...");
let db = Self::get_database_connection().await;
Migrator::down(&db, Some(steps))
.await
.expect("Failed to rollback migrations");
println!("Rollback completed successfully!");
}
async fn fresh_migrations<Migrator: MigratorTrait>() {
println!("WARNING: Dropping all tables and re-running migrations...");
let db = Self::get_database_connection().await;
Migrator::fresh(&db)
.await
.expect("Failed to refresh database");
println!("Database refreshed successfully!");
}
async fn run_scheduler_daemon_internal(bootstrap_fn: Option<BootstrapFn>) {
if let Some(bootstrap_fn) = bootstrap_fn {
bootstrap_fn().await;
}
println!("==============================================");
println!(" Ferro Scheduler Daemon");
println!("==============================================");
println!();
println!(" Note: Create tasks with `ferro make:task <name>`");
println!(" Press Ctrl+C to stop");
println!();
println!("==============================================");
eprintln!("Scheduler daemon is not yet configured.");
eprintln!("Create a scheduled task with: ferro make:task <name>");
eprintln!("Then register it in src/schedule.rs");
}
async fn run_scheduled_tasks_internal(bootstrap_fn: Option<BootstrapFn>) {
if let Some(bootstrap_fn) = bootstrap_fn {
bootstrap_fn().await;
}
println!("Running scheduled tasks...");
eprintln!("Scheduler is not yet configured.");
eprintln!("Create a scheduled task with: ferro make:task <name>");
}
async fn list_scheduled_tasks() {
println!("Registered scheduled tasks:");
println!();
eprintln!("No scheduled tasks registered.");
eprintln!("Create a scheduled task with: ferro make:task <name>");
}
}