use crate::handler::IHostedService;
use crate::mediator::Mediator;
use crate::middleware::IMiddleware;
use crate::pipeline::IPipelineBehavior;
use rust_dix::{IServiceResolver, ServiceCollection, ServiceLifetime};
use std::sync::Arc;
pub trait IServiceCollectionExt: Sized {
fn add_mediator(self) -> Self;
fn add_request_endpoints(self) -> Self;
fn add_controllers(self) -> Self;
fn add_middleware<T>(self) -> Self
where
T: IMiddleware + Default + Send + Sync + 'static;
fn add_typed_pipeline<B, Req, Rsp>(self) -> Self
where
B: IPipelineBehavior<Req, Rsp> + Default + 'static,
Req: Send + 'static,
Rsp: Send + 'static;
fn add_typed_pipeline_with<F, Req, Rsp>(self, factory: F) -> Self
where
F: Fn(&dyn IServiceResolver) -> Arc<dyn IPipelineBehavior<Req, Rsp>>
+ Send
+ Sync
+ 'static,
Req: Send + 'static,
Rsp: Send + 'static;
fn add_hosted_service<T>(self) -> Self
where
T: IHostedService + Default + Send + Sync + 'static;
}
impl IServiceCollectionExt for ServiceCollection {
fn add_mediator(self) -> Self {
MEDIATOR_ACTIVE.store(true, std::sync::atomic::Ordering::SeqCst);
self.transient::<Mediator>(|resolver| {
let provider = resolver
.provider_arc()
.expect("ServiceProvider not available from DI resolver");
let handler_cache = resolver
.get_any(std::any::type_name::<Arc<crate::route::scan::HandlerCache>>())
.and_then(|service| {
service
.downcast::<Arc<crate::route::scan::HandlerCache>>()
.ok()
})
.map(|service| Arc::clone(&*service))
.unwrap_or_else(|| Arc::new(crate::route::scan::HandlerCache::build()));
Arc::new(Mediator::new(provider, handler_cache))
})
}
fn add_request_endpoints(self) -> Self {
ENDPOINT_SCAN.store(true, std::sync::atomic::Ordering::SeqCst);
self
}
fn add_controllers(self) -> Self {
self
}
fn add_middleware<T>(self) -> Self
where
T: IMiddleware + Default + Send + Sync + 'static,
{
self.singleton::<dyn IMiddleware>(|_| Arc::new(T::default()))
}
fn add_typed_pipeline<B, Req, Rsp>(self) -> Self
where
B: IPipelineBehavior<Req, Rsp> + Default + 'static,
Req: Send + 'static,
Rsp: Send + 'static,
{
self.add_typed_pipeline_with::<_, Req, Rsp>(|_| {
Arc::new(B::default()) as Arc<dyn IPipelineBehavior<Req, Rsp>>
})
}
fn add_typed_pipeline_with<F, Req, Rsp>(self, factory: F) -> Self
where
F: Fn(&dyn IServiceResolver) -> Arc<dyn IPipelineBehavior<Req, Rsp>>
+ Send
+ Sync
+ 'static,
Req: Send + 'static,
Rsp: Send + 'static,
{
self.add(ServiceLifetime::Singleton, factory)
}
fn add_hosted_service<T>(self) -> Self
where
T: IHostedService + Default + Send + Sync + 'static,
{
self.singleton::<dyn IHostedService>(|_| Arc::new(T::default()))
}
}
use std::sync::atomic::AtomicBool;
static ENDPOINT_SCAN: AtomicBool = AtomicBool::new(false);
static MEDIATOR_ACTIVE: AtomicBool = AtomicBool::new(false);
pub fn should_scan_endpoints() -> bool {
ENDPOINT_SCAN.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn is_mediator_active() -> bool {
MEDIATOR_ACTIVE.load(std::sync::atomic::Ordering::SeqCst)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pipeline::{IPipelineBehavior, Next};
use std::sync::atomic::{AtomicU32, Ordering};
fn resolve<T: Send + Sync + 'static>(resolver: &dyn IServiceResolver) -> Arc<T> {
resolver
.get_any(std::any::type_name::<T>())
.and_then(|service| service.downcast::<Arc<T>>().ok())
.map(|service| Arc::clone(&*service))
.expect("the dependency must be registered before the pipeline")
}
#[derive(Default)]
struct Uppercase;
#[async_trait::async_trait]
impl IPipelineBehavior<u32, String> for Uppercase {
async fn handle(&self, req: u32, next: Next<u32, String>) -> crate::error::Result<String> {
let inner = next.run(req + 1).await?;
Ok(inner.to_uppercase())
}
}
fn string_terminal() -> Next<u32, String> {
Next::new(|n: u32| async move { Ok::<String, crate::error::Error>(format!("n={n}")) })
}
#[tokio::test]
async fn typed_behavior_registers_only_for_its_request_response_pair() {
let provider = ServiceCollection::new()
.add_typed_pipeline::<Uppercase, u32, String>()
.build()
.expect("provider must build");
let behaviors = provider.get_all::<dyn IPipelineBehavior<u32, String>>();
assert_eq!(behaviors.len(), 1);
let out = behaviors[0]
.handle(7, string_terminal())
.await
.expect("typed behavior must run");
assert_eq!(out, "N=8");
assert!(
provider
.get_all::<dyn IPipelineBehavior<u64, String>>()
.is_empty(),
"a typed behavior must only attach to its own request/response pair"
);
}
struct TickBehavior {
ticks: Arc<AtomicU32>,
}
#[async_trait::async_trait]
impl IPipelineBehavior<u32, u32> for TickBehavior {
async fn handle(&self, req: u32, next: Next<u32, u32>) -> crate::error::Result<u32> {
self.ticks.fetch_add(1, Ordering::SeqCst);
next.run(req).await
}
}
#[tokio::test]
async fn typed_add_pipeline_with_constructor_injects_from_the_resolver() {
let ticks = Arc::new(AtomicU32::new(0));
let provider = ServiceCollection::new()
.instance(Arc::clone(&ticks))
.add_typed_pipeline_with(|resolver| {
let ticks: Arc<AtomicU32> = resolve::<AtomicU32>(resolver);
Arc::new(TickBehavior { ticks }) as Arc<dyn IPipelineBehavior<u32, u32>>
})
.build()
.expect("provider must build");
let behavior = &provider.get_all::<dyn IPipelineBehavior<u32, u32>>()[0];
let out = behavior
.handle(5, Next::new(|n: u32| async move { Ok(n * 2) }))
.await
.expect("typed behavior must run");
assert_eq!(out, 10);
assert_eq!(ticks.load(Ordering::SeqCst), 1);
}
struct ShortCircuit;
#[async_trait::async_trait]
impl IPipelineBehavior<u32, String> for ShortCircuit {
async fn handle(
&self,
_req: u32,
_next: Next<u32, String>,
) -> crate::error::Result<String> {
Ok("short".to_string())
}
}
#[tokio::test]
async fn try_add_keeps_the_existing_registration() {
let provider = ServiceCollection::new()
.instance(Arc::new(1u32))
.try_add(|_| Arc::new(2u32))
.build()
.expect("provider must build");
let value = provider.get::<u32>().expect("registered");
assert_eq!(
*value, 1,
"try_add must not replace an earlier registration (MEDI TryAdd); rust-dix 0.8+ resolves the last registration otherwise"
);
}
#[tokio::test]
async fn typed_behavior_may_short_circuit_with_its_response_type() {
let out = ShortCircuit
.handle(1, string_terminal())
.await
.expect("short-circuit must return the typed response");
assert_eq!(out, "short");
}
}