folk-api 0.1.2

Plugin contract for the Folk PHP application server
Documentation
//! `ServerPlugin` helper trait for the common case: a plugin that runs one
//! long-lived background task (HTTP server, queue consumer, etc.) and shuts
//! down when the server signals shutdown.
//!
//! Most plugins should implement [`ServerPlugin`] rather than [`Plugin`]
//! directly, then wrap with [`ServerPluginWrapper`].

use std::sync::{Arc, Mutex};

use anyhow::{Context, Result};
use async_trait::async_trait;
use tokio::task::JoinHandle;
use tracing::{error, info, warn};

use crate::context::PluginContext;
use crate::plugin::Plugin;
use crate::rpc::RpcMethodDef;

/// Trait for plugins that run a single long-lived background task.
/// The `run` future should observe `ctx.shutdown` and return when signaled.
#[async_trait]
pub trait ServerPlugin: Send + Sync + 'static {
    /// Stable plugin name (same semantics as [`Plugin::name`]).
    fn name(&self) -> &'static str;

    /// Run the plugin's main loop. Return `Ok(())` on shutdown.
    async fn run(&self, ctx: PluginContext) -> Result<()>;

    /// Optional RPC method advertisements (default empty).
    fn rpc_methods(&self) -> Vec<RpcMethodDef> {
        Vec::new()
    }
}

/// Wraps a [`ServerPlugin`] so it can be used as a [`Plugin`].
/// Spawns `run` at boot, awaits the task during shutdown.
pub struct ServerPluginWrapper<S: ServerPlugin> {
    inner: Arc<S>,
    handle: Mutex<Option<JoinHandle<Result<()>>>>,
}

impl<S: ServerPlugin> ServerPluginWrapper<S> {
    pub fn new(inner: S) -> Self {
        Self {
            inner: Arc::new(inner),
            handle: Mutex::new(None),
        }
    }
}

#[async_trait]
impl<S: ServerPlugin> Plugin for ServerPluginWrapper<S> {
    fn name(&self) -> &'static str {
        self.inner.name()
    }

    async fn boot(&mut self, ctx: PluginContext) -> Result<()> {
        let inner = self.inner.clone();
        let handle = tokio::spawn(async move { inner.run(ctx).await });
        *self.handle.lock().expect("mutex") = Some(handle);
        info!(plugin = self.inner.name(), "server plugin booted");
        Ok(())
    }

    async fn shutdown(&self) -> Result<()> {
        let handle = {
            let mut slot = self
                .handle
                .lock()
                .map_err(|_| anyhow::anyhow!("ServerPluginWrapper handle mutex poisoned"))?;
            slot.take()
        };

        let Some(handle) = handle else {
            warn!(
                plugin = self.inner.name(),
                "shutdown called but no handle present"
            );
            return Ok(());
        };

        match handle.await {
            Ok(Ok(())) => {
                info!(plugin = self.inner.name(), "run loop completed cleanly");
                Ok(())
            }
            Ok(Err(err)) => {
                error!(plugin = self.inner.name(), error = ?err, "run loop returned error");
                Err(err).context("ServerPlugin run loop failed")
            }
            Err(join_err) => {
                error!(plugin = self.inner.name(), error = %join_err, "run loop join failed");
                Err(anyhow::anyhow!("ServerPlugin task join failed: {join_err}"))
            }
        }
    }

    fn rpc_methods(&self) -> Vec<RpcMethodDef> {
        self.inner.rpc_methods()
    }
}