zerodds_xrce_client/
lib.rs1#![cfg_attr(not(feature = "std"), no_std)]
33#![forbid(unsafe_code)]
34#![warn(missing_docs)]
35
36extern crate alloc;
37
38use alloc::vec::Vec;
39
40use zerodds_xrce::header::ClientKey;
41use zerodds_xrce::object_id::ObjectId;
42use zerodds_xrce::object_repr::ObjectVariant;
43use zerodds_xrce::submessages::Submessage;
44
45pub trait ClientTransport {
50 fn send(&mut self, payload: &[u8]) -> Result<(), ClientError>;
55
56 fn try_recv(&mut self) -> Result<Option<Vec<u8>>, ClientError>;
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum ClientState {
67 Disconnected,
70 Connecting,
73 Connected,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum ClientError {
80 InvalidState,
82 Transport,
84 QueueFull,
86}
87
88pub struct XrceClient<T: ClientTransport> {
90 client_key: ClientKey,
91 state: ClientState,
92 transport: T,
93 next_request_id: u16,
94 out_queue: Vec<Submessage>,
97}
98
99impl<T: ClientTransport> XrceClient<T> {
100 pub fn new(client_key: ClientKey, transport: T) -> Self {
102 Self {
103 client_key,
104 state: ClientState::Disconnected,
105 transport,
106 next_request_id: 1,
107 out_queue: Vec::new(),
108 }
109 }
110
111 #[must_use]
113 pub fn state(&self) -> ClientState {
114 self.state
115 }
116
117 #[must_use]
119 pub fn client_key(&self) -> ClientKey {
120 self.client_key
121 }
122
123 pub fn connect(&mut self) -> Result<(), ClientError> {
128 if self.state != ClientState::Disconnected {
129 return Err(ClientError::InvalidState);
130 }
131 self.state = ClientState::Connecting;
132 Ok(())
133 }
134
135 pub fn mark_connected(&mut self) -> Result<(), ClientError> {
141 if self.state != ClientState::Connecting {
142 return Err(ClientError::InvalidState);
143 }
144 self.state = ClientState::Connected;
145 Ok(())
146 }
147
148 pub fn create_object(
154 &mut self,
155 _object_id: ObjectId,
156 _representation: ObjectVariant,
157 ) -> Result<u16, ClientError> {
158 self.require_connected()?;
159 let req = self.next_request_id;
160 self.next_request_id = self.next_request_id.wrapping_add(1).max(1);
161 Ok(req)
164 }
165
166 pub fn delete_object(&mut self, _object_id: ObjectId) -> Result<u16, ClientError> {
171 self.require_connected()?;
172 let req = self.next_request_id;
173 self.next_request_id = self.next_request_id.wrapping_add(1).max(1);
174 Ok(req)
175 }
176
177 pub fn request_write(
182 &mut self,
183 _writer: ObjectId,
184 _payload: &[u8],
185 ) -> Result<u16, ClientError> {
186 self.require_connected()?;
187 let req = self.next_request_id;
188 self.next_request_id = self.next_request_id.wrapping_add(1).max(1);
189 Ok(req)
190 }
191
192 pub fn request_read(&mut self, _reader: ObjectId) -> Result<u16, ClientError> {
197 self.require_connected()?;
198 let req = self.next_request_id;
199 self.next_request_id = self.next_request_id.wrapping_add(1).max(1);
200 Ok(req)
201 }
202
203 pub fn disconnect(&mut self) {
205 self.state = ClientState::Disconnected;
206 self.out_queue.clear();
207 }
208
209 fn require_connected(&self) -> Result<(), ClientError> {
211 if self.state != ClientState::Connected {
212 return Err(ClientError::InvalidState);
213 }
214 Ok(())
215 }
216
217 #[must_use]
219 pub fn out_queue_len(&self) -> usize {
220 self.out_queue.len()
221 }
222
223 pub fn transport_mut(&mut self) -> &mut T {
225 &mut self.transport
226 }
227}
228
229#[cfg(test)]
230#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
231mod tests {
232 use super::*;
233 use alloc::vec::Vec;
234 use zerodds_xrce::header::CLIENT_KEY_LEN;
235
236 struct MockTransport {
238 sent: Vec<Vec<u8>>,
239 }
240 impl MockTransport {
241 fn new() -> Self {
242 Self { sent: Vec::new() }
243 }
244 }
245 impl ClientTransport for MockTransport {
246 fn send(&mut self, payload: &[u8]) -> Result<(), ClientError> {
247 self.sent.push(payload.to_vec());
248 Ok(())
249 }
250 fn try_recv(&mut self) -> Result<Option<Vec<u8>>, ClientError> {
251 Ok(None)
252 }
253 }
254
255 fn key() -> ClientKey {
256 ClientKey([0xAB; CLIENT_KEY_LEN])
257 }
258
259 #[test]
260 fn new_client_starts_disconnected() {
261 let c = XrceClient::new(key(), MockTransport::new());
262 assert_eq!(c.state(), ClientState::Disconnected);
263 assert_eq!(c.client_key(), key());
264 }
265
266 #[test]
267 fn connect_transitions_to_connecting() {
268 let mut c = XrceClient::new(key(), MockTransport::new());
269 c.connect().expect("connect");
270 assert_eq!(c.state(), ClientState::Connecting);
271 }
272
273 #[test]
274 fn mark_connected_transitions_to_connected() {
275 let mut c = XrceClient::new(key(), MockTransport::new());
276 c.connect().expect("connect");
277 c.mark_connected().expect("ack");
278 assert_eq!(c.state(), ClientState::Connected);
279 }
280
281 #[test]
282 fn double_connect_rejected() {
283 let mut c = XrceClient::new(key(), MockTransport::new());
284 c.connect().expect("first");
285 let err = c.connect().expect_err("second");
286 assert_eq!(err, ClientError::InvalidState);
287 }
288
289 #[test]
290 fn create_without_connect_rejected() {
291 let mut c = XrceClient::new(key(), MockTransport::new());
292 let oid = ObjectId::from_raw(0x0010);
293 let v = ObjectVariant::ByReference("topic-ref".into());
294 let err = c.create_object(oid, v).expect_err("disconnected");
295 assert_eq!(err, ClientError::InvalidState);
296 }
297
298 #[test]
299 fn write_without_connect_rejected() {
300 let mut c = XrceClient::new(key(), MockTransport::new());
301 let oid = ObjectId::from_raw(0x0010);
302 let err = c.request_write(oid, b"x").expect_err("disconnected");
303 assert_eq!(err, ClientError::InvalidState);
304 }
305
306 #[test]
307 fn read_without_connect_rejected() {
308 let mut c = XrceClient::new(key(), MockTransport::new());
309 let oid = ObjectId::from_raw(0x0010);
310 let err = c.request_read(oid).expect_err("disconnected");
311 assert_eq!(err, ClientError::InvalidState);
312 }
313
314 #[test]
315 fn full_lifecycle_creates_unique_request_ids() {
316 let mut c = XrceClient::new(key(), MockTransport::new());
317 c.connect().expect("conn");
318 c.mark_connected().expect("ack");
319 let oid = ObjectId::from_raw(0x0010);
320 let r1 = c
321 .create_object(oid, ObjectVariant::ByReference("a".into()))
322 .expect("create");
323 let r2 = c.delete_object(oid).expect("delete");
324 let r3 = c.request_write(oid, b"x").expect("write");
325 let r4 = c.request_read(oid).expect("read");
326 assert_eq!(r1, 1);
328 assert_eq!(r2, 2);
329 assert_eq!(r3, 3);
330 assert_eq!(r4, 4);
331 }
332
333 #[test]
334 fn disconnect_clears_state() {
335 let mut c = XrceClient::new(key(), MockTransport::new());
336 c.connect().expect("conn");
337 c.mark_connected().expect("ack");
338 c.disconnect();
339 assert_eq!(c.state(), ClientState::Disconnected);
340 }
341}