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}