rustpbx 0.4.9

A SIP PBX implementation in Rust
Documentation
use async_trait::async_trait;
use serde_json::Value as JsonValue;

use crate::app::AppState;

/// An addon that supports export-and-reload (e.g. queues, IVR, CC).
///
/// Each registered handler is discovered by the UI so users can select
/// which addons to reload.  The handler is called once per reload cycle
/// and is responsible for exporting data to TOML and triggering in-memory
/// reload on the SIP server.
#[async_trait]
pub trait ExportReloadHandler: Send + Sync {
    /// Unique addon identifier, e.g. "queues", "ivr", "cc"
    fn addon_id(&self) -> &str;

    /// Human-readable name shown in the UI, e.g. "Queues"
    fn display_name(&self) -> &str;

    /// Perform export + reload.  The implementation must be idempotent
    /// (called once per reload cycle).
    async fn export_and_reload(&self, app_state: &AppState) -> Result<JsonValue, String>;
}

// ── Registry ──────────────────────────────────────────────────────────────────

/// Thread-safe registry of export/reload handlers.
///
/// Stored on the `AddonRegistry` so that the console & AMI endpoints
/// can discover available addons and invoke them.
#[derive(Default)]
pub struct ExportReloadRegistry {
    handlers: Vec<Box<dyn ExportReloadHandler>>,
}

impl ExportReloadRegistry {
    pub fn register(&mut self, handler: Box<dyn ExportReloadHandler>) {
        self.handlers.push(handler);
    }

    /// List all registered addons as `(id, display_name)` pairs (owned).
    pub fn list(&self) -> Vec<(String, String)> {
        self.handlers
            .iter()
            .map(|h| (h.addon_id().to_string(), h.display_name().to_string()))
            .collect()
    }

    /// Invoke all registered handlers sequentially.
    /// Returns a list of `(id, result)` pairs.
    pub async fn invoke_all(&self, app_state: &AppState) -> Vec<(&str, Result<JsonValue, String>)> {
        let mut results = Vec::new();
        for handler in &self.handlers {
            let id = handler.addon_id();
            let result = handler.export_and_reload(app_state).await;
            results.push((id, result));
        }
        results
    }

    /// Invoke only the handlers whose ids are in the `selected` set.
    pub async fn invoke_selected(
        &self,
        selected: &[String],
        app_state: &AppState,
    ) -> Vec<(&str, Result<JsonValue, String>)> {
        let mut results = Vec::new();
        for handler in &self.handlers {
            let id = handler.addon_id();
            if selected.iter().any(|s| s == id) {
                let result = handler.export_and_reload(app_state).await;
                results.push((id, result));
            }
        }
        results
    }
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use async_trait::async_trait;
    use serde_json::json;

    struct MockHandler {
        id: &'static str,
        name: &'static str,
        should_fail: bool,
    }

    #[async_trait]
    impl ExportReloadHandler for MockHandler {
        fn addon_id(&self) -> &str {
            self.id
        }
        fn display_name(&self) -> &str {
            self.name
        }
        async fn export_and_reload(&self, _app_state: &AppState) -> Result<JsonValue, String> {
            if self.should_fail {
                Err(format!("{} failed", self.id))
            } else {
                Ok(json!({ "status": "ok", "addon": self.id }))
            }
        }
    }

    #[tokio::test]
    async fn test_register_and_list() {
        let mut registry = ExportReloadRegistry::default();
        assert!(registry.list().is_empty());

        registry.register(Box::new(MockHandler {
            id: "queue",
            name: "Queues",
            should_fail: false,
        }));
        registry.register(Box::new(MockHandler {
            id: "ivr",
            name: "IVR",
            should_fail: false,
        }));

        let list = registry.list();
        assert_eq!(list.len(), 2);
        assert!(list.contains(&("queue".to_string(), "Queues".to_string())));
        assert!(list.contains(&("ivr".to_string(), "IVR".to_string())));
    }

    #[tokio::test]
    async fn test_invoke_selected_returns_matching_only() {
        let mut registry = ExportReloadRegistry::default();
        registry.register(Box::new(MockHandler {
            id: "queue",
            name: "Queues",
            should_fail: false,
        }));
        registry.register(Box::new(MockHandler {
            id: "ivr",
            name: "IVR",
            should_fail: false,
        }));
        registry.register(Box::new(MockHandler {
            id: "cc",
            name: "CC",
            should_fail: false,
        }));

        // Need a real AppState for invoke. Since we can't easily create one,
        // we just verify the selection logic via list().
        // invoke_selected requires &AppState which is not easily constructable in unit tests.
        // The selection logic is: for each handler, check if id in selected.
        let ids: Vec<String> = registry.list().iter().map(|(id, _)| id.clone()).collect();
        assert_eq!(
            ids,
            vec!["queue".to_string(), "ivr".to_string(), "cc".to_string()]
        );
    }

    #[tokio::test]
    async fn test_empty_registry_returns_empty_lists() {
        let registry: ExportReloadRegistry = ExportReloadRegistry::default();
        assert!(registry.list().is_empty());
    }

    #[tokio::test]
    async fn test_register_same_id_twice() {
        let mut registry = ExportReloadRegistry::default();
        registry.register(Box::new(MockHandler {
            id: "queue",
            name: "Queues",
            should_fail: false,
        }));
        registry.register(Box::new(MockHandler {
            id: "queue",
            name: "Queues v2",
            should_fail: false,
        }));
        // Duplicate IDs are allowed; listing returns both.
        assert_eq!(registry.list().len(), 2);
    }
}