strut 0.0.2

Backend in Rust: convenient and configurable with Strut.
Documentation
use crate::AppConfig;
use tokio::runtime::Runtime;

/// Defines the **runtime wiring** stage of a Strut application.
///
/// This trait's primary responsibility is to construct the main Tokio
/// [`Runtime`] after the initial [`AppConfig`] has been resolved. It also
/// initializes other systems that depend on the configuration, such as logging
/// (`tracing`) and error reporting (`sentry`).
///
/// The default implementation provides a standard setup for a multithreaded
/// runtime and integrates enabled features. It can be replaced to customize
/// runtime behavior or component initialization.
///
/// ## Customization example
///
/// You can replace the default wiring to use a different type of Tokio runtime.
///
/// ```
/// use strut::{App, AppConfig, RuntimeWiring};
/// use tokio::runtime::Runtime;
///
/// fn main() {
///     App::launchpad(async_main())
///         .with_runtime_wiring(CustomRuntimeWiring)
///         .boot();
/// }
///
/// async fn async_main() {
///     println!("Running in current-thread runtime")
/// }
///
/// struct CustomRuntimeWiring;
///
/// impl RuntimeWiring for CustomRuntimeWiring {
///     fn make_runtime(&self) -> Runtime {
///         // Build on current thread instead of multi-thread
///         tokio::runtime::Builder::new_current_thread()
///             .enable_all()
///             .build()
///             .expect("it should be possible to build a tokio runtime")
///     }
/// }
/// ```
pub trait RuntimeWiring {
    /// Runs the runtime wiring stage.
    ///
    /// This is the entry point for the stage. It orchestrates the setup process,
    /// including initializing `sentry` and `tracing` before finally creating the
    /// application's main Tokio [`Runtime`]. It is not typically necessary to
    /// override this method directly.
    fn run(&self, _config: &'static AppConfig) -> Runtime {
        // Initialize the Sentry integration
        #[cfg(feature = "sentry")]
        let sentry_guard = self.init_sentry(_config);

        // Initialize the `tracing` crate
        #[cfg(feature = "tracing")]
        {
            let tracing_subscriber = self.make_tracing_subscriber(_config);
            self.init_tracing_subscriber(tracing_subscriber);
        }

        // Make the application’s main runtime
        let runtime = self.make_runtime();

        // With the runtime constructed, schedule flushing of Sentry events
        #[cfg(feature = "sentry")]
        self.schedule_sentry_flushing(&runtime, sentry_guard);

        runtime
    }

    /// Initializes the Sentry client using the application's configuration.
    ///
    /// This method returns a [`SentryGuard`], which must be kept alive for the
    /// duration of the application to ensure Sentry captures events. The guard is
    /// later passed to [`schedule_sentry_flushing`] to handle graceful shutdown.
    ///
    /// [`SentryGuard`]: strut_sentry::SentryGuard
    /// [`schedule_sentry_flushing`]: RuntimeWiring::schedule_sentry_flushing
    #[cfg(feature = "sentry")]
    fn init_sentry(&self, config: &'static AppConfig) -> strut_sentry::SentryGuard {
        strut_sentry::SentryIntegration::init(config.sentry())
    }

    /// Redirects logs from the `log` crate to the `tracing` ecosystem.
    ///
    /// This ensures that logs generated by dependencies using the standard `log`
    /// facade are captured and processed by the `tracing` subscriber. This method
    /// is available when the `tracing-log` feature is enabled.
    #[cfg(feature = "tracing-log")]
    fn init_log_tracer(&self, config: &'static AppConfig) {
        use tracing_log::{AsLog, LogTracer};

        let tracing_config = config.tracing();

        LogTracer::builder()
            .with_max_level(
                tracing_config
                    .verbosity()
                    .to_tracing_level_filter()
                    .as_log(),
            )
            .init()
            .expect("failed to initialize log tracer");
    }

    /// Creates the `tracing` subscriber for logging and telemetry.
    ///
    /// The default implementation assembles a subscriber with a formatting layer
    /// and, if the `sentry` feature is enabled, a layer for Sentry integration.
    /// Override this method to customize log formatting or add other layers.
    #[cfg(feature = "tracing")]
    fn make_tracing_subscriber(
        &self,
        config: &'static AppConfig,
    ) -> Box<dyn strut_tracing::Subscriber + Send + Sync> {
        use strut_tracing::SubscriberExt;

        // Start assembling the subscriber
        let registry = strut_tracing::Registry::default();

        // Add the formatted layer
        let registry = registry.with(strut_tracing::make_layer(config.tracing()));

        // Add the Sentry layer, if applicable
        #[cfg(feature = "sentry")]
        let registry = registry.with(strut_sentry::tracing::make_layer());

        Box::new(registry)
    }

    /// Sets the provided `tracing` subscriber as the global default.
    ///
    /// # Panics
    ///
    /// Panics if a global subscriber has already been set.
    #[cfg(feature = "tracing")]
    fn init_tracing_subscriber(
        &self,
        subscriber: Box<dyn strut_tracing::Subscriber + Send + Sync>,
    ) {
        use strut_tracing::SubscriberInitExt;

        subscriber.init()
    }

    /// Creates the main Tokio [`Runtime`] for the application.
    ///
    /// The default implementation builds a multithreaded runtime with all
    /// features enabled. Override this method to use a different kind of runtime
    /// (e.g., `new_current_thread`) or to customize its configuration.
    fn make_runtime(&self) -> Runtime {
        tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .expect("it should be possible to build a tokio runtime")
    }

    /// Schedules the Sentry client to flush its event buffer on shutdown.
    ///
    /// This takes ownership of the [`SentryGuard`] returned by `init_sentry` and
    /// ensures that its `drop` logic (which flushes events) is executed when the
    /// application's runtime is shutting down.
    ///
    /// [`SentryGuard`]: strut_sentry::SentryGuard
    #[cfg(feature = "sentry")]
    fn schedule_sentry_flushing(&self, runtime: &Runtime, sentry_guard: strut_sentry::SentryGuard) {
        strut_sentry::SentryIntegration::schedule_flushing(runtime, sentry_guard);
    }
}

/// The default `RuntimeWiring` implementation used by Strut.
///
/// This struct simply uses the default behavior provided by the
/// `RuntimeWiring` trait methods.
pub(crate) struct DefaultRuntimeWiring;

impl RuntimeWiring for DefaultRuntimeWiring {}