use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct WsMessage {
pub route: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub payload: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(rename = "refId", skip_serializing_if = "Option::is_none")]
pub ref_id: Option<String>,
}
impl WsMessage {
pub fn new(route: impl Into<String>, payload: serde_json::Value) -> Self {
Self {
route: route.into(),
payload: Some(payload),
id: Some(generate_short_id()),
ref_id: None,
}
}
pub fn route_only(route: impl Into<String>) -> Self {
Self {
route: route.into(),
payload: None,
id: Some(generate_short_id()),
ref_id: None,
}
}
pub fn sign_in(token: &str) -> Self {
Self::new(
"@wfm|cmd/auth/signIn",
serde_json::json!({
"token": token,
}),
)
}
pub fn sign_out() -> Self {
Self::route_only("@wfm|cmd/auth/signOut")
}
pub fn set_status(
status: &str,
duration: Option<u64>,
activity: Option<serde_json::Value>,
) -> Self {
let mut payload = serde_json::json!({
"status": status,
});
if let Some(d) = duration {
payload["duration"] = serde_json::json!(d);
}
if let Some(a) = activity {
payload["activity"] = a;
}
Self::new("@wfm|cmd/status/set", payload)
}
}
fn generate_short_id() -> String {
use uuid::Uuid;
let uuid = Uuid::new_v4();
let bytes = uuid.as_bytes();
let chars: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
.chars()
.collect();
bytes
.iter()
.take(11)
.map(|b| chars[(*b as usize) % chars.len()])
.collect()
}
#[derive(Debug, Clone)]
pub(crate) struct ParsedRoute {
pub module: String,
pub msg_type: String,
pub path: String,
pub parameter: Option<String>,
}
impl ParsedRoute {
pub fn parse(route: &str) -> Option<Self> {
if !route.starts_with('@') {
return None;
}
let (module, rest) = route.split_once('|')?;
let (msg_type, path_with_param) = rest.split_once('/')?;
let (path, parameter) = if let Some(colon_pos) = path_with_param.rfind(':') {
let potential_param = &path_with_param[colon_pos + 1..];
if potential_param.len() <= 10 && !potential_param.contains('/') {
(
path_with_param[..colon_pos].to_string(),
Some(potential_param.to_string()),
)
} else {
(path_with_param.to_string(), None)
}
} else {
(path_with_param.to_string(), None)
};
Some(Self {
module: module.to_string(),
msg_type: msg_type.to_string(),
path,
parameter,
})
}
#[allow(dead_code)]
pub fn is_response(&self) -> bool {
self.parameter.is_some()
}
#[allow(dead_code)]
pub fn is_ok(&self) -> bool {
self.parameter.as_deref() == Some("ok")
}
#[allow(dead_code)]
pub fn is_error(&self) -> bool {
self.parameter.as_deref() == Some("error")
}
#[allow(dead_code)]
pub fn full_route(&self) -> String {
format!("{}|{}/{}", self.module, self.msg_type, self.path)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_route_cmd() {
let route = ParsedRoute::parse("@wfm|cmd/auth/signIn").unwrap();
assert_eq!(route.module, "@wfm");
assert_eq!(route.msg_type, "cmd");
assert_eq!(route.path, "auth/signIn");
assert!(route.parameter.is_none());
}
#[test]
fn test_parse_route_cmd_with_ok() {
let route = ParsedRoute::parse("@wfm|cmd/auth/signIn:ok").unwrap();
assert_eq!(route.module, "@wfm");
assert_eq!(route.msg_type, "cmd");
assert_eq!(route.path, "auth/signIn");
assert_eq!(route.parameter, Some("ok".to_string()));
assert!(route.is_ok());
}
#[test]
fn test_parse_route_event() {
let route = ParsedRoute::parse("@wfm|event/reports/online").unwrap();
assert_eq!(route.module, "@wfm");
assert_eq!(route.msg_type, "event");
assert_eq!(route.path, "reports/online");
assert!(route.parameter.is_none());
}
#[test]
fn test_parse_route_subscription_event() {
let route = ParsedRoute::parse("@wfm|event/subscriptions/newOrder").unwrap();
assert_eq!(route.module, "@wfm");
assert_eq!(route.msg_type, "event");
assert_eq!(route.path, "subscriptions/newOrder");
}
#[test]
fn test_parse_route_subscribe_ok() {
let route = ParsedRoute::parse("@wfm|cmd/subscribe/newOrders:ok").unwrap();
assert_eq!(route.module, "@wfm");
assert_eq!(route.msg_type, "cmd");
assert_eq!(route.path, "subscribe/newOrders");
assert_eq!(route.parameter, Some("ok".to_string()));
}
#[test]
fn test_parse_route_status_event() {
let route = ParsedRoute::parse("@wfm|event/status/set").unwrap();
assert_eq!(route.module, "@wfm");
assert_eq!(route.msg_type, "event");
assert_eq!(route.path, "status/set");
}
#[test]
fn test_message_sign_in() {
let msg = WsMessage::sign_in("test-token");
assert_eq!(msg.route, "@wfm|cmd/auth/signIn");
assert!(msg.payload.is_some());
assert!(msg.id.is_some());
let payload = msg.payload.unwrap();
assert_eq!(payload["token"], "test-token");
}
#[test]
fn test_message_sign_out() {
let msg = WsMessage::sign_out();
assert_eq!(msg.route, "@wfm|cmd/auth/signOut");
}
#[test]
fn test_message_set_status() {
let msg = WsMessage::set_status("online", Some(3600), None);
assert_eq!(msg.route, "@wfm|cmd/status/set");
let payload = msg.payload.unwrap();
assert_eq!(payload["status"], "online");
assert_eq!(payload["duration"], 3600);
}
#[test]
fn test_short_id_generation() {
let id1 = generate_short_id();
let id2 = generate_short_id();
assert_eq!(id1.len(), 11);
assert_eq!(id2.len(), 11);
assert_ne!(id1, id2); }
}