Crate sansio

Crate sansio 

Source
Expand description

§Sans-IO Protocol Framework

A lightweight, zero-dependency framework for building protocol implementations that are completely decoupled from I/O operations.

§What is Sans-IO?

Sans-IO (French for “without I/O”) is an architectural pattern that separates protocol logic from I/O handling. This separation provides several key benefits:

  • Testability: Protocol logic can be tested without mocking sockets or async runtimes
  • Portability: Same protocol works in sync, async, embedded, or WASM environments
  • Composability: Multiple protocol layers can be easily stacked and combined
  • Debuggability: State machines can be inspected without I/O side effects

§no_std Support

This crate is no_std by default and works in any environment - embedded systems, WASM, or standard applications.

§Time Handling

The Time associated type is fully generic, allowing you to use any time representation:

  • With std: Use std::time::Instant or std::time::SystemTime
  • Embedded/bare-metal: Use u64 for tick counts, i64 for milliseconds, or custom time types
  • No timeouts: Use () (unit type) when timeout handling isn’t needed

This flexibility means timeout functionality works everywhere - you just choose the appropriate time type for your platform.

§The Protocol Trait

The Protocol trait provides a push-pull API for handling messages:

  • Push API (handle_*): Push data/events into the protocol
  • Pull API (poll_*): Poll results from the protocol

This design enables complete I/O independence while maintaining a clean, intuitive interface.

§Type Parameters

  • Rin: Input read message type (what you push in for reading)
  • Win: Input write message type (what you push in for writing)
  • Ein: Input event type (custom events specific to your protocol)

§Associated Types

  • Rout: Output read message type (what you poll after reading)
  • Wout: Output write message type (what you poll after writing)
  • Eout: Output event type (events generated by the protocol)
  • Error: Error type for operations
  • Time: Time/instant type for timeout handling (can be Instant, u64, i64, (), etc.)

§Basic Example

use sansio::Protocol;

/// A simple uppercase protocol: converts incoming strings to uppercase
struct UppercaseProtocol {
    routs: VecDeque<String>,
    wouts: VecDeque<String>,
}

impl UppercaseProtocol {
    fn new() -> Self {
        Self {
            routs: VecDeque::new(),
            wouts: VecDeque::new(),
        }
    }
}

impl Protocol<String, String, ()> for UppercaseProtocol {
    type Rout = String;
    type Wout = String;
    type Eout = ();
    type Error = MyError;
    type Time = ();  // No timeout handling needed

    fn handle_read(&mut self, msg: String) -> Result<(), Self::Error> {
        // Process incoming message
        self.routs.push_back(msg.to_uppercase());
        Ok(())
    }

    fn poll_read(&mut self) -> Option<Self::Rout> {
        // Return processed message
        self.routs.pop_front()
    }

    fn handle_write(&mut self, msg: String) -> Result<(), Self::Error> {
        // For this simple protocol, just pass through
        self.wouts.push_back(msg);
        Ok(())
    }

    fn poll_write(&mut self) -> Option<Self::Wout> {
        self.wouts.pop_front()
    }
}

// Usage
let mut protocol = UppercaseProtocol::new();

// Push data in
protocol.handle_read("hello".to_string()).unwrap();

// Pull results out
assert_eq!(protocol.poll_read(), Some("HELLO".to_string()));

§Timeout Handling Example

Protocols can handle time-based operations like heartbeats or retransmissions.

§Using std::time::Instant

use sansio::Protocol;
use std::time::{Duration, Instant};

/// A protocol that sends periodic heartbeats
struct HeartbeatProtocol {
    next_heartbeat: Option<Instant>,
    heartbeat_interval: Duration,
    pending_write: Option<Vec<u8>>,
}

impl HeartbeatProtocol {
    fn new(interval: Duration) -> Self {
        Self {
            next_heartbeat: None,
            heartbeat_interval: interval,
            pending_write: None,
        }
    }
}

impl Protocol<Vec<u8>, Vec<u8>, ()> for HeartbeatProtocol {
    type Rout = Vec<u8>;
    type Wout = Vec<u8>;
    type Eout = ();
    type Error = MyError;
    type Time = Instant;  // Using std::time::Instant

    fn handle_read(&mut self, msg: Vec<u8>) -> Result<(), Self::Error> {
        // Reset heartbeat timer on any received message
        self.next_heartbeat = Some(Instant::now() + self.heartbeat_interval);
        Ok(())
    }

    fn poll_read(&mut self) -> Option<Self::Rout> {
        None
    }

    fn handle_write(&mut self, msg: Vec<u8>) -> Result<(), Self::Error> {
        self.pending_write = Some(msg);
        Ok(())
    }

    fn poll_write(&mut self) -> Option<Self::Wout> {
        self.pending_write.take()
    }

    fn handle_timeout(&mut self, now: Instant) -> Result<(), Self::Error> {
        // Send heartbeat
        self.pending_write = Some(b"HEARTBEAT".to_vec());
        self.next_heartbeat = Some(now + self.heartbeat_interval);
        Ok(())
    }

    fn poll_timeout(&mut self) -> Option<Instant> {
        self.next_heartbeat
    }
}

§Using tick counts (no_std friendly)

use sansio::Protocol;

/// A protocol with timeout using tick counts (no_std friendly)
struct TickProtocol {
    next_timeout_tick: Option<u64>,
    timeout_interval: u64,
    messages: VecDeque<Vec<u8>>,
}

