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>>;
#[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(),
}
}
}
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(),
}
}
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)
}
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)));
}
}
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",
)
}