lmrc-http-common 0.3.16

Common HTTP utilities and patterns for LMRC Stack applications
Documentation
//! Server bootstrap utilities
//!
//! This module provides a builder pattern for bootstrapping Axum HTTP servers
//! with common patterns like configuration loading, tracing setup, and database
//! connections.
//!
//! ## Example
//!
//! ```rust,no_run
//! use axum::{Router, routing::get};
//! use lmrc_http_common::server::ServerBootstrap;
//!
//! #[tokio::main]
//! async fn main() -> anyhow::Result<()> {
//!     // Create a simple server
//!     ServerBootstrap::new()
//!         .with_tracing("myapp")
//!         .with_router(|_| {
//!             Router::new().route("/health", get(|| async { "OK" }))
//!         })
//!         .with_port(8080)
//!         .serve()
//!         .await
//! }
//! ```

use crate::config::ServerConfig;
use axum::Router;
use std::net::SocketAddr;

#[cfg(feature = "server")]
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};

/// Server bootstrap builder
///
/// Provides a fluent API for setting up an Axum HTTP server with common
/// infrastructure patterns.
///
/// ## Example with State
///
/// ```rust,no_run
/// use axum::{Router, routing::get, extract::State};
/// use lmrc_http_common::server::ServerBootstrap;
/// use std::sync::Arc;
///
/// #[derive(Clone)]
/// struct AppState {
///     message: String,
/// }
///
/// async fn handler(State(state): State<AppState>) -> String {
///     state.message.clone()
/// }
///
/// #[tokio::main]
/// async fn main() -> anyhow::Result<()> {
///     let state = AppState {
///         message: "Hello, World!".to_string(),
///     };
///
///     ServerBootstrap::with_state(state)
///         .with_tracing("myapp")
///         .with_router(|state| {
///             Router::new()
///                 .route("/", get(handler))
///                 .with_state(state.clone())
///         })
///         .with_port(8080)
///         .serve()
///         .await
/// }
/// ```
pub struct ServerBootstrap<S = ()> {
    state: Option<S>,
    router_builder: Option<Box<dyn FnOnce(S) -> Router + Send>>,
    host: String,
    port: u16,
    tracing_initialized: bool,
    tracing_filter: Option<String>,
}

impl ServerBootstrap<()> {
    /// Create a new server bootstrap without state
    pub fn new() -> Self {
        Self {
            state: None,
            router_builder: None,
            host: "0.0.0.0".to_string(),
            port: 8080,
            tracing_initialized: false,
            tracing_filter: None,
        }
    }
}

impl<S> ServerBootstrap<S>
where
    S: Clone + Send + 'static,
{
    /// Create a new server bootstrap with state
    pub fn with_state(state: S) -> Self {
        Self {
            state: Some(state),
            router_builder: None,
            host: "0.0.0.0".to_string(),
            port: 8080,
            tracing_initialized: false,
            tracing_filter: None,
        }
    }

    /// Set the host to bind to
    pub fn with_host(mut self, host: impl Into<String>) -> Self {
        self.host = host.into();
        self
    }

    /// Set the port to bind to
    pub fn with_port(mut self, port: u16) -> Self {
        self.port = port;
        self
    }

    /// Load server configuration from environment
    pub fn with_server_config(mut self, config: ServerConfig) -> Self {
        self.host = config.host;
        self.port = config.port;
        self
    }

    /// Initialize tracing with a default filter
    ///
    /// # Arguments
    ///
    /// * `app_name` - The application name for the default filter
    ///
    /// Sets up tracing with `RUST_LOG` environment variable, or defaults to
    /// `{app_name}=debug,tower_http=debug` if not set.
    pub fn with_tracing(mut self, app_name: &str) -> Self {
        let filter = format!("{}=debug,tower_http=debug", app_name);
        self.tracing_filter = Some(filter);
        self
    }

    /// Initialize tracing with a custom filter
    pub fn with_tracing_filter(mut self, filter: impl Into<String>) -> Self {
        self.tracing_filter = Some(filter.into());
        self
    }

    /// Set the router builder function
    ///
    /// The function receives the application state and should return a configured Router.
    pub fn with_router<F>(mut self, builder: F) -> Self
    where
        F: FnOnce(S) -> Router + Send + 'static,
    {
        self.router_builder = Some(Box::new(builder));
        self
    }

    /// Start the server
    ///
    /// This will:
    /// 1. Initialize tracing if configured
    /// 2. Build the router
    /// 3. Bind to the specified address
    /// 4. Start serving requests
    pub async fn serve(mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        // Initialize tracing if configured
        #[cfg(feature = "server")]
        if let Some(filter) = self.tracing_filter.take()
            && !self.tracing_initialized {
                tracing_subscriber::registry()
                    .with(
                        EnvFilter::try_from_default_env()
                            .unwrap_or_else(|_| filter.into()),
                    )
                    .with(tracing_subscriber::fmt::layer())
                    .init();
                self.tracing_initialized = true;
        }

        // Load .env file if present
        #[cfg(feature = "server")]
        dotenvy::dotenv().ok();

        // Get state
        let state = self.state.ok_or("State is required")?;

        // Build router
        let router_builder = self
            .router_builder
            .ok_or("Router builder is required")?;
        let app = router_builder(state);

        // Create socket address
        let addr: SocketAddr = format!("{}:{}", self.host, self.port)
            .parse()
            .map_err(|e| format!("Invalid socket address: {}", e))?;

        tracing::info!("Starting server on {}", addr);

        // Bind and serve
        let listener = tokio::net::TcpListener::bind(addr).await?;
        axum::serve(listener, app).await?;

        Ok(())
    }
}

impl Default for ServerBootstrap<()> {
    fn default() -> Self {
        Self::new()
    }
}

/// Quick start server with minimal configuration
///
/// Useful for simple services that don't need custom state or complex setup.
///
/// ## Example
///
/// ```rust,no_run
/// use axum::{Router, routing::get};
/// use lmrc_http_common::server::quick_start;
///
/// #[tokio::main]
/// async fn main() -> anyhow::Result<()> {
///     let router = Router::new().route("/health", get(|| async { "OK" }));
///
///     quick_start("myapp", router, 8080).await
/// }
/// ```
pub async fn quick_start(
    app_name: &str,
    router: Router,
    port: u16,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    ServerBootstrap::with_state(())
        .with_tracing(app_name)
        .with_port(port)
        .with_router(|_| router)
        .serve()
        .await
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_server_bootstrap_builder() {
        let bootstrap = ServerBootstrap::new()
            .with_host("127.0.0.1")
            .with_port(3000);

        assert_eq!(bootstrap.host, "127.0.0.1");
        assert_eq!(bootstrap.port, 3000);
    }

    #[test]
    fn test_server_bootstrap_with_state() {
        #[derive(Clone)]
        struct TestState {
            value: i32,
        }

        let state = TestState { value: 42 };
        let bootstrap = ServerBootstrap::with_state(state);

        assert!(bootstrap.state.is_some());
        assert_eq!(bootstrap.state.unwrap().value, 42);
    }

    #[test]
    fn test_server_config_integration() {
        let config = ServerConfig {
            host: "192.168.1.1".to_string(),
            port: 9000,
            cors_origins: vec![],
        };

        let bootstrap = ServerBootstrap::new().with_server_config(config);

        assert_eq!(bootstrap.host, "192.168.1.1");
        assert_eq!(bootstrap.port, 9000);
    }
}