folk-api 0.1.2

Plugin contract for the Folk PHP application server
Documentation
//! Integration test: a fake plugin and a fake executor wire up correctly.

use std::sync::Arc;

use anyhow::Result;
use async_trait::async_trait;
use bytes::Bytes;
use folk_api::{Executor, Plugin, PluginContext, RpcMethodDef};
use tokio::sync::watch;

struct StubExecutor;

#[async_trait]
impl Executor for StubExecutor {
    async fn execute_method(&self, _: &str, payload: Bytes) -> Result<Bytes> {
        Ok(payload)
    }
}

struct DemoPlugin {
    booted: bool,
}

#[async_trait]
impl Plugin for DemoPlugin {
    fn name(&self) -> &'static str {
        "demo"
    }

    async fn boot(&mut self, _ctx: PluginContext) -> Result<()> {
        self.booted = true;
        Ok(())
    }

    async fn shutdown(&self) -> Result<()> {
        Ok(())
    }

    fn rpc_methods(&self) -> Vec<RpcMethodDef> {
        vec![RpcMethodDef::new("demo.ping", "ping the demo plugin")]
    }
}

#[tokio::test]
async fn plugin_boot_and_executor_round_trip() {
    let (_tx, rx) = watch::channel(false);

    let mut plugin = DemoPlugin { booted: false };
    let executor: Arc<dyn Executor> = Arc::new(StubExecutor);

    let ctx = PluginContext {
        executor: executor.clone(),
        shutdown: rx,
        rpc_registrar: None,
        health_registry: None,
        metrics_registry: None,
    };

    plugin.boot(ctx).await.unwrap();
    assert!(plugin.booted);

    // Round-trip through the executor:
    let payload = Bytes::from("hello");
    let response = executor.execute(payload.clone()).await.unwrap();
    assert_eq!(response, payload);
}

#[test]
fn folk_api_version_is_set() {
    assert!(!folk_api::FOLK_API_VERSION.is_empty());
    assert!(folk_api::FOLK_API_VERSION.starts_with("0."));
}

#[tokio::test]
async fn execute_direct_default_returns_error() {
    let executor: Arc<dyn Executor> = Arc::new(StubExecutor);
    let request: String = "test".to_string();
    let result = executor.execute_direct("test", &request).await;
    assert!(result.is_err(), "default execute_direct should return Err");
}

#[tokio::test]
async fn execute_direct_custom_impl_works() {
    struct DirectExecutor;

    #[async_trait]
    impl Executor for DirectExecutor {
        async fn execute_method(&self, _: &str, _: Bytes) -> Result<Bytes> {
            Ok(Bytes::new())
        }

        async fn execute_direct(
            &self,
            method: &str,
            request: &(dyn std::any::Any + Send + Sync),
        ) -> Result<Box<dyn std::any::Any + Send>> {
            let s = request.downcast_ref::<String>().unwrap();
            Ok(Box::new(format!("{method}:{s}")))
        }
    }

    let executor: Arc<dyn Executor> = Arc::new(DirectExecutor);
    let request = "hello".to_string();
    let result = executor.execute_direct("greet", &request).await.unwrap();
    let response = result.downcast::<String>().unwrap();
    assert_eq!(*response, "greet:hello");
}