1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use crate::hash::semantic_hash;
10use crate::qom::QomReport;
11use crate::stype::SType;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct MplEnvelope {
16 pub id: String,
18
19 pub stype: String,
21
22 pub payload: serde_json::Value,
24
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub args_stype: Option<String>,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub profile: Option<String>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub sem_hash: Option<String>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub provenance: Option<Provenance>,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub qom_report: Option<QomReport>,
44
45 #[serde(default, skip_serializing_if = "Vec::is_empty")]
47 pub features: Vec<String>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub timestamp: Option<DateTime<Utc>>,
52}
53
54impl MplEnvelope {
55 pub fn new(stype: impl Into<String>, payload: serde_json::Value) -> Self {
57 Self {
58 id: Uuid::new_v4().to_string(),
59 stype: stype.into(),
60 payload,
61 args_stype: None,
62 profile: None,
63 sem_hash: None,
64 provenance: None,
65 qom_report: None,
66 features: Vec::new(),
67 timestamp: Some(Utc::now()),
68 }
69 }
70
71 pub fn from_stype(stype: &SType, payload: serde_json::Value) -> Self {
73 Self::new(stype.id(), payload)
74 }
75
76 pub fn with_args_stype(mut self, args_stype: impl Into<String>) -> Self {
78 self.args_stype = Some(args_stype.into());
79 self
80 }
81
82 pub fn with_profile(mut self, profile: impl Into<String>) -> Self {
84 self.profile = Some(profile.into());
85 self
86 }
87
88 pub fn with_provenance(mut self, provenance: Provenance) -> Self {
90 self.provenance = Some(provenance);
91 self
92 }
93
94 pub fn with_features(mut self, features: Vec<String>) -> Self {
96 self.features = features;
97 self
98 }
99
100 pub fn compute_hash(&mut self) -> crate::error::Result<()> {
102 self.sem_hash = Some(semantic_hash(&self.payload)?);
103 Ok(())
104 }
105
106 pub fn verify_hash(&self) -> crate::error::Result<bool> {
108 match &self.sem_hash {
109 Some(expected) => {
110 let actual = semantic_hash(&self.payload)?;
111 Ok(&actual == expected)
112 }
113 None => Ok(true), }
115 }
116
117 pub fn with_qom_report(mut self, report: QomReport) -> Self {
119 self.qom_report = Some(report);
120 self
121 }
122
123 pub fn parsed_stype(&self) -> crate::error::Result<SType> {
125 SType::parse(&self.stype)
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct Provenance {
132 pub intent: String,
134
135 #[serde(default, skip_serializing_if = "Vec::is_empty")]
137 pub inputs_ref: Vec<String>,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub consent_ref: Option<String>,
142
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub policy_ref: Option<String>,
146
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub agent: Option<String>,
150
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub parent_id: Option<String>,
154
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub signature: Option<String>,
158
159 #[serde(default, skip_serializing_if = "Vec::is_empty")]
161 pub artifacts: Vec<Artifact>,
162}
163
164impl Provenance {
165 pub fn new(intent: impl Into<String>) -> Self {
167 Self {
168 intent: intent.into(),
169 inputs_ref: Vec::new(),
170 consent_ref: None,
171 policy_ref: None,
172 agent: None,
173 parent_id: None,
174 signature: None,
175 artifacts: Vec::new(),
176 }
177 }
178
179 pub fn with_inputs(mut self, inputs: Vec<String>) -> Self {
181 self.inputs_ref = inputs;
182 self
183 }
184
185 pub fn with_consent(mut self, consent_ref: impl Into<String>) -> Self {
187 self.consent_ref = Some(consent_ref.into());
188 self
189 }
190
191 pub fn with_policy(mut self, policy_ref: impl Into<String>) -> Self {
193 self.policy_ref = Some(policy_ref.into());
194 self
195 }
196
197 pub fn with_agent(mut self, agent: impl Into<String>) -> Self {
199 self.agent = Some(agent.into());
200 self
201 }
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct Artifact {
207 #[serde(rename = "ref")]
209 pub reference: String,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub artifact_type: Option<String>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub text: Option<String>,
218
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub url: Option<String>,
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use serde_json::json;
228
229 #[test]
230 fn test_envelope_creation() {
231 let envelope = MplEnvelope::new(
232 "org.calendar.Event.v1",
233 json!({
234 "title": "Meeting",
235 "start": "2025-01-01T10:00:00Z"
236 }),
237 );
238
239 assert!(!envelope.id.is_empty());
240 assert_eq!(envelope.stype, "org.calendar.Event.v1");
241 assert!(envelope.timestamp.is_some());
242 }
243
244 #[test]
245 fn test_envelope_with_hash() {
246 let mut envelope = MplEnvelope::new("org.test.Test.v1", json!({"key": "value"}));
247 envelope.compute_hash().unwrap();
248
249 assert!(envelope.sem_hash.is_some());
250 assert!(envelope.sem_hash.as_ref().unwrap().starts_with("b3:"));
251 assert!(envelope.verify_hash().unwrap());
252 }
253
254 #[test]
255 fn test_envelope_serialization() {
256 let envelope = MplEnvelope::new("org.test.Test.v1", json!({"test": true}))
257 .with_profile("qom-basic")
258 .with_provenance(Provenance::new("test.action.v1"));
259
260 let json = serde_json::to_string(&envelope).unwrap();
261 let deserialized: MplEnvelope = serde_json::from_str(&json).unwrap();
262
263 assert_eq!(deserialized.stype, envelope.stype);
264 assert_eq!(deserialized.profile, envelope.profile);
265 }
266}