impl TickProtocol {
    fn new(timeout_interval: u64) -> Self {
        Self {
            next_timeout_tick: None,
            timeout_interval,
            messages: VecDeque::new(),
        }
    }
}

impl Protocol<Vec<u8>, Vec<u8>, ()> for TickProtocol {
    type Rout = Vec<u8>;
    type Wout = Vec<u8>;
    type Eout = ();
    type Error = MyError;
    type Time = u64;  // Using tick counts for embedded systems

    fn handle_read(&mut self, msg: Vec<u8>) -> Result<(), Self::Error> {
        self.messages.push_back(msg);
        Ok(())
    }

    fn poll_read(&mut self) -> Option<Self::Rout> {
        self.messages.pop_front()
    }

    fn handle_write(&mut self, msg: Vec<u8>) -> Result<(), Self::Error> {
        self.messages.push_back(msg);
        Ok(())
    }

    fn poll_write(&mut self) -> Option<Self::Wout> {
        self.messages.pop_front()
    }

    fn handle_timeout(&mut self, current_tick: u64) -> Result<(), Self::Error> {
        // Handle timeout at current tick
        self.next_timeout_tick = Some(current_tick + self.timeout_interval);
        Ok(())
    }

    fn poll_timeout(&mut self) -> Option<u64> {
        self.next_timeout_tick
    }
}

§Event Handling Example

Protocols can generate and handle custom events:

use sansio::Protocol;

/// Custom event types
#[derive(Debug, PartialEq)]
enum ConnectionEvent {
    Connected,
    Disconnected,
    Error(String),
}

/// Protocol that tracks connection state
struct ConnectionProtocol {
    connected: bool,
    event_queue: VecDeque<ConnectionEvent>,
}

impl ConnectionProtocol {
    fn new() -> Self {
        Self {
            connected: false,
            event_queue: VecDeque::new(),
        }
    }
}

impl Protocol<String, String, ConnectionEvent> for ConnectionProtocol {
    type Rout = String;
    type Wout = String;
    type Eout = ConnectionEvent;
    type Error = MyError;
    type Time = ();  // No timeout needed

    fn handle_read(&mut self, msg: String) -> Result<(), Self::Error> {
        Ok(())
    }

    fn poll_read(&mut self) -> Option<Self::Rout> {
        None
    }

    fn handle_write(&mut self, msg: String) -> Result<(), Self::Error> {
        Ok(())
    }

    fn poll_write(&mut self) -> Option<Self::Wout> {
        None
    }

    fn handle_event(&mut self, evt: ConnectionEvent) -> Result<(), Self::Error> {
        match evt {
            ConnectionEvent::Connected => {
                self.connected = true;
                self.event_queue.push_back(ConnectionEvent::Connected);
            }
            ConnectionEvent::Disconnected => {
                self.connected = false;
                self.event_queue.push_back(ConnectionEvent::Disconnected);
            }
            ConnectionEvent::Error(msg) => {
                self.event_queue.push_back(ConnectionEvent::Error(msg));
            }
        }
        Ok(())
    }

    fn poll_event(&mut self) -> Option<Self::Eout> {
        self.event_queue.pop_front()
    }
}

§Protocol Composition

Protocols can be layered and composed. Here’s a simple example of wrapping one protocol with another:

use sansio::Protocol;

/// A protocol wrapper that logs all messages
struct LoggingWrapper<P> {
    inner: P,
}

impl<P> LoggingWrapper<P> {
    fn new(inner: P) -> Self {
        Self { inner }
    }
}

impl<P, Rin, Win, Ein> Protocol<Rin, Win, Ein> for LoggingWrapper<P>
where
    P: Protocol<Rin, Win, Ein>,
    Rin: std::fmt::Debug,
    Win: std::fmt::Debug,
{
    type Rout = P::Rout;
    type Wout = P::Wout;
    type Eout = P::Eout;
    type Error = P::Error;
    type Time = P::Time;  // Inherit time type from wrapped protocol

    fn handle_read(&mut self, msg: Rin) -> Result<(), Self::Error> {
        println!("READ: {:?}", msg);
        self.inner.handle_read(msg)
    }

    fn poll_read(&mut self) -> Option<Self::Rout> {
        self.inner.poll_read()
    }

    fn handle_write(&mut self, msg: Win) -> Result<(), Self::Error> {
        println!("WRITE: {:?}", msg);
        self.inner.handle_write(msg)
    }

    fn poll_write(&mut self) -> Option<Self::Wout> {
        self.inner.poll_write()
    }
}

§Testing Protocols

Sans-IO protocols are trivial to test since they don’t involve any I/O:

#[test]
fn test_protocol() {
    let mut protocol = UppercaseProtocol::new();

    // Test single message
    protocol.handle_read("hello".to_string()).unwrap();
    assert_eq!(protocol.poll_read(), Some("HELLO".to_string()));

    // Test multiple messages
    protocol.handle_read("foo".to_string()).unwrap();
    protocol.handle_read("bar".to_string()).unwrap();
    assert_eq!(protocol.poll_read(), Some("FOO".to_string()));
    assert_eq!(protocol.poll_read(), Some("BAR".to_string()));
    assert_eq!(protocol.poll_read(), None);
}

Traits§

Protocol
A Sans-IO protocol abstraction.