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    /// 参数说明:
73    /// - `content_type`:正文声明的媒体类型原文。
74    /// - `schema_id`:正文解释合同的 schema 身份,可按 validate 规则为空。
75    /// - `body`:需要完整保留的原始正文。
76    ///
77    /// 返回:保存原始字节且 validate 通过时返回正文,媒体类型、schema 或适用的 JSON 校验失败时报错,不重新编码正文或证明业务 schema 语义。
78    pub fn new(
79        content_type: impl Into<String>,
80        schema_id: impl Into<String>,
81        body: Vec<u8>,
82    ) -> Result<Self, SagaPayloadError> {
83        let payload = Self {
84            content_type: content_type.into(),
85            schema_id: schema_id.into(),
86            body,
87        };
88        payload.validate()?;
89        Ok(payload)
90    }
91
92    /// 业务作用:在进入摘要、业务解码或持久消息前复验公开正文结构。
93    ///
94    /// 参数说明: 无。
95    ///
96    /// 返回:媒体与 schema 合法、JSON 可解析时成功;不改写原始正文。
97    pub fn validate(&self) -> Result<(), SagaPayloadError> {
98        self.contract().validate()?;
99        if self.content_type == "application/json" {
100            serde_json::from_slice::<serde_json::Value>(&self.body)
101                .map_err(|_| SagaPayloadError)?;
102        }
103        Ok(())
104    }
105
106    /// 业务作用:提取定义与 capability 必须共同确认的正文解释合同。
107    ///
108    /// 参数说明: 无。
109    ///
110    /// 返回:不包含业务字节的媒体类型和 schema 身份。
111    pub fn contract(&self) -> SagaPayloadContract {
112        SagaPayloadContract {
113            content_type: self.content_type.clone(),
114            schema_id: self.schema_id.clone(),
115        }
116    }
117
118    /// 业务作用:把已有 JSON 值转换为无 schema 的规范 JSON 字节正文。
119    ///
120    /// 参数说明:
121    /// - `value`:调用方已构造或解析的 JSON 值。
122    ///
123    /// 返回:返回 application/json、空 schema_id 与 JSON 值重新序列化的字节;不保留原始空白或字段文本形式,也不执行外部 schema 校验。
124    pub fn json(value: serde_json::Value) -> Self {
125        Self {
126            content_type: "application/json".to_owned(),
127            schema_id: String::new(),
128            body: serde_json::to_vec(&value).expect("JSON value serialization"),
129        }
130    }
131}
132
133/// 正文解释合同或编码不合法的稳定拒绝,不包含业务正文。
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub struct SagaPayloadError;
136
137impl std::fmt::Display for SagaPayloadError {
138    /// 业务作用:输出可安全返回给调用者的正文合同拒绝。
139    ///
140    /// 参数说明:
141    /// - `formatter`:接收固定公开错误文本的格式器。
142    ///
143    /// 返回:成功写入固定合同拒绝文本时成功,格式器失败时传播错误;不输出正文、schema 或底层详情。
144    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        formatter.write_str("Saga payload contract is invalid")
146    }
147}
148
149impl std::error::Error for SagaPayloadError {}