tap_msg/message/tap_message_trait.rs
1//! Traits for TAP message conversion and validation.
2//!
3//! This module provides traits for converting between DIDComm messages
4//! and TAP-specific message bodies, as well as validation of those bodies.
5
6use crate::didcomm::PlainMessage;
7use crate::error::{Error, Result};
8use crate::message::policy::Policy;
9use crate::message::{Authorize, Participant, RemoveAgent, ReplaceAgent, UpdatePolicies};
10use chrono::Utc;
11use serde::de::DeserializeOwned;
12use serde::Serialize;
13
14/// A trait for TAP message body types that can be serialized to and deserialized from DIDComm messages.
15pub trait TapMessageBody: Serialize + DeserializeOwned + Send + Sync {
16 /// Get the message type string for this body type.
17 fn message_type() -> &'static str
18 where
19 Self: Sized;
20
21 /// Validate the message body.
22 fn validate(&self) -> Result<()>;
23
24 /// Convert this body to a DIDComm message.
25 fn to_didcomm(&self, from: &str) -> Result<PlainMessage> {
26 // Create a JSON representation of self with explicit type field
27 let mut body_json =
28 serde_json::to_value(self).map_err(|e| Error::SerializationError(e.to_string()))?;
29
30 // Ensure the @type field is correctly set in the body
31 if let Some(body_obj) = body_json.as_object_mut() {
32 // Add or update the @type field with the message type
33 body_obj.insert(
34 "@type".to_string(),
35 serde_json::Value::String(Self::message_type().to_string()),
36 );
37 }
38
39 // Create a unique ID for the message
40 let id = uuid::Uuid::new_v4().to_string();
41
42 // Get current timestamp in milliseconds since Unix epoch
43 let now = Utc::now().timestamp_millis() as u64;
44
45 // Extract agent DIDs directly from the message body
46 let mut agent_dids = Vec::new();
47
48 // Extract from the body JSON
49 if let Some(body_obj) = body_json.as_object() {
50 // Extract from originator
51 if let Some(originator) = body_obj.get("originator") {
52 if let Some(originator_obj) = originator.as_object() {
53 if let Some(id) = originator_obj.get("id") {
54 if let Some(id_str) = id.as_str() {
55 if id_str.starts_with("did:") {
56 agent_dids.push(id_str.to_string());
57 }
58 }
59 }
60 }
61 }
62
63 // Extract from beneficiary
64 if let Some(beneficiary) = body_obj.get("beneficiary") {
65 if let Some(beneficiary_obj) = beneficiary.as_object() {
66 if let Some(id) = beneficiary_obj.get("id") {
67 if let Some(id_str) = id.as_str() {
68 if id_str.starts_with("did:") {
69 agent_dids.push(id_str.to_string());
70 }
71 }
72 }
73 }
74 }
75
76 // Extract from agents array
77 if let Some(agents) = body_obj.get("agents") {
78 if let Some(agents_array) = agents.as_array() {
79 for agent in agents_array {
80 if let Some(agent_obj) = agent.as_object() {
81 if let Some(id) = agent_obj.get("id") {
82 if let Some(id_str) = id.as_str() {
83 if id_str.starts_with("did:") {
84 agent_dids.push(id_str.to_string());
85 }
86 }
87 }
88 }
89 }
90 }
91 }
92 }
93
94 // Remove duplicates
95 agent_dids.sort();
96 agent_dids.dedup();
97
98 // If from_did is provided, remove it from the recipients list to avoid sending to self
99 agent_dids.retain(|did| did != from);
100
101 // Create the message
102 let message = PlainMessage {
103 id,
104 typ: "application/didcomm-plain+json".to_string(),
105 type_: Self::message_type().to_string(),
106 from: from.to_string(),
107 to: agent_dids,
108 thid: None,
109 pthid: None,
110 created_time: Some(now),
111 expires_time: None,
112 extra_headers: std::collections::HashMap::new(),
113 from_prior: None,
114 body: body_json,
115 attachments: None,
116 };
117
118 Ok(message)
119 }
120
121 /// Convert this body to a DIDComm message with a custom routing path.
122 ///
123 /// This method allows specifying an explicit list of recipient DIDs, overriding
124 /// the automatic extraction of participants from the message body.
125 ///
126 /// # Arguments
127 ///
128 /// * `from` - The sender DID
129 /// * `to` - An iterator of recipient DIDs
130 ///
131 /// # Returns
132 ///
133 /// A new DIDComm message with the specified routing
134 fn to_didcomm_with_route<'a, I>(&self, from: &str, to: I) -> Result<PlainMessage>
135 where
136 I: IntoIterator<Item = &'a str>,
137 {
138 // First create a message with the sender, automatically extracting agent DIDs
139 let mut message = self.to_didcomm(from)?;
140
141 // Override with explicitly provided recipients if any
142 let to_vec: Vec<String> = to.into_iter().map(String::from).collect();
143 if !to_vec.is_empty() {
144 message.to = to_vec;
145 }
146
147 Ok(message)
148 }
149
150 /// Extract this body type from a DIDComm message.
151 fn from_didcomm(message: &PlainMessage) -> Result<Self>
152 where
153 Self: Sized,
154 {
155 // Verify that this is the correct message type
156 if message.type_ != Self::message_type() {
157 return Err(Error::InvalidMessageType(format!(
158 "Expected message type {}, but found {}",
159 Self::message_type(),
160 message.type_
161 )));
162 }
163
164 // Create a copy of the message body that we can modify
165 let mut body_json = message.body.clone();
166
167 // Ensure the @type field is present for deserialization
168 if let Some(body_obj) = body_json.as_object_mut() {
169 // Add or update the @type field to ensure it's present
170 body_obj.insert(
171 "@type".to_string(),
172 serde_json::Value::String(Self::message_type().to_string()),
173 );
174 }
175
176 // Extract and deserialize the body
177 let body = serde_json::from_value(body_json).map_err(|e| {
178 Error::SerializationError(format!("Failed to deserialize message body: {}", e))
179 })?;
180
181 Ok(body)
182 }
183}
184
185/// A trait for messages that can be connected to a prior Connect message.
186///
187/// This trait provides functionality for linking messages to a previous Connect message,
188/// enabling the building of message chains in the TAP protocol.
189pub trait Connectable {
190 /// Connect this message to a prior Connect message by setting the parent thread ID (pthid).
191 ///
192 /// # Arguments
193 ///
194 /// * `connect_id` - The ID of the Connect message to link to
195 ///
196 /// # Returns
197 ///
198 /// Self reference for fluent interface chaining
199 fn with_connection(&mut self, connect_id: &str) -> &mut Self;
200
201 /// Check if this message is connected to a prior Connect message.
202 ///
203 /// # Returns
204 ///
205 /// `true` if this message has a connection (pthid) set, `false` otherwise
206 fn has_connection(&self) -> bool;
207
208 /// Get the connection ID if present.
209 ///
210 /// # Returns
211 ///
212 /// The Connect message ID this message is connected to, or None if not connected
213 fn connection_id(&self) -> Option<&str>;
214}
215
216/// Trait for types that can be represented as TAP messages.
217///
218/// This trait provides utility methods for working with DIDComm Messages in the context of the TAP protocol.
219pub trait TapMessage {
220 /// Validates a TAP message.
221 ///
222 /// This method checks if the message meets all the requirements
223 /// for a valid TAP message, including:
224 ///
225 /// - Having a valid ID
226 /// - Having a valid created timestamp
227 /// - Having a valid TAP message type
228 /// - Having a valid body structure
229 ///
230 /// # Returns
231 ///
232 /// `Ok(())` if the message is valid, otherwise an error
233 fn validate(&self) -> Result<()>;
234
235 /// Checks if this message is a TAP message.
236 fn is_tap_message(&self) -> bool;
237
238 /// Gets the TAP message type from this message.
239 fn get_tap_type(&self) -> Option<String>;
240
241 /// Extract a specific message body type from this message.
242 ///
243 /// # Type Parameters
244 ///
245 /// * `T` - The type of the body to extract, must implement `TapMessageBody`
246 ///
247 /// # Returns
248 ///
249 /// The message body if the type matches T, otherwise an error
250 fn body_as<T: TapMessageBody>(&self) -> Result<T>;
251
252 /// Get all participant DIDs from this message.
253 ///
254 /// This includes all DIDs that are involved in the message, such as sender, recipients, and
255 /// any other participants mentioned in the message body.
256 ///
257 /// # Returns
258 ///
259 /// List of participant DIDs
260 fn get_all_participants(&self) -> Vec<String>;
261
262 /// Create a reply to this message.
263 ///
264 /// # Type Parameters
265 ///
266 /// * `T` - The TAP message body type to create
267 ///
268 /// # Arguments
269 ///
270 /// * `body` - The reply message body
271 /// * `creator_did` - DID of the creator of this reply
272 ///
273 /// # Returns
274 ///
275 /// A new DIDComm message that is properly linked to this message as a reply
276 fn create_reply<T: TapMessageBody>(&self, body: &T, creator_did: &str) -> Result<PlainMessage> {
277 // Create the base message with creator as sender
278 let mut message = body.to_didcomm(creator_did)?;
279
280 // Set the thread ID to maintain the conversation thread
281 if let Some(thread_id) = self.thread_id() {
282 message.thid = Some(thread_id.to_string());
283 } else {
284 // If no thread ID exists, use the original message ID as the thread ID
285 message.thid = Some(self.message_id().to_string());
286 }
287
288 // Set the parent thread ID if this thread is part of a larger transaction
289 if let Some(parent_thread_id) = self.parent_thread_id() {
290 message.pthid = Some(parent_thread_id.to_string());
291 }
292
293 // Get all participants from the message
294 let participant_dids = self.get_all_participants();
295
296 // Set recipients to all participants except the creator
297 let recipients: Vec<String> = participant_dids
298 .into_iter()
299 .filter(|did| did != creator_did)
300 .collect();
301
302 if !recipients.is_empty() {
303 message.to = recipients;
304 }
305
306 Ok(message)
307 }
308
309 /// Get the message type for this message
310 fn message_type(&self) -> &'static str;
311
312 /// Get the thread ID for this message
313 fn thread_id(&self) -> Option<&str>;
314
315 /// Get the parent thread ID for this message
316 fn parent_thread_id(&self) -> Option<&str>;
317
318 /// Get the message ID for this message
319 fn message_id(&self) -> &str;
320}
321
322// Implement TapMessage trait for PlainMessage
323impl TapMessage for PlainMessage {
324 fn validate(&self) -> Result<()> {
325 // Check if it's a TAP message first
326 if !self.is_tap_message() {
327 return Err(Error::Validation("Not a TAP message".to_string()));
328 }
329
330 // Check ID
331 if self.id.is_empty() {
332 return Err(Error::Validation(
333 "Message must have a non-empty ID".to_string(),
334 ));
335 }
336
337 // Check created time
338 if self.created_time.is_none() {
339 return Err(Error::Validation(
340 "Message must have a created timestamp".to_string(),
341 ));
342 }
343
344 // Body validation is type-specific and handled during body extraction
345 Ok(())
346 }
347
348 fn is_tap_message(&self) -> bool {
349 self.type_.starts_with("https://tap.rsvp/schema/1.0#")
350 }
351
352 fn get_tap_type(&self) -> Option<String> {
353 if self.is_tap_message() {
354 Some(self.type_.clone())
355 } else {
356 None
357 }
358 }
359
360 fn body_as<T: TapMessageBody>(&self) -> Result<T> {
361 // Check if the message type matches the expected type
362 if self.type_ != T::message_type() {
363 return Err(Error::Validation(format!(
364 "Message type mismatch. Expected {}, got {}",
365 T::message_type(),
366 self.type_
367 )));
368 }
369
370 // Create a copy of the body that we can modify
371 let mut body_json = self.body.clone();
372
373 // Ensure the @type field is present for deserialization
374 if let Some(body_obj) = body_json.as_object_mut() {
375 // Add or update the @type field to ensure it's present
376 body_obj.insert(
377 "@type".to_string(),
378 serde_json::Value::String(T::message_type().to_string()),
379 );
380 }
381
382 // Extract and deserialize the body
383 let body = serde_json::from_value(body_json).map_err(|e| {
384 Error::SerializationError(format!("Failed to deserialize message body: {}", e))
385 })?;
386
387 Ok(body)
388 }
389
390 fn get_all_participants(&self) -> Vec<String> {
391 let mut participants = Vec::new();
392
393 // Add sender
394 if !self.from.is_empty() {
395 participants.push(self.from.clone());
396 }
397
398 // Add recipients
399 participants.extend(self.to.clone());
400
401 participants
402 }
403
404 fn message_type(&self) -> &'static str {
405 match self.type_.as_str() {
406 "https://tap.rsvp/schema/1.0#transfer" => "https://tap.rsvp/schema/1.0#transfer",
407 "https://tap.rsvp/schema/1.0#payment-request" => {
408 "https://tap.rsvp/schema/1.0#payment-request"
409 }
410 "https://tap.rsvp/schema/1.0#connect" => "https://tap.rsvp/schema/1.0#connect",
411 "https://tap.rsvp/schema/1.0#authorize" => "https://tap.rsvp/schema/1.0#authorize",
412 "https://tap.rsvp/schema/1.0#reject" => "https://tap.rsvp/schema/1.0#reject",
413 "https://tap.rsvp/schema/1.0#settle" => "https://tap.rsvp/schema/1.0#settle",
414 "https://tap.rsvp/schema/1.0#update-party" => {
415 "https://tap.rsvp/schema/1.0#update-party"
416 }
417 "https://tap.rsvp/schema/1.0#update-policies" => {
418 "https://tap.rsvp/schema/1.0#update-policies"
419 }
420 "https://didcomm.org/present-proof/3.0/presentation" => {
421 "https://didcomm.org/present-proof/3.0/presentation"
422 }
423 _ => "unknown",
424 }
425 }
426
427 fn thread_id(&self) -> Option<&str> {
428 self.thid.as_deref()
429 }
430
431 fn parent_thread_id(&self) -> Option<&str> {
432 self.pthid.as_deref()
433 }
434
435 fn message_id(&self) -> &str {
436 &self.id
437 }
438}
439
440// Implement Connectable trait for PlainMessage
441impl Connectable for PlainMessage {
442 fn with_connection(&mut self, connect_id: &str) -> &mut Self {
443 self.pthid = Some(connect_id.to_string());
444 self
445 }
446
447 fn has_connection(&self) -> bool {
448 self.pthid.is_some()
449 }
450
451 fn connection_id(&self) -> Option<&str> {
452 self.pthid.as_deref()
453 }
454}
455
456/// Creates a new TAP message from a message body.
457///
458/// This function constructs a DIDComm message with the appropriate
459/// TAP message type and body.
460///
461/// # Arguments
462///
463/// * `body` - The message body to include
464/// * `id` - Optional message ID (will be generated if None)
465/// * `from_did` - Sender DID
466/// * `to_dids` - Recipient DIDs (will override automatically extracted agent DIDs)
467///
468/// # Returns
469///
470/// A DIDComm message object ready for further processing
471pub fn create_tap_message<T: TapMessageBody>(
472 body: &T,
473 id: Option<String>,
474 from_did: &str,
475 to_dids: &[&str],
476) -> Result<PlainMessage> {
477 // Create the base message from the body, passing the from_did
478 let mut message = body.to_didcomm(from_did)?;
479
480 // Set custom ID if provided
481 if let Some(custom_id) = id {
482 message.id = custom_id;
483 }
484
485 // Override with explicitly provided recipients if any
486 if !to_dids.is_empty() {
487 message.to = to_dids.iter().map(|&s| s.to_string()).collect();
488 }
489
490 Ok(message)
491}
492
493/// Authorizable trait implementation for various TAP message types.
494/// This module defines the Authorizable trait, which allows message types
495/// to be authorized, and implementations for relevant TAP message types.
496/// Authorizable trait for types that can be authorized or can generate authorization-related messages.
497pub trait Authorizable {
498 /// Create an Authorize message for this object.
499 fn authorize(&self, note: Option<String>) -> Authorize;
500
501 /// Create an UpdatePolicies message for this object.
502 fn update_policies(&self, transaction_id: String, policies: Vec<Policy>) -> UpdatePolicies;
503
504 /// Create a ReplaceAgent message for this object.
505 fn replace_agent(
506 &self,
507 transaction_id: String,
508 original_agent: String,
509 replacement: Participant,
510 ) -> ReplaceAgent;
511
512 /// Create a RemoveAgent message for this object.
513 fn remove_agent(&self, transaction_id: String, agent: String) -> RemoveAgent;
514}