#![cfg_attr(not(feature = "std"), no_std)]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
extern crate alloc;
use alloc::vec::Vec;
use zerodds_xrce::header::ClientKey;
use zerodds_xrce::object_id::ObjectId;
use zerodds_xrce::object_repr::ObjectVariant;
use zerodds_xrce::submessages::Submessage;
pub trait ClientTransport {
fn send(&mut self, payload: &[u8]) -> Result<(), ClientError>;
fn try_recv(&mut self) -> Result<Option<Vec<u8>>, ClientError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientState {
Disconnected,
Connecting,
Connected,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientError {
InvalidState,
Transport,
QueueFull,
}
pub struct XrceClient<T: ClientTransport> {
client_key: ClientKey,
state: ClientState,
transport: T,
next_request_id: u16,
out_queue: Vec<Submessage>,
}
impl<T: ClientTransport> XrceClient<T> {
pub fn new(client_key: ClientKey, transport: T) -> Self {
Self {
client_key,
state: ClientState::Disconnected,
transport,
next_request_id: 1,
out_queue: Vec::new(),
}
}
#[must_use]
pub fn state(&self) -> ClientState {
self.state
}
#[must_use]
pub fn client_key(&self) -> ClientKey {
self.client_key
}
pub fn connect(&mut self) -> Result<(), ClientError> {
if self.state != ClientState::Disconnected {
return Err(ClientError::InvalidState);
}
self.state = ClientState::Connecting;
Ok(())
}
pub fn mark_connected(&mut self) -> Result<(), ClientError> {
if self.state != ClientState::Connecting {
return Err(ClientError::InvalidState);
}
self.state = ClientState::Connected;
Ok(())
}
pub fn create_object(
&mut self,
_object_id: ObjectId,
_representation: ObjectVariant,
) -> Result<u16, ClientError> {
self.require_connected()?;
let req = self.next_request_id;
self.next_request_id = self.next_request_id.wrapping_add(1).max(1);
Ok(req)
}
pub fn delete_object(&mut self, _object_id: ObjectId) -> Result<u16, ClientError> {
self.require_connected()?;
let req = self.next_request_id;
self.next_request_id = self.next_request_id.wrapping_add(1).max(1);
Ok(req)
}
pub fn request_write(
&mut self,
_writer: ObjectId,
_payload: &[u8],
) -> Result<u16, ClientError> {
self.require_connected()?;
let req = self.next_request_id;
self.next_request_id = self.next_request_id.wrapping_add(1).max(1);
Ok(req)
}
pub fn request_read(&mut self, _reader: ObjectId) -> Result<u16, ClientError> {
self.require_connected()?;
let req = self.next_request_id;
self.next_request_id = self.next_request_id.wrapping_add(1).max(1);
Ok(req)
}
pub fn disconnect(&mut self) {
self.state = ClientState::Disconnected;
self.out_queue.clear();
}
fn require_connected(&self) -> Result<(), ClientError> {
if self.state != ClientState::Connected {
return Err(ClientError::InvalidState);
}
Ok(())
}
#[must_use]
pub fn out_queue_len(&self) -> usize {
self.out_queue.len()
}
pub fn transport_mut(&mut self) -> &mut T {
&mut self.transport
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
use alloc::vec::Vec;
use zerodds_xrce::header::CLIENT_KEY_LEN;
struct MockTransport {
sent: Vec<Vec<u8>>,
}
impl MockTransport {
fn new() -> Self {
Self { sent: Vec::new() }
}
}
impl ClientTransport for MockTransport {
fn send(&mut self, payload: &[u8]) -> Result<(), ClientError> {
self.sent.push(payload.to_vec());
Ok(())
}
fn try_recv(&mut self) -> Result<Option<Vec<u8>>, ClientError> {
Ok(None)
}
}
fn key() -> ClientKey {
ClientKey([0xAB; CLIENT_KEY_LEN])
}
#[test]
fn new_client_starts_disconnected() {
let c = XrceClient::new(key(), MockTransport::new());
assert_eq!(c.state(), ClientState::Disconnected);
assert_eq!(c.client_key(), key());
}
#[test]
fn connect_transitions_to_connecting() {
let mut c = XrceClient::new(key(), MockTransport::new());
c.connect().expect("connect");
assert_eq!(c.state(), ClientState::Connecting);
}
#[test]
fn mark_connected_transitions_to_connected() {
let mut c = XrceClient::new(key(), MockTransport::new());
c.connect().expect("connect");
c.mark_connected().expect("ack");
assert_eq!(c.state(), ClientState::Connected);
}
#[test]
fn double_connect_rejected() {
let mut c = XrceClient::new(key(), MockTransport::new());
c.connect().expect("first");
let err = c.connect().expect_err("second");
assert_eq!(err, ClientError::InvalidState);
}
#[test]
fn create_without_connect_rejected() {
let mut c = XrceClient::new(key(), MockTransport::new());
let oid = ObjectId::from_raw(0x0010);
let v = ObjectVariant::ByReference("topic-ref".into());
let err = c.create_object(oid, v).expect_err("disconnected");
assert_eq!(err, ClientError::InvalidState);
}
#[test]
fn write_without_connect_rejected() {
let mut c = XrceClient::new(key(), MockTransport::new());
let oid = ObjectId::from_raw(0x0010);
let err = c.request_write(oid, b"x").expect_err("disconnected");
assert_eq!(err, ClientError::InvalidState);
}
#[test]
fn read_without_connect_rejected() {
let mut c = XrceClient::new(key(), MockTransport::new());
let oid = ObjectId::from_raw(0x0010);
let err = c.request_read(oid).expect_err("disconnected");
assert_eq!(err, ClientError::InvalidState);
}
#[test]
fn full_lifecycle_creates_unique_request_ids() {
let mut c = XrceClient::new(key(), MockTransport::new());
c.connect().expect("conn");
c.mark_connected().expect("ack");
let oid = ObjectId::from_raw(0x0010);
let r1 = c
.create_object(oid, ObjectVariant::ByReference("a".into()))
.expect("create");
let r2 = c.delete_object(oid).expect("delete");
let r3 = c.request_write(oid, b"x").expect("write");
let r4 = c.request_read(oid).expect("read");
assert_eq!(r1, 1);
assert_eq!(r2, 2);
assert_eq!(r3, 3);
assert_eq!(r4, 4);
}
#[test]
fn disconnect_clears_state() {
let mut c = XrceClient::new(key(), MockTransport::new());
c.connect().expect("conn");
c.mark_connected().expect("ack");
c.disconnect();
assert_eq!(c.state(), ClientState::Disconnected);
}
}