mod handler;
mod invocation;
use std::{
collections::HashMap,
error::Error,
fmt,
future::Future,
pin::Pin,
sync::{Arc, Mutex},
};
use self::{handler::Handler, invocation::parse_invocation};
pub type ResponseFuture = Pin<Box<dyn Future<Output = String> + Send + 'static>>;
pub struct Dispatch {
pub id: u64,
pub response: ResponseFuture,
}
#[derive(Debug)]
pub enum DispatchError {
InvalidInvocation(serde_json::Error),
UnknownCommand(String),
}
impl fmt::Display for DispatchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidInvocation(error) => write!(f, "invalid HMI invocation: {error}"),
Self::UnknownCommand(command) => write!(f, "unknown HMI command: {command}"),
}
}
}
impl Error for DispatchError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::InvalidInvocation(error) => Some(error),
Self::UnknownCommand(_) => None,
}
}
}
#[derive(Clone, Default)]
pub struct Bridge {
handlers: Arc<Mutex<HashMap<String, Handler>>>,
}
impl Bridge {
pub fn new() -> Self {
Self::default()
}
pub fn handle(
&mut self,
cmd: impl Into<String>,
f: impl Fn(String) -> String + Send + Sync + 'static,
) {
self.handlers
.lock()
.unwrap()
.insert(cmd.into(), Handler::sync(f));
}
pub fn handle_async<F, Fut>(&mut self, cmd: impl Into<String>, f: F)
where
F: Fn(String) -> Fut + Send + Sync + 'static,
Fut: Future<Output = String> + Send + 'static,
{
self.handlers
.lock()
.unwrap()
.insert(cmd.into(), Handler::async_handler(f));
}
pub fn invoke(&self, cmd: &str, payload: impl Into<String>) -> Option<ResponseFuture> {
let handler = self.handlers.lock().unwrap().get(cmd).cloned()?;
Some(handler.call(payload.into()))
}
pub fn dispatch(&self, message: &str) -> Result<Dispatch, DispatchError> {
let invocation = parse_invocation(message).map_err(DispatchError::InvalidInvocation)?;
let response = self
.invoke(&invocation.cmd, invocation.payload.to_string())
.ok_or_else(|| DispatchError::UnknownCommand(invocation.cmd.clone()))?;
Ok(Dispatch {
id: invocation.id,
response,
})
}
pub fn is_empty(&self) -> bool {
self.handlers.lock().unwrap().is_empty()
}
pub fn len(&self) -> usize {
self.handlers.lock().unwrap().len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invokes_sync_handler_without_a_runtime_dependency() {
let mut bridge = Bridge::new();
bridge.handle("echo", |payload| payload);
let response = bridge.invoke("echo", r#"{"ok":true}"#).unwrap();
assert_eq!(pollster(response), r#"{"ok":true}"#);
}
#[test]
fn dispatches_json_envelope() {
let mut bridge = Bridge::new();
bridge.handle_async("answer", |_| async { "42".into() });
let dispatch = bridge
.dispatch(r#"{"id":7,"cmd":"answer","payload":null}"#)
.unwrap();
assert_eq!(dispatch.id, 7);
assert_eq!(pollster(dispatch.response), "42");
}
#[test]
fn reports_invalid_and_unknown_invocations() {
let bridge = Bridge::new();
assert!(matches!(
bridge.dispatch("not json"),
Err(DispatchError::InvalidInvocation(_))
));
assert!(matches!(
bridge.dispatch(r#"{"id":1,"cmd":"missing","payload":null}"#),
Err(DispatchError::UnknownCommand(command)) if command == "missing"
));
}
fn pollster(mut future: ResponseFuture) -> String {
use std::task::{Context, Poll, Waker};
let mut context = Context::from_waker(Waker::noop());
match future.as_mut().poll(&mut context) {
Poll::Ready(value) => value,
Poll::Pending => panic!("test future unexpectedly pending"),
}
}
}