//! # 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, } #[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, #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, pub payload: Value, } pub fn parse_request(bytes: &[u8]) -> Result { 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 { 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 { 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")); } }