use crate::error::CanoError;
use cano_macros::resource;
use std::any::Any;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
use std::hash::Hash;
use std::ops::Range;
use std::sync::Arc;
#[resource]
pub trait Resource: Send + Sync + 'static {
async fn setup(&self) -> Result<(), CanoError> {
Ok(())
}
async fn teardown(&self) -> Result<(), CanoError> {
Ok(())
}
}
pub struct Resources<TResourceKey = Cow<'static, str>>
where
TResourceKey: Hash + Eq + Send + Sync + 'static,
{
data: HashMap<TResourceKey, Box<dyn Any + Send + Sync>>,
lifecycle: Vec<Arc<dyn Resource>>,
}
impl<TResourceKey: Hash + Eq + Send + Sync + 'static> Resources<TResourceKey> {
#[must_use]
pub fn new() -> Self {
Self {
data: HashMap::new(),
lifecycle: Vec::new(),
}
}
#[must_use]
pub fn empty() -> Self {
Self::new()
}
#[must_use]
pub fn with_capacity(n: usize) -> Self {
Self {
data: HashMap::with_capacity(n),
lifecycle: Vec::with_capacity(n),
}
}
#[must_use]
pub fn insert<R: Resource + 'static>(
mut self,
key: impl Into<TResourceKey>,
resource: R,
) -> Self {
let arc = Arc::new(resource);
let key = key.into();
assert!(
!self.data.contains_key(&key),
"duplicate resource key inserted into Resources; use try_insert to handle duplicates as Result"
);
self.data.insert(key, Box::new(Arc::clone(&arc)));
self.lifecycle.push(arc as Arc<dyn Resource>);
self
}
pub fn try_insert<R: Resource + 'static>(
mut self,
key: impl Into<TResourceKey>,
resource: R,
) -> Result<Self, CanoError> {
let arc = Arc::new(resource);
let key = key.into();
if self.data.contains_key(&key) {
return Err(CanoError::resource_duplicate_key(
"duplicate resource key inserted into Resources",
));
}
self.data.insert(key, Box::new(Arc::clone(&arc)));
self.lifecycle.push(arc as Arc<dyn Resource>);
Ok(self)
}
pub fn get<R, Q>(&self, key: &Q) -> Result<Arc<R>, CanoError>
where
R: Resource + 'static,
TResourceKey: std::borrow::Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let boxed = self.data.get(key).ok_or_else(|| {
CanoError::ResourceNotFound(format!(
"no resource found for the given key (requested type: {})",
std::any::type_name::<R>(),
))
})?;
boxed
.downcast_ref::<Arc<R>>()
.map(Arc::clone)
.ok_or_else(|| {
CanoError::ResourceTypeMismatch(format!(
"resource found but the requested type {} does not match the stored type",
std::any::type_name::<R>(),
))
})
}
pub async fn setup_all(&self) -> Result<(), CanoError> {
if self.lifecycle.is_empty() {
return Ok(());
}
for (idx, resource) in self.lifecycle.iter().enumerate() {
if let Err(e) = resource.setup().await {
self.teardown_range(0..idx).await;
return Err(e);
}
}
Ok(())
}
pub async fn teardown_all(&self) {
self.teardown_range(0..self.lifecycle.len()).await;
}
pub(crate) async fn teardown_range(&self, range: Range<usize>) {
for resource in self.lifecycle[range].iter().rev() {
if let Err(e) = resource.teardown().await {
#[cfg(feature = "tracing")]
tracing::error!("resource teardown failed: {e}");
#[cfg(not(feature = "tracing"))]
eprintln!("cano: resource teardown error: {e}");
}
}
}
pub(crate) fn lifecycle_len(&self) -> usize {
self.lifecycle.len()
}
}
impl<TResourceKey: Hash + Eq + Send + Sync + 'static> Default for Resources<TResourceKey> {
fn default() -> Self {
Self::new()
}
}
impl<TResourceKey: fmt::Debug + Hash + Eq + Send + Sync + 'static> fmt::Debug
for Resources<TResourceKey>
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Resources")
.field("count", &self.lifecycle.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
#[derive(Debug)]
struct TypeA;
#[resource]
impl Resource for TypeA {}
#[derive(Debug)]
struct TypeB;
#[resource]
impl Resource for TypeB {}
struct TrackingResource {
log: Arc<Mutex<Vec<String>>>,
name: String,
}
impl TrackingResource {
fn new(name: impl Into<String>, log: Arc<Mutex<Vec<String>>>) -> Self {
Self {
log,
name: name.into(),
}
}
}
#[resource]
impl Resource for TrackingResource {
async fn setup(&self) -> Result<(), CanoError> {
self.log
.lock()
.unwrap()
.push(format!("setup:{}", self.name));
Ok(())
}
async fn teardown(&self) -> Result<(), CanoError> {
self.log
.lock()
.unwrap()
.push(format!("teardown:{}", self.name));
Ok(())
}
}
struct FailingResource {
log: Arc<Mutex<Vec<String>>>,
name: String,
}
impl FailingResource {
fn new(name: impl Into<String>, log: Arc<Mutex<Vec<String>>>) -> Self {
Self {
log,
name: name.into(),
}
}
}
#[resource]
impl Resource for FailingResource {
async fn setup(&self) -> Result<(), CanoError> {
self.log
.lock()
.unwrap()
.push(format!("setup:{}", self.name));
Err(CanoError::generic(format!(
"setup failed for {}",
self.name
)))
}
async fn teardown(&self) -> Result<(), CanoError> {
self.log
.lock()
.unwrap()
.push(format!("teardown:{}", self.name));
Ok(())
}
}
struct FailingTeardownResource {
log: Arc<Mutex<Vec<String>>>,
name: String,
}
impl FailingTeardownResource {
fn new(name: impl Into<String>, log: Arc<Mutex<Vec<String>>>) -> Self {
Self {
log,
name: name.into(),
}
}
}
#[resource]
impl Resource for FailingTeardownResource {
async fn setup(&self) -> Result<(), CanoError> {
self.log
.lock()
.unwrap()
.push(format!("setup:{}", self.name));
Ok(())
}
async fn teardown(&self) -> Result<(), CanoError> {
self.log
.lock()
.unwrap()
.push(format!("teardown:{}", self.name));
Err(CanoError::generic(format!(
"teardown failed for {}",
self.name
)))
}
}
#[test]
fn test_empty_is_empty() {
let r = Resources::<String>::empty();
assert_eq!(
r.lifecycle.len(),
0,
"Resources::empty() should have no lifecycle entries"
);
assert_eq!(
r.data.len(),
0,
"Resources::empty() should have no data entries"
);
}
#[tokio::test]
async fn test_insert_get_roundtrip_string_key() {
let resources: Resources<String> = Resources::new().insert("a".to_string(), TypeA);
let result = resources.get::<TypeA, str>("a");
assert!(result.is_ok(), "expected Ok, got {result:?}");
}
#[tokio::test]
async fn test_get_missing_key() {
let resources: Resources<String> = Resources::new();
let result = resources.get::<TypeA, str>("absent");
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.category(), "resource_not_found");
}
#[tokio::test]
async fn test_get_wrong_type() {
let resources: Resources<String> = Resources::new().insert("a".to_string(), TypeA);
assert!(resources.get::<TypeA, str>("a").is_ok());
let err = resources.get::<TypeB, str>("a").unwrap_err();
assert_eq!(err.category(), "resource_type_mismatch");
let missing_err = resources.get::<TypeA, str>("missing").unwrap_err();
assert_eq!(missing_err.category(), "resource_not_found");
assert_ne!(err.category(), missing_err.category());
}
#[tokio::test]
async fn test_arc_identity() {
let resources: Resources<String> = Resources::new().insert("a".to_string(), TypeA);
let arc1 = resources.get::<TypeA, str>("a").unwrap();
let arc2 = resources.get::<TypeA, str>("a").unwrap();
assert!(Arc::ptr_eq(&arc1, &arc2), "expected same underlying Arc");
}
#[tokio::test]
async fn test_default_equals_new() {
let by_default: Resources<String> = Resources::default();
let by_new: Resources<String> = Resources::new();
assert_eq!(by_default.data.len(), by_new.data.len());
assert_eq!(by_default.lifecycle.len(), by_new.lifecycle.len());
}
#[tokio::test]
async fn test_with_capacity() {
let resources: Resources<String> = Resources::with_capacity(64);
assert_eq!(resources.lifecycle.len(), 0);
assert_eq!(resources.data.len(), 0);
}
#[tokio::test]
async fn test_lifecycle_setup_insertion_order() {
let log: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let resources: Resources<String> = Resources::new()
.insert(
"a".to_string(),
TrackingResource::new("A", Arc::clone(&log)),
)
.insert(
"b".to_string(),
TrackingResource::new("B", Arc::clone(&log)),
)
.insert(
"c".to_string(),
TrackingResource::new("C", Arc::clone(&log)),
);
resources.setup_all().await.unwrap();
let events = log.lock().unwrap().clone();
assert_eq!(events, ["setup:A", "setup:B", "setup:C"]);
}
#[tokio::test]
async fn test_lifecycle_teardown_lifo_order() {
let log: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let resources: Resources<String> = Resources::new()
.insert(
"a".to_string(),
TrackingResource::new("A", Arc::clone(&log)),
)
.insert(
"b".to_string(),
TrackingResource::new("B", Arc::clone(&log)),
)
.insert(
"c".to_string(),
TrackingResource::new("C", Arc::clone(&log)),
);
resources.setup_all().await.unwrap();
log.lock().unwrap().clear();
resources.teardown_range(0..resources.lifecycle.len()).await;
let events = log.lock().unwrap().clone();
assert_eq!(events, ["teardown:C", "teardown:B", "teardown:A"]);
}
#[tokio::test]
async fn test_setup_failure_partial_rollback() {
let log: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let resources: Resources<String> = Resources::new()
.insert(
"r0".to_string(),
TrackingResource::new("R0", Arc::clone(&log)),
)
.insert(
"r1".to_string(),
TrackingResource::new("R1", Arc::clone(&log)),
)
.insert(
"r2".to_string(),
FailingResource::new("R2", Arc::clone(&log)),
)
.insert(
"r3".to_string(),
TrackingResource::new("R3", Arc::clone(&log)),
);
let result = resources.setup_all().await;
assert!(result.is_err(), "expected setup_all to fail");
let events = log.lock().unwrap().clone();
assert_eq!(
events,
[
"setup:R0",
"setup:R1",
"setup:R2",
"teardown:R1",
"teardown:R0"
],
"unexpected lifecycle events: {events:?}"
);
}
#[tokio::test]
async fn test_teardown_continues_past_failures() {
let log: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let resources: Resources<String> = Resources::new()
.insert(
"r0".to_string(),
FailingTeardownResource::new("R0", Arc::clone(&log)),
)
.insert(
"r1".to_string(),
FailingTeardownResource::new("R1", Arc::clone(&log)),
)
.insert(
"r2".to_string(),
FailingTeardownResource::new("R2", Arc::clone(&log)),
);
resources.teardown_range(0..resources.lifecycle.len()).await;
let events = log.lock().unwrap().clone();
assert_eq!(events, ["teardown:R2", "teardown:R1", "teardown:R0"]);
}
#[test]
#[should_panic(expected = "duplicate resource key inserted into Resources")]
fn test_insert_panics_on_duplicate() {
let _resources: Resources<String> = Resources::new()
.insert("dup".to_string(), TypeA)
.insert("dup".to_string(), TypeA);
}
#[test]
fn test_try_insert_returns_duplicate_key_error() {
let first = Resources::<String>::new().insert("dup".to_string(), TypeA);
let err = first
.try_insert("dup".to_string(), TypeA)
.expect_err("try_insert must reject duplicates");
assert_eq!(err.category(), "resource_duplicate_key");
assert!(err.message().contains("duplicate"));
}
#[test]
fn test_try_insert_succeeds_for_fresh_key() {
let resources = Resources::<String>::new()
.insert("a".to_string(), TypeA)
.try_insert("b".to_string(), TypeB)
.expect("fresh key must insert");
assert!(resources.get::<TypeA, str>("a").is_ok());
assert!(resources.get::<TypeB, str>("b").is_ok());
}
}