1use chrono::{Duration, Utc};
6use serde::{Deserialize, Serialize};
7use serde_json::{Value, json};
8use uuid::Uuid;
9
10use crate::constants::{ACP_VERSION, DEFAULT_CRYPTO_SUITE};
11use crate::errors::{AcpError, AcpResult};
12use crate::json_support::{self, JsonMap};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
16pub enum MessageClass {
17 Send,
18 Ack,
19 Fail,
20 Capabilities,
21 Compensate,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
26pub enum DeliveryState {
27 Pending,
28 Delivered,
29 Acknowledged,
30 Failed,
31 Declined,
32 Expired,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
37pub enum DeliveryMode {
38 Auto,
39 Direct,
40 Relay,
41 Amqp,
42 Mqtt,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct WrappedContentKey {
47 pub recipient: String,
48 #[serde(rename = "ephemeral_public_key")]
49 pub ephemeral_public_key: String,
50 pub nonce: String,
51 pub ciphertext: String,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct Envelope {
56 #[serde(rename = "acp_version")]
57 pub acp_version: String,
58 #[serde(rename = "message_class")]
59 pub message_class: MessageClass,
60 #[serde(rename = "message_id")]
61 pub message_id: String,
62 #[serde(rename = "operation_id")]
63 pub operation_id: String,
64 pub timestamp: String,
65 #[serde(rename = "expires_at")]
66 pub expires_at: String,
67 pub sender: String,
68 pub recipients: Vec<String>,
69 #[serde(rename = "context_id")]
70 pub context_id: String,
71 #[serde(rename = "crypto_suite")]
72 pub crypto_suite: String,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub tenant: Option<String>,
75 #[serde(
76 rename = "correlation_id",
77 default,
78 skip_serializing_if = "Option::is_none"
79 )]
80 pub correlation_id: Option<String>,
81 #[serde(
82 rename = "in_reply_to",
83 default,
84 skip_serializing_if = "Option::is_none"
85 )]
86 pub in_reply_to: Option<String>,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90pub struct ProtectedPayload {
91 pub nonce: String,
92 pub ciphertext: String,
93 #[serde(rename = "wrapped_content_keys")]
94 pub wrapped_content_keys: Vec<WrappedContentKey>,
95 #[serde(rename = "payload_hash")]
96 pub payload_hash: String,
97 #[serde(rename = "signature_kid")]
98 pub signature_kid: String,
99 pub signature: String,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct AcpMessage {
104 pub envelope: Envelope,
105 #[serde(rename = "protected")]
106 pub protected_payload: ProtectedPayload,
107 #[serde(
108 rename = "sender_identity_document",
109 default,
110 skip_serializing_if = "Option::is_none"
111 )]
112 pub sender_identity_document: Option<JsonMap>,
113}
114
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116pub struct DeliveryOutcome {
117 pub recipient: String,
118 pub state: DeliveryState,
119 #[serde(
120 rename = "status_code",
121 default,
122 skip_serializing_if = "Option::is_none"
123 )]
124 pub status_code: Option<u16>,
125 #[serde(
126 rename = "response_class",
127 default,
128 skip_serializing_if = "Option::is_none"
129 )]
130 pub response_class: Option<MessageClass>,
131 #[serde(
132 rename = "reason_code",
133 default,
134 skip_serializing_if = "Option::is_none"
135 )]
136 pub reason_code: Option<String>,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub detail: Option<String>,
139 #[serde(
140 rename = "response_message",
141 default,
142 skip_serializing_if = "Option::is_none"
143 )]
144 pub response_message: Option<JsonMap>,
145}
146
147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
148pub struct SendResult {
149 #[serde(rename = "operation_id")]
150 pub operation_id: String,
151 #[serde(rename = "message_id")]
152 pub message_id: String,
153 #[serde(rename = "message_ids", default, skip_serializing_if = "Vec::is_empty")]
154 pub message_ids: Vec<String>,
155 #[serde(default)]
156 pub outcomes: Vec<DeliveryOutcome>,
157}
158
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub struct CompensateInstruction {
161 #[serde(rename = "operation_id")]
162 pub operation_id: String,
163 pub reason: String,
164 #[serde(default)]
165 pub actions: Vec<JsonMap>,
166}
167
168impl Envelope {
169 #[allow(clippy::too_many_arguments)]
170 pub fn build(
171 sender: impl Into<String>,
172 recipients: Vec<String>,
173 message_class: MessageClass,
174 context_id: impl Into<String>,
175 expires_in_seconds: i64,
176 operation_id: Option<String>,
177 tenant: Option<String>,
178 correlation_id: Option<String>,
179 in_reply_to: Option<String>,
180 crypto_suite: Option<String>,
181 ) -> AcpResult<Self> {
182 let now = Utc::now();
183 let expires_at = now + Duration::seconds(expires_in_seconds.max(1));
184 let env = Self {
185 acp_version: ACP_VERSION.to_string(),
186 message_class,
187 message_id: Uuid::new_v4().to_string(),
188 operation_id: operation_id.unwrap_or_else(|| Uuid::new_v4().to_string()),
189 timestamp: now.to_rfc3339(),
190 expires_at: expires_at.to_rfc3339(),
191 sender: sender.into(),
192 recipients,
193 context_id: context_id.into(),
194 crypto_suite: crypto_suite.unwrap_or_else(|| DEFAULT_CRYPTO_SUITE.to_string()),
195 tenant,
196 correlation_id,
197 in_reply_to,
198 };
199 env.validate()?;
200 Ok(env)
201 }
202
203 pub fn validate(&self) -> AcpResult<()> {
204 if self.sender.trim().is_empty() {
205 return Err(AcpError::Validation(
206 "Envelope sender is required".to_string(),
207 ));
208 }
209 if self.recipients.is_empty() {
210 return Err(AcpError::Validation(
211 "Envelope recipients must not be empty".to_string(),
212 ));
213 }
214 let ts = chrono::DateTime::parse_from_rfc3339(&self.timestamp)
215 .map_err(|e| AcpError::Validation(format!("Invalid timestamp: {e}")))?;
216 let exp = chrono::DateTime::parse_from_rfc3339(&self.expires_at)
217 .map_err(|e| AcpError::Validation(format!("Invalid expires_at: {e}")))?;
218 if exp <= ts {
219 return Err(AcpError::Validation(
220 "Envelope expires_at must be after timestamp".to_string(),
221 ));
222 }
223 Ok(())
224 }
225
226 pub fn is_expired(&self) -> bool {
227 chrono::DateTime::parse_from_rfc3339(&self.expires_at)
228 .map(|exp| exp <= Utc::now())
229 .unwrap_or(true)
230 }
231
232 pub fn to_map(&self) -> AcpResult<JsonMap> {
233 json_support::to_map(self)
234 }
235}
236
237impl ProtectedPayload {
238 pub fn to_signable_value(&self) -> Value {
239 let mut keys = self.wrapped_content_keys.clone();
240 keys.sort_by(|a, b| a.recipient.cmp(&b.recipient));
241 json!({
242 "nonce": self.nonce,
243 "ciphertext": self.ciphertext,
244 "wrapped_content_keys": keys,
245 "payload_hash": self.payload_hash,
246 "signature_kid": self.signature_kid,
247 })
248 }
249}
250
251impl AcpMessage {
252 pub fn to_map(&self) -> AcpResult<JsonMap> {
253 json_support::to_map(self)
254 }
255
256 pub fn from_map(value: &JsonMap) -> AcpResult<Self> {
257 serde_json::from_value(Value::Object(value.clone())).map_err(AcpError::from)
258 }
259
260 pub fn to_json(&self) -> AcpResult<String> {
261 let value = serde_json::to_value(self)?;
262 json_support::canonical_json_string(&value)
263 }
264}
265
266impl SendResult {
267 pub fn to_map(&self) -> AcpResult<JsonMap> {
268 json_support::to_map(self)
269 }
270}
271
272pub fn build_ack_payload(
273 received_message_id: impl Into<String>,
274 status: impl Into<String>,
275) -> JsonMap {
276 let mut payload = JsonMap::new();
277 payload.insert("status".to_string(), Value::String(status.into()));
278 payload.insert(
279 "received_message_id".to_string(),
280 Value::String(received_message_id.into()),
281 );
282 payload
283}
284
285pub fn build_fail_payload(
286 reason_code: impl Into<String>,
287 detail: impl Into<String>,
288 retriable: bool,
289) -> JsonMap {
290 let mut payload = JsonMap::new();
291 payload.insert("reason_code".to_string(), Value::String(reason_code.into()));
292 payload.insert("detail".to_string(), Value::String(detail.into()));
293 payload.insert("retriable".to_string(), Value::Bool(retriable));
294 payload
295}