use bytes::Bytes;
use serde::{Deserialize, Serialize};
use crate::error::{MessageReject, Result, RiftError};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Command {
pub command: String,
pub correlation_id: String,
pub timeout_ms: u32,
pub idempotency_key: Option<String>,
pub payload: serde_json::Value,
pub schema: String,
}
impl Command {
pub fn new(
command: impl Into<String>,
correlation_id: impl Into<String>,
timeout_ms: u32,
schema: impl Into<String>,
payload: serde_json::Value,
) -> Self {
Self {
command: command.into(),
correlation_id: correlation_id.into(),
timeout_ms,
idempotency_key: None,
payload,
schema: schema.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Reply {
pub correlation_id: String,
pub status: ReplyStatus,
pub payload: Option<serde_json::Value>,
pub error: Option<ReplyError>,
pub server_time: i64,
}
impl Reply {
pub fn ok(correlation_id: impl Into<String>, payload: serde_json::Value) -> Self {
Self {
correlation_id: correlation_id.into(),
status: ReplyStatus::Ok,
payload: Some(payload),
error: None,
server_time: 0,
}
}
pub fn error(
correlation_id: impl Into<String>,
code: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
correlation_id: correlation_id.into(),
status: ReplyStatus::Error,
payload: None,
error: Some(ReplyError {
code: code.into(),
message: message.into(),
}),
server_time: 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReplyStatus {
Ok,
Error,
Timeout,
Rejected,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplyError {
pub code: String,
pub message: String,
}
pub fn encode_command(c: &Command) -> Result<Bytes> {
if c.correlation_id.is_empty() {
return Err(RiftError::Message(MessageReject::Rejected(
"command requires correlation_id".into(),
)));
}
Ok(Bytes::from(serde_json::to_vec(c)?))
}
pub fn decode_command(bytes: &[u8]) -> Result<Command> {
let c: Command = serde_json::from_slice(bytes)?;
if c.correlation_id.is_empty() {
return Err(RiftError::Message(MessageReject::Rejected(
"command requires correlation_id".into(),
)));
}
Ok(c)
}
pub fn encode_reply(r: &Reply) -> Result<Bytes> {
Ok(Bytes::from(serde_json::to_vec(r)?))
}
pub fn decode_reply(bytes: &[u8]) -> Result<Reply> {
Ok(serde_json::from_slice(bytes)?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn command_round_trip() {
let c = Command::new(
"room.create",
"corr-1",
5000,
"room.create@1.0",
serde_json::json!({"name": "general"}),
);
let bytes = encode_command(&c).unwrap();
let back = decode_command(&bytes).unwrap();
assert_eq!(back.correlation_id, "corr-1");
}
#[test]
fn command_requires_correlation_id() {
let c = Command {
command: "x".into(),
correlation_id: "".into(),
timeout_ms: 100,
idempotency_key: None,
payload: serde_json::Value::Null,
schema: "x@1.0".into(),
};
assert!(encode_command(&c).is_err());
}
#[test]
fn reply_ok_and_error() {
let ok = Reply::ok("c1", serde_json::json!({"id": 7}));
assert_eq!(ok.status, ReplyStatus::Ok);
let err = Reply::error("c1", "RIFT_AUTH_INVALID", "bad token");
assert_eq!(err.status, ReplyStatus::Error);
}
}