a3s-boot 0.1.1

Adapter-first modular Rust web framework for A3S inspired by Nest.js
Documentation
#![cfg(feature = "cache")]

use a3s_boot::{
    BootApplication, Cache, CacheModule, CacheOptions, Module, ModuleRef, ProviderDefinition,
    Result,
};
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct CatDto {
    id: String,
    name: String,
}

#[test]
fn cache_sets_gets_removes_and_clears_typed_values() {
    let cache = Cache::in_memory();
    let cat = CatDto {
        id: "1".to_string(),
        name: "Milo".to_string(),
    };

    cache.set("cat:1", &cat).unwrap();
    let cached = cache.get::<CatDto>("cat:1").unwrap();
    let removed = cache.remove("cat:1").unwrap();
    let missing = cache.get::<CatDto>("cat:1").unwrap();

    cache.set("cat:2", &cat).unwrap();
    cache.clear().unwrap();

    assert_eq!(cached, Some(cat));
    assert!(removed);
    assert_eq!(missing, None);
    assert_eq!(cache.get::<CatDto>("cat:2").unwrap(), None);
}

#[test]
fn cache_default_ttl_expires_values() {
    let cache = Cache::in_memory().with_default_ttl(Duration::from_millis(10));

    cache
        .set(
            "cat:ttl",
            &CatDto {
                id: "ttl".to_string(),
                name: "Short".to_string(),
            },
        )
        .unwrap();

    assert!(cache.get::<CatDto>("cat:ttl").unwrap().is_some());
    std::thread::sleep(Duration::from_millis(25));
    assert_eq!(cache.get::<CatDto>("cat:ttl").unwrap(), None);
}

#[test]
fn cache_get_or_insert_uses_cached_values() {
    let cache = Cache::in_memory();
    let calls = AtomicUsize::new(0);

    let first = cache
        .get_or_insert_with("cat:1", || {
            calls.fetch_add(1, Ordering::SeqCst);
            Ok(CatDto {
                id: "1".to_string(),
                name: "Milo".to_string(),
            })
        })
        .unwrap();
    let second = cache
        .get_or_insert_with("cat:1", || {
            calls.fetch_add(1, Ordering::SeqCst);
            Ok(CatDto {
                id: "1".to_string(),
                name: "Luna".to_string(),
            })
        })
        .unwrap();

    assert_eq!(first, second);
    assert_eq!(calls.load(Ordering::SeqCst), 1);
}

#[derive(Debug)]
struct CachedCatsService {
    cache: Arc<Cache>,
}

impl CachedCatsService {
    fn find_one(&self, id: &str) -> Result<CatDto> {
        self.cache.get_or_insert_with(format!("cat:{id}"), || {
            Ok(CatDto {
                id: id.to_string(),
                name: "Milo".to_string(),
            })
        })
    }
}

#[derive(Debug)]
struct UsesCacheModule {
    cache_module: CacheModule,
}

impl Module for UsesCacheModule {
    fn name(&self) -> &'static str {
        "uses-cache"
    }

    fn imports(&self) -> Vec<Arc<dyn Module>> {
        vec![Arc::new(self.cache_module.clone())]
    }

    fn providers(&self) -> Result<Vec<ProviderDefinition>> {
        Ok(vec![ProviderDefinition::factory::<CachedCatsService, _>(
            |module_ref: &ModuleRef| {
                Ok(CachedCatsService {
                    cache: module_ref.get::<Cache>()?,
                })
            },
        )])
    }
}

#[test]
fn cache_module_exports_cache_to_importing_modules() {
    let app = BootApplication::builder()
        .import(UsesCacheModule {
            cache_module: CacheModule::in_memory_with_options(
                "cache",
                CacheOptions::new().with_default_ttl(Duration::from_secs(60)),
            ),
        })
        .build()
        .unwrap();

    let cats = app.get::<CachedCatsService>().unwrap();
    let first = cats.find_one("1").unwrap();
    let second = cats.find_one("1").unwrap();

    assert_eq!(first, second);
}

#[derive(Debug)]
struct UsesNamedCacheModule {
    cache_module: CacheModule,
}

impl Module for UsesNamedCacheModule {
    fn name(&self) -> &'static str {
        "uses-named-cache"
    }

    fn imports(&self) -> Vec<Arc<dyn Module>> {
        vec![Arc::new(self.cache_module.clone())]
    }

    fn providers(&self) -> Result<Vec<ProviderDefinition>> {
        Ok(vec![ProviderDefinition::factory::<CachedCatsService, _>(
            |module_ref: &ModuleRef| {
                Ok(CachedCatsService {
                    cache: module_ref.get_named::<Cache>("app-cache")?,
                })
            },
        )])
    }
}

#[derive(Debug)]
struct UsesGlobalCacheModule;

impl Module for UsesGlobalCacheModule {
    fn name(&self) -> &'static str {
        "uses-global-cache"
    }

    fn providers(&self) -> Result<Vec<ProviderDefinition>> {
        Ok(vec![ProviderDefinition::factory::<CachedCatsService, _>(
            |module_ref: &ModuleRef| {
                Ok(CachedCatsService {
                    cache: module_ref.get::<Cache>()?,
                })
            },
        )])
    }
}

#[test]
fn cache_module_supports_named_and_global_exports() {
    let named = BootApplication::builder()
        .import(UsesNamedCacheModule {
            cache_module: CacheModule::in_memory("named-cache").named("app-cache"),
        })
        .build()
        .unwrap();
    assert!(named.get::<CachedCatsService>().is_ok());
    assert!(named.get_optional::<Cache>().unwrap().is_none());
    assert!(named.get_named::<Cache>("app-cache").is_ok());

    let global = BootApplication::builder()
        .import(CacheModule::in_memory("global-cache").global())
        .import(UsesGlobalCacheModule)
        .build()
        .unwrap();
    assert!(global.get::<CachedCatsService>().is_ok());
    assert!(global.get::<Cache>().is_ok());
}