Skip to main content

saddle/
application.rs

1use std::{future::Future, net::SocketAddr, pin::Pin};
2
3use saddle_core::{ComponentLifecycle, ErrorKind};
4use saddle_db::{Database as ManagedDatabase, DatabaseConfig};
5use saddle_observability::{Observer, ObserverConfig};
6use saddle_runtime::Application;
7use saddle_service::{
8    ExternalDispatcherBuilder, Service, ServiceDescriptor, ServiceHandler, ServiceRegistryBuilder,
9    ServiceResolver,
10};
11use serde::{Serialize, de::DeserializeOwned};
12
13use crate::{Result, SaddleError, http::HttpServer};
14
15type Exposure = Box<dyn FnOnce(&mut ExternalDispatcherBuilder) -> Result<()> + Send>;
16type ConfigureFuture = Pin<Box<dyn Future<Output = Result<Application>> + Send>>;
17
18/// Fixed V1 process configuration.
19///
20/// These values describe deployment facts. They do not select alternative
21/// runtimes, transports, database drivers, or logging implementations.
22#[derive(Clone)]
23pub struct SaddleConfig {
24    application: crate::ApplicationId,
25    database: DatabaseConfig,
26    listen: SocketAddr,
27    observability: ObserverConfig,
28}
29
30impl SaddleConfig {
31    pub fn new(
32        application: impl Into<crate::ApplicationId>,
33        database_url: impl Into<String>,
34        listen: SocketAddr,
35    ) -> Self {
36        Self {
37            application: application.into(),
38            database: DatabaseConfig::new(database_url),
39            listen,
40            observability: ObserverConfig::default(),
41        }
42    }
43}
44
45/// V1 business assembly surface available after managed I/O is initialized.
46pub struct SaddleBuilder {
47    database: ManagedDatabase,
48    services: ServiceRegistryBuilder,
49    exposures: Vec<Exposure>,
50}
51
52impl SaddleBuilder {
53    fn new(database: ManagedDatabase) -> Self {
54        Self {
55            database,
56            services: ServiceRegistryBuilder::new(),
57            exposures: Vec::new(),
58        }
59    }
60
61    /// Returns the process-wide managed database capability for injection into
62    /// Service implementations.
63    pub fn database(&self) -> crate::db::Database {
64        crate::db::Database(self.database.clone())
65    }
66
67    pub fn register<S, H>(&mut self, descriptor: ServiceDescriptor, handler: H) -> Result<()>
68    where
69        S: Service,
70        H: ServiceHandler<S>,
71    {
72        self.services.register::<S, H>(descriptor, handler)
73    }
74
75    pub fn register_with<S, H, F>(
76        &mut self,
77        descriptor: ServiceDescriptor,
78        factory: F,
79    ) -> Result<()>
80    where
81        S: Service,
82        H: ServiceHandler<S>,
83        F: FnOnce(&ServiceResolver<'_>) -> Result<H> + Send + 'static,
84    {
85        self.services.register_with::<S, H, F>(descriptor, factory)
86    }
87
88    /// Exposes one registered Service through Saddle's fixed HTTP/JSON entry.
89    pub fn expose_json<S>(&mut self, route: impl Into<String>)
90    where
91        S: Service,
92        S::Request: DeserializeOwned,
93        S::Response: Serialize,
94    {
95        let route = route.into();
96        self.exposures
97            .push(Box::new(move |builder| builder.expose_json::<S>(route)));
98    }
99}
100
101/// Fixed process entry for a Saddle application.
102pub struct Saddle;
103
104impl Saddle {
105    pub fn run<F>(config: SaddleConfig, configure: F) -> Result<()>
106    where
107        F: FnOnce(&mut SaddleBuilder) -> Result<()> + Send + 'static,
108    {
109        Application::run_with(move || bootstrap(config, configure))
110    }
111}
112
113fn bootstrap<F>(config: SaddleConfig, configure: F) -> ConfigureFuture
114where
115    F: FnOnce(&mut SaddleBuilder) -> Result<()> + Send + 'static,
116{
117    Box::pin(async move {
118        let observer = saddle_observability::init(config.observability)
119            .map_err(|_| startup_error("saddle.observability_initialization_failed"))?
120            .clone();
121        let database = match ManagedDatabase::connect(config.database, observer.clone()).await {
122            Ok(database) => database,
123            Err(error) => {
124                let _ = observer.shutdown().await;
125                return Err(error);
126            }
127        };
128
129        let mut builder = SaddleBuilder::new(database.clone());
130        if let Err(error) = configure(&mut builder) {
131            shutdown_failed_assembly(&database, &observer).await;
132            return Err(error);
133        }
134        let registry = match builder.services.build(observer.clone()) {
135            Ok(registry) => registry,
136            Err(error) => {
137                shutdown_failed_assembly(&database, &observer).await;
138                return Err(error);
139            }
140        };
141
142        let mut application = Application::new();
143        let requests = application.request_lifecycle();
144        let application_id = config.application;
145        let mut dispatcher =
146            ExternalDispatcherBuilder::new(application_id.clone(), registry, requests);
147        for exposure in builder.exposures {
148            if let Err(error) = exposure(&mut dispatcher) {
149                shutdown_failed_assembly(&database, &observer).await;
150                return Err(error);
151            }
152        }
153        let server = HttpServer::new(
154            config.listen,
155            application_id,
156            dispatcher.build(),
157            observer.clone(),
158        );
159        if let Err(error) = application.register(observer.clone()) {
160            shutdown_failed_assembly(&database, &observer).await;
161            return Err(error);
162        }
163        if let Err(error) = application.register(database.clone()) {
164            shutdown_failed_assembly(&database, &observer).await;
165            return Err(error);
166        }
167        if let Err(error) = application.register(server) {
168            shutdown_failed_assembly(&database, &observer).await;
169            return Err(error);
170        }
171        Ok(application)
172    })
173}
174
175async fn shutdown_failed_assembly(database: &ManagedDatabase, observer: &Observer) {
176    let _ = ComponentLifecycle::shutdown(database).await;
177    let _ = ComponentLifecycle::shutdown(observer).await;
178}
179
180fn startup_error(code: &'static str) -> SaddleError {
181    SaddleError::new(
182        ErrorKind::Infrastructure,
183        code,
184        "Saddle application initialization failed",
185    )
186}