1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::didcomm::PlainMessage;
10use crate::error::{Error, Result};
11use crate::impl_tap_message;
12use crate::message::tap_message_trait::{Connectable, TapMessageBody};
13use chrono::Utc;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct TransactionLimits {
18 pub max_amount: Option<String>,
20
21 pub max_total_amount: Option<String>,
23
24 pub max_transactions: Option<u64>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ConnectionConstraints {
31 pub transaction_limits: Option<TransactionLimits>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Connect {
38 pub transaction_id: String,
40
41 pub agent_id: String,
43
44 #[serde(rename = "for")]
46 pub for_: String,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub role: Option<String>,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub constraints: Option<ConnectionConstraints>,
55}
56
57impl Connect {
58 pub fn new(transaction_id: &str, agent_id: &str, for_id: &str, role: Option<&str>) -> Self {
60 Self {
61 transaction_id: transaction_id.to_string(),
62 agent_id: agent_id.to_string(),
63 for_: for_id.to_string(),
64 role: role.map(|s| s.to_string()),
65 constraints: None,
66 }
67 }
68
69 pub fn with_constraints(mut self, constraints: ConnectionConstraints) -> Self {
71 self.constraints = Some(constraints);
72 self
73 }
74}
75
76impl Connectable for Connect {
77 fn with_connection(&mut self, _connect_id: &str) -> &mut Self {
78 self
80 }
81
82 fn has_connection(&self) -> bool {
83 false
84 }
85
86 fn connection_id(&self) -> Option<&str> {
87 None
88 }
89}
90
91impl TapMessageBody for Connect {
92 fn message_type() -> &'static str {
93 "https://tap.rsvp/schema/1.0#connect"
94 }
95
96 fn validate(&self) -> Result<()> {
97 if self.transaction_id.is_empty() {
98 return Err(Error::Validation("transaction_id is required".to_string()));
99 }
100 if self.agent_id.is_empty() {
101 return Err(Error::Validation("agent_id is required".to_string()));
102 }
103 if self.for_.is_empty() {
104 return Err(Error::Validation("for is required".to_string()));
105 }
106 Ok(())
107 }
108
109 fn to_didcomm(&self, from_did: &str) -> Result<PlainMessage> {
110 let mut body_json =
112 serde_json::to_value(self).map_err(|e| Error::SerializationError(e.to_string()))?;
113
114 if let Some(body_obj) = body_json.as_object_mut() {
116 body_obj.insert(
117 "@type".to_string(),
118 serde_json::Value::String(Self::message_type().to_string()),
119 );
120 }
122
123 let id = uuid::Uuid::new_v4().to_string(); let created_time = Utc::now().timestamp_millis() as u64;
126
127 let to = vec![self.agent_id.clone()];
129
130 let message = PlainMessage {
132 id,
133 typ: "application/didcomm-plain+json".to_string(), type_: Self::message_type().to_string(),
135 from: from_did.to_string(),
136 to, thid: Some(self.transaction_id.clone()),
138 pthid: None, created_time: Some(created_time),
140 expires_time: None,
141 extra_headers: std::collections::HashMap::new(),
142 from_prior: None,
143 body: body_json,
144 attachments: None,
145 };
146
147 Ok(message)
148 }
149
150 fn from_didcomm(message: &PlainMessage) -> Result<Self> {
151 let body = message
152 .body
153 .as_object()
154 .ok_or_else(|| Error::Validation("Message body is not a JSON object".to_string()))?;
155
156 let transfer_id = body
157 .get("transaction_id")
158 .and_then(|v| v.as_str())
159 .ok_or_else(|| Error::Validation("Missing or invalid transaction_id".to_string()))?;
160
161 let agent_id = body
162 .get("agent_id")
163 .and_then(|v| v.as_str())
164 .ok_or_else(|| Error::Validation("Missing or invalid agent_id".to_string()))?;
165
166 let for_id = body
167 .get("for")
168 .and_then(|v| v.as_str())
169 .ok_or_else(|| Error::Validation("Missing or invalid for".to_string()))?;
170
171 let role = body
172 .get("role")
173 .and_then(|v| v.as_str())
174 .map(ToString::to_string);
175
176 let constraints = if let Some(constraints_value) = body.get("constraints") {
177 if constraints_value.is_null() {
178 None
179 } else {
180 let constraints_json = serde_json::to_value(constraints_value).map_err(|e| {
182 Error::SerializationError(format!("Invalid constraints: {}", e))
183 })?;
184
185 Some(serde_json::from_value(constraints_json).map_err(|e| {
186 Error::SerializationError(format!("Invalid constraints format: {}", e))
187 })?)
188 }
189 } else {
190 None
191 };
192
193 Ok(Connect {
194 transaction_id: transfer_id.to_string(),
195 agent_id: agent_id.to_string(),
196 for_: for_id.to_string(),
197 role,
198 constraints,
199 })
200 }
201}
202
203impl_tap_message!(Connect);
204
205impl TapMessageBody for AuthorizationRequired {
206 fn message_type() -> &'static str {
207 "https://tap.rsvp/schema/1.0#authorizationrequired"
208 }
209
210 fn validate(&self) -> Result<()> {
211 if self.url.is_empty() {
212 return Err(Error::Validation(
213 "Authorization URL is required".to_string(),
214 ));
215 }
216
217 if let Some(expires) = self.metadata.get("expires") {
219 if let Some(expires_str) = expires.as_str() {
220 if !expires_str.contains('T') || !expires_str.contains(':') {
222 return Err(Error::Validation(
223 "Invalid expiry date format. Expected ISO8601/RFC3339 format".to_string(),
224 ));
225 }
226 }
227 }
228
229 Ok(())
230 }
231
232 fn to_didcomm(&self, from: &str) -> Result<PlainMessage> {
233 let mut body_json =
235 serde_json::to_value(self).map_err(|e| Error::SerializationError(e.to_string()))?;
236
237 if let Some(body_obj) = body_json.as_object_mut() {
239 body_obj.insert(
240 "@type".to_string(),
241 serde_json::Value::String(Self::message_type().to_string()),
242 );
243 }
244
245 let now = Utc::now().timestamp() as u64;
246
247 let message = PlainMessage {
249 id: uuid::Uuid::new_v4().to_string(),
250 typ: "application/didcomm-plain+json".to_string(),
251 type_: Self::message_type().to_string(),
252 body: body_json,
253 from: from.to_string(),
254 to: Vec::new(), thid: None,
256 pthid: None,
257 created_time: Some(now),
258 expires_time: None,
259 extra_headers: std::collections::HashMap::new(),
260 from_prior: None,
261 attachments: None,
262 };
263
264 Ok(message)
265 }
266
267 fn from_didcomm(message: &PlainMessage) -> Result<Self> {
268 if message.type_ != Self::message_type() {
270 return Err(Error::InvalidMessageType(format!(
271 "Expected {} but got {}",
272 Self::message_type(),
273 message.type_
274 )));
275 }
276
277 let auth_req: AuthorizationRequired = serde_json::from_value(message.body.clone())
279 .map_err(|e| Error::SerializationError(e.to_string()))?;
280
281 Ok(auth_req)
282 }
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct OutOfBand {
288 pub goal_code: String,
290
291 pub goal: String,
293
294 pub service: String,
296
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub accept: Option<Vec<String>>,
300
301 #[serde(skip_serializing_if = "Option::is_none")]
303 pub handshake_protocols: Option<Vec<String>>,
304
305 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
307 pub metadata: HashMap<String, serde_json::Value>,
308}
309
310impl OutOfBand {
311 pub fn new(goal_code: String, goal: String, service: String) -> Self {
313 Self {
314 goal_code,
315 goal,
316 service,
317 accept: None,
318 handshake_protocols: None,
319 metadata: HashMap::new(),
320 }
321 }
322}
323
324impl TapMessageBody for OutOfBand {
325 fn message_type() -> &'static str {
326 "https://tap.rsvp/schema/1.0#outofband"
327 }
328
329 fn validate(&self) -> Result<()> {
330 if self.goal_code.is_empty() {
331 return Err(Error::Validation("Goal code is required".to_string()));
332 }
333
334 if self.service.is_empty() {
335 return Err(Error::Validation("Service is required".to_string()));
336 }
337
338 Ok(())
339 }
340
341 fn to_didcomm(&self, from: &str) -> Result<PlainMessage> {
342 let mut body_json =
344 serde_json::to_value(self).map_err(|e| Error::SerializationError(e.to_string()))?;
345
346 if let Some(body_obj) = body_json.as_object_mut() {
348 body_obj.insert(
349 "@type".to_string(),
350 serde_json::Value::String(Self::message_type().to_string()),
351 );
352 }
353
354 let now = Utc::now().timestamp() as u64;
355
356 let message = PlainMessage {
358 id: uuid::Uuid::new_v4().to_string(),
359 typ: "application/didcomm-plain+json".to_string(),
360 type_: Self::message_type().to_string(),
361 body: body_json,
362 from: from.to_string(),
363 to: Vec::new(), thid: None,
365 pthid: None,
366 created_time: Some(now),
367 expires_time: None,
368 extra_headers: std::collections::HashMap::new(),
369 from_prior: None,
370 attachments: None,
371 };
372
373 Ok(message)
374 }
375
376 fn from_didcomm(message: &PlainMessage) -> Result<Self> {
377 if message.type_ != Self::message_type() {
379 return Err(Error::InvalidMessageType(format!(
380 "Expected {} but got {}",
381 Self::message_type(),
382 message.type_
383 )));
384 }
385
386 let oob: OutOfBand = serde_json::from_value(message.body.clone())
388 .map_err(|e| Error::SerializationError(e.to_string()))?;
389
390 Ok(oob)
391 }
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct AuthorizationRequired {
397 pub url: String,
399
400 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
402 pub metadata: HashMap<String, serde_json::Value>,
403}
404
405impl AuthorizationRequired {
406 pub fn new(url: String, expires: String) -> Self {
408 let mut metadata = HashMap::new();
409 metadata.insert("expires".to_string(), serde_json::Value::String(expires));
410
411 Self { url, metadata }
412 }
413
414 pub fn add_metadata(mut self, key: &str, value: serde_json::Value) -> Self {
416 self.metadata.insert(key.to_string(), value);
417 self
418 }
419}