use async_trait::async_trait;
use parking_lot::{MappedRwLockReadGuard, MappedRwLockWriteGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
use smol::future;
use smol::io::AsyncRead;
use std::any::TypeId;
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tracing::debug;
use crate::resource::id::ResourceId;
use crate::resource::manager::ResourceLoadContext;
pub mod manager;
pub mod id;
pub mod source;
pub mod watcher;
struct SubResourceRef<P: ?Sized + Send + Sync + 'static> {
type_id: Option<TypeId>,
update_fn: Box<dyn (
Fn(Arc<Resource<P>>) -> Box<dyn Future<Output = Result<(), ResourceLoadError>> + Send + 'static>
) + Send + Sync + 'static>,
}
impl<P: ?Sized + Send + Sync + 'static> SubResourceRef<P> {
fn from_sub_resource<T: ?Sized + Send + Sync + 'static>(
resource: &Arc<Resource<T>>,
loader: Arc<dyn SubResourceLoader<T, P>>,
) -> Self {
let weak = Arc::downgrade(resource);
Self {
type_id: Some(TypeId::of::<T>()),
update_fn: Box::new(move |parent| Box::new({
let async_weak = weak.clone();
let async_loader = loader.clone();
async move {
if let Some(resource) = async_weak.upgrade() {
let parent_value = parent.read();
let (resource_mut, drop_checker) = ResourceMut::from_resource(resource.clone());
async_loader.update(resource_mut, &parent_value).await?;
match drop_checker.recv().await {
Ok(_) => { debug!("Drop-checker received an unexpected message"); }
Err(_) => { }
}
resource.update_sub_resources().await;
}
Ok(())
}
}))
}
}
fn from_callback<F, Fut>(callback: F) -> Self
where
F: (Fn(Arc<Resource<P>>) -> Fut) + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
let callback = Arc::new(callback);
Self {
type_id: None,
update_fn: Box::new(move |parent| {
let callback = callback.clone();
Box::new(async move {
callback(parent).await;
Ok(())
})
})
}
}
async fn update(&self, parent: Arc<Resource<P>>) -> Result<(), ResourceLoadError> {
Box::into_pin((self.update_fn)(parent)).await
}
}
impl<T: ?Sized + Send + Sync + 'static> Debug for SubResourceRef<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut ds = f.debug_struct("SubResource");
match &self.type_id {
Some(type_id) => ds
.field("type", &"SubResource")
.field("type_id", &type_id),
None => ds.field("type", &"Callback"),
}.finish_non_exhaustive()
}
}
pub type ResourceLock<'a, T> = MappedRwLockReadGuard<'a, T>;
pub type ResourceMutLock<'a, T> = MappedRwLockWriteGuard<'a, T>;
#[derive(Debug)]
pub struct Resource<T: ?Sized + Send + Sync + 'static> {
id: ResourceId,
value: RwLock<Box<T>>,
sub_resources: smol::lock::Mutex<Vec<SubResourceRef<T>>>,
}
impl<T: ?Sized + Send + Sync + 'static> Resource<T> {
#[inline]
pub(in crate::resource) fn new(id: ResourceId, value: Box<T>) -> Self {
Self {
id,
value: RwLock::new(value),
sub_resources: smol::lock::Mutex::new(Vec::new()),
}
}
pub(in crate::resource) async fn update_sub_resources(self: &Arc<Self>) {
let sub_resources = self.sub_resources.lock().await;
for sub_resource in &*sub_resources {
match sub_resource.update(self.clone()).await {
Ok(_) => {},
Err(error) => debug!(
parent_id = %self.id,
%error,
"Failed to update sub-resource",
),
}
}
}
#[inline]
pub fn id(&self) -> &ResourceId {
&self.id
}
#[inline]
pub fn read(&self) -> ResourceLock<'_, T> {
RwLockReadGuard::map(self.value.read(), |b| b.as_ref())
}
pub async fn attach_sub_resource<S: ?Sized + Send + Sync + 'static>(
&self,
loader: impl SubResourceLoader<S, T>,
) -> ResourceLoadResult<S> {
let sub_value = loader.load(&self.read()).await?;
let mut sub_resources = self.sub_resources.lock().await;
let sub_id = format!("{}/<sub-{}>", self.id, sub_resources.len()).into();
let sub_resource = Arc::new(Resource::new(sub_id, sub_value));
sub_resources.push(SubResourceRef::from_sub_resource(&sub_resource, Arc::new(loader)));
Ok(sub_resource)
}
#[inline]
pub fn attach_sub_resource_blocking<S: ?Sized + Send + Sync + 'static>(
&self,
loader: impl SubResourceLoader<S, T>,
) -> ResourceLoadResult<S> {
future::block_on(self.attach_sub_resource(loader))
}
pub async fn attach_update_callback<F, Fut>(&self, callback: F)
where
F: (Fn(Arc<Resource<T>>) -> Fut) + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
let mut sub_resources = self.sub_resources.lock().await;
sub_resources.push(SubResourceRef::from_callback(callback));
}
#[inline]
pub fn attach_update_callback_blocking<F, Fut>(&self, callback: F)
where
F: (Fn(Arc<Resource<T>>) -> Fut) + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
future::block_on(self.attach_update_callback(callback))
}
}
pub struct ResourceMut<T: ?Sized + Send + Sync + 'static> {
inner: Arc<Resource<T>>,
#[allow(dead_code)] drop_notice: smol::channel::Sender<()>,
}
impl<T: ?Sized + Send + Sync + 'static> ResourceMut<T> {
#[inline]
pub(in crate::resource) fn from_resource(
resource: Arc<Resource<T>>
) -> (Self, smol::channel::Receiver<()>) {
let (drop_notice, drop_checker) = smol::channel::bounded(1);
(Self {
inner: resource,
drop_notice,
}, drop_checker)
}
#[inline]
pub fn resource(&self) -> &Arc<Resource<T>> {
&self.inner
}
#[inline]
pub fn read(&self) -> ResourceLock<'_, T> {
RwLockReadGuard::map(self.inner.value.read(), |b| b.as_ref())
}
#[inline]
pub fn write(&self) -> ResourceMutLock<'_, T> {
RwLockWriteGuard::map(self.inner.value.write(), |b| b.as_mut())
}
#[inline]
pub fn replace(&self, value: Box<T>) {
*self.inner.value.write() = value;
}
}
#[derive(Clone, Debug)]
pub enum ResourceLoadError {
NotFound(ResourceId),
TypeMismatch{
requested: TypeId,
actual: TypeId,
},
CyclicLoad {
parents: Vec<ResourceId>,
id: ResourceId,
},
ReadError(Arc<dyn Error + Send + Sync + 'static>),
}
impl ResourceLoadError {
#[inline]
pub fn from_mismatch<T: ?Sized + Send + Sync + 'static>(actual: TypeId) -> Self {
Self::TypeMismatch { actual, requested: TypeId::of::<T>() }
}
#[inline]
pub fn from_error(e: impl Error + Send + Sync + 'static) -> Self {
Self::ReadError(Arc::new(e))
}
}
impl<'a> From<&'a str> for ResourceLoadError {
#[inline]
fn from(value: &'a str) -> Self {
Self::ReadError(Box::<dyn Error + Send + Sync>::from(value).into())
}
}
impl From<String> for ResourceLoadError {
#[inline]
fn from(value: String) -> Self {
Self::ReadError(Box::<dyn Error + Send + Sync>::from(value).into())
}
}
impl From<std::io::Error> for ResourceLoadError {
#[inline]
fn from(value: std::io::Error) -> Self {
Self::ReadError(Arc::new(value))
}
}
impl Display for ResourceLoadError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound(id) =>
write!(f, "No resource found that is associated with ID: '{id}'"),
Self::TypeMismatch { requested, actual } =>
write!(f, "Requested type ({requested:?}) does not match previously loaded type ({actual:?})"),
Self::CyclicLoad { parents, id } =>
write!(f, "Cyclic load detected: {parents:?} => {id}"),
Self::ReadError(err) =>
std::fmt::Display::fmt(&err, f),
}
}
}
impl Error for ResourceLoadError {}
pub type ResourceLoadResult<T> = Result<Arc<Resource<T>>, ResourceLoadError>;
pub type ResourceReadData = Pin<Box<dyn AsyncRead + Send>>;
#[async_trait]
pub trait ResourceLoader<T: ?Sized + Send + Sync + 'static>: Send + Sync + 'static {
async fn load(
&self,
data: ResourceReadData,
ctx: &ResourceLoadContext,
) -> Result<Box<T>, ResourceLoadError>;
async fn update(
&self,
resource: ResourceMut<T>,
new_value: Box<T>,
) {
resource.replace(new_value);
}
}
#[async_trait]
pub trait SubResourceLoader<T, P>: Send + Sync + 'static
where
T: ?Sized + Send + Sync + 'static,
P: ?Sized + Send + Sync + 'static,
{
async fn load(&self, parent: &P) -> Result<Box<T>, ResourceLoadError>;
async fn update(&self, resource: ResourceMut<T>, parent: &P)
-> Result<(), ResourceLoadError>
{
resource.replace(self.load(parent).await?);
Ok(())
}
}
pub trait ResourceDefaultLoader: Send + Sync + 'static {
type Loader: ResourceLoader<Self>;
fn default_loader() -> Self::Loader;
}
#[cfg(test)]
mod tests {
use super::manager::tests::*;
use super::*;
use macro_rules_attribute::apply;
use rstest::rstest;
use smol_macros::test as smol_test;
use std::time::Duration;
use test_log::test as test_log;
#[derive(Clone, Debug)]
enum SubStringLoaderError {
NoSuffix,
NoUpdate,
}
impl Display for SubStringLoaderError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
SubStringLoaderError::NoSuffix => f.write_str("No suffix specified"),
SubStringLoaderError::NoUpdate => f.write_str("Updates are not allowed"),
}
}
}
impl Error for SubStringLoaderError {}
struct SubStringLoader {
suffix: Option<String>,
no_update: bool,
}
impl SubStringLoader {
fn new(suffix: impl Into<String>) -> Self {
Self { suffix: Some(suffix.into()), no_update: false }
}
fn err() -> Self {
Self { suffix: None, no_update: true }
}
fn no_update(suffix: impl Into<String>) -> Self {
Self { suffix: Some(suffix.into()), no_update: true }
}
}
#[async_trait]
impl SubResourceLoader<String, String> for SubStringLoader {
async fn load(&self, parent: &String) -> Result<Box<String>, ResourceLoadError> {
match &self.suffix {
Some(suffix) => Ok(Box::new(parent.clone() + "-" + &suffix)),
None => Err(ResourceLoadError::from_error(SubStringLoaderError::NoSuffix)),
}
}
async fn update(&self, resource: ResourceMut<String>, parent: &String) -> Result<(), ResourceLoadError> {
if self.no_update {
Err(ResourceLoadError::from_error(SubStringLoaderError::NoUpdate))
} else {
resource.replace(self.load(parent).await?);
Ok(())
}
}
}
#[rstest]
#[case::err(SubStringLoader::err(), None)]
#[case::ok(SubStringLoader::new("subvalue"), Some("subvalue"))]
#[test_attr(test_log(apply(smol_test)))]
async fn test_sub_resource(
test_resource_ctx: TestResourceContext<1>,
#[case] sub_loader: SubStringLoader,
#[case] expected_sub_value: Option<&str>,
) {
test_resource_ctx.data_maps[0].lock().insert("key", b"value", "h_value");
let resource = test_resource_ctx.manager.get_with_loader("key", TestResLoader::new("key")).await
.expect("Resource should load");
let result = resource.attach_sub_resource(sub_loader).await;
match expected_sub_value {
Some(expected_value) => {
let sub_resource = result.expect("Sub-resource should load");
assert_eq!(*sub_resource.read(), format!("value-{expected_value}"));
},
None => {
result.expect_err("Sub-resource should fail to load");
}
}
}
#[rstest]
#[case::err([SubStringLoader::no_update("subvalue")], ["value-subvalue"])]
#[case::ok([SubStringLoader::new("subvalue")], ["new_value-subvalue"])]
#[case::mixed(
[SubStringLoader::no_update("subvalue1"), SubStringLoader::new("subvalue2")],
["value-subvalue1", "new_value-subvalue2"],
)]
#[test_attr(test_log(apply(smol_test)))]
async fn test_sub_resource_update(
test_resource_ctx: TestResourceContext<1>,
#[case] sub_loaders: impl IntoIterator<Item=SubStringLoader>,
#[case] expected_sub_values: impl IntoIterator<Item=&str>,
) {
test_resource_ctx.data_maps[0].lock().insert("key", b"value", "h_value");
let resource = test_resource_ctx.manager.get_with_loader("key", TestResLoader::new("key")).await
.expect("Resource should load");
let mut sub_resources = Vec::new();
for sub_loader in sub_loaders {
let sub_resource = resource.attach_sub_resource(sub_loader).await
.expect("Sub-resource should load");
sub_resources.push(sub_resource);
}
test_resource_ctx.data_maps[0].lock().insert("key", b"new_value", "h_new_value");
timeout(
test_resource_ctx.manager.test_ctx().sync_update.wait_count(1),
Duration::from_millis(200),
).await;
let expected_sub_values = expected_sub_values.into_iter().collect::<Vec<_>>();
assert_eq!(expected_sub_values.len(), sub_resources.len());
for (idx, expected_sub_value) in expected_sub_values.into_iter().enumerate() {
assert_eq!(&*sub_resources[idx].read(), expected_sub_value);
}
}
#[rstest]
#[test_attr(test_log(apply(smol_test)))]
async fn test_chained_sub_resource(
test_resource_ctx: TestResourceContext<1>,
) {
test_resource_ctx.data_maps[0].lock().insert("key", b"value", "h_value");
let resource = test_resource_ctx.manager.get_with_loader("key", TestResLoader::new("key")).await
.expect("Resource should load");
let sub_resource = resource.attach_sub_resource(SubStringLoader::new("subvalue")).await
.expect("Sub-resource should load");
let sub_sub_resource = sub_resource.attach_sub_resource(SubStringLoader::new("subsubvalue")).await
.expect("Sub-sub-resource should load");
assert_eq!(&*sub_sub_resource.read(), "value-subvalue-subsubvalue");
test_resource_ctx.data_maps[0].lock().insert("key", b"new_value", "h_new_value");
timeout(
test_resource_ctx.manager.test_ctx().sync_update.wait_count(1),
Duration::from_millis(200)
).await;
assert_eq!(&*sub_sub_resource.read(), "new_value-subvalue-subsubvalue");
}
}