#![allow(deprecated)]
use crate::handler::IHostedService;
use crate::mediator::Mediator;
use crate::middleware::IMiddleware;
use crate::pipeline::{IErasedPipelineBehavior, 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;
#[deprecated(
since = "0.10.0",
note = "use add_typed_pipeline::<B, Req, Rsp>() with the typed IPipelineBehavior"
)]
fn add_pipeline<T>(self) -> Self
where
T: IErasedPipelineBehavior + Default + Send + Sync + 'static;
#[deprecated(
since = "0.10.0",
note = "use add_typed_pipeline_with(...) with the typed IPipelineBehavior"
)]
fn add_pipeline_with<F>(self, factory: F) -> Self
where
F: Fn(&dyn IServiceResolver) -> Arc<dyn IErasedPipelineBehavior> + Send + Sync + '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");
Arc::new(Mediator::new(provider))
})
}
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)
}
#[allow(deprecated)]
fn add_pipeline<T>(self) -> Self
where
T: IErasedPipelineBehavior + Default + Send + Sync + 'static,
{
self.add_pipeline_with(|_| Arc::new(T::default()) as Arc<dyn IErasedPipelineBehavior>)
}
#[allow(deprecated)]
fn add_pipeline_with<F>(self, factory: F) -> Self
where
F: Fn(&dyn IServiceResolver) -> Arc<dyn IErasedPipelineBehavior> + Send + Sync + '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 {
#![allow(deprecated)]
use super::*;
use crate::pipeline::{
BoxedNextFn, BoxedPipelineFuture, IErasedPipelineBehavior, IPipelineBehavior, Next,
};
use std::any::Any;
use std::sync::atomic::{AtomicU32, Ordering};
struct TickingBehavior {
ticks: Arc<AtomicU32>,
}
#[async_trait::async_trait]
impl IErasedPipelineBehavior for TickingBehavior {
async fn handle(
&self,
req: Box<dyn Any + Send>,
next: BoxedNextFn,
) -> crate::error::Result<Box<dyn Any + Send>> {
self.ticks.fetch_add(1, Ordering::SeqCst);
next(req).await
}
}
fn terminal() -> BoxedNextFn {
Box::new(|_req: Box<dyn Any + Send>| -> BoxedPipelineFuture {
Box::pin(async { Ok(Box::new("handler-response") as Box<dyn Any + Send>) })
})
}
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")
}
#[tokio::test]
async fn legacy_add_pipeline_with_constructor_injects_from_the_resolver() {
let ticks = Arc::new(AtomicU32::new(0));
let provider = ServiceCollection::new()
.instance(Arc::clone(&ticks))
.add_pipeline_with(|resolver| {
let ticks: Arc<AtomicU32> = resolve::<AtomicU32>(resolver);
Arc::new(TickingBehavior { ticks }) as Arc<dyn IErasedPipelineBehavior>
})
.build()
.expect("provider must build");
let behavior = provider
.get::<dyn IErasedPipelineBehavior>()
.expect("the behavior must be registered as a singleton");
let response = behavior
.handle(Box::new("request"), terminal())
.await
.expect("behavior must pass through to the next step");
assert_eq!(
*response.downcast::<&str>().expect("handler response"),
"handler-response"
);
assert_eq!(ticks.load(Ordering::SeqCst), 1);
provider
.get::<dyn IErasedPipelineBehavior>()
.expect("the behavior must be registered as a singleton")
.handle(Box::new("request"), terminal())
.await
.expect("behavior must pass through to the next step");
assert_eq!(ticks.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn legacy_add_pipeline_keeps_registering_default_behaviors() {
#[derive(Default)]
struct NoopBehavior;
#[async_trait::async_trait]
impl IErasedPipelineBehavior for NoopBehavior {
async fn handle(
&self,
req: Box<dyn Any + Send>,
next: BoxedNextFn,
) -> crate::error::Result<Box<dyn Any + Send>> {
next(req).await
}
}
let provider = ServiceCollection::new()
.add_pipeline::<NoopBehavior>()
.build()
.expect("provider must build");
let response = provider
.get::<dyn IErasedPipelineBehavior>()
.expect("`add_pipeline` must still register a zero-dependency behavior")
.handle(Box::new("request"), terminal())
.await
.expect("behavior must pass through to the next step");
assert_eq!(
*response.downcast::<&str>().expect("handler response"),
"handler-response"
);
}
#[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 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");
}
}