switchy_database 0.3.0

Switchy database package
//! Global database configuration and initialization
//!
//! This module provides global database instance management for applications
//! that need a singleton database connection. It includes integration with
//! the actix-web framework for dependency injection.
//!
//! # Usage
//!
//! Initialize the global database instance at application startup:
//!
//! ```rust,ignore
//! use switchy_database::config;
//! use std::sync::Arc;
//!
//! # async fn example(db: Box<dyn switchy_database::Database>) {
//! // Initialize global database
//! config::init(Arc::new(db));
//!
//! // In actix-web handlers, use ConfigDatabase for dependency injection
//! // It will automatically extract the global database instance
//! # }
//! ```
//!
//! # Actix-web Integration
//!
//! When the `api` feature is enabled, [`ConfigDatabase`](crate::config::ConfigDatabase) implements actix-web's
//! `FromRequest` trait, allowing automatic extraction in handlers:
//!
//! ```rust,ignore
//! use actix_web::{web, HttpResponse};
//! use switchy_database::config::ConfigDatabase;
//!
//! async fn my_handler(db: ConfigDatabase) -> HttpResponse {
//!     // Use db as &dyn Database via Deref
//!     let results = db.select("users").execute(&*db).await?;
//!     HttpResponse::Ok().json(results)
//! }
//! ```

use std::{
    ops::Deref,
    sync::{Arc, LazyLock, RwLock},
};

use crate::Database;

#[allow(clippy::type_complexity)]
static DATABASE: LazyLock<Arc<RwLock<Option<Arc<Box<dyn Database>>>>>> =
    LazyLock::new(|| Arc::new(RwLock::new(None)));

/// Initialize the global database instance
///
/// Sets the global database singleton that will be used by [`ConfigDatabase`]
/// throughout the application. This should be called once at application startup
/// before any database operations.
///
/// # Panics
///
/// * If fails to get a writer to the `DATABASE` `RwLock`
pub fn init(database: Arc<Box<dyn Database>>) {
    *DATABASE.write().unwrap() = Some(database);
}

/// Wrapper for the global database instance that implements actix-web's `FromRequest`
///
/// This struct provides access to the global database configured via [`init`].
/// It dereferences to `dyn Database` for convenient access.
///
/// ## Actix-web Integration
///
/// When the `api` feature is enabled, this implements `FromRequest` for automatic
/// dependency injection in actix-web handlers.
///
/// ## Examples
///
/// ```rust,ignore
/// use actix_web::{web, HttpResponse};
/// use switchy_database::config::ConfigDatabase;
///
/// async fn my_handler(db: ConfigDatabase) -> HttpResponse {
///     let users = db.select("users").execute(&*db).await.unwrap();
///     HttpResponse::Ok().json(users)
/// }
/// ```
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone)]
pub struct ConfigDatabase {
    /// The global database instance wrapped in Arc for thread-safe sharing
    pub database: Arc<Box<dyn Database>>,
}

impl From<&ConfigDatabase> for Arc<Box<dyn Database>> {
    fn from(value: &ConfigDatabase) -> Self {
        value.database.clone()
    }
}

impl From<ConfigDatabase> for Arc<Box<dyn Database>> {
    fn from(value: ConfigDatabase) -> Self {
        value.database
    }
}

impl From<Arc<Box<dyn Database>>> for ConfigDatabase {
    fn from(value: Arc<Box<dyn Database>>) -> Self {
        Self { database: value }
    }
}

impl<'a> From<&'a ConfigDatabase> for &'a dyn Database {
    fn from(value: &'a ConfigDatabase) -> Self {
        &**value.database
    }
}

impl Deref for ConfigDatabase {
    type Target = dyn Database;

    fn deref(&self) -> &Self::Target {
        &**self.database
    }
}

#[cfg(feature = "api")]
mod api {
    use actix_web::{FromRequest, HttpRequest, dev::Payload, error::ErrorInternalServerError};
    use futures::future::{Ready, err, ok};

    use super::DATABASE;

    impl FromRequest for super::ConfigDatabase {
        type Error = actix_web::Error;
        type Future = Ready<Result<Self, actix_web::Error>>;

        fn from_request(_req: &HttpRequest, _: &mut Payload) -> Self::Future {
            let Some(database) = DATABASE.read().unwrap().clone() else {
                return err(ErrorInternalServerError("Config database not initialized"));
            };

            ok(Self { database })
        }
    }
}

#[cfg(all(test, feature = "simulator"))]
mod tests {
    use super::*;
    use crate::simulator::SimulationDatabase;

    #[test_log::test]
    fn test_config_database_from_arc() {
        let db = Arc::new(
            Box::new(SimulationDatabase::new_for_path(None).unwrap()) as Box<dyn Database>
        );
        let config_db: ConfigDatabase = db.clone().into();

        assert!(std::ptr::addr_eq(
            std::ptr::addr_of!(**config_db.database),
            std::ptr::addr_of!(**db)
        ));
    }

    #[test_log::test]
    fn test_config_database_from_ref() {
        let db = Arc::new(
            Box::new(SimulationDatabase::new_for_path(None).unwrap()) as Box<dyn Database>
        );
        let config_db = ConfigDatabase {
            database: db.clone(),
        };

        let arc_db: Arc<Box<dyn Database>> = (&config_db).into();
        assert!(std::ptr::addr_eq(
            std::ptr::addr_of!(**arc_db),
            std::ptr::addr_of!(**db)
        ));
    }

    #[test_log::test]
    fn test_config_database_into_arc() {
        let db = Arc::new(
            Box::new(SimulationDatabase::new_for_path(None).unwrap()) as Box<dyn Database>
        );
        let config_db = ConfigDatabase {
            database: db.clone(),
        };

        let arc_db: Arc<Box<dyn Database>> = config_db.into();
        assert!(std::ptr::addr_eq(
            std::ptr::addr_of!(**arc_db),
            std::ptr::addr_of!(**db)
        ));
    }

    #[test_log::test]
    fn test_config_database_deref() {
        let db = Arc::new(
            Box::new(SimulationDatabase::new_for_path(None).unwrap()) as Box<dyn Database>
        );
        let config_db = ConfigDatabase {
            database: db.clone(),
        };

        // Should be able to use as &dyn Database via Deref
        let _db_ref: &dyn Database = &*config_db;
    }

    #[test_log::test]
    fn test_config_database_ref_into_dyn_database() {
        let db = Arc::new(
            Box::new(SimulationDatabase::new_for_path(None).unwrap()) as Box<dyn Database>
        );
        let config_db = ConfigDatabase { database: db };

        let db_ref: &dyn Database = (&config_db).into();
        // Just verify the conversion works
        let _ = db_ref;
    }

    #[test_log::test]
    fn test_init_and_reuse() {
        // This test uses a static global, so we need to be careful
        // We'll just verify init doesn't panic
        let db = Arc::new(
            Box::new(SimulationDatabase::new_for_path(None).unwrap()) as Box<dyn Database>
        );
        init(db);

        // Calling init again should just replace the database
        let db2 = Arc::new(
            Box::new(SimulationDatabase::new_for_path(None).unwrap()) as Box<dyn Database>
        );
        init(db2);
    }
}