saddle-framework 0.1.1

The single business-facing facade for Saddle applications
Documentation
use std::{future::Future, net::SocketAddr, pin::Pin};

use saddle_core::{ComponentLifecycle, ErrorKind};
use saddle_db::{Database as ManagedDatabase, DatabaseConfig};
use saddle_observability::{Observer, ObserverConfig};
use saddle_runtime::Application;
use saddle_service::{
    ExternalDispatcherBuilder, Service, ServiceDescriptor, ServiceHandler, ServiceRegistryBuilder,
    ServiceResolver,
};
use serde::{Serialize, de::DeserializeOwned};

use crate::{Result, SaddleError, http::HttpServer};

type Exposure = Box<dyn FnOnce(&mut ExternalDispatcherBuilder) -> Result<()> + Send>;
type ConfigureFuture = Pin<Box<dyn Future<Output = Result<Application>> + Send>>;

/// Fixed V1 process configuration.
///
/// These values describe deployment facts. They do not select alternative
/// runtimes, transports, database drivers, or logging implementations.
#[derive(Clone)]
pub struct SaddleConfig {
    application: crate::ApplicationId,
    database: DatabaseConfig,
    listen: SocketAddr,
    observability: ObserverConfig,
}

impl SaddleConfig {
    pub fn new(
        application: impl Into<crate::ApplicationId>,
        database_url: impl Into<String>,
        listen: SocketAddr,
    ) -> Self {
        Self {
            application: application.into(),
            database: DatabaseConfig::new(database_url),
            listen,
            observability: ObserverConfig::default(),
        }
    }
}

/// V1 business assembly surface available after managed I/O is initialized.
pub struct SaddleBuilder {
    database: ManagedDatabase,
    services: ServiceRegistryBuilder,
    exposures: Vec<Exposure>,
}

impl SaddleBuilder {
    fn new(database: ManagedDatabase) -> Self {
        Self {
            database,
            services: ServiceRegistryBuilder::new(),
            exposures: Vec::new(),
        }
    }

    /// Returns the process-wide managed database capability for injection into
    /// Service implementations.
    pub fn database(&self) -> crate::db::Database {
        crate::db::Database(self.database.clone())
    }

    pub fn register<S, H>(&mut self, descriptor: ServiceDescriptor, handler: H) -> Result<()>
    where
        S: Service,
        H: ServiceHandler<S>,
    {
        self.services.register::<S, H>(descriptor, handler)
    }

    pub fn register_with<S, H, F>(
        &mut self,
        descriptor: ServiceDescriptor,
        factory: F,
    ) -> Result<()>
    where
        S: Service,
        H: ServiceHandler<S>,
        F: FnOnce(&ServiceResolver<'_>) -> Result<H> + Send + 'static,
    {
        self.services.register_with::<S, H, F>(descriptor, factory)
    }

    /// Exposes one registered Service through Saddle's fixed HTTP/JSON entry.
    pub fn expose_json<S>(&mut self, route: impl Into<String>)
    where
        S: Service,
        S::Request: DeserializeOwned,
        S::Response: Serialize,
    {
        let route = route.into();
        self.exposures
            .push(Box::new(move |builder| builder.expose_json::<S>(route)));
    }
}

/// Fixed process entry for a Saddle application.
pub struct Saddle;

impl Saddle {
    pub fn run<F>(config: SaddleConfig, configure: F) -> Result<()>
    where
        F: FnOnce(&mut SaddleBuilder) -> Result<()> + Send + 'static,
    {
        Application::run_with(move || bootstrap(config, configure))
    }
}

fn bootstrap<F>(config: SaddleConfig, configure: F) -> ConfigureFuture
where
    F: FnOnce(&mut SaddleBuilder) -> Result<()> + Send + 'static,
{
    Box::pin(async move {
        let observer = saddle_observability::init(config.observability)
            .map_err(|_| startup_error("saddle.observability_initialization_failed"))?
            .clone();
        let database = match ManagedDatabase::connect(config.database, observer.clone()).await {
            Ok(database) => database,
            Err(error) => {
                let _ = observer.shutdown().await;
                return Err(error);
            }
        };

        let mut builder = SaddleBuilder::new(database.clone());
        if let Err(error) = configure(&mut builder) {
            shutdown_failed_assembly(&database, &observer).await;
            return Err(error);
        }
        let registry = match builder.services.build(observer.clone()) {
            Ok(registry) => registry,
            Err(error) => {
                shutdown_failed_assembly(&database, &observer).await;
                return Err(error);
            }
        };

        let mut application = Application::new();
        let requests = application.request_lifecycle();
        let application_id = config.application;
        let mut dispatcher =
            ExternalDispatcherBuilder::new(application_id.clone(), registry, requests);
        for exposure in builder.exposures {
            if let Err(error) = exposure(&mut dispatcher) {
                shutdown_failed_assembly(&database, &observer).await;
                return Err(error);
            }
        }
        let server = HttpServer::new(
            config.listen,
            application_id,
            dispatcher.build(),
            observer.clone(),
        );
        if let Err(error) = application.register(observer.clone()) {
            shutdown_failed_assembly(&database, &observer).await;
            return Err(error);
        }
        if let Err(error) = application.register(database.clone()) {
            shutdown_failed_assembly(&database, &observer).await;
            return Err(error);
        }
        if let Err(error) = application.register(server) {
            shutdown_failed_assembly(&database, &observer).await;
            return Err(error);
        }
        Ok(application)
    })
}

async fn shutdown_failed_assembly(database: &ManagedDatabase, observer: &Observer) {
    let _ = ComponentLifecycle::shutdown(database).await;
    let _ = ComponentLifecycle::shutdown(observer).await;
}

fn startup_error(code: &'static str) -> SaddleError {
    SaddleError::new(
        ErrorKind::Infrastructure,
        code,
        "Saddle application initialization failed",
    )
}