use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use crate::call::domain::MediaCapability;
#[derive(Debug, thiserror::Error)]
pub enum AppRuntimeError {
#[error("application not running")]
NotRunning,
#[error("application already running: {0}")]
AlreadyRunning(String),
#[error("unknown application: {0}")]
UnknownApp(String),
#[error("media capability not satisfied: {0:?}")]
CapabilityNotSatisfied(Vec<MediaCapability>),
#[error("failed to start application: {0}")]
StartFailed(String),
#[error("failed to inject event: {0}")]
InjectFailed(String),
#[error("failed to stop application: {0}")]
StopFailed(String),
#[error("application error: {0}")]
AppError(#[from] anyhow::Error),
}
pub type AppResult<T> = Result<T, AppRuntimeError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum AppStatus {
#[default]
Idle,
Starting,
Running,
Stopping,
Stopped,
Failed,
}
impl std::fmt::Display for AppStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AppStatus::Idle => write!(f, "idle"),
AppStatus::Starting => write!(f, "starting"),
AppStatus::Running => write!(f, "running"),
AppStatus::Stopping => write!(f, "stopping"),
AppStatus::Stopped => write!(f, "stopped"),
AppStatus::Failed => write!(f, "failed"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppDescriptor {
pub name: String,
pub description: Option<String>,
pub required_capabilities: Vec<MediaCapability>,
pub supports_bypass: bool,
pub version: Option<String>,
}
impl AppDescriptor {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: None,
required_capabilities: Vec::new(),
supports_bypass: false,
version: None,
}
}
pub fn ivr() -> Self {
Self {
name: "ivr".to_string(),
description: Some("Interactive Voice Response".to_string()),
required_capabilities: vec![MediaCapability::Full],
supports_bypass: false,
version: None,
}
}
pub fn voicemail() -> Self {
Self {
name: "voicemail".to_string(),
description: Some("Voicemail recording and playback".to_string()),
required_capabilities: vec![MediaCapability::Full],
supports_bypass: false,
version: None,
}
}
pub fn queue() -> Self {
Self {
name: "queue".to_string(),
description: Some("Call queue with hold music".to_string()),
required_capabilities: vec![MediaCapability::Full],
supports_bypass: false,
version: None,
}
}
pub fn signaling_only() -> Self {
Self {
name: "signaling".to_string(),
description: Some("Signaling-only application".to_string()),
required_capabilities: vec![],
supports_bypass: true,
version: None,
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn with_capabilities(mut self, caps: Vec<MediaCapability>) -> Self {
self.required_capabilities = caps;
self
}
pub fn with_bypass_support(mut self, supports: bool) -> Self {
self.supports_bypass = supports;
self
}
pub fn check_capabilities(&self, available: &[MediaCapability]) -> CapabilityCheckResult {
if self.required_capabilities.is_empty() {
return CapabilityCheckResult::Satisfied;
}
let missing: Vec<_> = self
.required_capabilities
.iter()
.filter(|req| {
!available.iter().any(|avail| {
matches!(
(avail, req),
(MediaCapability::Full, _)
| (MediaCapability::Limited, MediaCapability::Limited)
| (MediaCapability::Limited, MediaCapability::SignalingOnly)
| (
MediaCapability::SignalingOnly,
MediaCapability::SignalingOnly
)
)
})
})
.cloned()
.collect();
if missing.is_empty() {
CapabilityCheckResult::Satisfied
} else {
CapabilityCheckResult::Missing(missing)
}
}
}
#[derive(Debug, Clone)]
pub enum CapabilityCheckResult {
Satisfied,
Missing(Vec<MediaCapability>),
}
impl CapabilityCheckResult {
pub fn is_satisfied(&self) -> bool {
matches!(self, CapabilityCheckResult::Satisfied)
}
}
#[async_trait]
pub trait AppRuntime: Send + Sync {
fn as_any(&self) -> &dyn std::any::Any;
async fn start_app(
&self,
app_name: &str,
params: Option<serde_json::Value>,
auto_answer: bool,
) -> AppResult<()>;
async fn stop_app(&self, reason: Option<String>) -> AppResult<()>;
fn inject_event(&self, event: serde_json::Value) -> AppResult<()>;
fn is_running(&self) -> bool;
fn status(&self) -> AppStatus;
fn current_app(&self) -> Option<String>;
fn required_capabilities(&self) -> Vec<MediaCapability>;
fn app_descriptor(&self, app_name: &str) -> Option<AppDescriptor>;
fn get_queue_name(&self) -> Option<String> {
None
}
}
#[async_trait]
pub trait AppRuntimeFactory: Send + Sync {
fn create_runtime(
&self,
session_id: &str,
handle: crate::proxy::proxy_call::state::SipSessionHandle,
) -> Arc<dyn AppRuntime>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn app_descriptor_ivr() {
let desc = AppDescriptor::ivr();
assert_eq!(desc.name, "ivr");
assert!(!desc.supports_bypass);
assert!(!desc.required_capabilities.is_empty());
}
#[test]
fn app_descriptor_voicemail() {
let desc = AppDescriptor::voicemail();
assert_eq!(desc.name, "voicemail");
assert!(!desc.supports_bypass);
}
#[test]
fn app_descriptor_signaling() {
let desc = AppDescriptor::signaling_only();
assert!(desc.supports_bypass);
assert!(desc.required_capabilities.is_empty());
}
#[test]
fn capability_check_satisfied() {
let desc = AppDescriptor::signaling_only();
let result = desc.check_capabilities(&[]);
assert!(result.is_satisfied());
}
#[test]
fn capability_check_missing() {
let desc = AppDescriptor::ivr(); let result = desc.check_capabilities(&[MediaCapability::Limited]);
assert!(!result.is_satisfied());
}
#[test]
fn app_status_display() {
assert_eq!(AppStatus::Idle.to_string(), "idle");
assert_eq!(AppStatus::Running.to_string(), "running");
assert_eq!(AppStatus::Failed.to_string(), "failed");
}
}