use serde::{Deserialize, Serialize};
use std::ops::Range;
pub const PROTOCOL_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum MessageFormat {
Json,
#[default]
MessagePack,
Binary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum Compression {
None,
#[default]
Zstd,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Message {
Handshake {
version: u32,
format: MessageFormat,
compression: Compression,
},
HandshakeAck {
version: u32,
format: MessageFormat,
compression: Compression,
},
SubscribeTiles {
subscription_id: String,
bbox: [f64; 4],
zoom_range: Range<u8>,
tile_size: Option<u32>,
},
SubscribeFeatures {
subscription_id: String,
bbox: Option<[f64; 4]>,
filters: Option<Vec<(String, String)>>,
layer: Option<String>,
},
SubscribeEvents {
subscription_id: String,
event_types: Vec<EventType>,
},
Unsubscribe {
subscription_id: String,
},
TileData {
subscription_id: String,
tile: (u32, u32, u8),
data: Vec<u8>,
mime_type: String,
},
FeatureData {
subscription_id: String,
geojson: String,
change_type: ChangeType,
},
Event {
subscription_id: String,
event_type: EventType,
payload: serde_json::Value,
timestamp: String,
},
Error {
code: String,
message: String,
request_id: Option<String>,
},
Ping {
id: u64,
},
Pong {
id: u64,
},
Ack {
request_id: String,
success: bool,
message: Option<String>,
},
}
impl<'de> serde::Deserialize<'de> for Message {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error as _;
if deserializer.is_human_readable() {
let value = serde_json::Value::deserialize(deserializer).map_err(D::Error::custom)?;
let type_str = value
.get("type")
.and_then(|t| t.as_str())
.ok_or_else(|| D::Error::custom("missing 'type' field in Message"))?;
match type_str {
"handshake" => {
#[derive(serde::Deserialize)]
struct HandshakeData {
version: u32,
format: MessageFormat,
compression: Compression,
}
let d: HandshakeData =
serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(Message::Handshake {
version: d.version,
format: d.format,
compression: d.compression,
})
}
"handshake_ack" => {
#[derive(serde::Deserialize)]
struct HandshakeAckData {
version: u32,
format: MessageFormat,
compression: Compression,
}
let d: HandshakeAckData =
serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(Message::HandshakeAck {
version: d.version,
format: d.format,
compression: d.compression,
})
}
"subscribe_tiles" => {
#[derive(serde::Deserialize)]
struct SubscribeTilesData {
subscription_id: String,
bbox: [f64; 4],
zoom_range: Range<u8>,
tile_size: Option<u32>,
}
let d: SubscribeTilesData =
serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(Message::SubscribeTiles {
subscription_id: d.subscription_id,
bbox: d.bbox,
zoom_range: d.zoom_range,
tile_size: d.tile_size,
})
}
"subscribe_features" => {
#[derive(serde::Deserialize)]
struct SubscribeFeaturesData {
subscription_id: String,
bbox: Option<[f64; 4]>,
filters: Option<Vec<(String, String)>>,
layer: Option<String>,
}
let d: SubscribeFeaturesData =
serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(Message::SubscribeFeatures {
subscription_id: d.subscription_id,
bbox: d.bbox,
filters: d.filters,
layer: d.layer,
})
}
"subscribe_events" => {
#[derive(serde::Deserialize)]
struct SubscribeEventsData {
subscription_id: String,
event_types: Vec<EventType>,
}
let d: SubscribeEventsData =
serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(Message::SubscribeEvents {
subscription_id: d.subscription_id,
event_types: d.event_types,
})
}
"unsubscribe" => {
#[derive(serde::Deserialize)]
struct UnsubscribeData {
subscription_id: String,
}
let d: UnsubscribeData =
serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(Message::Unsubscribe {
subscription_id: d.subscription_id,
})
}
"tile_data" => {
#[derive(serde::Deserialize)]
struct TileDataData {
subscription_id: String,
tile: (u32, u32, u8),
data: Vec<u8>,
mime_type: String,
}
let d: TileDataData =
serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(Message::TileData {
subscription_id: d.subscription_id,
tile: d.tile,
data: d.data,
mime_type: d.mime_type,
})
}
"feature_data" => {
#[derive(serde::Deserialize)]
struct FeatureDataData {
subscription_id: String,
geojson: String,
change_type: ChangeType,
}
let d: FeatureDataData =
serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(Message::FeatureData {
subscription_id: d.subscription_id,
geojson: d.geojson,
change_type: d.change_type,
})
}
"event" => {
#[derive(serde::Deserialize)]
struct EventData {
subscription_id: String,
event_type: EventType,
payload: serde_json::Value,
timestamp: String,
}
let d: EventData = serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(Message::Event {
subscription_id: d.subscription_id,
event_type: d.event_type,
payload: d.payload,
timestamp: d.timestamp,
})
}
"error" => {
#[derive(serde::Deserialize)]
struct ErrorData {
code: String,
message: String,
request_id: Option<String>,
}
let d: ErrorData = serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(Message::Error {
code: d.code,
message: d.message,
request_id: d.request_id,
})
}
"ping" => {
#[derive(serde::Deserialize)]
struct PingData {
id: u64,
}
let d: PingData = serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(Message::Ping { id: d.id })
}
"pong" => {
#[derive(serde::Deserialize)]
struct PongData {
id: u64,
}
let d: PongData = serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(Message::Pong { id: d.id })
}
"ack" => {
#[derive(serde::Deserialize)]
struct AckData {
request_id: String,
success: bool,
message: Option<String>,
}
let d: AckData = serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(Message::Ack {
request_id: d.request_id,
success: d.success,
message: d.message,
})
}
other => Err(D::Error::custom(format!("unknown Message type: {other}"))),
}
} else {
#[derive(serde::Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum MessageInner {
Handshake {
version: u32,
format: MessageFormat,
compression: Compression,
},
HandshakeAck {
version: u32,
format: MessageFormat,
compression: Compression,
},
SubscribeTiles {
subscription_id: String,
bbox: [f64; 4],
zoom_range: Range<u8>,
tile_size: Option<u32>,
},
SubscribeFeatures {
subscription_id: String,
bbox: Option<[f64; 4]>,
filters: Option<Vec<(String, String)>>,
layer: Option<String>,
},
SubscribeEvents {
subscription_id: String,
event_types: Vec<EventType>,
},
Unsubscribe {
subscription_id: String,
},
TileData {
subscription_id: String,
tile: (u32, u32, u8),
data: Vec<u8>,
mime_type: String,
},
FeatureData {
subscription_id: String,
geojson: String,
change_type: ChangeType,
},
Event {
subscription_id: String,
event_type: EventType,
payload: serde_json::Value,
timestamp: String,
},
Error {
code: String,
message: String,
request_id: Option<String>,
},
Ping {
id: u64,
},
Pong {
id: u64,
},
Ack {
request_id: String,
success: bool,
message: Option<String>,
},
}
let inner = MessageInner::deserialize(deserializer)?;
Ok(match inner {
MessageInner::Handshake {
version,
format,
compression,
} => Message::Handshake {
version,
format,
compression,
},
MessageInner::HandshakeAck {
version,
format,
compression,
} => Message::HandshakeAck {
version,
format,
compression,
},
MessageInner::SubscribeTiles {
subscription_id,
bbox,
zoom_range,
tile_size,
} => Message::SubscribeTiles {
subscription_id,
bbox,
zoom_range,
tile_size,
},
MessageInner::SubscribeFeatures {
subscription_id,
bbox,
filters,
layer,
} => Message::SubscribeFeatures {
subscription_id,
bbox,
filters,
layer,
},
MessageInner::SubscribeEvents {
subscription_id,
event_types,
} => Message::SubscribeEvents {
subscription_id,
event_types,
},
MessageInner::Unsubscribe { subscription_id } => {
Message::Unsubscribe { subscription_id }
}
MessageInner::TileData {
subscription_id,
tile,
data,
mime_type,
} => Message::TileData {
subscription_id,
tile,
data,
mime_type,
},
MessageInner::FeatureData {
subscription_id,
geojson,
change_type,
} => Message::FeatureData {
subscription_id,
geojson,
change_type,
},
MessageInner::Event {
subscription_id,
event_type,
payload,
timestamp,
} => Message::Event {
subscription_id,
event_type,
payload,
timestamp,
},
MessageInner::Error {
code,
message,
request_id,
} => Message::Error {
code,
message,
request_id,
},
MessageInner::Ping { id } => Message::Ping { id },
MessageInner::Pong { id } => Message::Pong { id },
MessageInner::Ack {
request_id,
success,
message,
} => Message::Ack {
request_id,
success,
message,
},
})
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ChangeType {
Added,
Updated,
Deleted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventType {
FileChange,
ProcessingStatus,
Error,
Progress,
Custom,
}
impl Message {
pub fn to_json(&self) -> crate::error::Result<String> {
serde_json::to_string(self).map_err(Into::into)
}
pub fn from_json(s: &str) -> crate::error::Result<Self> {
serde_json::from_str(s).map_err(Into::into)
}
pub fn to_msgpack(&self) -> crate::error::Result<Vec<u8>> {
rmp_serde::to_vec(self).map_err(Into::into)
}
pub fn from_msgpack(data: &[u8]) -> crate::error::Result<Self> {
rmp_serde::from_slice(data).map_err(Into::into)
}
pub fn compress(data: &[u8], level: i32) -> crate::error::Result<Vec<u8>> {
oxiarc_zstd::encode_all(data, level)
.map_err(|e| crate::error::Error::Compression(e.to_string()))
}
pub fn decompress(data: &[u8]) -> crate::error::Result<Vec<u8>> {
oxiarc_zstd::decode_all(data).map_err(|e| crate::error::Error::Decompression(e.to_string()))
}
pub fn encode(
&self,
format: MessageFormat,
compression: Compression,
) -> crate::error::Result<Vec<u8>> {
let data = match format {
MessageFormat::Json => self.to_json()?.into_bytes(),
MessageFormat::MessagePack | MessageFormat::Binary => self.to_msgpack()?,
};
match compression {
Compression::None => Ok(data),
Compression::Zstd => Self::compress(&data, 3),
}
}
pub fn decode(
data: &[u8],
format: MessageFormat,
compression: Compression,
) -> crate::error::Result<Self> {
let decompressed = match compression {
Compression::None => data.to_vec(),
Compression::Zstd => Self::decompress(data)?,
};
match format {
MessageFormat::Json => {
let s = String::from_utf8(decompressed)
.map_err(|e| crate::error::Error::Deserialization(e.to_string()))?;
Self::from_json(&s)
}
MessageFormat::MessagePack | MessageFormat::Binary => Self::from_msgpack(&decompressed),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpatialFilter {
pub bbox: [f64; 4],
pub crs: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemporalFilter {
pub start: Option<String>,
pub end: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubscriptionFilter {
pub spatial: Option<SpatialFilter>,
pub temporal: Option<TemporalFilter>,
pub attributes: Option<Vec<(String, String)>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_json_roundtrip() {
let msg = Message::Ping { id: 42 };
let json_str = msg.to_json().expect("Failed to serialize message to JSON");
let decoded =
Message::from_json(&json_str).expect("Failed to deserialize message from JSON");
assert!(matches!(decoded, Message::Ping { id: 42 }));
}
#[test]
fn test_message_msgpack_roundtrip() {
let msg = Message::Ping { id: 42 };
let msgpack_bytes = msg
.to_msgpack()
.expect("Failed to serialize message to MessagePack");
let decoded = Message::from_msgpack(&msgpack_bytes)
.expect("Failed to deserialize message from MessagePack");
assert!(matches!(decoded, Message::Ping { id: 42 }));
}
#[test]
fn test_compression_roundtrip() {
let data = b"Hello, WebSocket!";
let compressed = Message::compress(data, 3).expect("Failed to compress data");
let decompressed = Message::decompress(&compressed).expect("Failed to decompress data");
assert_eq!(data, decompressed.as_slice());
}
#[test]
fn test_message_encode_decode() {
let msg = Message::SubscribeTiles {
subscription_id: "test-123".to_string(),
bbox: [-180.0, -90.0, 180.0, 90.0],
zoom_range: 0..14,
tile_size: Some(256),
};
let encoded = msg
.encode(MessageFormat::Json, Compression::None)
.expect("Failed to encode message as JSON");
let decoded = Message::decode(&encoded, MessageFormat::Json, Compression::None)
.expect("Failed to decode message from JSON");
assert!(
matches!(
decoded,
Message::SubscribeTiles {
subscription_id,
bbox,
zoom_range,
tile_size,
} if subscription_id == "test-123"
&& bbox == [-180.0, -90.0, 180.0, 90.0]
&& zoom_range == (0..14)
&& tile_size == Some(256)
),
"Decoded message does not match expected values"
);
let encoded = msg
.encode(MessageFormat::MessagePack, Compression::Zstd)
.expect("Failed to encode message as MessagePack with Zstd");
let decoded = Message::decode(&encoded, MessageFormat::MessagePack, Compression::Zstd)
.expect("Failed to decode message from MessagePack with Zstd");
assert!(
matches!(
decoded,
Message::SubscribeTiles {
subscription_id,
bbox,
zoom_range,
tile_size,
} if subscription_id == "test-123"
&& bbox == [-180.0, -90.0, 180.0, 90.0]
&& zoom_range == (0..14)
&& tile_size == Some(256)
),
"Decoded message does not match expected values"
);
}
}