1use std::pin::Pin;
14
15use async_trait::async_trait;
16use futures::channel::oneshot;
17use futures::Stream;
18
19use arora_types::call::{Call, CallResult};
20use arora_types::data::{Key, StateChange};
21
22#[derive(Debug, Clone, Default, PartialEq, Eq)]
26pub struct DeviceInfo {
27 pub name: Option<String>,
28 pub description: Option<String>,
29 pub model_family: Option<String>,
30 pub hardware_version: Option<String>,
31 pub software_version: Option<String>,
32 pub owners: Vec<String>,
33}
34
35#[derive(Debug, Clone)]
38pub enum BridgeOp {
39 Get(Vec<Key>),
41 Update(StateChange),
43 Call(Call),
45 ListKeys {
49 prefix: Option<String>,
51 },
52 ListMethods {
56 prefix: Option<String>,
58 },
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum BridgeError {
64 Disconnected(String),
66 Unregistered,
68 Other(String),
70}
71
72impl std::fmt::Display for BridgeError {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 match self {
75 BridgeError::Disconnected(m) => write!(f, "bridge disconnected: {m}"),
76 BridgeError::Unregistered => write!(f, "device unregistered from the remote"),
77 BridgeError::Other(m) => write!(f, "{m}"),
78 }
79 }
80}
81
82impl std::error::Error for BridgeError {}
83
84pub type BridgeResult<T> = Result<T, BridgeError>;
85
86pub struct BridgeCommand {
92 pub op: BridgeOp,
93 reply: oneshot::Sender<Result<CallResult, String>>,
94}
95
96impl BridgeCommand {
97 pub fn new(op: BridgeOp, reply: oneshot::Sender<Result<CallResult, String>>) -> Self {
99 Self { op, reply }
100 }
101
102 pub fn reply(self, result: Result<CallResult, String>) {
104 let _ = self.reply.send(result);
105 }
106}
107
108pub type DeviceInfoStream = Pin<Box<dyn Stream<Item = BridgeResult<Option<DeviceInfo>>> + Send>>;
110pub type DataRequestedStream = Pin<Box<dyn Stream<Item = bool> + Send>>;
112pub type CommandStream = Pin<Box<dyn Stream<Item = BridgeCommand> + Send>>;
114
115#[async_trait]
120pub trait Bridge: Send + Sync {
121 async fn get_device_info(&self) -> BridgeResult<Option<DeviceInfo>>;
123
124 async fn device_info_updated(&self) -> BridgeResult<DeviceInfoStream>;
126
127 async fn update_device_info(
129 &self,
130 info: Option<DeviceInfo>,
131 ) -> BridgeResult<Option<DeviceInfo>>;
132
133 async fn data_requested(&self) -> DataRequestedStream;
135
136 async fn send_data(&self, data: StateChange) -> BridgeResult<()>;
138
139 async fn commands(&self) -> CommandStream;
141}
142
143#[derive(Clone, Default)]
146pub struct FakeBridge;
147
148impl FakeBridge {
149 pub fn new() -> Self {
150 Self
151 }
152}
153
154#[async_trait]
155impl Bridge for FakeBridge {
156 async fn get_device_info(&self) -> BridgeResult<Option<DeviceInfo>> {
157 Ok(None)
158 }
159
160 async fn device_info_updated(&self) -> BridgeResult<DeviceInfoStream> {
161 Ok(Box::pin(futures::stream::empty()))
162 }
163
164 async fn update_device_info(
165 &self,
166 info: Option<DeviceInfo>,
167 ) -> BridgeResult<Option<DeviceInfo>> {
168 Ok(info)
169 }
170
171 async fn data_requested(&self) -> DataRequestedStream {
172 Box::pin(futures::stream::empty())
173 }
174
175 async fn send_data(&self, _data: StateChange) -> BridgeResult<()> {
176 Ok(())
177 }
178
179 async fn commands(&self) -> CommandStream {
180 Box::pin(futures::stream::empty())
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use futures::StreamExt;
188
189 #[tokio::test]
190 async fn fake_bridge_is_usable_as_trait_object() {
191 let bridge: Box<dyn Bridge> = Box::new(FakeBridge::new());
192 assert_eq!(bridge.get_device_info().await.unwrap(), None);
193 bridge.send_data(StateChange::new()).await.unwrap();
194 assert!(bridge
195 .device_info_updated()
196 .await
197 .unwrap()
198 .next()
199 .await
200 .is_none());
201 assert!(bridge.commands().await.next().await.is_none());
202 }
203
204 #[tokio::test]
205 async fn command_reply_round_trips() {
206 let (tx, rx) = oneshot::channel();
207 let cmd = BridgeCommand::new(BridgeOp::Get(vec![Key::from("a")]), tx);
208 match &cmd.op {
209 BridgeOp::Get(keys) => assert_eq!(keys[0], Key::from("a")),
210 _ => panic!("wrong op"),
211 }
212 cmd.reply(Err("not implemented".to_string()));
213 assert!(rx.await.unwrap().is_err());
214 }
215}