use std::future::Future;
use std::marker::PhantomData;
use super::markers::ResourceKind;
use super::sets::{Empty, Has};
use super::tracked::{ResourceEffect, Tracked};
use crate::effect::trait_def::Effect;
pub struct ResourceBracket<R, Acq, Use, Rel>
where
R: ResourceKind,
{
acquire: Acq,
use_fn: Use,
release: Rel,
_phantom: PhantomData<R>,
}
impl<R, Acq, Use, Rel> ResourceBracket<R, Acq, Use, Rel>
where
R: ResourceKind,
{
pub fn new(acquire: Acq, use_fn: Use, release: Rel) -> Self {
ResourceBracket {
acquire,
use_fn,
release,
_phantom: PhantomData,
}
}
}
impl<R, Acq, Use, Rel> std::fmt::Debug for ResourceBracket<R, Acq, Use, Rel>
where
R: ResourceKind,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ResourceBracket")
.field("resource", &R::NAME)
.field("acquire", &"<effect>")
.field("use_fn", &"<function>")
.field("release", &"<function>")
.finish()
}
}
impl<R, Acq, Use, Rel, UseEff, T, U, E, Env, RelFut> Effect for ResourceBracket<R, Acq, Use, Rel>
where
R: ResourceKind,
Acq: Effect<Output = T, Error = E, Env = Env>,
Use: FnOnce(&T) -> UseEff + Send,
UseEff: Effect<Output = U, Error = E, Env = Env>,
Rel: FnOnce(T) -> RelFut + Send,
RelFut: Future<Output = Result<(), E>> + Send,
T: Send,
U: Send,
E: Send + std::fmt::Debug,
Env: Clone + Send + Sync,
{
type Output = U;
type Error = E;
type Env = Env;
async fn run(self, env: &Self::Env) -> Result<U, E> {
let resource = self.acquire.run(env).await?;
let result = (self.use_fn)(&resource).run(env).await;
let release_result = (self.release)(resource).await;
if let Err(ref rel_err) = release_result {
#[cfg(feature = "tracing")]
tracing::warn!("Resource {} cleanup failed: {:?}", R::NAME, rel_err);
#[cfg(not(feature = "tracing"))]
eprintln!("Resource {} cleanup failed: {:?}", R::NAME, rel_err);
}
result
}
}
impl<R, Acq, Use, Rel, UseEff, T, U, E, Env, RelFut> ResourceEffect
for ResourceBracket<R, Acq, Use, Rel>
where
R: ResourceKind,
Acq: Effect<Output = T, Error = E, Env = Env>,
Use: FnOnce(&T) -> UseEff + Send,
UseEff: Effect<Output = U, Error = E, Env = Env>,
Rel: FnOnce(T) -> RelFut + Send,
RelFut: Future<Output = Result<(), E>> + Send,
T: Send,
U: Send,
E: Send + std::fmt::Debug,
Env: Clone + Send + Sync,
{
type Acquires = Empty;
type Releases = Empty;
}
pub fn resource_bracket<R, Acq, Use, Rel, UseEff, T, U, E, Env, RelFut>(
acquire: Acq,
release: Rel,
use_fn: Use,
) -> ResourceBracket<R, Acq, Use, Rel>
where
R: ResourceKind,
Acq: Effect<Output = T, Error = E, Env = Env>,
Use: FnOnce(&T) -> UseEff + Send,
UseEff: Effect<Output = U, Error = E, Env = Env>,
Rel: FnOnce(T) -> RelFut + Send,
RelFut: Future<Output = Result<(), E>> + Send,
T: Send,
U: Send,
E: Send + std::fmt::Debug,
Env: Clone + Send + Sync,
{
ResourceBracket {
acquire,
use_fn,
release,
_phantom: PhantomData,
}
}
pub fn tracked_resource_bracket<R, Acq, Use, Rel, UseEff, T, U, E, Env, RelFut>(
acquire: Acq,
release: Rel,
use_fn: Use,
) -> Tracked<ResourceBracket<R, Acq, Use, Rel>, Empty, Empty>
where
R: ResourceKind,
Acq: ResourceEffect<Output = T, Error = E, Env = Env, Acquires = Has<R>, Releases = Empty>,
Use: FnOnce(&T) -> UseEff + Send,
UseEff: ResourceEffect<Output = U, Error = E, Env = Env, Acquires = Empty, Releases = Empty>,
Rel: FnOnce(T) -> RelFut + Send,
RelFut: Future<Output = Result<(), E>> + Send,
T: Send,
U: Send,
E: Send + std::fmt::Debug,
Env: Clone + Send + Sync,
{
Tracked::new(ResourceBracket {
acquire,
use_fn,
release,
_phantom: PhantomData,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::effect::constructors::{fail, pure};
use crate::effect::resource::ext::ResourceEffectExt;
use crate::effect::resource::markers::FileRes;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
#[tokio::test]
async fn resource_bracket_happy_path() {
let released = Arc::new(AtomicBool::new(false));
let released_clone = released.clone();
let result = resource_bracket::<FileRes, _, _, _, _, _, _, _, _, _>(
pure::<_, String, ()>(42),
move |_: i32| {
released_clone.store(true, Ordering::SeqCst);
async { Ok(()) }
},
|val: &i32| pure::<_, String, ()>(*val * 2),
)
.run(&())
.await;
assert_eq!(result, Ok(84));
assert!(released.load(Ordering::SeqCst));
}
#[tokio::test]
async fn resource_bracket_releases_on_use_failure() {
let released = Arc::new(AtomicBool::new(false));
let released_clone = released.clone();
let result = resource_bracket::<FileRes, _, _, _, _, _, _, _, _, _>(
pure::<_, String, ()>(42),
move |_: i32| {
released_clone.store(true, Ordering::SeqCst);
async { Ok(()) }
},
|_: &i32| fail::<i32, String, ()>("use failed".to_string()),
)
.run(&())
.await;
assert_eq!(result, Err("use failed".to_string()));
assert!(
released.load(Ordering::SeqCst),
"cleanup must run on failure"
);
}
#[tokio::test]
async fn resource_bracket_acquire_failure_no_release() {
let released = Arc::new(AtomicBool::new(false));
let released_clone = released.clone();
let result = resource_bracket::<FileRes, _, _, _, _, _, _, _, _, _>(
fail::<i32, String, ()>("acquire failed".to_string()),
move |_: i32| {
released_clone.store(true, Ordering::SeqCst);
async { Ok(()) }
},
|val: &i32| pure::<_, String, ()>(*val * 2),
)
.run(&())
.await;
assert_eq!(result, Err("acquire failed".to_string()));
assert!(
!released.load(Ordering::SeqCst),
"cleanup must NOT run when acquire fails"
);
}
#[test]
fn resource_bracket_debug() {
let bracket = resource_bracket::<FileRes, _, _, _, _, _, _, _, _, _>(
pure::<_, String, ()>(42),
|_: i32| async { Ok(()) },
|val: &i32| pure::<_, String, ()>(*val),
);
let debug_str = format!("{:?}", bracket);
assert!(debug_str.contains("ResourceBracket"));
assert!(debug_str.contains("File"));
}
fn _assert_resource_neutral<T: ResourceEffect<Acquires = Empty, Releases = Empty>>() {}
#[test]
fn resource_bracket_is_neutral() {
type BracketType = ResourceBracket<
FileRes,
crate::effect::combinators::Pure<i32, String, ()>,
fn(&i32) -> crate::effect::combinators::Pure<i32, String, ()>,
fn(i32) -> std::future::Ready<Result<(), String>>,
>;
_assert_resource_neutral::<BracketType>();
}
#[tokio::test]
async fn tracked_resource_bracket_works() {
let released = Arc::new(AtomicBool::new(false));
let released_clone = released.clone();
let acquire = pure::<_, String, ()>(42).acquires::<FileRes>();
let result = tracked_resource_bracket::<FileRes, _, _, _, _, _, _, _, _, _>(
acquire,
move |_: i32| {
released_clone.store(true, Ordering::SeqCst);
async { Ok(()) }
},
|val: &i32| pure::<_, String, ()>(*val * 2).neutral(),
)
.run(&())
.await;
assert_eq!(result, Ok(84));
assert!(released.load(Ordering::SeqCst));
}
#[tokio::test]
async fn resource_bracket_logs_cleanup_error_on_success() {
let result = resource_bracket::<FileRes, _, _, _, _, _, _, _, _, _>(
pure::<_, String, ()>(42),
|_: i32| async { Err::<(), String>("cleanup failed".to_string()) },
|val: &i32| pure::<_, String, ()>(*val * 2),
)
.run(&())
.await;
assert_eq!(result, Ok(84));
}
#[tokio::test]
async fn resource_bracket_logs_cleanup_error_on_use_failure() {
let result = resource_bracket::<FileRes, _, _, _, _, _, _, _, _, _>(
pure::<_, String, ()>(42),
|_: i32| async { Err::<(), String>("cleanup failed".to_string()) },
|_: &i32| fail::<i32, String, ()>("use failed".to_string()),
)
.run(&())
.await;
assert_eq!(result, Err("use failed".to_string()));
}
}