Skip to main content

nasaga_core/
payload.rs

1//! 业务正文的原始字节与不可变 schema 合同。
2
3use serde::{Deserialize, Serialize};
4
5/// 业务作用:把媒体类型与 schema 身份绑定到步骤定义,禁止端点轮换改变正文解释规则。
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(deny_unknown_fields)]
8pub struct SagaPayloadContract {
9    /// 小写规范媒体类型,不包含传输参数。
10    pub content_type: String,
11    /// 业务 schema 的稳定身份;无 schema 的 JSON 使用空字符串。
12    pub schema_id: String,
13}
14
15impl Default for SagaPayloadContract {
16    /// 业务作用:保留无 schema JSON 步骤的既有业务合同。
17    ///
18    /// 参数说明: 无。
19    ///
20    /// 返回:application/json 与空 schema 身份。
21    fn default() -> Self {
22        Self {
23            content_type: "application/json".to_owned(),
24            schema_id: String::new(),
25        }
26    }
27}
28
29impl SagaPayloadContract {
30    /// 业务作用:验证媒体类型与 schema 的规范形式,避免签名或摘要接受等价别名。
31    ///
32    /// 参数说明: 无。
33    ///
34    /// 返回:字段规范且二进制正文具有 schema 身份时成功,否则返回稳定拒绝。
35    pub fn validate(&self) -> Result<(), SagaPayloadError> {
36        let Some((kind, subtype)) = self.content_type.split_once('/') else {
37            return Err(SagaPayloadError);
38        };
39        if self.content_type.len() > 128
40            || kind.is_empty()
41            || subtype.is_empty()
42            || !kind
43                .bytes()
44                .chain(subtype.bytes())
45                .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b"!#$&^_.+-".contains(&b))
46            || self.schema_id.len() > 256
47            || self.schema_id.trim() != self.schema_id
48            || self.schema_id.chars().any(char::is_control)
49            || (self.content_type != "application/json" && self.schema_id.is_empty())
50        {
51            return Err(SagaPayloadError);
52        }
53        Ok(())
54    }
55}
56
57/// 业务作用:在 API、Outbox 与参与方之间保留正文的精确字节和 schema 身份。
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(deny_unknown_fields)]
60pub struct SagaPayload {
61    /// 正文的规范媒体类型。
62    pub content_type: String,
63    /// 与冻结步骤一致的 schema 身份。
64    pub schema_id: String,
65    /// 原始业务字节;JSON 空白、整数文本和对象字段顺序均保持不变。
66    pub body: Vec<u8>,
67}
68
69impl SagaPayload {
70    /// 业务作用:构造可进入摘要和持久消息的原始正文,不重新编码业务内容。
71    ///
72    /// 参数说明:content_type 与 schema_id 声明解释合同,body 是原始字节。
73    ///
74    /// 返回:合同合法时返回正文;非法媒体类型、schema 或 JSON 返回错误。
75    pub fn new(
76        content_type: impl Into<String>,
77        schema_id: impl Into<String>,
78        body: Vec<u8>,
79    ) -> Result<Self, SagaPayloadError> {
80        let payload = Self {
81            content_type: content_type.into(),
82            schema_id: schema_id.into(),
83            body,
84        };
85        payload.validate()?;
86        Ok(payload)
87    }
88
89    /// 业务作用:在进入摘要、业务解码或持久消息前复验公开正文结构。
90    ///
91    /// 参数说明: 无。
92    ///
93    /// 返回:媒体与 schema 合法、JSON 可解析时成功;不改写原始正文。
94    pub fn validate(&self) -> Result<(), SagaPayloadError> {
95        self.contract().validate()?;
96        if self.content_type == "application/json" {
97            serde_json::from_slice::<serde_json::Value>(&self.body)
98                .map_err(|_| SagaPayloadError)?;
99        }
100        Ok(())
101    }
102
103    /// 业务作用:提取定义与 capability 必须共同确认的正文解释合同。
104    ///
105    /// 参数说明: 无。
106    ///
107    /// 返回:不包含业务字节的媒体类型和 schema 身份。
108    pub fn contract(&self) -> SagaPayloadContract {
109        SagaPayloadContract {
110            content_type: self.content_type.clone(),
111            schema_id: self.schema_id.clone(),
112        }
113    }
114
115    /// 业务作用:把已有 JSON 值转换为无 schema 的规范 JSON 字节正文。
116    ///
117    /// 参数说明:value 是调用方已经解析的 JSON 值。
118    ///
119    /// 返回:无损整数序列化的 JSON 正文;需要保留输入文本时使用 new。
120    pub fn json(value: serde_json::Value) -> Self {
121        Self {
122            content_type: "application/json".to_owned(),
123            schema_id: String::new(),
124            body: serde_json::to_vec(&value).expect("JSON value serialization"),
125        }
126    }
127}
128
129/// 正文解释合同或编码不合法的稳定拒绝,不包含业务正文。
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct SagaPayloadError;
132
133impl std::fmt::Display for SagaPayloadError {
134    /// 业务作用:输出可安全返回给调用者的正文合同拒绝。
135    ///
136    /// 参数说明:formatter 是文本输出目标。
137    ///
138    /// 返回:写入固定错误文本,不包含正文或 schema 内容。
139    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        formatter.write_str("Saga payload contract is invalid")
141    }
142}
143
144impl std::error::Error for SagaPayloadError {}