use std::pin::Pin;
use async_trait::async_trait;
use futures_channel::oneshot;
use futures_core::Stream;
use arora_types::call::{Call, CallError, CallResult};
use arora_types::data::{Key, StateChange};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DeviceInfo {
pub name: Option<String>,
pub description: Option<String>,
pub model_family: Option<String>,
pub hardware_version: Option<String>,
pub software_version: Option<String>,
pub owners: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum BridgeOp {
Get(Vec<Key>),
Update(StateChange),
Call(Call),
ListKeys {
prefix: Option<String>,
},
ListMethods {
prefix: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BridgeError {
Disconnected(String),
Unregistered,
Other(String),
}
impl std::fmt::Display for BridgeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BridgeError::Disconnected(m) => write!(f, "bridge disconnected: {m}"),
BridgeError::Unregistered => write!(f, "device unregistered from the remote"),
BridgeError::Other(m) => write!(f, "{m}"),
}
}
}
impl std::error::Error for BridgeError {}
pub type BridgeResult<T> = Result<T, BridgeError>;
pub struct BridgeCommand {
pub op: BridgeOp,
reply: oneshot::Sender<Result<CallResult, String>>,
}
impl BridgeCommand {
pub fn new(op: BridgeOp, reply: oneshot::Sender<Result<CallResult, String>>) -> Self {
Self { op, reply }
}
pub fn reply(self, result: Result<CallResult, String>) {
let _ = self.reply.send(result);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessDecision {
Allowed,
Rejected,
}
pub struct AccessRequest {
pub client_id: String,
pub user_id: Option<String>,
pub permission: String,
responder: oneshot::Sender<AccessDecision>,
}
impl AccessRequest {
pub fn new(
client_id: impl Into<String>,
user_id: Option<String>,
permission: impl Into<String>,
) -> (Self, oneshot::Receiver<AccessDecision>) {
let (responder, rx) = oneshot::channel();
(
Self {
client_id: client_id.into(),
user_id,
permission: permission.into(),
responder,
},
rx,
)
}
pub fn respond(self, decision: AccessDecision) {
let _ = self.responder.send(decision);
}
}
impl std::fmt::Debug for AccessRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AccessRequest")
.field("client_id", &self.client_id)
.field("user_id", &self.user_id)
.field("permission", &self.permission)
.finish_non_exhaustive()
}
}
pub type AccessRequestStream = Pin<Box<dyn Stream<Item = AccessRequest> + Send>>;
pub enum Inbound {
Command(BridgeCommand),
DeviceInfo(BridgeResult<Option<DeviceInfo>>),
DataRequested(bool),
}
pub type InboundStream = Pin<Box<dyn Stream<Item = Inbound> + Send>>;
pub type CallFuture<'a> =
Pin<Box<dyn std::future::Future<Output = Result<CallResult, CallError>> + Send + 'a>>;
pub trait Caller {
fn call(&self, call: Call) -> CallFuture<'_>;
}
#[async_trait]
pub trait Bridge: Send + Sync {
fn take_inbound(&mut self) -> InboundStream;
fn try_send(&mut self, change: &StateChange);
async fn get_device_info(&self) -> BridgeResult<Option<DeviceInfo>>;
async fn update_device_info(
&self,
info: Option<DeviceInfo>,
) -> BridgeResult<Option<DeviceInfo>>;
async fn device_id(&self) -> Option<String> {
None
}
async fn access_requests(&self) -> AccessRequestStream {
Box::pin(futures_util::stream::pending())
}
}
#[derive(Clone, Default)]
pub struct FakeBridge;
impl FakeBridge {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl Bridge for FakeBridge {
fn take_inbound(&mut self) -> InboundStream {
Box::pin(futures_util::stream::pending())
}
fn try_send(&mut self, _change: &StateChange) {}
async fn get_device_info(&self) -> BridgeResult<Option<DeviceInfo>> {
Ok(None)
}
async fn update_device_info(
&self,
info: Option<DeviceInfo>,
) -> BridgeResult<Option<DeviceInfo>> {
Ok(info)
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures::StreamExt;
#[tokio::test]
async fn fake_bridge_is_usable_as_trait_object() {
let mut bridge: Box<dyn Bridge> = Box::new(FakeBridge::new());
assert_eq!(bridge.get_device_info().await.unwrap(), None);
let mut inbound = bridge.take_inbound();
assert!(futures::poll!(inbound.next()).is_pending());
bridge.try_send(&StateChange::new());
}
#[tokio::test]
async fn command_reply_round_trips() {
let (tx, rx) = oneshot::channel();
let cmd = BridgeCommand::new(BridgeOp::Get(vec![Key::from("a")]), tx);
match &cmd.op {
BridgeOp::Get(keys) => assert_eq!(keys[0], Key::from("a")),
_ => panic!("wrong op"),
}
cmd.reply(Err("not implemented".to_string()));
assert!(rx.await.unwrap().is_err());
}
#[tokio::test]
async fn access_request_decision_round_trips() {
let (req, rx) = AccessRequest::new("client-1", Some("user-1".into()), "join the session");
assert_eq!(req.client_id, "client-1");
assert_eq!(req.permission, "join the session");
req.respond(AccessDecision::Allowed);
assert_eq!(rx.await.unwrap(), AccessDecision::Allowed);
}
#[tokio::test]
async fn dropping_an_access_request_cancels_its_decision() {
let (req, rx) = AccessRequest::new("client-1", None, "claim the device");
drop(req);
assert!(rx.await.is_err(), "no decision should arrive");
}
#[tokio::test]
async fn default_bridge_has_no_identity_and_a_pending_request_stream() {
let bridge: Box<dyn Bridge> = Box::new(FakeBridge::new());
assert_eq!(bridge.device_id().await, None);
let mut requests = bridge.access_requests().await;
assert!(futures::poll!(requests.next()).is_pending());
}
}