use super::types::{IpcEnvelope, IpcError, IpcResponse};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
pub const PROTOCOL_VERSION: u8 = 0x02;
pub const MIN_SUPPORTED_VERSION: u8 = 0x01;
pub const MAX_SUPPORTED_VERSION: u8 = 0x02;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProtocolCapability {
FormatByte,
MessagePack,
Streaming,
Push,
Discovery,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityFlags(u8);
impl CapabilityFlags {
pub const NONE: Self = Self(0);
const FORMAT_BYTE: u8 = 1 << 0;
const MESSAGEPACK: u8 = 1 << 1;
const STREAMING: u8 = 1 << 2;
const PUSH: u8 = 1 << 3;
const DISCOVERY: u8 = 1 << 4;
pub const V2_ALL: Self = Self(
Self::FORMAT_BYTE | Self::MESSAGEPACK | Self::STREAMING | Self::PUSH | Self::DISCOVERY,
);
#[must_use]
pub const fn supports(self, capability: ProtocolCapability) -> bool {
let flag = match capability {
ProtocolCapability::FormatByte => Self::FORMAT_BYTE,
ProtocolCapability::MessagePack => Self::MESSAGEPACK,
ProtocolCapability::Streaming => Self::STREAMING,
ProtocolCapability::Push => Self::PUSH,
ProtocolCapability::Discovery => Self::DISCOVERY,
};
self.0 & flag != 0
}
#[must_use]
pub const fn as_array(self) -> [Option<ProtocolCapability>; 5] {
[
if self.0 & Self::FORMAT_BYTE != 0 {
Some(ProtocolCapability::FormatByte)
} else {
None
},
if self.0 & Self::MESSAGEPACK != 0 {
Some(ProtocolCapability::MessagePack)
} else {
None
},
if self.0 & Self::STREAMING != 0 {
Some(ProtocolCapability::Streaming)
} else {
None
},
if self.0 & Self::PUSH != 0 {
Some(ProtocolCapability::Push)
} else {
None
},
if self.0 & Self::DISCOVERY != 0 {
Some(ProtocolCapability::Discovery)
} else {
None
},
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProtocolVersion {
pub major: u8,
capabilities: CapabilityFlags,
pub header_size: u8,
}
impl ProtocolVersion {
pub const V1: Self = Self {
major: 0x01,
capabilities: CapabilityFlags::NONE,
header_size: 6,
};
pub const V2: Self = Self {
major: 0x02,
capabilities: CapabilityFlags::V2_ALL,
header_size: 7,
};
pub const CURRENT: Self = Self::V2;
#[must_use]
pub const fn supports(&self, capability: ProtocolCapability) -> bool {
self.capabilities.supports(capability)
}
#[must_use]
pub const fn supports_format_byte(&self) -> bool {
self.capabilities.supports(ProtocolCapability::FormatByte)
}
#[must_use]
pub const fn supports_messagepack(&self) -> bool {
self.capabilities.supports(ProtocolCapability::MessagePack)
}
#[must_use]
pub const fn supports_streaming(&self) -> bool {
self.capabilities.supports(ProtocolCapability::Streaming)
}
#[must_use]
pub const fn supports_push(&self) -> bool {
self.capabilities.supports(ProtocolCapability::Push)
}
#[must_use]
pub const fn supports_discovery(&self) -> bool {
self.capabilities.supports(ProtocolCapability::Discovery)
}
#[must_use]
pub const fn capabilities(&self) -> CapabilityFlags {
self.capabilities
}
#[must_use]
pub const fn from_byte(version: u8) -> Option<Self> {
match version {
0x01 => Some(Self::V1),
0x02 => Some(Self::V2),
_ => None,
}
}
#[must_use]
pub const fn is_supported(version: u8) -> bool {
version >= MIN_SUPPORTED_VERSION && version <= MAX_SUPPORTED_VERSION
}
#[must_use]
pub const fn negotiate(client_version: u8, server_version: u8) -> Option<u8> {
if client_version < MIN_SUPPORTED_VERSION || server_version < MIN_SUPPORTED_VERSION {
return None;
}
let negotiated = if client_version < server_version {
client_version
} else {
server_version
};
if negotiated <= MAX_SUPPORTED_VERSION {
Some(negotiated)
} else {
Some(MAX_SUPPORTED_VERSION)
}
}
#[must_use]
pub const fn description(&self) -> &'static str {
match self.major {
0x01 => "v1 (JSON only, basic messaging)",
0x02 => "v2 (multi-format, streaming, push, discovery)",
_ => "unknown version",
}
}
#[must_use]
pub const fn supported_versions() -> &'static [Self] {
&[Self::V1, Self::V2]
}
}
impl Default for ProtocolVersion {
fn default() -> Self {
Self::CURRENT
}
}
impl std::fmt::Display for ProtocolVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "v{} ({})", self.major, self.description())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Format {
#[default]
Json,
#[cfg(feature = "ipc-messagepack")]
MessagePack,
}
impl Format {
pub const JSON_BYTE: u8 = 0x01;
pub const MESSAGEPACK_BYTE: u8 = 0x02;
#[must_use]
pub const fn to_byte(self) -> u8 {
match self {
Self::Json => Self::JSON_BYTE,
#[cfg(feature = "ipc-messagepack")]
Self::MessagePack => Self::MESSAGEPACK_BYTE,
}
}
#[must_use]
pub const fn from_byte(byte: u8) -> Option<Self> {
match byte {
Self::JSON_BYTE => Some(Self::Json),
#[cfg(feature = "ipc-messagepack")]
Self::MESSAGEPACK_BYTE => Some(Self::MessagePack),
_ => None,
}
}
pub fn serialize<T: Serialize>(self, value: &T) -> Result<Vec<u8>, IpcError> {
match self {
Self::Json => serde_json::to_vec(value).map_err(IpcError::from),
#[cfg(feature = "ipc-messagepack")]
Self::MessagePack => rmp_serde::to_vec_named(value).map_err(|e| {
IpcError::SerializationError(format!("MessagePack serialization failed: {e}"))
}),
}
}
pub fn deserialize<T: DeserializeOwned>(self, bytes: &[u8]) -> Result<T, IpcError> {
match self {
Self::Json => serde_json::from_slice(bytes).map_err(IpcError::from),
#[cfg(feature = "ipc-messagepack")]
Self::MessagePack => rmp_serde::from_slice(bytes).map_err(|e| {
IpcError::SerializationError(format!("MessagePack deserialization failed: {e}"))
}),
}
}
}
pub const MSG_TYPE_REQUEST: u8 = 0x01;
pub const MSG_TYPE_RESPONSE: u8 = 0x02;
pub const MSG_TYPE_ERROR: u8 = 0x03;
pub const MSG_TYPE_HEARTBEAT: u8 = 0x04;
pub const MSG_TYPE_PUSH: u8 = 0x05;
pub const MSG_TYPE_SUBSCRIBE: u8 = 0x06;
pub const MSG_TYPE_UNSUBSCRIBE: u8 = 0x07;
pub const MSG_TYPE_DISCOVER: u8 = 0x08;
pub const MSG_TYPE_STREAM: u8 = 0x09;
pub const HEADER_SIZE_V1: usize = 6;
pub const HEADER_SIZE_V2: usize = 7;
pub const HEADER_SIZE: usize = HEADER_SIZE_V2;
pub const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024;
const V1_MESSAGE_TYPES: &[u8] = &[
MSG_TYPE_REQUEST,
MSG_TYPE_RESPONSE,
MSG_TYPE_ERROR,
MSG_TYPE_HEARTBEAT,
];
const V2_MESSAGE_TYPES: &[u8] = &[
MSG_TYPE_REQUEST,
MSG_TYPE_RESPONSE,
MSG_TYPE_ERROR,
MSG_TYPE_HEARTBEAT,
MSG_TYPE_PUSH,
MSG_TYPE_SUBSCRIBE,
MSG_TYPE_UNSUBSCRIBE,
MSG_TYPE_DISCOVER,
MSG_TYPE_STREAM,
];
fn validate_message_type(version: u8, msg_type: u8) -> Result<(), IpcError> {
let valid_types = match version {
0x01 => V1_MESSAGE_TYPES,
0x02 => V2_MESSAGE_TYPES,
_ => {
return Err(IpcError::ProtocolError(format!(
"Unknown protocol version: {version:#04x}"
)))
}
};
if valid_types.contains(&msg_type) {
Ok(())
} else {
Err(IpcError::ProtocolError(format!(
"Unknown message type {msg_type:#04x} for protocol v{version}"
)))
}
}
async fn read_header<R>(reader: &mut R) -> Result<(u32, u8, u8, Format), IpcError>
where
R: AsyncRead + Unpin,
{
let mut base_header = [0u8; HEADER_SIZE_V1];
reader.read_exact(&mut base_header).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::UnexpectedEof {
IpcError::ConnectionClosed
} else {
IpcError::IoError(e.to_string())
}
})?;
let length = u32::from_be_bytes([
base_header[0],
base_header[1],
base_header[2],
base_header[3],
]);
let version = base_header[4];
let msg_type = base_header[5];
if !ProtocolVersion::is_supported(version) {
return Err(IpcError::UnsupportedProtocolVersion {
received: version,
min_supported: MIN_SUPPORTED_VERSION,
max_supported: MAX_SUPPORTED_VERSION,
});
}
validate_message_type(version, msg_type)?;
let format = if version >= 0x02 {
let mut format_byte = [0u8; 1];
reader.read_exact(&mut format_byte).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::UnexpectedEof {
IpcError::ConnectionClosed
} else {
IpcError::IoError(e.to_string())
}
})?;
Format::from_byte(format_byte[0]).ok_or_else(|| {
IpcError::ProtocolError(format!(
"Unknown serialization format: {:#04x}",
format_byte[0]
))
})?
} else {
Format::Json
};
Ok((length, version, msg_type, format))
}
async fn write_header<W>(
writer: &mut W,
payload_length: u32,
msg_type: u8,
format: Format,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
write_header_with_version(writer, payload_length, msg_type, format, PROTOCOL_VERSION).await
}
async fn write_header_with_version<W>(
writer: &mut W,
payload_length: u32,
msg_type: u8,
format: Format,
version: u8,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
if version >= 0x02 {
let mut header = [0u8; HEADER_SIZE_V2];
header[..4].copy_from_slice(&payload_length.to_be_bytes());
header[4] = version;
header[5] = msg_type;
header[6] = format.to_byte();
writer
.write_all(&header)
.await
.map_err(|e| IpcError::IoError(e.to_string()))
} else {
let mut header = [0u8; HEADER_SIZE_V1];
header[..4].copy_from_slice(&payload_length.to_be_bytes());
header[4] = version;
header[5] = msg_type;
writer
.write_all(&header)
.await
.map_err(|e| IpcError::IoError(e.to_string()))
}
}
pub async fn read_frame<R>(
reader: &mut R,
max_size: usize,
) -> Result<(u8, Format, Vec<u8>), IpcError>
where
R: AsyncRead + Unpin,
{
let (length, _version, msg_type, format) = read_header(reader).await?;
let length_usize = length as usize;
if length_usize > max_size {
return Err(IpcError::ProtocolError(format!(
"Frame size {length_usize} exceeds maximum {max_size}"
)));
}
if length_usize > MAX_FRAME_SIZE {
return Err(IpcError::ProtocolError(format!(
"Frame size {length_usize} exceeds hard limit {MAX_FRAME_SIZE}"
)));
}
let mut payload = vec![0u8; length_usize];
reader.read_exact(&mut payload).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::UnexpectedEof {
IpcError::ConnectionClosed
} else {
IpcError::IoError(e.to_string())
}
})?;
Ok((msg_type, format, payload))
}
pub async fn write_frame<W>(
writer: &mut W,
msg_type: u8,
format: Format,
payload: &[u8],
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
let length: u32 = payload
.len()
.try_into()
.map_err(|_| IpcError::ProtocolError("Payload too large for u32".to_string()))?;
write_header(writer, length, msg_type, format).await?;
writer
.write_all(payload)
.await
.map_err(|e| IpcError::IoError(e.to_string()))?;
writer
.flush()
.await
.map_err(|e| IpcError::IoError(e.to_string()))?;
Ok(())
}
pub async fn read_envelope<R>(
reader: &mut R,
max_size: usize,
) -> Result<(IpcEnvelope, Format), IpcError>
where
R: AsyncRead + Unpin,
{
let (msg_type, format, payload) = read_frame(reader, max_size).await?;
if msg_type != MSG_TYPE_REQUEST {
return Err(IpcError::ProtocolError(format!(
"Expected request message type ({MSG_TYPE_REQUEST:#04x}), got {msg_type:#04x}"
)));
}
let envelope = format.deserialize(&payload)?;
Ok((envelope, format))
}
pub async fn write_envelope<W>(writer: &mut W, envelope: &IpcEnvelope) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
write_envelope_with_format(writer, envelope, Format::Json).await
}
pub async fn write_envelope_with_format<W>(
writer: &mut W,
envelope: &IpcEnvelope,
format: Format,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
let payload = format.serialize(envelope)?;
write_frame(writer, MSG_TYPE_REQUEST, format, &payload).await
}
pub async fn read_response<R>(reader: &mut R, max_size: usize) -> Result<IpcResponse, IpcError>
where
R: AsyncRead + Unpin,
{
let (msg_type, format, payload) = read_frame(reader, max_size).await?;
if !matches!(msg_type, MSG_TYPE_RESPONSE | MSG_TYPE_ERROR) {
return Err(IpcError::ProtocolError(format!(
"Expected response message type, got {msg_type:#04x}"
)));
}
format.deserialize(&payload)
}
pub async fn write_response<W>(writer: &mut W, response: &IpcResponse) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
write_response_with_format(writer, response, Format::Json).await
}
pub async fn write_response_with_format<W>(
writer: &mut W,
response: &IpcResponse,
format: Format,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
let msg_type = if response.success {
MSG_TYPE_RESPONSE
} else {
MSG_TYPE_ERROR
};
let payload = format.serialize(response)?;
write_frame(writer, msg_type, format, &payload).await
}
pub async fn write_heartbeat<W>(writer: &mut W) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
write_frame(writer, MSG_TYPE_HEARTBEAT, Format::Json, &[]).await
}
#[must_use]
pub const fn is_heartbeat(msg_type: u8) -> bool {
msg_type == MSG_TYPE_HEARTBEAT
}
#[must_use]
pub const fn is_subscribe(msg_type: u8) -> bool {
msg_type == MSG_TYPE_SUBSCRIBE
}
#[must_use]
pub const fn is_unsubscribe(msg_type: u8) -> bool {
msg_type == MSG_TYPE_UNSUBSCRIBE
}
pub async fn write_push<W>(
writer: &mut W,
push: &super::types::IpcPushNotification,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
write_push_with_format(writer, push, Format::Json).await
}
pub async fn write_push_with_format<W>(
writer: &mut W,
push: &super::types::IpcPushNotification,
format: Format,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
let payload = format.serialize(push)?;
write_frame(writer, MSG_TYPE_PUSH, format, &payload).await
}
#[allow(dead_code)]
pub async fn read_push<R>(
reader: &mut R,
max_size: usize,
) -> Result<super::types::IpcPushNotification, IpcError>
where
R: AsyncRead + Unpin,
{
let (msg_type, format, payload) = read_frame(reader, max_size).await?;
if msg_type != MSG_TYPE_PUSH {
return Err(IpcError::ProtocolError(format!(
"Expected push message type ({MSG_TYPE_PUSH:#04x}), got {msg_type:#04x}"
)));
}
format.deserialize(&payload)
}
#[allow(dead_code)]
pub async fn write_subscribe<W>(
writer: &mut W,
request: &super::types::IpcSubscribeRequest,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
write_subscribe_with_format(writer, request, Format::Json).await
}
#[allow(dead_code)]
pub async fn write_subscribe_with_format<W>(
writer: &mut W,
request: &super::types::IpcSubscribeRequest,
format: Format,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
let payload = format.serialize(request)?;
write_frame(writer, MSG_TYPE_SUBSCRIBE, format, &payload).await
}
#[allow(dead_code)]
pub async fn write_unsubscribe<W>(
writer: &mut W,
request: &super::types::IpcUnsubscribeRequest,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
write_unsubscribe_with_format(writer, request, Format::Json).await
}
#[allow(dead_code)]
pub async fn write_unsubscribe_with_format<W>(
writer: &mut W,
request: &super::types::IpcUnsubscribeRequest,
format: Format,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
let payload = format.serialize(request)?;
write_frame(writer, MSG_TYPE_UNSUBSCRIBE, format, &payload).await
}
pub async fn write_subscription_response<W>(
writer: &mut W,
response: &super::types::IpcSubscriptionResponse,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
write_subscription_response_with_format(writer, response, Format::Json).await
}
pub async fn write_subscription_response_with_format<W>(
writer: &mut W,
response: &super::types::IpcSubscriptionResponse,
format: Format,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
let msg_type = if response.success {
MSG_TYPE_RESPONSE
} else {
MSG_TYPE_ERROR
};
let payload = format.serialize(response)?;
write_frame(writer, msg_type, format, &payload).await
}
#[must_use]
pub const fn is_discover(msg_type: u8) -> bool {
msg_type == MSG_TYPE_DISCOVER
}
#[allow(dead_code)]
pub async fn write_discover<W>(
writer: &mut W,
request: &super::types::IpcDiscoverRequest,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
write_discover_with_format(writer, request, Format::Json).await
}
#[allow(dead_code)]
pub async fn write_discover_with_format<W>(
writer: &mut W,
request: &super::types::IpcDiscoverRequest,
format: Format,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
let payload = format.serialize(request)?;
write_frame(writer, MSG_TYPE_DISCOVER, format, &payload).await
}
pub async fn write_discovery_response<W>(
writer: &mut W,
response: &super::types::IpcDiscoverResponse,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
write_discovery_response_with_format(writer, response, Format::Json).await
}
pub async fn write_discovery_response_with_format<W>(
writer: &mut W,
response: &super::types::IpcDiscoverResponse,
format: Format,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
let msg_type = if response.success {
MSG_TYPE_RESPONSE
} else {
MSG_TYPE_ERROR
};
let payload = format.serialize(response)?;
write_frame(writer, msg_type, format, &payload).await
}
#[must_use]
pub const fn is_stream(msg_type: u8) -> bool {
msg_type == MSG_TYPE_STREAM
}
pub async fn write_stream_frame<W>(
writer: &mut W,
frame: &super::types::IpcStreamFrame,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
write_stream_frame_with_format(writer, frame, Format::Json).await
}
pub async fn write_stream_frame_with_format<W>(
writer: &mut W,
frame: &super::types::IpcStreamFrame,
format: Format,
) -> Result<(), IpcError>
where
W: AsyncWrite + Unpin,
{
let payload = format.serialize(frame)?;
write_frame(writer, MSG_TYPE_STREAM, format, &payload).await
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[tokio::test]
async fn test_write_read_frame() {
let mut buffer = Vec::new();
let payload = b"test payload";
write_frame(&mut buffer, MSG_TYPE_REQUEST, Format::Json, payload)
.await
.unwrap();
let mut reader = Cursor::new(buffer);
let (msg_type, format, read_payload) = read_frame(&mut reader, 1024).await.unwrap();
assert_eq!(msg_type, MSG_TYPE_REQUEST);
assert_eq!(format, Format::Json);
assert_eq!(read_payload, payload);
}
#[tokio::test]
async fn test_write_read_envelope() {
let mut buffer = Vec::new();
let envelope = IpcEnvelope::with_correlation_id(
"test_123",
"actor",
"TestMessage",
serde_json::json!({ "value": 42 }),
);
write_envelope(&mut buffer, &envelope).await.unwrap();
let mut reader = Cursor::new(buffer);
let (read_envelope, format) = read_envelope(&mut reader, 1024).await.unwrap();
assert_eq!(format, Format::Json);
assert_eq!(read_envelope.correlation_id, "test_123");
assert_eq!(read_envelope.target, "actor");
assert_eq!(read_envelope.message_type, "TestMessage");
}
#[tokio::test]
async fn test_write_read_response() {
let mut buffer = Vec::new();
let response =
IpcResponse::success("test_123", Some(serde_json::json!({ "result": "ok" })));
write_response(&mut buffer, &response).await.unwrap();
let mut reader = Cursor::new(buffer);
let read_response = read_response(&mut reader, 1024).await.unwrap();
assert!(read_response.success);
assert_eq!(read_response.correlation_id, "test_123");
}
#[tokio::test]
async fn test_unsupported_protocol_version() {
let mut buffer = Vec::new();
buffer.extend_from_slice(&4u32.to_be_bytes()); buffer.push(0xFF); buffer.push(MSG_TYPE_REQUEST);
buffer.push(Format::JSON_BYTE); buffer.extend_from_slice(b"test");
let mut reader = Cursor::new(buffer);
let result = read_frame(&mut reader, 1024).await;
assert!(matches!(
result,
Err(IpcError::UnsupportedProtocolVersion {
received: 0xFF,
min_supported: MIN_SUPPORTED_VERSION,
max_supported: MAX_SUPPORTED_VERSION,
})
));
}
#[test]
fn test_protocol_version_from_byte() {
assert_eq!(ProtocolVersion::from_byte(0x01), Some(ProtocolVersion::V1));
assert_eq!(ProtocolVersion::from_byte(0x02), Some(ProtocolVersion::V2));
assert_eq!(ProtocolVersion::from_byte(0x03), None);
assert_eq!(ProtocolVersion::from_byte(0xFF), None);
}
#[test]
fn test_protocol_version_is_supported() {
assert!(ProtocolVersion::is_supported(0x01));
assert!(ProtocolVersion::is_supported(0x02));
assert!(!ProtocolVersion::is_supported(0x00));
assert!(!ProtocolVersion::is_supported(0x03));
assert!(!ProtocolVersion::is_supported(0xFF));
}
#[test]
fn test_protocol_version_negotiate() {
assert_eq!(ProtocolVersion::negotiate(0x01, 0x02), Some(0x01));
assert_eq!(ProtocolVersion::negotiate(0x02, 0x01), Some(0x01));
assert_eq!(ProtocolVersion::negotiate(0x02, 0x02), Some(0x02));
assert_eq!(ProtocolVersion::negotiate(0x00, 0x02), None);
assert_eq!(ProtocolVersion::negotiate(0x01, 0x00), None);
}
#[test]
fn test_protocol_version_capabilities() {
let v1 = ProtocolVersion::V1;
assert!(!v1.supports_format_byte());
assert!(!v1.supports_messagepack());
assert!(!v1.supports_streaming());
assert!(!v1.supports_push());
assert!(!v1.supports_discovery());
assert_eq!(v1.header_size, 6);
let v2 = ProtocolVersion::V2;
assert!(v2.supports_format_byte());
assert!(v2.supports_messagepack());
assert!(v2.supports_streaming());
assert!(v2.supports_push());
assert!(v2.supports_discovery());
assert_eq!(v2.header_size, 7);
}
#[test]
fn test_protocol_capability_enum() {
use super::ProtocolCapability;
let v2 = ProtocolVersion::V2;
assert!(v2.supports(ProtocolCapability::FormatByte));
assert!(v2.supports(ProtocolCapability::MessagePack));
assert!(v2.supports(ProtocolCapability::Streaming));
assert!(v2.supports(ProtocolCapability::Push));
assert!(v2.supports(ProtocolCapability::Discovery));
let v1 = ProtocolVersion::V1;
assert!(!v1.supports(ProtocolCapability::FormatByte));
assert!(!v1.supports(ProtocolCapability::MessagePack));
assert!(!v1.supports(ProtocolCapability::Streaming));
assert!(!v1.supports(ProtocolCapability::Push));
assert!(!v1.supports(ProtocolCapability::Discovery));
}
#[test]
fn test_protocol_version_constants() {
assert_eq!(MIN_SUPPORTED_VERSION, 0x01);
assert_eq!(MAX_SUPPORTED_VERSION, 0x02);
assert_eq!(PROTOCOL_VERSION, 0x02);
assert_eq!(HEADER_SIZE_V1, 6);
assert_eq!(HEADER_SIZE_V2, 7);
assert_eq!(HEADER_SIZE, HEADER_SIZE_V2);
}
#[test]
fn test_protocol_version_display() {
let v1 = ProtocolVersion::V1;
let v2 = ProtocolVersion::V2;
assert!(v1.to_string().contains("v1"));
assert!(v2.to_string().contains("v2"));
}
#[tokio::test]
async fn test_invalid_message_type() {
let mut buffer = Vec::new();
buffer.extend_from_slice(&4u32.to_be_bytes()); buffer.push(PROTOCOL_VERSION);
buffer.push(0xFF); buffer.push(Format::JSON_BYTE); buffer.extend_from_slice(b"test");
let mut reader = Cursor::new(buffer);
let result = read_frame(&mut reader, 1024).await;
assert!(matches!(result, Err(IpcError::ProtocolError(_))));
}
#[tokio::test]
async fn test_invalid_format() {
let mut buffer = Vec::new();
buffer.extend_from_slice(&4u32.to_be_bytes()); buffer.push(PROTOCOL_VERSION);
buffer.push(MSG_TYPE_REQUEST);
buffer.push(0xFF); buffer.extend_from_slice(b"test");
let mut reader = Cursor::new(buffer);
let result = read_frame(&mut reader, 1024).await;
assert!(matches!(result, Err(IpcError::ProtocolError(_))));
}
#[tokio::test]
async fn test_frame_too_large() {
let mut buffer = Vec::new();
buffer.extend_from_slice(&10000u32.to_be_bytes()); buffer.push(PROTOCOL_VERSION);
buffer.push(MSG_TYPE_REQUEST);
buffer.push(Format::JSON_BYTE);
let mut reader = Cursor::new(buffer);
let result = read_frame(&mut reader, 100).await;
assert!(matches!(result, Err(IpcError::ProtocolError(_))));
}
#[tokio::test]
async fn test_heartbeat() {
let mut buffer = Vec::new();
write_heartbeat(&mut buffer).await.unwrap();
let mut reader = Cursor::new(buffer);
let (msg_type, _format, payload) = read_frame(&mut reader, 1024).await.unwrap();
assert!(is_heartbeat(msg_type));
assert!(payload.is_empty());
}
#[tokio::test]
async fn test_connection_closed_on_partial_read() {
let mut reader = Cursor::new(Vec::<u8>::new());
let result = read_frame(&mut reader, 1024).await;
assert!(matches!(result, Err(IpcError::ConnectionClosed)));
}
#[test]
fn test_header_constants() {
assert_eq!(HEADER_SIZE, 7);
assert_eq!(PROTOCOL_VERSION, 0x02);
assert_eq!(MSG_TYPE_REQUEST, 0x01);
assert_eq!(MSG_TYPE_RESPONSE, 0x02);
assert_eq!(MSG_TYPE_ERROR, 0x03);
assert_eq!(MSG_TYPE_HEARTBEAT, 0x04);
}
#[test]
fn test_format_roundtrip() {
assert_eq!(
Format::from_byte(Format::Json.to_byte()),
Some(Format::Json)
);
#[cfg(feature = "ipc-messagepack")]
assert_eq!(
Format::from_byte(Format::MessagePack.to_byte()),
Some(Format::MessagePack)
);
assert_eq!(Format::from_byte(0xFF), None);
}
#[test]
fn test_subscription_message_type_helpers() {
assert!(is_subscribe(MSG_TYPE_SUBSCRIBE));
assert!(!is_subscribe(MSG_TYPE_REQUEST));
assert!(!is_subscribe(MSG_TYPE_UNSUBSCRIBE));
assert!(is_unsubscribe(MSG_TYPE_UNSUBSCRIBE));
assert!(!is_unsubscribe(MSG_TYPE_REQUEST));
assert!(!is_unsubscribe(MSG_TYPE_SUBSCRIBE));
}
#[tokio::test]
async fn test_write_read_push_notification() {
use super::super::types::IpcPushNotification;
let mut buffer = Vec::new();
let notification = IpcPushNotification::new(
"PriceUpdate",
Some("price_service".to_string()),
serde_json::json!({ "price": 100.50 }),
);
write_push(&mut buffer, ¬ification).await.unwrap();
let mut reader = Cursor::new(buffer);
let read_notification = read_push(&mut reader, 1024).await.unwrap();
assert_eq!(read_notification.message_type, "PriceUpdate");
assert_eq!(
read_notification.source_actor,
Some("price_service".to_string())
);
assert_eq!(read_notification.payload["price"], 100.50);
}
#[tokio::test]
async fn test_write_subscribe_request() {
use super::super::types::IpcSubscribeRequest;
let mut buffer = Vec::new();
let request =
IpcSubscribeRequest::new(vec!["PriceUpdate".to_string(), "OrderStatus".to_string()]);
write_subscribe(&mut buffer, &request).await.unwrap();
let mut reader = Cursor::new(buffer);
let (msg_type, format, payload) = read_frame(&mut reader, 1024).await.unwrap();
assert!(is_subscribe(msg_type));
assert_eq!(format, Format::Json);
let parsed: IpcSubscribeRequest = serde_json::from_slice(&payload).unwrap();
assert_eq!(parsed.message_types.len(), 2);
assert!(parsed.message_types.contains(&"PriceUpdate".to_string()));
}
#[tokio::test]
async fn test_write_unsubscribe_request() {
use super::super::types::IpcUnsubscribeRequest;
let mut buffer = Vec::new();
let request = IpcUnsubscribeRequest::new(vec!["PriceUpdate".to_string()]);
write_unsubscribe(&mut buffer, &request).await.unwrap();
let mut reader = Cursor::new(buffer);
let (msg_type, format, payload) = read_frame(&mut reader, 1024).await.unwrap();
assert!(is_unsubscribe(msg_type));
assert_eq!(format, Format::Json);
let parsed: IpcUnsubscribeRequest = serde_json::from_slice(&payload).unwrap();
assert_eq!(parsed.message_types.len(), 1);
assert_eq!(parsed.message_types[0], "PriceUpdate");
}
#[tokio::test]
async fn test_unsubscribe_all() {
use super::super::types::IpcUnsubscribeRequest;
let mut buffer = Vec::new();
let request = IpcUnsubscribeRequest::unsubscribe_all();
write_unsubscribe(&mut buffer, &request).await.unwrap();
let mut reader = Cursor::new(buffer);
let (msg_type, format, payload) = read_frame(&mut reader, 1024).await.unwrap();
assert!(is_unsubscribe(msg_type));
assert_eq!(format, Format::Json);
let parsed: IpcUnsubscribeRequest = serde_json::from_slice(&payload).unwrap();
assert!(parsed.message_types.is_empty());
}
}