Files
rustybeds/src/brokers/payload.rs

126 lines
4.0 KiB
Rust

//! # brokers/payload.rs — AMQP Envelope Contracts
//!
//! Strict 1.0 request/response contracts for broker message bodies.
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const ENVELOPE_VERSION: &str = "1.0";
#[derive(Debug, Deserialize, Serialize)]
pub struct BrokerRequestEnvelope {
pub version: String,
pub op: String,
pub template: String,
#[serde(default)]
pub correlation_id: String,
#[serde(default)]
pub payload: HashMap<String, Value>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct BrokerResponseEnvelope {
pub version: String,
pub op: String,
pub correlation_id: String,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
pub payload: Value,
}
pub fn parse_request(bytes: &[u8]) -> Result<BrokerRequestEnvelope, String> {
let env: BrokerRequestEnvelope =
serde_json::from_slice(bytes).map_err(|e| format!("invalid envelope JSON: {}", e))?;
if env.version != ENVELOPE_VERSION {
return Err(format!(
"unsupported envelope version '{}', expected '{}'",
env.version, ENVELOPE_VERSION
));
}
if env.op.trim().is_empty() {
return Err("missing required field 'op'".to_string());
}
if env.template.trim().is_empty() {
return Err("missing required field 'template'".to_string());
}
Ok(env)
}
pub fn success_response(op: &str, correlation_id: &str, payload: Value) -> Vec<u8> {
let response = BrokerResponseEnvelope {
version: ENVELOPE_VERSION.to_string(),
op: op.to_string(),
correlation_id: correlation_id.to_string(),
status: "ok".to_string(),
error_code: None,
message: None,
payload,
};
serde_json::to_vec(&response).unwrap_or_else(|_| b"{}".to_vec())
}
pub fn error_response(op: &str, correlation_id: &str, code: &str, message: &str) -> Vec<u8> {
let response = BrokerResponseEnvelope {
version: ENVELOPE_VERSION.to_string(),
op: op.to_string(),
correlation_id: correlation_id.to_string(),
status: "error".to_string(),
error_code: Some(code.to_string()),
message: Some(message.to_string()),
payload: Value::Object(serde_json::Map::new()),
};
serde_json::to_vec(&response).unwrap_or_else(|_| b"{}".to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_valid_request_envelope() {
let json = r#"{"version":"1.0","op":"fetch","template":"Logger","correlation_id":"c1","payload":{"limit":10}}"#;
let env = parse_request(json.as_bytes()).expect("parse should succeed");
assert_eq!(env.version, "1.0");
assert_eq!(env.op, "fetch");
assert_eq!(env.template, "Logger");
assert_eq!(env.correlation_id, "c1");
assert_eq!(env.payload["limit"], 10);
}
#[test]
fn rejects_legacy_unversioned_shape() {
let json = r#"{"template":"Logger","data":{"limit":10}}"#;
let err = parse_request(json.as_bytes()).expect_err("parse should fail");
assert!(err.contains("invalid envelope JSON") || err.contains("missing required"));
}
#[test]
fn rejects_wrong_version() {
let json = r#"{"version":"0.9","op":"fetch","template":"Logger","payload":{}}"#;
let err = parse_request(json.as_bytes()).expect_err("parse should fail");
assert!(err.contains("unsupported envelope version"));
}
#[test]
fn builds_error_response_envelope() {
let bytes = error_response("write", "c123", "INVALID_PAYLOAD", "bad");
let env: BrokerResponseEnvelope = serde_json::from_slice(&bytes).expect("response should parse");
assert_eq!(env.version, ENVELOPE_VERSION);
assert_eq!(env.op, "write");
assert_eq!(env.correlation_id, "c123");
assert_eq!(env.status, "error");
assert_eq!(env.error_code.as_deref(), Some("INVALID_PAYLOAD"));
}
}