cordis-core 0.0.2

A typed, scope-based plugin runtime inspired by Cordis
Documentation
use std::sync::{
    Arc,
    atomic::{AtomicUsize, Ordering},
};

use cordis_rs::{App, Error, Plugin, PluginContext, Resource, Result};

struct CooperativeTaskPlugin;
impl Plugin for CooperativeTaskPlugin {
    type Config = Arc<AtomicUsize>;

    fn name(&self) -> &'static str {
        "cooperative-task"
    }

    async fn apply(&self, ctx: PluginContext, exits: Arc<Self::Config>) -> Result<()> {
        ctx.spawn(move |token| async move {
            token.cancelled().await;
            exits.fetch_add(1, Ordering::SeqCst);
            Ok(())
        })?;
        Ok(())
    }
}

#[tokio::test]
async fn scope_dispose_cancels_and_waits_for_cooperative_task() -> Result<()> {
    let app = App::new();
    let exits = Arc::new(AtomicUsize::new(0));
    let scope = app.install(CooperativeTaskPlugin, exits.clone()).await?;

    scope.dispose().await?;
    assert_eq!(exits.load(Ordering::SeqCst), 1);
    Ok(())
}

struct FailingTaskPlugin;
impl Plugin for FailingTaskPlugin {
    type Config = ();

    fn name(&self) -> &'static str {
        "failing-task"
    }

    async fn apply(&self, ctx: PluginContext, _: Arc<Self::Config>) -> Result<()> {
        ctx.spawn(|token| async move {
            token.cancelled().await;
            Err(Error::cleanup("task failed"))
        })?;
        Ok(())
    }
}

#[tokio::test]
async fn task_error_is_returned_by_scope_dispose() -> Result<()> {
    let app = App::new();
    let scope = app.install(FailingTaskPlugin, ()).await?;

    let error = scope.dispose().await.unwrap_err();
    assert!(matches!(error, Error::Cleanup(message) if message == "task failed"));
    Ok(())
}

struct ManagedResource {
    cancelled: Arc<AtomicUsize>,
    disposed: Arc<AtomicUsize>,
}

impl Resource for ManagedResource {
    fn cancel(&self) {
        self.cancelled.fetch_add(1, Ordering::SeqCst);
    }

    async fn dispose(self: Box<Self>) -> Result<()> {
        self.disposed.fetch_add(1, Ordering::SeqCst);
        Ok(())
    }
}

struct ResourcePlugin;
impl Plugin for ResourcePlugin {
    type Config = (Arc<AtomicUsize>, Arc<AtomicUsize>);

    fn name(&self) -> &'static str {
        "managed-resource"
    }

    async fn apply(&self, ctx: PluginContext, config: Arc<Self::Config>) -> Result<()> {
        ctx.manage(ManagedResource {
            cancelled: config.0.clone(),
            disposed: config.1.clone(),
        })
    }
}

#[tokio::test]
async fn managed_resource_is_cancelled_then_disposed_once() -> Result<()> {
    let app = App::new();
    let cancelled = Arc::new(AtomicUsize::new(0));
    let disposed = Arc::new(AtomicUsize::new(0));
    let scope = app
        .install(ResourcePlugin, (cancelled.clone(), disposed.clone()))
        .await?;

    scope.dispose().await?;
    app.shutdown().await?;
    assert_eq!(cancelled.load(Ordering::SeqCst), 1);
    assert_eq!(disposed.load(Ordering::SeqCst), 1);
    Ok(())
}