use crate::context::RequestContext;
use crate::extract::FromRequest;
use crate::request::Request;
use crate::response::{IntoResponse, Response, ResponseBody, StatusCode};
use parking_lot::Mutex;
use parking_lot::RwLock;
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::future::Future;
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
use std::pin::Pin;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DependencyScope {
Function,
Request,
}
pub type CleanupFn = Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send>;
pub struct CleanupStack {
cleanups: Mutex<Vec<CleanupFn>>,
}
impl CleanupStack {
#[must_use]
pub fn new() -> Self {
Self {
cleanups: Mutex::new(Vec::new()),
}
}
pub fn push(&self, cleanup: CleanupFn) {
let mut guard = self.cleanups.lock();
guard.push(cleanup);
}
pub fn take_cleanups(&self) -> Vec<CleanupFn> {
let mut guard = self.cleanups.lock();
let mut cleanups = std::mem::take(&mut *guard);
cleanups.reverse(); cleanups
}
#[must_use]
pub fn len(&self) -> usize {
let guard = self.cleanups.lock();
guard.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub async fn run_cleanups(&self) -> usize {
let cleanups = self.take_cleanups();
let mut completed = 0;
for cleanup in cleanups {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (cleanup)()));
match result {
Ok(future) => {
if Self::run_cleanup_future(future).await {
completed += 1;
}
}
Err(_) => {
}
}
}
completed
}
async fn run_cleanup_future(mut future: Pin<Box<dyn Future<Output = ()> + Send>>) -> bool {
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::task::Poll;
std::future::poll_fn(move |cx| {
match catch_unwind(AssertUnwindSafe(|| future.as_mut().poll(cx))) {
Ok(Poll::Ready(())) => Poll::Ready(true),
Ok(Poll::Pending) => Poll::Pending,
Err(_) => Poll::Ready(false), }
})
.await
}
}
impl Default for CleanupStack {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for CleanupStack {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CleanupStack")
.field("count", &self.len())
.finish()
}
}
pub trait FromDependencyWithCleanup: Clone + Send + Sync + 'static {
type Value: Clone + Send + Sync + 'static;
type Error: IntoResponse + Send + Sync + 'static;
fn setup(
ctx: &RequestContext,
req: &mut Request,
) -> impl Future<Output = Result<(Self::Value, Option<CleanupFn>), Self::Error>> + Send;
}
#[derive(Debug, Clone)]
pub struct DependsCleanup<T, C = DefaultDependencyConfig>(pub T, PhantomData<C>);
impl<T, C> DependsCleanup<T, C> {
#[must_use]
pub fn new(value: T) -> Self {
Self(value, PhantomData)
}
#[must_use]
pub fn into_inner(self) -> T {
self.0
}
}
impl<T, C> Deref for DependsCleanup<T, C> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T, C> DerefMut for DependsCleanup<T, C> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T, C> FromRequest for DependsCleanup<T, C>
where
T: FromDependencyWithCleanup<Value = T>,
C: DependsConfig,
{
type Error = T::Error;
async fn from_request(ctx: &RequestContext, req: &mut Request) -> Result<Self, Self::Error> {
let _ = ctx.checkpoint();
let scope = C::SCOPE.unwrap_or(DependencyScope::Request);
let use_cache = C::USE_CACHE && scope == DependencyScope::Request;
if use_cache {
if let Some(cached) = ctx.dependency_cache().get::<T::Value>() {
return Ok(DependsCleanup::new(cached));
}
}
let type_name = std::any::type_name::<T>();
if let Some(cycle) = ctx.resolution_stack().check_cycle::<T>(type_name) {
handle_circular_dependency(cycle);
}
if let Some(scope_err) = ctx
.resolution_stack()
.check_scope_violation(type_name, scope)
{
handle_scope_violation(scope_err);
}
ctx.resolution_stack().push::<T>(type_name, scope);
let _guard = ResolutionGuard::new(ctx.resolution_stack());
let _ = ctx.checkpoint();
let (value, cleanup) = T::setup(ctx, req).await?;
if let Some(cleanup_fn) = cleanup {
ctx.cleanup_stack().push(cleanup_fn);
}
if use_cache {
ctx.dependency_cache().insert::<T::Value>(value.clone());
}
Ok(DependsCleanup::new(value))
}
}
pub trait DependsConfig {
const USE_CACHE: bool;
const SCOPE: Option<DependencyScope>;
}
#[derive(Debug, Clone, Copy)]
pub struct DefaultDependencyConfig;
pub type DefaultConfig = DefaultDependencyConfig;
impl DependsConfig for DefaultDependencyConfig {
const USE_CACHE: bool = true;
const SCOPE: Option<DependencyScope> = None;
}
#[derive(Debug, Clone, Copy)]
pub struct NoCache;
impl DependsConfig for NoCache {
const USE_CACHE: bool = false;
const SCOPE: Option<DependencyScope> = Some(DependencyScope::Function);
}
#[derive(Debug, Clone)]
pub struct CircularDependencyError {
pub cycle: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct DependencyScopeError {
pub request_scoped_type: String,
pub function_scoped_type: String,
}
impl CircularDependencyError {
#[must_use]
pub fn new(cycle: Vec<String>) -> Self {
Self { cycle }
}
#[must_use]
pub fn cycle_path(&self) -> String {
self.cycle.join(" -> ")
}
}
impl std::fmt::Display for CircularDependencyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Circular dependency detected: {}", self.cycle_path())
}
}
impl std::error::Error for CircularDependencyError {}
impl IntoResponse for CircularDependencyError {
fn into_response(self) -> Response {
let body = format!(
r#"{{"detail":"Circular dependency detected: {}"}}"#,
self.cycle_path()
);
Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
.header("content-type", b"application/json".to_vec())
.body(ResponseBody::Bytes(body.into_bytes()))
}
}
impl DependencyScopeError {
#[must_use]
pub fn new(request_scoped_type: String, function_scoped_type: String) -> Self {
Self {
request_scoped_type,
function_scoped_type,
}
}
}
impl std::fmt::Display for DependencyScopeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Dependency scope violation: request-scoped '{}' depends on function-scoped '{}'. \
Request-scoped dependencies cannot depend on function-scoped dependencies \
because the cached value becomes stale.",
self.request_scoped_type, self.function_scoped_type
)
}
}
impl std::error::Error for DependencyScopeError {}
impl IntoResponse for DependencyScopeError {
fn into_response(self) -> Response {
let body = format!(
r#"{{"detail":"Dependency scope violation: request-scoped '{}' depends on function-scoped '{}'. Request-scoped dependencies cannot depend on function-scoped dependencies."}}"#,
self.request_scoped_type, self.function_scoped_type
);
Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
.header("content-type", b"application/json".to_vec())
.body(ResponseBody::Bytes(body.into_bytes()))
}
}
#[cold]
#[inline(never)]
fn handle_circular_dependency(cycle: Vec<String>) -> ! {
let err = CircularDependencyError::new(cycle);
panic!(
"\n\n\
╔══════════════════════════════════════════════════════════════════════╗\n\
║ CIRCULAR DEPENDENCY DETECTED ║\n\
╠══════════════════════════════════════════════════════════════════════╣\n\
║ {}\n\
╠══════════════════════════════════════════════════════════════════════╣\n\
║ This is a configuration error in your dependency graph. ║\n\
║ ║\n\
║ To fix: ║\n\
║ 1. Review the cycle path above ║\n\
║ 2. Break the cycle by removing or refactoring one dependency ║\n\
║ 3. Consider using lazy initialization or scoped dependencies ║\n\
╚══════════════════════════════════════════════════════════════════════╝\n",
err
);
}
#[cold]
#[inline(never)]
fn handle_scope_violation(scope_err: DependencyScopeError) -> ! {
panic!(
"\n\n\
╔══════════════════════════════════════════════════════════════════════╗\n\
║ DEPENDENCY SCOPE VIOLATION ║\n\
╠══════════════════════════════════════════════════════════════════════╣\n\
║ {}\n\
╠══════════════════════════════════════════════════════════════════════╣\n\
║ Request-scoped dependencies are cached per-request and must not ║\n\
║ depend on function-scoped dependencies (which are created fresh ║\n\
║ each time). ║\n\
║ ║\n\
║ To fix: ║\n\
║ 1. Change the inner dependency to request scope ║\n\
║ 2. Or change the outer dependency to function scope ║\n\
║ 3. Or restructure to avoid the dependency ║\n\
╚══════════════════════════════════════════════════════════════════════╝\n",
scope_err
);
}
pub struct ResolutionStack {
stack: RwLock<Vec<(TypeId, String, DependencyScope)>>,
}
impl ResolutionStack {
#[must_use]
pub fn new() -> Self {
Self {
stack: RwLock::new(Vec::new()),
}
}
pub fn check_cycle<T: 'static>(&self, type_name: &str) -> Option<Vec<String>> {
let type_id = TypeId::of::<T>();
let guard = self.stack.read();
if let Some(pos) = guard.iter().position(|(id, _, _)| *id == type_id) {
let mut cycle: Vec<String> = guard[pos..]
.iter()
.map(|(_, name, _)| name.clone())
.collect();
cycle.push(type_name.to_owned());
return Some(cycle);
}
None
}
pub fn check_scope_violation(
&self,
type_name: &str,
scope: DependencyScope,
) -> Option<DependencyScopeError> {
if scope != DependencyScope::Function {
return None;
}
let guard = self.stack.read();
for (_, name, dep_scope) in guard.iter().rev() {
if *dep_scope == DependencyScope::Request {
return Some(DependencyScopeError::new(
name.clone(),
type_name.to_owned(),
));
}
}
None
}
pub fn push<T: 'static>(&self, type_name: &str, scope: DependencyScope) {
let mut guard = self.stack.write();
guard.push((TypeId::of::<T>(), type_name.to_owned(), scope));
}
pub fn pop(&self) {
let mut guard = self.stack.write();
guard.pop();
}
#[must_use]
pub fn depth(&self) -> usize {
let guard = self.stack.read();
guard.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.depth() == 0
}
}
impl Default for ResolutionStack {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for ResolutionStack {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let guard = self.stack.read();
f.debug_struct("ResolutionStack")
.field("depth", &guard.len())
.field(
"types",
&guard
.iter()
.map(|(_, name, scope)| format!("{}({:?})", name, scope))
.collect::<Vec<_>>(),
)
.finish()
}
}
pub struct ResolutionGuard<'a> {
stack: &'a ResolutionStack,
}
impl<'a> ResolutionGuard<'a> {
fn new(stack: &'a ResolutionStack) -> Self {
Self { stack }
}
}
impl Drop for ResolutionGuard<'_> {
fn drop(&mut self) {
self.stack.pop();
}
}
#[derive(Debug, Clone)]
pub struct Depends<T, C = DefaultDependencyConfig>(pub T, PhantomData<C>);
impl<T, C> Depends<T, C> {
#[must_use]
pub fn new(value: T) -> Self {
Self(value, PhantomData)
}
#[must_use]
pub fn into_inner(self) -> T {
self.0
}
}
impl<T, C> Deref for Depends<T, C> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T, C> DerefMut for Depends<T, C> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
pub trait FromDependency: Clone + Send + Sync + 'static {
type Error: IntoResponse + Send + Sync + 'static;
fn from_dependency(
ctx: &RequestContext,
req: &mut Request,
) -> impl Future<Output = Result<Self, Self::Error>> + Send;
}
impl<T, C> FromRequest for Depends<T, C>
where
T: FromDependency,
C: DependsConfig,
{
type Error = T::Error;
async fn from_request(ctx: &RequestContext, req: &mut Request) -> Result<Self, Self::Error> {
let _ = ctx.checkpoint();
if let Some(result) = ctx.dependency_overrides().resolve::<T>(ctx, req).await {
return result.map(Depends::new);
}
let scope = C::SCOPE.unwrap_or(DependencyScope::Request);
let use_cache = C::USE_CACHE && scope == DependencyScope::Request;
if use_cache {
if let Some(cached) = ctx.dependency_cache().get::<T>() {
return Ok(Depends::new(cached));
}
}
let type_name = std::any::type_name::<T>();
if let Some(cycle) = ctx.resolution_stack().check_cycle::<T>(type_name) {
handle_circular_dependency(cycle);
}
if let Some(scope_err) = ctx
.resolution_stack()
.check_scope_violation(type_name, scope)
{
handle_scope_violation(scope_err);
}
ctx.resolution_stack().push::<T>(type_name, scope);
let _guard = ResolutionGuard::new(ctx.resolution_stack());
let _ = ctx.checkpoint();
let value = T::from_dependency(ctx, req).await?;
if use_cache {
ctx.dependency_cache().insert::<T>(value.clone());
}
Ok(Depends::new(value))
}
}
pub struct DependencyCache {
inner: RwLock<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
}
impl DependencyCache {
#[must_use]
pub fn new() -> Self {
Self {
inner: RwLock::new(HashMap::new()),
}
}
#[must_use]
pub fn get<T: Clone + Send + Sync + 'static>(&self) -> Option<T> {
let guard = self.inner.read();
guard
.get(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast_ref::<T>())
.cloned()
}
pub fn insert<T: Clone + Send + Sync + 'static>(&self, value: T) {
let mut guard = self.inner.write();
guard.insert(TypeId::of::<T>(), Box::new(value));
}
pub fn clear(&self) {
let mut guard = self.inner.write();
guard.clear();
}
#[must_use]
pub fn len(&self) -> usize {
let guard = self.inner.read();
guard.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Default for DependencyCache {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for DependencyCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DependencyCache")
.field("size", &self.len())
.finish()
}
}
type OverrideBox = Box<dyn Any + Send + Sync>;
type OverrideFuture = Pin<Box<dyn Future<Output = Result<OverrideBox, OverrideBox>> + Send>>;
type OverrideFn = Arc<dyn Fn(&RequestContext, &mut Request) -> OverrideFuture + Send + Sync>;
pub struct DependencyOverrides {
inner: RwLock<HashMap<TypeId, OverrideFn>>,
}
impl DependencyOverrides {
#[must_use]
pub fn new() -> Self {
Self {
inner: RwLock::new(HashMap::new()),
}
}
pub fn insert<T, F, Fut>(&self, f: F)
where
T: FromDependency,
F: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<T, T::Error>> + Send + 'static,
{
let wrapper: OverrideFn = Arc::new(move |ctx, req| {
let fut = f(ctx, req);
Box::pin(async move {
match fut.await {
Ok(value) => Ok(Box::new(value) as OverrideBox),
Err(err) => Err(Box::new(err) as OverrideBox),
}
})
});
let mut guard = self.inner.write();
guard.insert(TypeId::of::<T>(), wrapper);
}
pub fn insert_value<T>(&self, value: T)
where
T: FromDependency,
{
self.insert::<T, _, _>(move |_ctx, _req| {
let value = value.clone();
async move { Ok(value) }
});
}
pub fn clear(&self) {
let mut guard = self.inner.write();
guard.clear();
}
pub async fn resolve<T>(
&self,
ctx: &RequestContext,
req: &mut Request,
) -> Option<Result<T, T::Error>>
where
T: FromDependency,
{
let override_fn = {
let guard = self.inner.read();
guard.get(&TypeId::of::<T>()).cloned()
};
let override_fn = override_fn?;
match override_fn(ctx, req).await {
Ok(value) => match value.downcast::<T>() {
Ok(value) => Some(Ok(*value)),
Err(_) => {
debug_assert!(
false,
"dependency override type mismatch: expected {}, stored override returned wrong type",
std::any::type_name::<T>()
);
None
}
},
Err(err) => match err.downcast::<T::Error>() {
Ok(err) => Some(Err(*err)),
Err(_) => {
debug_assert!(
false,
"dependency override error type mismatch: expected {}, stored override returned wrong error type",
std::any::type_name::<T::Error>()
);
None
}
},
}
}
#[must_use]
pub fn len(&self) -> usize {
let guard = self.inner.read();
guard.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Default for DependencyOverrides {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for DependencyOverrides {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DependencyOverrides")
.field("size", &self.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::HttpError;
use crate::request::Method;
use asupersync::Cx;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
fn test_context(overrides: Option<Arc<DependencyOverrides>>) -> RequestContext {
let cx = Cx::for_testing();
let request_id = 1;
if let Some(overrides) = overrides {
RequestContext::with_overrides(cx, request_id, overrides)
} else {
RequestContext::new(cx, request_id)
}
}
fn empty_request() -> Request {
Request::new(Method::Get, "/")
}
#[derive(Clone)]
struct CounterDep {
value: usize,
}
impl FromDependency for CounterDep {
type Error = HttpError;
async fn from_dependency(
_ctx: &RequestContext,
_req: &mut Request,
) -> Result<Self, Self::Error> {
Ok(CounterDep { value: 1 })
}
}
#[test]
fn depends_basic_resolution() {
let ctx = test_context(None);
let mut req = empty_request();
let dep = futures_executor::block_on(Depends::<CounterDep>::from_request(&ctx, &mut req))
.expect("dependency resolution failed");
assert_eq!(dep.value, 1);
}
#[derive(Clone)]
struct CountingDep;
impl FromDependency for CountingDep {
type Error = HttpError;
async fn from_dependency(
ctx: &RequestContext,
_req: &mut Request,
) -> Result<Self, Self::Error> {
let count = ctx
.dependency_cache()
.get::<Arc<AtomicUsize>>()
.unwrap_or_else(|| Arc::new(AtomicUsize::new(0)));
count.fetch_add(1, Ordering::SeqCst);
ctx.dependency_cache().insert(Arc::clone(&count));
Ok(CountingDep)
}
}
#[test]
fn depends_caches_per_request() {
let ctx = test_context(None);
let mut req = empty_request();
let _ = futures_executor::block_on(Depends::<CountingDep>::from_request(&ctx, &mut req))
.expect("first resolution failed");
let _ = futures_executor::block_on(Depends::<CountingDep>::from_request(&ctx, &mut req))
.expect("second resolution failed");
let counter = ctx
.dependency_cache()
.get::<Arc<AtomicUsize>>()
.expect("missing counter");
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[test]
fn depends_no_cache_config() {
let ctx = test_context(None);
let mut req = empty_request();
let _ = futures_executor::block_on(Depends::<CountingDep, NoCache>::from_request(
&ctx, &mut req,
))
.expect("first resolution failed");
let _ = futures_executor::block_on(Depends::<CountingDep, NoCache>::from_request(
&ctx, &mut req,
))
.expect("second resolution failed");
let counter = ctx
.dependency_cache()
.get::<Arc<AtomicUsize>>()
.expect("missing counter");
assert_eq!(counter.load(Ordering::SeqCst), 2);
}
#[derive(Clone)]
struct DepB;
impl FromDependency for DepB {
type Error = HttpError;
async fn from_dependency(
_ctx: &RequestContext,
_req: &mut Request,
) -> Result<Self, Self::Error> {
Ok(DepB)
}
}
#[derive(Clone)]
struct DepA;
impl FromDependency for DepA {
type Error = HttpError;
async fn from_dependency(
ctx: &RequestContext,
req: &mut Request,
) -> Result<Self, Self::Error> {
let _ = Depends::<DepB>::from_request(ctx, req).await?;
Ok(DepA)
}
}
#[test]
fn depends_nested_resolution() {
let ctx = test_context(None);
let mut req = empty_request();
let _ = futures_executor::block_on(Depends::<DepA>::from_request(&ctx, &mut req))
.expect("nested resolution failed");
}
#[derive(Clone, Debug)]
struct OverrideDep {
value: usize,
}
impl FromDependency for OverrideDep {
type Error = HttpError;
async fn from_dependency(
_ctx: &RequestContext,
_req: &mut Request,
) -> Result<Self, Self::Error> {
Ok(OverrideDep { value: 1 })
}
}
#[test]
fn depends_override_substitution() {
let overrides = Arc::new(DependencyOverrides::new());
overrides.insert_value(OverrideDep { value: 42 });
let ctx = test_context(Some(overrides));
let mut req = empty_request();
let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
.expect("override resolution failed");
assert_eq!(dep.value, 42);
}
#[derive(Clone, Debug)]
struct ErrorDep;
impl FromDependency for ErrorDep {
type Error = HttpError;
async fn from_dependency(
_ctx: &RequestContext,
_req: &mut Request,
) -> Result<Self, Self::Error> {
Err(HttpError::bad_request().with_detail("boom"))
}
}
#[test]
fn depends_error_propagation() {
let ctx = test_context(None);
let mut req = empty_request();
let err = futures_executor::block_on(Depends::<ErrorDep>::from_request(&ctx, &mut req))
.expect_err("expected dependency error");
assert_eq!(err.status.as_u16(), 400);
}
#[derive(Clone)]
struct DepC;
impl FromDependency for DepC {
type Error = HttpError;
async fn from_dependency(
ctx: &RequestContext,
req: &mut Request,
) -> Result<Self, Self::Error> {
let _ = Depends::<DepA>::from_request(ctx, req).await?;
let _ = Depends::<DepB>::from_request(ctx, req).await?;
Ok(DepC)
}
}
#[test]
fn depends_complex_graph() {
let ctx = test_context(None);
let mut req = empty_request();
let _ = futures_executor::block_on(Depends::<DepC>::from_request(&ctx, &mut req))
.expect("complex graph resolution failed");
}
#[test]
fn resolution_stack_detects_simple_cycle() {
struct TypeA;
struct TypeB;
let stack = ResolutionStack::new();
assert!(stack.check_cycle::<TypeA>("TypeA").is_none());
stack.push::<TypeA>("TypeA", DependencyScope::Request);
assert!(stack.check_cycle::<TypeB>("TypeB").is_none());
stack.push::<TypeB>("TypeB", DependencyScope::Request);
let cycle = stack.check_cycle::<TypeA>("TypeA");
assert!(cycle.is_some(), "Should detect A -> B -> A cycle");
let cycle_path = cycle.unwrap();
assert_eq!(cycle_path.len(), 3); assert_eq!(cycle_path[0], "TypeA");
assert_eq!(cycle_path[1], "TypeB");
assert_eq!(cycle_path[2], "TypeA");
}
#[test]
fn resolution_stack_detects_long_cycle() {
struct TypeA;
struct TypeB;
struct TypeC;
struct TypeD;
let stack = ResolutionStack::new();
stack.push::<TypeA>("TypeA", DependencyScope::Request);
stack.push::<TypeB>("TypeB", DependencyScope::Request);
stack.push::<TypeC>("TypeC", DependencyScope::Request);
stack.push::<TypeD>("TypeD", DependencyScope::Request);
let cycle = stack.check_cycle::<TypeA>("TypeA");
assert!(cycle.is_some(), "Should detect A -> B -> C -> D -> A cycle");
let cycle_path = cycle.unwrap();
assert_eq!(cycle_path.len(), 5); assert_eq!(cycle_path[0], "TypeA");
assert_eq!(cycle_path[4], "TypeA");
}
#[test]
fn resolution_stack_allows_diamond() {
struct Top;
struct Left;
struct Right;
struct Bottom;
let stack = ResolutionStack::new();
stack.push::<Top>("Top", DependencyScope::Request);
stack.push::<Left>("Left", DependencyScope::Request);
stack.push::<Bottom>("Bottom", DependencyScope::Request);
stack.pop(); stack.pop();
stack.push::<Right>("Right", DependencyScope::Request);
assert!(
stack.check_cycle::<Bottom>("Bottom").is_none(),
"Diamond pattern should not be detected as a cycle"
);
stack.push::<Bottom>("Bottom", DependencyScope::Request);
stack.pop(); stack.pop(); stack.pop();
assert!(stack.is_empty());
}
#[test]
fn resolution_stack_detects_self_dependency() {
struct SelfRef;
let stack = ResolutionStack::new();
stack.push::<SelfRef>("SelfRef", DependencyScope::Request);
let cycle = stack.check_cycle::<SelfRef>("SelfRef");
assert!(cycle.is_some(), "Should detect self-dependency");
let cycle_path = cycle.unwrap();
assert_eq!(cycle_path.len(), 2); assert_eq!(cycle_path[0], "SelfRef");
assert_eq!(cycle_path[1], "SelfRef");
}
#[test]
fn resolution_stack_basic() {
let stack = ResolutionStack::new();
assert!(stack.is_empty());
assert_eq!(stack.depth(), 0);
stack.push::<CounterDep>("CounterDep", DependencyScope::Request);
assert!(!stack.is_empty());
assert_eq!(stack.depth(), 1);
assert!(stack.check_cycle::<ErrorDep>("ErrorDep").is_none());
stack.push::<ErrorDep>("ErrorDep", DependencyScope::Request);
assert_eq!(stack.depth(), 2);
let cycle = stack.check_cycle::<CounterDep>("CounterDep");
assert!(cycle.is_some());
let cycle_path = cycle.unwrap();
assert!(cycle_path.contains(&"CounterDep".to_string()));
stack.pop();
assert_eq!(stack.depth(), 1);
stack.pop();
assert!(stack.is_empty());
}
#[test]
fn circular_dependency_error_formatting() {
let err = CircularDependencyError::new(vec![
"DbPool".to_string(),
"UserService".to_string(),
"AuthService".to_string(),
"DbPool".to_string(),
]);
let msg = err.to_string();
assert!(msg.contains("Circular dependency detected"));
assert!(msg.contains("DbPool -> UserService -> AuthService -> DbPool"));
assert_eq!(
err.cycle_path(),
"DbPool -> UserService -> AuthService -> DbPool"
);
}
#[test]
fn circular_dependency_error_into_response() {
let err =
CircularDependencyError::new(vec!["A".to_string(), "B".to_string(), "A".to_string()]);
let response = err.into_response();
assert_eq!(response.status().as_u16(), 500);
}
#[derive(Clone)]
struct DiamondBottom {
id: u32,
}
#[derive(Clone)]
struct DiamondLeft {
bottom_id: u32,
}
#[derive(Clone)]
struct DiamondRight {
bottom_id: u32,
}
#[derive(Clone)]
struct DiamondTop {
left_id: u32,
right_id: u32,
}
impl FromDependency for DiamondBottom {
type Error = HttpError;
async fn from_dependency(
_ctx: &RequestContext,
_req: &mut Request,
) -> Result<Self, Self::Error> {
Ok(DiamondBottom { id: 42 })
}
}
impl FromDependency for DiamondLeft {
type Error = HttpError;
async fn from_dependency(
ctx: &RequestContext,
req: &mut Request,
) -> Result<Self, Self::Error> {
let bottom = Depends::<DiamondBottom>::from_request(ctx, req).await?;
Ok(DiamondLeft {
bottom_id: bottom.id,
})
}
}
impl FromDependency for DiamondRight {
type Error = HttpError;
async fn from_dependency(
ctx: &RequestContext,
req: &mut Request,
) -> Result<Self, Self::Error> {
let bottom = Depends::<DiamondBottom>::from_request(ctx, req).await?;
Ok(DiamondRight {
bottom_id: bottom.id,
})
}
}
impl FromDependency for DiamondTop {
type Error = HttpError;
async fn from_dependency(
ctx: &RequestContext,
req: &mut Request,
) -> Result<Self, Self::Error> {
let left = Depends::<DiamondLeft>::from_request(ctx, req).await?;
let right = Depends::<DiamondRight>::from_request(ctx, req).await?;
Ok(DiamondTop {
left_id: left.bottom_id,
right_id: right.bottom_id,
})
}
}
#[test]
fn diamond_pattern_resolves_correctly() {
let ctx = test_context(None);
let mut req = empty_request();
let result =
futures_executor::block_on(Depends::<DiamondTop>::from_request(&ctx, &mut req));
assert!(result.is_ok(), "Diamond pattern should not be a cycle");
let top = result.unwrap();
assert_eq!(top.left_id, 42);
assert_eq!(top.right_id, 42);
}
#[derive(Clone)]
struct NestedInnerDep {
value: String,
}
impl FromDependency for NestedInnerDep {
type Error = HttpError;
async fn from_dependency(
_ctx: &RequestContext,
_req: &mut Request,
) -> Result<Self, Self::Error> {
Ok(NestedInnerDep {
value: "original".to_string(),
})
}
}
#[derive(Clone)]
struct NestedOuterDep {
inner_value: String,
}
impl FromDependency for NestedOuterDep {
type Error = HttpError;
async fn from_dependency(
ctx: &RequestContext,
req: &mut Request,
) -> Result<Self, Self::Error> {
let inner = Depends::<NestedInnerDep>::from_request(ctx, req).await?;
Ok(NestedOuterDep {
inner_value: inner.value.clone(),
})
}
}
#[test]
fn override_affects_nested_dependencies() {
let overrides = Arc::new(DependencyOverrides::new());
overrides.insert_value(NestedInnerDep {
value: "overridden".to_string(),
});
let ctx = test_context(Some(overrides));
let mut req = empty_request();
let result =
futures_executor::block_on(Depends::<NestedOuterDep>::from_request(&ctx, &mut req))
.expect("nested override resolution failed");
assert_eq!(
result.inner_value, "overridden",
"Override should propagate to nested dependencies"
);
}
#[test]
fn clear_overrides_restores_original() {
let overrides = Arc::new(DependencyOverrides::new());
overrides.insert_value(OverrideDep { value: 99 });
assert_eq!(overrides.len(), 1);
overrides.clear();
assert_eq!(overrides.len(), 0);
assert!(overrides.is_empty());
let ctx = test_context(Some(overrides));
let mut req = empty_request();
let result =
futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
.expect("resolution after clear failed");
assert_eq!(result.value, 1, "After clear, should resolve to original");
}
#[derive(Clone)]
struct DepX {
x: i32,
}
#[derive(Clone)]
struct DepY {
y: i32,
}
#[derive(Clone)]
struct DepZ {
z: i32,
}
impl FromDependency for DepX {
type Error = HttpError;
async fn from_dependency(
_ctx: &RequestContext,
_req: &mut Request,
) -> Result<Self, Self::Error> {
Ok(DepX { x: 10 })
}
}
impl FromDependency for DepY {
type Error = HttpError;
async fn from_dependency(
_ctx: &RequestContext,
_req: &mut Request,
) -> Result<Self, Self::Error> {
Ok(DepY { y: 20 })
}
}
impl FromDependency for DepZ {
type Error = HttpError;
async fn from_dependency(
_ctx: &RequestContext,
_req: &mut Request,
) -> Result<Self, Self::Error> {
Ok(DepZ { z: 30 })
}
}
#[test]
fn multiple_independent_dependencies() {
let ctx = test_context(None);
let mut req = empty_request();
let dep_x =
futures_executor::block_on(Depends::<DepX>::from_request(&ctx, &mut req)).unwrap();
let dep_y =
futures_executor::block_on(Depends::<DepY>::from_request(&ctx, &mut req)).unwrap();
let dep_z =
futures_executor::block_on(Depends::<DepZ>::from_request(&ctx, &mut req)).unwrap();
assert_eq!(dep_x.x, 10);
assert_eq!(dep_y.y, 20);
assert_eq!(dep_z.z, 30);
}
#[test]
fn request_scope_isolation() {
let ctx1 = test_context(None);
let mut req1 = empty_request();
let _ = futures_executor::block_on(Depends::<CountingDep>::from_request(&ctx1, &mut req1))
.unwrap();
let counter1 = ctx1.dependency_cache().get::<Arc<AtomicUsize>>().unwrap();
let ctx2 = test_context(None);
let mut req2 = empty_request();
let _ = futures_executor::block_on(Depends::<CountingDep>::from_request(&ctx2, &mut req2))
.unwrap();
let counter2 = ctx2.dependency_cache().get::<Arc<AtomicUsize>>().unwrap();
assert_eq!(counter1.load(Ordering::SeqCst), 1);
assert_eq!(counter2.load(Ordering::SeqCst), 1);
assert!(!Arc::ptr_eq(&counter1, &counter2));
}
#[test]
fn function_scope_no_caching() {
let ctx = test_context(None);
let mut req = empty_request();
let _ = futures_executor::block_on(Depends::<CountingDep, NoCache>::from_request(
&ctx, &mut req,
))
.unwrap();
let _ = futures_executor::block_on(Depends::<CountingDep, NoCache>::from_request(
&ctx, &mut req,
))
.unwrap();
let _ = futures_executor::block_on(Depends::<CountingDep, NoCache>::from_request(
&ctx, &mut req,
))
.unwrap();
let counter = ctx.dependency_cache().get::<Arc<AtomicUsize>>().unwrap();
assert_eq!(
counter.load(Ordering::SeqCst),
3,
"NoCache should resolve 3 times"
);
}
#[derive(Clone)]
struct AsyncDep {
computed: u64,
}
impl FromDependency for AsyncDep {
type Error = HttpError;
async fn from_dependency(
_ctx: &RequestContext,
_req: &mut Request,
) -> Result<Self, Self::Error> {
let result = async {
let a = 21u64;
let b = 21u64;
a + b
}
.await;
Ok(AsyncDep { computed: result })
}
}
#[test]
fn async_dependency_resolution() {
let ctx = test_context(None);
let mut req = empty_request();
let dep = futures_executor::block_on(Depends::<AsyncDep>::from_request(&ctx, &mut req))
.expect("async dependency resolution failed");
assert_eq!(dep.computed, 42);
}
#[derive(Clone, Debug)]
struct DepThatDependsOnError;
impl FromDependency for DepThatDependsOnError {
type Error = HttpError;
async fn from_dependency(
ctx: &RequestContext,
req: &mut Request,
) -> Result<Self, Self::Error> {
let _ = Depends::<ErrorDep>::from_request(ctx, req).await?;
Ok(DepThatDependsOnError)
}
}
#[test]
fn nested_error_propagation() {
let ctx = test_context(None);
let mut req = empty_request();
let result = futures_executor::block_on(Depends::<DepThatDependsOnError>::from_request(
&ctx, &mut req,
));
assert!(result.is_err(), "Nested error should propagate");
let err = result.unwrap_err();
assert_eq!(err.status.as_u16(), 400);
}
#[test]
fn dependency_cache_operations() {
let cache = DependencyCache::new();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
cache.insert::<String>("test".to_string());
assert!(!cache.is_empty());
assert_eq!(cache.len(), 1);
cache.insert::<i32>(42);
assert_eq!(cache.len(), 2);
let retrieved = cache.get::<String>().unwrap();
assert_eq!(retrieved, "test");
cache.clear();
assert!(cache.is_empty());
assert!(cache.get::<String>().is_none());
}
#[test]
fn dynamic_override_resolver() {
let overrides = Arc::new(DependencyOverrides::new());
overrides.insert::<OverrideDep, _, _>(|_ctx, _req| async move {
Ok(OverrideDep { value: 100 })
});
let ctx = test_context(Some(overrides));
let mut req = empty_request();
let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
.expect("dynamic override failed");
assert_eq!(dep.value, 100);
}
#[test]
fn overrides_new_is_empty() {
let overrides = DependencyOverrides::new();
assert!(overrides.is_empty());
assert_eq!(overrides.len(), 0);
}
#[test]
fn overrides_default_is_empty() {
let overrides = DependencyOverrides::default();
assert!(overrides.is_empty());
assert_eq!(overrides.len(), 0);
}
#[test]
fn overrides_debug_format() {
let overrides = DependencyOverrides::new();
let debug = format!("{:?}", overrides);
assert!(debug.contains("DependencyOverrides"));
assert!(debug.contains("size"));
}
#[test]
fn overrides_insert_value_increments_len() {
let overrides = DependencyOverrides::new();
assert_eq!(overrides.len(), 0);
overrides.insert_value(OverrideDep { value: 42 });
assert_eq!(overrides.len(), 1);
assert!(!overrides.is_empty());
}
#[test]
fn overrides_multiple_types_registered() {
let overrides = Arc::new(DependencyOverrides::new());
overrides.insert_value(OverrideDep { value: 10 });
overrides.insert_value(NestedInnerDep {
value: "mocked".to_string(),
});
assert_eq!(overrides.len(), 2);
let ctx = test_context(Some(overrides.clone()));
let mut req = empty_request();
let dep1 = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
.expect("OverrideDep override failed");
assert_eq!(dep1.value, 10);
let mut req2 = empty_request();
let dep2 =
futures_executor::block_on(Depends::<NestedInnerDep>::from_request(&ctx, &mut req2))
.expect("NestedInnerDep override failed");
assert_eq!(dep2.value, "mocked");
}
#[test]
fn overrides_replace_same_type() {
let overrides = Arc::new(DependencyOverrides::new());
overrides.insert_value(OverrideDep { value: 1 });
assert_eq!(overrides.len(), 1);
overrides.insert_value(OverrideDep { value: 999 });
assert_eq!(overrides.len(), 1);
let ctx = test_context(Some(overrides));
let mut req = empty_request();
let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
.expect("override resolution failed");
assert_eq!(dep.value, 999);
}
#[test]
fn overrides_clear_removes_all() {
let overrides = DependencyOverrides::new();
overrides.insert_value(OverrideDep { value: 42 });
overrides.insert_value(NestedInnerDep {
value: "mock".to_string(),
});
assert_eq!(overrides.len(), 2);
overrides.clear();
assert!(overrides.is_empty());
assert_eq!(overrides.len(), 0);
}
#[test]
fn overrides_resolve_returns_none_for_unregistered_type() {
let overrides = Arc::new(DependencyOverrides::new());
overrides.insert_value(OverrideDep { value: 42 });
let ctx = test_context(Some(overrides.clone()));
let mut req = empty_request();
let result =
futures_executor::block_on(overrides.resolve::<NestedInnerDep>(&ctx, &mut req));
assert!(result.is_none(), "Unregistered type should resolve to None");
}
#[test]
fn overrides_resolve_some_for_registered_type() {
let overrides = Arc::new(DependencyOverrides::new());
overrides.insert_value(OverrideDep { value: 77 });
let ctx = test_context(Some(overrides.clone()));
let mut req = empty_request();
let result = futures_executor::block_on(overrides.resolve::<OverrideDep>(&ctx, &mut req));
assert!(result.is_some());
let dep = result.unwrap().expect("resolve should succeed");
assert_eq!(dep.value, 77);
}
#[test]
fn overrides_not_affect_unrelated_dependency() {
let overrides = Arc::new(DependencyOverrides::new());
overrides.insert_value(NestedInnerDep {
value: "overridden".to_string(),
});
let ctx = test_context(Some(overrides));
let mut req = empty_request();
let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
.expect("should resolve from real implementation");
assert_eq!(
dep.value, 1,
"Unoverridden dep should use real implementation"
);
}
#[test]
fn overrides_take_precedence_over_cache() {
let overrides = Arc::new(DependencyOverrides::new());
overrides.insert_value(OverrideDep { value: 42 });
let ctx = test_context(Some(overrides));
ctx.dependency_cache().insert(OverrideDep { value: 999 });
let mut req = empty_request();
let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
.expect("override should take precedence");
assert_eq!(dep.value, 42, "Override should take precedence over cache");
}
#[test]
fn overrides_dynamic_resolver_can_return_error() {
let overrides = Arc::new(DependencyOverrides::new());
overrides.insert::<OverrideDep, _, _>(|_ctx, _req| async move {
Err(
HttpError::new(crate::response::StatusCode::INTERNAL_SERVER_ERROR)
.with_detail("override error"),
)
});
let ctx = test_context(Some(overrides));
let mut req = empty_request();
let err = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
.expect_err("override should return error");
assert_eq!(err.status.as_u16(), 500);
}
#[test]
fn overrides_insert_value_works_for_multiple_resolves() {
let overrides = Arc::new(DependencyOverrides::new());
overrides.insert_value(OverrideDep { value: 7 });
let ctx = test_context(Some(overrides));
for _ in 0..5 {
let mut req = empty_request();
let dep =
futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
.expect("repeated resolve should work");
assert_eq!(dep.value, 7);
}
}
#[test]
fn overrides_dynamic_resolver_accesses_request() {
let overrides = Arc::new(DependencyOverrides::new());
overrides.insert::<OverrideDep, _, _>(|_ctx, req| {
let value = req.get_extension::<usize>().copied().unwrap_or(0);
async move { Ok(OverrideDep { value }) }
});
let ctx = test_context(Some(overrides));
let mut req = empty_request();
req.insert_extension(42usize);
let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
.expect("dynamic resolver with request access failed");
assert_eq!(dep.value, 42, "Dynamic resolver should read from request");
}
#[test]
fn overrides_after_clear_fall_back_to_real_dependency() {
let overrides = Arc::new(DependencyOverrides::new());
overrides.insert_value(OverrideDep { value: 999 });
let ctx = test_context(Some(overrides.clone()));
let mut req = empty_request();
let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
.unwrap();
assert_eq!(dep.value, 999);
overrides.clear();
let ctx2 = test_context(Some(overrides));
let mut req2 = empty_request();
let dep2 =
futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx2, &mut req2))
.unwrap();
assert_eq!(dep2.value, 1, "After clear, real dependency should be used");
}
#[test]
fn overrides_without_overrides_use_real_dependency() {
let ctx = test_context(None);
let mut req = empty_request();
let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(&ctx, &mut req))
.unwrap();
assert_eq!(
dep.value, 1,
"Without overrides, real dependency should be used"
);
}
#[test]
fn resolution_guard_cleanup() {
let stack = ResolutionStack::new();
stack.push::<CounterDep>("CounterDep", DependencyScope::Request);
assert_eq!(stack.depth(), 1);
{
let _guard = ResolutionGuard::new(&stack);
stack.push::<ErrorDep>("ErrorDep", DependencyScope::Request);
assert_eq!(stack.depth(), 2);
}
assert_eq!(stack.depth(), 1);
}
#[test]
fn cleanup_stack_basic() {
let stack = CleanupStack::new();
assert!(stack.is_empty());
assert_eq!(stack.len(), 0);
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
stack.push(Box::new(move || {
Box::pin(async move {
counter_clone.fetch_add(1, Ordering::SeqCst);
}) as Pin<Box<dyn Future<Output = ()> + Send>>
}));
assert!(!stack.is_empty());
assert_eq!(stack.len(), 1);
let completed = futures_executor::block_on(stack.run_cleanups());
assert_eq!(completed, 1);
assert_eq!(counter.load(Ordering::SeqCst), 1);
assert!(stack.is_empty());
}
#[test]
fn cleanup_stack_lifo_order() {
let order = Arc::new(parking_lot::Mutex::new(Vec::<i32>::new()));
let stack = CleanupStack::new();
for i in 1..=3 {
let order_clone = Arc::clone(&order);
stack.push(Box::new(move || {
Box::pin(async move {
order_clone.lock().push(i);
}) as Pin<Box<dyn Future<Output = ()> + Send>>
}));
}
futures_executor::block_on(stack.run_cleanups());
let executed_order = order.lock().clone();
assert_eq!(
executed_order,
vec![3, 2, 1],
"Cleanups should run in LIFO order"
);
}
#[test]
fn cleanup_stack_take_cleanups() {
let stack = CleanupStack::new();
for _ in 0..3 {
stack.push(Box::new(|| {
Box::pin(async {}) as Pin<Box<dyn Future<Output = ()> + Send>>
}));
}
assert_eq!(stack.len(), 3);
let cleanups = stack.take_cleanups();
assert_eq!(cleanups.len(), 3);
assert!(stack.is_empty());
}
#[test]
fn cleanup_stack_multiple_runs() {
let counter = Arc::new(AtomicUsize::new(0));
let stack = CleanupStack::new();
let counter_clone = Arc::clone(&counter);
stack.push(Box::new(move || {
Box::pin(async move {
counter_clone.fetch_add(1, Ordering::SeqCst);
}) as Pin<Box<dyn Future<Output = ()> + Send>>
}));
futures_executor::block_on(stack.run_cleanups());
let counter_clone = Arc::clone(&counter);
stack.push(Box::new(move || {
Box::pin(async move {
counter_clone.fetch_add(10, Ordering::SeqCst);
}) as Pin<Box<dyn Future<Output = ()> + Send>>
}));
futures_executor::block_on(stack.run_cleanups());
assert_eq!(counter.load(Ordering::SeqCst), 11);
}
#[test]
fn cleanup_stack_panic_continues() {
let order = Arc::new(parking_lot::Mutex::new(Vec::<i32>::new()));
let stack = CleanupStack::new();
let order_clone = Arc::clone(&order);
stack.push(Box::new(move || {
Box::pin(async move {
order_clone.lock().push(1);
}) as Pin<Box<dyn Future<Output = ()> + Send>>
}));
stack.push(Box::new(|| -> Pin<Box<dyn Future<Output = ()> + Send>> {
panic!("cleanup 2 panics");
}));
let order_clone = Arc::clone(&order);
stack.push(Box::new(move || {
Box::pin(async move {
order_clone.lock().push(3);
}) as Pin<Box<dyn Future<Output = ()> + Send>>
}));
let completed = futures_executor::block_on(stack.run_cleanups());
assert_eq!(completed, 2, "Should report 2 successful cleanups");
let executed_order = order.lock().clone();
assert_eq!(
executed_order,
vec![3, 1],
"Cleanups should continue after panic"
);
}
#[test]
fn cleanup_runs_after_handler_error() {
let cleanup_ran = Arc::new(AtomicBool::new(false));
let cleanup_ran_clone = Arc::clone(&cleanup_ran);
let ctx = test_context(None);
ctx.cleanup_stack().push(Box::new(move || {
Box::pin(async move {
cleanup_ran_clone.store(true, Ordering::SeqCst);
}) as Pin<Box<dyn Future<Output = ()> + Send>>
}));
let handler_result: Result<(), HttpError> =
Err(HttpError::new(StatusCode::INTERNAL_SERVER_ERROR).with_detail("handler failed"));
futures_executor::block_on(ctx.cleanup_stack().run_cleanups());
assert!(
cleanup_ran.load(Ordering::SeqCst),
"Cleanup should run even after handler error"
);
assert!(handler_result.is_err());
}
#[derive(Clone)]
struct TrackedResource {
id: u32,
}
impl FromDependencyWithCleanup for TrackedResource {
type Value = TrackedResource;
type Error = HttpError;
async fn setup(
ctx: &RequestContext,
_req: &mut Request,
) -> Result<(Self::Value, Option<CleanupFn>), Self::Error> {
let tracker = ctx
.dependency_cache()
.get::<Arc<parking_lot::Mutex<Vec<String>>>>()
.unwrap_or_else(|| {
let t = Arc::new(parking_lot::Mutex::new(Vec::new()));
ctx.dependency_cache().insert(Arc::clone(&t));
t
});
tracker.lock().push("setup:resource".to_string());
let cleanup_tracker = Arc::clone(&tracker);
let cleanup = Box::new(move || {
Box::pin(async move {
cleanup_tracker.lock().push("cleanup:resource".to_string());
}) as Pin<Box<dyn Future<Output = ()> + Send>>
}) as CleanupFn;
Ok((TrackedResource { id: 42 }, Some(cleanup)))
}
}
#[test]
fn depends_cleanup_registers_cleanup() {
let ctx = test_context(None);
let mut req = empty_request();
let dep = futures_executor::block_on(DependsCleanup::<TrackedResource>::from_request(
&ctx, &mut req,
))
.expect("cleanup dependency resolution failed");
assert_eq!(dep.id, 42);
assert_eq!(ctx.cleanup_stack().len(), 1);
let tracker = ctx
.dependency_cache()
.get::<Arc<parking_lot::Mutex<Vec<String>>>>()
.unwrap();
let events = tracker.lock().clone();
assert_eq!(events, vec!["setup:resource"]);
futures_executor::block_on(ctx.cleanup_stack().run_cleanups());
let events = tracker.lock().clone();
assert_eq!(events, vec!["setup:resource", "cleanup:resource"]);
}
#[derive(Clone)]
struct NoCleanupResource {
value: String,
}
impl FromDependencyWithCleanup for NoCleanupResource {
type Value = NoCleanupResource;
type Error = HttpError;
async fn setup(
_ctx: &RequestContext,
_req: &mut Request,
) -> Result<(Self::Value, Option<CleanupFn>), Self::Error> {
Ok((
NoCleanupResource {
value: "no cleanup".to_string(),
},
None, ))
}
}
#[test]
fn depends_cleanup_no_cleanup_fn() {
let ctx = test_context(None);
let mut req = empty_request();
let dep = futures_executor::block_on(DependsCleanup::<NoCleanupResource>::from_request(
&ctx, &mut req,
))
.expect("no cleanup dependency resolution failed");
assert_eq!(dep.value, "no cleanup");
assert!(ctx.cleanup_stack().is_empty());
}
#[derive(Clone)]
struct OuterWithCleanup {
inner_id: u32,
}
impl FromDependencyWithCleanup for OuterWithCleanup {
type Value = OuterWithCleanup;
type Error = HttpError;
async fn setup(
ctx: &RequestContext,
req: &mut Request,
) -> Result<(Self::Value, Option<CleanupFn>), Self::Error> {
let inner = DependsCleanup::<TrackedResource>::from_request(ctx, req).await?;
let tracker = ctx
.dependency_cache()
.get::<Arc<parking_lot::Mutex<Vec<String>>>>()
.unwrap();
tracker.lock().push("setup:outer".to_string());
let cleanup_tracker = Arc::clone(&tracker);
let cleanup = Box::new(move || {
Box::pin(async move {
cleanup_tracker.lock().push("cleanup:outer".to_string());
}) as Pin<Box<dyn Future<Output = ()> + Send>>
}) as CleanupFn;
Ok((OuterWithCleanup { inner_id: inner.id }, Some(cleanup)))
}
}
#[test]
fn depends_cleanup_nested_lifo() {
let ctx = test_context(None);
let mut req = empty_request();
let dep = futures_executor::block_on(DependsCleanup::<OuterWithCleanup>::from_request(
&ctx, &mut req,
))
.expect("nested cleanup dependency resolution failed");
assert_eq!(dep.inner_id, 42);
assert_eq!(ctx.cleanup_stack().len(), 2);
let tracker = ctx
.dependency_cache()
.get::<Arc<parking_lot::Mutex<Vec<String>>>>()
.unwrap();
let events_before = tracker.lock().clone();
assert_eq!(events_before, vec!["setup:resource", "setup:outer"]);
futures_executor::block_on(ctx.cleanup_stack().run_cleanups());
let events_after = tracker.lock().clone();
assert_eq!(
events_after,
vec![
"setup:resource",
"setup:outer",
"cleanup:outer",
"cleanup:resource"
],
"Cleanups should run in LIFO order"
);
}
#[test]
fn depends_cleanup_caching() {
let ctx = test_context(None);
let mut req = empty_request();
let _dep1 = futures_executor::block_on(DependsCleanup::<TrackedResource>::from_request(
&ctx, &mut req,
))
.unwrap();
let _dep2 = futures_executor::block_on(DependsCleanup::<TrackedResource>::from_request(
&ctx, &mut req,
))
.unwrap();
assert_eq!(ctx.cleanup_stack().len(), 1);
let tracker = ctx
.dependency_cache()
.get::<Arc<parking_lot::Mutex<Vec<String>>>>()
.unwrap();
let events = tracker.lock().clone();
assert_eq!(events, vec!["setup:resource"]);
}
#[test]
fn scope_error_formatting() {
let err = DependencyScopeError::new("CachedUser".to_string(), "DbConnection".to_string());
let msg = err.to_string();
assert!(msg.contains("Dependency scope violation"));
assert!(msg.contains("request-scoped 'CachedUser'"));
assert!(msg.contains("function-scoped 'DbConnection'"));
assert!(msg.contains("cached value becomes stale"));
}
#[test]
fn scope_error_into_response() {
let err = DependencyScopeError::new("A".to_string(), "B".to_string());
let response = err.into_response();
assert_eq!(response.status().as_u16(), 500);
}
#[test]
fn resolution_stack_detects_scope_violation() {
#[allow(dead_code)]
struct RequestScoped;
#[allow(dead_code)]
struct FunctionScoped;
let stack = ResolutionStack::new();
stack.push::<RequestScoped>("RequestScoped", DependencyScope::Request);
let violation = stack.check_scope_violation("FunctionScoped", DependencyScope::Function);
assert!(
violation.is_some(),
"Should detect request -> function scope violation"
);
let err = violation.unwrap();
assert_eq!(err.request_scoped_type, "RequestScoped");
assert_eq!(err.function_scoped_type, "FunctionScoped");
}
#[test]
fn resolution_stack_allows_request_to_request() {
#[allow(dead_code)]
struct RequestA;
#[allow(dead_code)]
struct RequestB;
let stack = ResolutionStack::new();
stack.push::<RequestA>("RequestA", DependencyScope::Request);
let violation = stack.check_scope_violation("RequestB", DependencyScope::Request);
assert!(violation.is_none(), "Request -> Request should be allowed");
}
#[test]
fn resolution_stack_allows_function_to_function() {
#[allow(dead_code)]
struct FunctionA;
#[allow(dead_code)]
struct FunctionB;
let stack = ResolutionStack::new();
stack.push::<FunctionA>("FunctionA", DependencyScope::Function);
let violation = stack.check_scope_violation("FunctionB", DependencyScope::Function);
assert!(
violation.is_none(),
"Function -> Function should be allowed"
);
}
#[test]
fn resolution_stack_allows_function_to_request() {
#[allow(dead_code)]
struct FunctionScoped;
#[allow(dead_code)]
struct RequestScoped;
let stack = ResolutionStack::new();
stack.push::<FunctionScoped>("FunctionScoped", DependencyScope::Function);
let violation = stack.check_scope_violation("RequestScoped", DependencyScope::Request);
assert!(violation.is_none(), "Function -> Request should be allowed");
}
#[test]
fn resolution_stack_nested_scope_violation() {
#[allow(dead_code)]
struct OuterRequest;
#[allow(dead_code)]
struct MiddleRequest;
#[allow(dead_code)]
struct InnerFunction;
let stack = ResolutionStack::new();
stack.push::<OuterRequest>("OuterRequest", DependencyScope::Request);
stack.push::<MiddleRequest>("MiddleRequest", DependencyScope::Request);
let violation = stack.check_scope_violation("InnerFunction", DependencyScope::Function);
assert!(violation.is_some(), "Should detect nested scope violation");
let err = violation.unwrap();
assert_eq!(err.request_scoped_type, "MiddleRequest");
assert_eq!(err.function_scoped_type, "InnerFunction");
}
#[test]
fn resolution_stack_empty_no_scope_violation() {
let stack = ResolutionStack::new();
let violation_fn = stack.check_scope_violation("SomeDep", DependencyScope::Function);
let violation_req = stack.check_scope_violation("SomeDep", DependencyScope::Request);
assert!(
violation_fn.is_none(),
"Empty stack should allow function scope"
);
assert!(
violation_req.is_none(),
"Empty stack should allow request scope"
);
}
#[test]
fn function_scoped_resolves_fresh_each_time() {
let ctx = test_context(None);
let mut req = empty_request();
let _ = futures_executor::block_on(Depends::<CountingDep, NoCache>::from_request(
&ctx, &mut req,
))
.unwrap();
let _ = futures_executor::block_on(Depends::<CountingDep, NoCache>::from_request(
&ctx, &mut req,
))
.unwrap();
let counter = ctx.dependency_cache().get::<Arc<AtomicUsize>>().unwrap();
assert_eq!(
counter.load(Ordering::SeqCst),
2,
"Function-scoped should resolve 2 times (not cached)"
);
}
#[test]
fn request_scoped_cached_within_request() {
let ctx = test_context(None);
let mut req = empty_request();
let _ = futures_executor::block_on(Depends::<CountingDep>::from_request(&ctx, &mut req))
.unwrap();
let _ = futures_executor::block_on(Depends::<CountingDep>::from_request(&ctx, &mut req))
.unwrap();
let counter = ctx.dependency_cache().get::<Arc<AtomicUsize>>().unwrap();
assert_eq!(
counter.load(Ordering::SeqCst),
1,
"Request-scoped should resolve only once (cached)"
);
}
#[test]
fn request_scope_cleanup_only_once() {
let ctx = test_context(None);
let mut req = empty_request();
let _ = futures_executor::block_on(DependsCleanup::<TrackedResource>::from_request(
&ctx, &mut req,
))
.unwrap();
let _ = futures_executor::block_on(DependsCleanup::<TrackedResource>::from_request(
&ctx, &mut req,
))
.unwrap();
assert_eq!(
ctx.cleanup_stack().len(),
1,
"Request-scoped should only register cleanup once"
);
let tracker = ctx
.dependency_cache()
.get::<Arc<parking_lot::Mutex<Vec<String>>>>()
.unwrap();
assert_eq!(tracker.lock().len(), 1);
assert_eq!(tracker.lock()[0], "setup:resource");
futures_executor::block_on(ctx.cleanup_stack().run_cleanups());
let events = tracker.lock().clone();
assert_eq!(
events,
vec!["setup:resource", "cleanup:resource"],
"Cleanup should run exactly once for cached dependency"
);
}
#[test]
fn function_scope_cleanup_each_time() {
#[derive(Clone)]
#[allow(dead_code)]
struct FunctionScopedWithCleanup {
id: u32,
}
impl FromDependencyWithCleanup for FunctionScopedWithCleanup {
type Value = FunctionScopedWithCleanup;
type Error = HttpError;
async fn setup(
ctx: &RequestContext,
_req: &mut Request,
) -> Result<(Self::Value, Option<CleanupFn>), Self::Error> {
let counter = ctx
.dependency_cache()
.get::<Arc<AtomicUsize>>()
.unwrap_or_else(|| {
let c = Arc::new(AtomicUsize::new(0));
ctx.dependency_cache().insert(Arc::clone(&c));
c
});
let cleanup_counter = Arc::clone(&counter);
let cleanup = Box::new(move || {
Box::pin(async move {
cleanup_counter.fetch_add(1, Ordering::SeqCst);
}) as Pin<Box<dyn Future<Output = ()> + Send>>
}) as CleanupFn;
Ok((FunctionScopedWithCleanup { id: 42 }, Some(cleanup)))
}
}
let ctx = test_context(None);
let mut req = empty_request();
let _ = futures_executor::block_on(
DependsCleanup::<FunctionScopedWithCleanup, NoCache>::from_request(&ctx, &mut req),
)
.unwrap();
let _ = futures_executor::block_on(
DependsCleanup::<FunctionScopedWithCleanup, NoCache>::from_request(&ctx, &mut req),
)
.unwrap();
let _ = futures_executor::block_on(
DependsCleanup::<FunctionScopedWithCleanup, NoCache>::from_request(&ctx, &mut req),
)
.unwrap();
assert_eq!(
ctx.cleanup_stack().len(),
3,
"Function-scoped should register cleanup each time"
);
futures_executor::block_on(ctx.cleanup_stack().run_cleanups());
let counter = ctx.dependency_cache().get::<Arc<AtomicUsize>>().unwrap();
assert_eq!(
counter.load(Ordering::SeqCst),
3,
"All 3 cleanups should have run"
);
}
}