bloom-web-core 0.1.1

Core functionality for the Bloom web framework
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use anyhow::Result;
use sqlx::mysql::MySqlPoolOptions;
use sqlx::MySqlPool;
use utoipa_swagger_ui::SwaggerUi;
use crate::config::Settings;
use crate::{logger, entity_registry, swagger_registry};

pub struct ServerRun;

impl ServerRun {
    /// Starts the HTTP server with optional Swagger UI support.
    ///
    /// This function initializes the database connection pool, creates necessary tables,
    /// starts scheduled jobs, configures CORS settings, and starts the HTTP server.
    ///
    /// # Arguments
    /// * `enable_swagger` - Whether to enable Swagger UI documentation
    ///
    /// # Returns
    /// * `Result<()>` - Ok if server starts successfully, Err otherwise
    async fn start(enable_swagger: bool) -> Result<()> {
        let settings = Settings::load().map_err(|e| anyhow::anyhow!("Failed to load config: {}", e))?;

        let pool: MySqlPool = MySqlPoolOptions::new()
            .max_connections(5)
            .connect(&settings.database_url)
            .await
            .map_err(|e| anyhow::anyhow!("Failed to connect to DB: {}", e))?;

        entity_registry::run_all(&pool).await.map_err(|e| anyhow::anyhow!("Failed to create tables: {}", e))?;

        crate::scheduler_registry::start_all(&pool);

        let cors_cfg = settings.cors.clone().unwrap_or_default();
        let enable_swagger_ui = enable_swagger;

        let server = HttpServer::new(move || {
            use actix_cors::Cors;
            use actix_web::http::{header, Method};
            use actix_web::middleware::Condition;

            let mut cors = Cors::default();

            if let Some(origins) = &cors_cfg.allowed_origins {
                for o in origins { cors = cors.allowed_origin(o); }
            } else {
                cors = cors.allow_any_origin();
            }

            if let Some(methods) = &cors_cfg.allowed_methods {
                let parsed: Vec<Method> = methods.iter().filter_map(|m| m.parse::<Method>().ok()).collect();
                if !parsed.is_empty() { cors = cors.allowed_methods(parsed); } else { cors = cors.allow_any_method(); }
            } else {
                cors = cors.allow_any_method();
            }

            if let Some(headers) = &cors_cfg.allowed_headers {
                for h in headers {
                    if let Ok(hh) = h.parse::<header::HeaderName>() { cors = cors.allowed_header(hh); }
                }
            } else {
                cors = cors.allow_any_header();
            }

            if cors_cfg.allow_credentials.unwrap_or(false) { cors = cors.supports_credentials(); }
            if let Some(max_age) = cors_cfg.max_age { cors = cors.max_age(max_age as usize); }

            let mut app = App::new()
                .app_data(web::Data::new(pool.clone()))
                .wrap(Condition::new(cors_cfg.enabled, cors))
                .configure(|cfg| crate::controller_registry::configure_all(cfg))
                .route("/", web::get().to(health_check));

            if enable_swagger_ui {
                let openapi = swagger_registry::generate_openapi();

                app = app.service(
                    SwaggerUi::new("/swagger-ui/{_:.*}")
                        .url("/api-docs/openapi.json", openapi),
                );
            }

            app
        })
            .bind(("127.0.0.1", settings.port))?
            .run();
        logger!(INFO, "Server running on http://127.0.0.1:{}", settings.port);
        if enable_swagger_ui {
            logger!(INFO, "Swagger UI available at http://127.0.0.1:{}/swagger-ui/", settings.port);
        }
        server.await.map_err(|e| anyhow::anyhow!("Server failed: {}", e))
    }

    /// Enables Swagger UI and starts the server.
    ///
    /// This is a convenience method that starts the server with Swagger UI enabled.
    ///
    /// # Returns
    /// * `Result<()>` - Ok if server starts successfully, Err otherwise
    pub async fn enable_swagger(self) -> Result<()> {
        Self::start(true).await
    }
}

impl std::future::IntoFuture for ServerRun {
    type Output = Result<()>;
    type IntoFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>>;

    /// Converts ServerRun into a Future that starts the server without Swagger UI.
    fn into_future(self) -> Self::IntoFuture {
        Box::pin(async move { ServerRun::start(false).await })
    }
}

/// Creates a new ServerRun instance.
///
/// This function returns a ServerRun struct that can be used to start the HTTP server.
///
/// # Returns
/// * `ServerRun` - A new ServerRun instance
pub fn run() -> ServerRun { ServerRun }

/// Health check endpoint handler.
///
/// This function handles health check requests and returns an empty HTTP 200 response.
///
/// # Returns
/// * `impl Responder` - An HTTP response indicating the server is healthy
async fn health_check() -> impl Responder {
    HttpResponse::Ok().body("")
}