protobuilder 0.1.0

Type-safe message protocol builder with RON serialization and configurable framing
Documentation
# ProtoBuilder

High-level message protocol builder for async Rust.

ProtoBuilder simplifies building type-safe communication protocols over async streams by combining:
- **Enum-based message types** - Define your protocol messages as Rust enums with compile-time type safety
- **Automatic serialization** - Uses RON (Rusty Object Notation) for human-readable, text-based message format
- **Binary framing layer** - Configurable framing strategies (length-prefix or chunked) for message boundary detection
- **Async/Tokio native** - Built from the ground up for async Rust

## Features

- Type-safe protocol definitions using Rust enums
- Length-prefix framing (u16 or u32) for simple message boundaries
- Chunked framing for large messages (32KB+)
- Split protocol support for concurrent read/write
- Clean error handling with custom error types

## Installation

Add this to your `Cargo.toml`:

```toml
[dependencies]
protobuilder = "0.1.0"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
```

## Quick Start

```rust,no_run
use protobuilder::{Protocol, LengthPrefix, Result};
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug, Clone)]
enum Request {
    Ping,
    Message(String),
}

#[derive(Serialize, Deserialize, Debug, Clone)]
enum Response {
    Pong,
    Reply(String),
}

#[tokio::main]
async fn main() -> Result<()> {
    // Connect to a server
    let stream = tokio::net::TcpStream::connect("127.0.0.1:9999").await?;

    // Build the protocol with length-prefix framing
    let mut proto = Protocol::<_, Request, Response, _>::builder()
        .framing(LengthPrefix::u32())
        .build(stream)?;

    // Send and receive messages
    proto.send(Request::Ping).await?;
    let response = proto.recv().await?;
    println!("Received: {:?}", response);

    Ok(())
}
```

## Framing Strategies

### Length Prefix

Simple framing using a length header before each message:

```rust
use protobuilder::LengthPrefix;

// 16-bit length prefix (max 64KB messages)
let framing = LengthPrefix::u16();

// 32-bit length prefix (max 4GB messages)
let framing = LengthPrefix::u32();

// Custom max size limit
let framing = LengthPrefix::u32().with_max_size(1024 * 1024); // 1MB
```

### Chunked Framing

For large messages that need to be split across multiple chunks:

```rust
use protobuilder::ChunkedFraming;

// 8KB chunks
let framing = ChunkedFraming::new(8 * 1024);

// With custom max message size
let framing = ChunkedFraming::new(32 * 1024)
    .with_max_message_size(10 * 1024 * 1024); // 10MB
```

## Split Protocol

For concurrent read/write operations, split the protocol into halves:

```rust,no_run
# use protobuilder::{Protocol, LengthPrefix, Result};
# use serde::{Serialize, Deserialize};
# #[derive(Serialize, Deserialize)]
# enum Request { Ping }
# #[derive(Serialize, Deserialize)]
# enum Response { Pong }
# async fn example() -> Result<()> {
# let stream = tokio::net::TcpStream::connect("127.0.0.1:9999").await?;
let proto = Protocol::<_, Request, Response, _>::builder()
    .framing(LengthPrefix::u32())
    .build(stream)?;

let mut split_proto = proto.split();

// Now you can use split_proto in concurrent tasks
tokio::spawn(async move {
    split_proto.send(Request::Ping).await.unwrap();
});
# Ok(())
# }
```

## Complete Example

See `examples/basic.rs` for a working client-server example:

```rust
use protobuilder::{Protocol, LengthPrefix, Result};
use serde::{Serialize, Deserialize};
use tokio::net::{TcpListener, TcpStream};

#[derive(Serialize, Deserialize, Debug, Clone)]
enum ClientPacket {
    Ping,
    Message(String),
}

#[derive(Serialize, Deserialize, Debug, Clone)]
enum ServerPacket {
    Pong,
    Reply(String),
}

// Server
async fn server() -> Result<()> {
    let listener = TcpListener::bind("127.0.0.1:9999").await?;
    
    loop {
        let (stream, _) = listener.accept().await?;
        tokio::spawn(async move {
            let mut proto = Protocol::<_, ServerPacket, ClientPacket, _>::builder()
                .framing(LengthPrefix::u32())
                .build(stream)
                .unwrap();

            while let Ok(packet) = proto.recv().await {
                match packet {
                    ClientPacket::Ping => {
                        let _ = proto.send(ServerPacket::Pong).await;
                    }
                    ClientPacket::Message(text) => {
                        let _ = proto.send(ServerPacket::Reply(format!("Echo: {}", text))).await;
                    }
                }
            }
        });
    }
}
```

## Examples

Run the basic example:

```bash
cargo run --example basic
```

Run the large data example (demonstrates chunked framing):

```bash
cargo run --example large_data
```

## API Overview

### Protocol Builder

```rust,ignore
// Generic form:
Protocol::<Stream, SendType, RecvType, FramingType>::builder()
    .framing(framing_strategy)
    .build(stream)

// See the Quick Start example above for a complete working example
```

### Protocol Methods

- `send(packet)` - Send a message
- `recv()` - Receive a message
- `split()` - Split into read/write halves
- `into_inner()` - Extract the underlying stream
- `get_ref()` / `get_mut()` - Access the underlying stream

### SplitProtocol Methods

- `send(packet)` - Send a message (from the writer half)
- `recv()` - Receive a message (from the reader half)
- `into_halves()` - Extract the reader, writer, and framing components

### Error Types

```rust
pub enum ProtocolError {
    Io(std::io::Error),
    Framing(String),
    Serialization(String),
    Deserialization(String),
    TooLarge(usize),
}

pub type Result<T> = std::result::Result<T, ProtocolError>;
```

## How It Works

1. **Serialization**: Your enum messages are serialized to RON text (e.g., `Message("Hello")``"Message(\"Hello\")"`)
2. **Framing**: The text payload is wrapped with a binary framing layer for message boundary detection:
   - **Length Prefix**: Prepends a u16/u32 length header (big-endian)
   - **Chunked**: Splits large payloads into chunks with message IDs and continuation flags
3. **Transmission**: The framed binary data is written to/read from the async stream

**Note**: The wire format is binary (due to the framing layer), but the message payload itself is human-readable RON text. For pure binary serialization (e.g., bincode, protobuf), this crate is not suitable.

## License

Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for details.