use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::Value;
use crate::traits::AsrAdapter;
#[derive(Debug, Clone)]
pub enum ModelSource {
LocalPath(PathBuf),
}
#[derive(Debug, Clone)]
pub struct AdapterRequest {
pub language: String,
pub runtime: String,
pub model_source: ModelSource,
pub options: Value,
}
#[derive(Debug)]
pub enum RouterError {
UnknownRuntime(String),
InvalidRequest(String),
InstantiationFailed { runtime: String, message: String },
}
impl std::fmt::Display for RouterError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownRuntime(id) => write!(f, "unknown runtime: {id}"),
Self::InvalidRequest(msg) => write!(f, "invalid request: {msg}"),
Self::InstantiationFailed { runtime, message } => {
write!(f, "failed to instantiate runtime {runtime}: {message}")
}
}
}
}
impl std::error::Error for RouterError {}
#[async_trait]
pub trait AsrRuntimeFactory: Send + Sync {
fn id(&self) -> &'static str;
async fn instantiate(&self, req: &AdapterRequest) -> Result<Arc<dyn AsrAdapter>, RouterError>;
}
#[derive(Default)]
pub struct AsrRouter {
factories: HashMap<&'static str, Arc<dyn AsrRuntimeFactory>>,
}
impl AsrRouter {
pub fn new() -> Self {
Self::default()
}
pub fn register<F>(mut self, factory: F) -> Self
where
F: AsrRuntimeFactory + 'static,
{
self.factories.insert(factory.id(), Arc::new(factory));
self
}
pub async fn dispatch(&self, req: AdapterRequest) -> Result<Arc<dyn AsrAdapter>, RouterError> {
let factory = self
.factories
.get(req.runtime.as_str())
.ok_or_else(|| RouterError::UnknownRuntime(req.runtime.clone()))?
.clone();
factory.instantiate(&req).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::{AsrAdapter, AsrError};
use crate::types::{AudioChunk, Transcript};
struct StubAdapter;
#[async_trait]
impl AsrAdapter for StubAdapter {
async fn transcribe(&self, _audio: &[AudioChunk]) -> Result<Transcript, AsrError> {
Ok(Transcript::default())
}
}
struct StubFactory {
id: &'static str,
}
#[async_trait]
impl AsrRuntimeFactory for StubFactory {
fn id(&self) -> &'static str {
self.id
}
async fn instantiate(
&self,
_req: &AdapterRequest,
) -> Result<Arc<dyn AsrAdapter>, RouterError> {
Ok(Arc::new(StubAdapter))
}
}
struct FailingFactory;
#[async_trait]
impl AsrRuntimeFactory for FailingFactory {
fn id(&self) -> &'static str {
"failing"
}
async fn instantiate(
&self,
_req: &AdapterRequest,
) -> Result<Arc<dyn AsrAdapter>, RouterError> {
Err(RouterError::InstantiationFailed {
runtime: self.id().to_string(),
message: "deliberate test failure".into(),
})
}
}
fn req(runtime: &str) -> AdapterRequest {
AdapterRequest {
language: "ja".into(),
runtime: runtime.into(),
model_source: ModelSource::LocalPath(PathBuf::from("/tmp/model")),
options: Value::Null,
}
}
#[tokio::test]
async fn dispatch_returns_adapter_from_registered_factory() {
let router = AsrRouter::new().register(StubFactory { id: "stub" });
let adapter = router.dispatch(req("stub")).await;
assert!(adapter.is_ok());
}
#[tokio::test]
async fn dispatch_unknown_runtime_returns_unknown_runtime() {
let router = AsrRouter::new();
match router.dispatch(req("missing")).await {
Err(RouterError::UnknownRuntime(id)) => assert_eq!(id, "missing"),
Err(other) => panic!("expected UnknownRuntime, got {other:?}"),
Ok(_) => panic!("expected error, got Ok"),
}
}
#[tokio::test]
async fn dispatch_propagates_factory_failure() {
let router = AsrRouter::new().register(FailingFactory);
match router.dispatch(req("failing")).await {
Err(RouterError::InstantiationFailed { runtime, message }) => {
assert_eq!(runtime, "failing");
assert!(message.contains("deliberate"));
}
Err(other) => panic!("expected InstantiationFailed, got {other:?}"),
Ok(_) => panic!("expected error, got Ok"),
}
}
#[tokio::test]
async fn register_is_chainable_and_dispatches_correct_factory() {
let router = AsrRouter::new()
.register(StubFactory { id: "alpha" })
.register(StubFactory { id: "beta" });
assert!(router.dispatch(req("alpha")).await.is_ok());
assert!(router.dispatch(req("beta")).await.is_ok());
match router.dispatch(req("gamma")).await {
Err(RouterError::UnknownRuntime(_)) => {}
Err(other) => panic!("expected UnknownRuntime, got {other:?}"),
Ok(_) => panic!("expected error, got Ok"),
}
}
#[test]
fn router_error_display_messages_include_context() {
let unknown = RouterError::UnknownRuntime("foo".into()).to_string();
assert!(unknown.contains("foo"));
let failed = RouterError::InstantiationFailed {
runtime: "bar".into(),
message: "boom".into(),
}
.to_string();
assert!(failed.contains("bar") && failed.contains("boom"));
}
}