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