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::{
10    AddAgents, Agent, Authorize, Cancel, ConfirmRelationship, Party, Reject, RemoveAgent,
11    ReplaceAgent, Revert, Settle, UpdateParty, UpdatePolicies,
12};
13use chrono::Utc;
14use serde::de::DeserializeOwned;
15use serde::Serialize;
16
17/// A trait for TAP message body types that can be serialized to and deserialized from DIDComm messages.
18pub trait TapMessageBody: Serialize + DeserializeOwned + Send + Sync {
19    /// Get the message type string for this body type.
20    fn message_type() -> &'static str
21    where
22        Self: Sized;
23
24    /// Validate the message body.
25    fn validate(&self) -> Result<()>;
26
27    /// Convert this body to a DIDComm message.
28    fn to_didcomm(&self, from: &str) -> Result<PlainMessage> {
29        // Create a JSON representation of self with explicit type field
30        let mut body_json =
31            serde_json::to_value(self).map_err(|e| Error::SerializationError(e.to_string()))?;
32
33        // Ensure the @type field is correctly set in the body
34        if let Some(body_obj) = body_json.as_object_mut() {
35            // Add or update the @type field with the message type
36            body_obj.insert(
37                "@type".to_string(),
38                serde_json::Value::String(Self::message_type().to_string()),
39            );
40        }
41
42        // Create a unique ID for the message
43        let id = uuid::Uuid::new_v4().to_string();
44
45        // Get current timestamp in milliseconds since Unix epoch
46        let now = Utc::now().timestamp_millis() as u64;
47
48        // Extract agent DIDs directly from the message body
49        let mut agent_dids = Vec::new();
50
51        // Extract from the body JSON
52        if let Some(body_obj) = body_json.as_object() {
53            // Extract from originator
54            if let Some(originator) = body_obj.get("originator") {
55                if let Some(originator_obj) = originator.as_object() {
56                    if let Some(id) = originator_obj.get("id") {
57                        if let Some(id_str) = id.as_str() {
58                            if id_str.starts_with("did:") {
59                                agent_dids.push(id_str.to_string());
60                            }
61                        }
62                    }
63                }
64            }
65
66            // Extract from beneficiary
67            if let Some(beneficiary) = body_obj.get("beneficiary") {
68                if let Some(beneficiary_obj) = beneficiary.as_object() {
69                    if let Some(id) = beneficiary_obj.get("id") {
70                        if let Some(id_str) = id.as_str() {
71                            if id_str.starts_with("did:") {
72                                agent_dids.push(id_str.to_string());
73                            }
74                        }
75                    }
76                }
77            }
78
79            // Extract from agents array
80            if let Some(agents) = body_obj.get("agents") {
81                if let Some(agents_array) = agents.as_array() {
82                    for agent in agents_array {
83                        if let Some(agent_obj) = agent.as_object() {
84                            if let Some(id) = agent_obj.get("id") {
85                                if let Some(id_str) = id.as_str() {
86                                    if id_str.starts_with("did:") {
87                                        agent_dids.push(id_str.to_string());
88                                    }
89                                }
90                            }
91                        }
92                    }
93                }
94            }
95        }
96
97        // Remove duplicates
98        agent_dids.sort();
99        agent_dids.dedup();
100
101        // If from_did is provided, remove it from the recipients list to avoid sending to self
102        agent_dids.retain(|did| did != from);
103
104        // Create the message
105        let message = PlainMessage {
106            id,
107            typ: "application/didcomm-plain+json".to_string(),
108            type_: Self::message_type().to_string(),
109            from: from.to_string(),
110            to: agent_dids,
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 custom routing path.
125    ///
126    /// This method allows specifying an explicit list of recipient DIDs, overriding
127    /// the automatic extraction of participants from the message body.
128    ///
129    /// # Arguments
130    ///
131    /// * `from` - The sender DID
132    /// * `to` - An iterator of recipient DIDs
133    ///
134    /// # Returns
135    ///
136    /// A new DIDComm message with the specified routing
137    fn to_didcomm_with_route<'a, I>(&self, from: &str, to: I) -> Result<PlainMessage>
138    where
139        I: IntoIterator<Item = &'a str>,
140    {
141        // First create a message with the sender, automatically extracting agent DIDs
142        let mut message = self.to_didcomm(from)?;
143
144        // Override with explicitly provided recipients if any
145        let to_vec: Vec<String> = to.into_iter().map(String::from).collect();
146        if !to_vec.is_empty() {
147            message.to = to_vec;
148        }
149
150        Ok(message)
151    }
152
153    /// Extract this body type from a DIDComm message.
154    fn from_didcomm(message: &PlainMessage) -> Result<Self>
155    where
156        Self: Sized,
157    {
158        // Verify that this is the correct message type
159        if message.type_ != Self::message_type() {
160            return Err(Error::InvalidMessageType(format!(
161                "Expected message type {}, but found {}",
162                Self::message_type(),
163                message.type_
164            )));
165        }
166
167        // Create a copy of the message body that we can modify
168        let mut body_json = message.body.clone();
169
170        // Ensure the @type field is present for deserialization
171        if let Some(body_obj) = body_json.as_object_mut() {
172            // Add or update the @type field to ensure it's present
173            body_obj.insert(
174                "@type".to_string(),
175                serde_json::Value::String(Self::message_type().to_string()),
176            );
177        }
178
179        // Extract and deserialize the body
180        let body = serde_json::from_value(body_json).map_err(|e| {
181            Error::SerializationError(format!("Failed to deserialize message body: {}", e))
182        })?;
183
184        Ok(body)
185    }
186}
187
188/// A trait for messages that can be connected to a prior Connect message.
189///
190/// This trait provides functionality for linking messages to a previous Connect message,
191/// enabling the building of message chains in the TAP protocol.
192pub trait Connectable {
193    /// Connect this message to a prior Connect message by setting the parent thread ID (pthid).
194    ///
195    /// # Arguments
196    ///
197    /// * `connection_id` - The ID of the Connect message to link to
198    ///
199    /// # Returns
200    ///
201    /// Self reference for fluent interface chaining
202    fn with_connection(&mut self, connection_id: &str) -> &mut Self;
203
204    /// Check if this message is connected to a prior Connect message.
205    ///
206    /// # Returns
207    ///
208    /// `true` if this message has a connection (pthid) set, `false` otherwise
209    fn has_connection(&self) -> bool;
210
211    /// Get the connection ID if present.
212    ///
213    /// # Returns
214    ///
215    /// The Connect message ID this message is connected to, or None if not connected
216    fn connection_id(&self) -> Option<&str>;
217}
218
219/// Trait for types that can be represented as TAP messages.
220///
221/// This trait provides utility methods for working with DIDComm Messages in the context of the TAP protocol.
222pub trait TapMessage {
223    /// Validates a TAP message.
224    ///
225    /// This method checks if the message meets all the requirements
226    /// for a valid TAP message, including:
227    ///
228    /// - Having a valid ID
229    /// - Having a valid created timestamp
230    /// - Having a valid TAP message type
231    /// - Having a valid body structure
232    ///
233    /// # Returns
234    ///
235    /// `Ok(())` if the message is valid, otherwise an error
236    fn validate(&self) -> Result<()>;
237
238    /// Checks if this message is a TAP message.
239    fn is_tap_message(&self) -> bool;
240
241    /// Gets the TAP message type from this message.
242    fn get_tap_type(&self) -> Option<String>;
243
244    /// Extract a specific message body type from this message.
245    ///
246    /// # Type Parameters
247    ///
248    /// * `T` - The type of the body to extract, must implement `TapMessageBody`
249    ///
250    /// # Returns
251    ///
252    /// The message body if the type matches T, otherwise an error
253    fn body_as<T: TapMessageBody>(&self) -> Result<T>;
254
255    /// Get all participant DIDs from this message.
256    ///
257    /// This includes all DIDs that are involved in the message, such as sender, recipients, and
258    /// any other participants mentioned in the message body.
259    ///
260    /// # Returns
261    ///
262    /// List of participant DIDs
263    fn get_all_participants(&self) -> Vec<String>;
264
265    /// Create a reply to this message.
266    ///
267    /// # Type Parameters
268    ///
269    /// * `T` - The TAP message body type to create
270    ///
271    /// # Arguments
272    ///
273    /// * `body` - The reply message body
274    /// * `creator_did` - DID of the creator of this reply
275    ///
276    /// # Returns
277    ///
278    /// A new DIDComm message that is properly linked to this message as a reply
279    fn create_reply<T: TapMessageBody>(&self, body: &T, creator_did: &str) -> Result<PlainMessage> {
280        // Create the base message with creator as sender
281        let mut message = body.to_didcomm(creator_did)?;
282
283        // Set the thread ID to maintain the conversation thread
284        if let Some(thread_id) = self.thread_id() {
285            message.thid = Some(thread_id.to_string());
286        } else {
287            // If no thread ID exists, use the original message ID as the thread ID
288            message.thid = Some(self.message_id().to_string());
289        }
290
291        // Set the parent thread ID if this thread is part of a larger transaction
292        if let Some(parent_thread_id) = self.parent_thread_id() {
293            message.pthid = Some(parent_thread_id.to_string());
294        }
295
296        // Get all participants from the message
297        let participant_dids = self.get_all_participants();
298
299        // Set recipients to all participants except the creator
300        let recipients: Vec<String> = participant_dids
301            .into_iter()
302            .filter(|did| did != creator_did)
303            .collect();
304
305        if !recipients.is_empty() {
306            message.to = recipients;
307        }
308
309        Ok(message)
310    }
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 thread_id(&self) -> Option<&str> {
405        self.thid.as_deref()
406    }
407
408    fn parent_thread_id(&self) -> Option<&str> {
409        self.pthid.as_deref()
410    }
411
412    fn message_id(&self) -> &str {
413        &self.id
414    }
415}
416
417// Implement Connectable trait for PlainMessage
418impl Connectable for PlainMessage {
419    fn with_connection(&mut self, connection_id: &str) -> &mut Self {
420        self.pthid = Some(connection_id.to_string());
421        self
422    }
423
424    fn has_connection(&self) -> bool {
425        self.pthid.is_some()
426    }
427
428    fn connection_id(&self) -> Option<&str> {
429        self.pthid.as_deref()
430    }
431}
432
433/// Helper function to convert an untyped PlainMessage to a typed PlainMessage.
434///
435/// This is used internally to convert the result of create_reply to a typed message.
436pub fn typed_plain_message<T: TapMessageBody>(reply: PlainMessage, body: T) -> PlainMessage<T> {
437    PlainMessage {
438        id: reply.id,
439        typ: reply.typ,
440        type_: reply.type_,
441        body,
442        from: reply.from,
443        to: reply.to,
444        thid: reply.thid,
445        pthid: reply.pthid,
446        extra_headers: reply.extra_headers,
447        created_time: reply.created_time,
448        expires_time: reply.expires_time,
449        from_prior: reply.from_prior,
450        attachments: reply.attachments,
451    }
452}
453
454/// Creates a new TAP message from a message body.
455///
456/// This function constructs a DIDComm message with the appropriate
457/// TAP message type and body.
458///
459/// # Arguments
460///
461/// * `body` - The message body to include
462/// * `id` - Optional message ID (will be generated if None)
463/// * `from_did` - Sender DID
464/// * `to_dids` - Recipient DIDs (will override automatically extracted agent DIDs)
465///
466/// # Returns
467///
468/// A DIDComm message object ready for further processing
469pub fn create_tap_message<T: TapMessageBody>(
470    body: &T,
471    id: Option<String>,
472    from_did: &str,
473    to_dids: &[&str],
474) -> Result<PlainMessage> {
475    // Create the base message from the body, passing the from_did
476    let mut message = body.to_didcomm(from_did)?;
477
478    // Set custom ID if provided
479    if let Some(custom_id) = id {
480        message.id = custom_id;
481    }
482
483    // Override with explicitly provided recipients if any
484    if !to_dids.is_empty() {
485        message.to = to_dids.iter().map(|&s| s.to_string()).collect();
486    }
487
488    Ok(message)
489}
490
491/// Authorizable trait for TAIP-4 authorization messages.
492///
493/// This trait provides methods for creating authorization-related messages
494/// as defined in TAIP-4, excluding the Settle message which is handled
495/// separately in the Transaction trait.
496pub trait Authorizable: TapMessage {
497    /// Create an Authorize message for this object (TAIP-4).
498    ///
499    /// # Arguments
500    /// * `creator_did` - The DID of the agent creating this authorization
501    /// * `settlement_address` - Optional settlement address in CAIP-10 format
502    /// * `expiry` - Optional expiry timestamp in ISO 8601 format
503    fn authorize(
504        &self,
505        creator_did: &str,
506        settlement_address: Option<&str>,
507        expiry: Option<&str>,
508    ) -> PlainMessage<Authorize>;
509
510    /// Create a Cancel message for this object (TAIP-4).
511    ///
512    /// # Arguments
513    /// * `creator_did` - The DID of the agent creating this cancellation
514    /// * `by` - The party wishing to cancel (e.g., "originator" or "beneficiary")
515    /// * `reason` - Optional reason for cancellation
516    fn cancel(&self, creator_did: &str, by: &str, reason: Option<&str>) -> PlainMessage<Cancel>;
517
518    /// Create a Reject message for this object (TAIP-4).
519    ///
520    /// # Arguments
521    /// * `creator_did` - The DID of the agent creating this rejection
522    /// * `reason` - Reason for rejection
523    fn reject(&self, creator_did: &str, reason: &str) -> PlainMessage<Reject>;
524}
525
526/// Transaction trait for managing transaction lifecycle operations.
527///
528/// This trait provides methods for transaction processing operations
529/// as defined in TAIPs 5-9, including agent management, party updates,
530/// policy management, and settlement.
531pub trait Transaction: TapMessage {
532    /// Create a Settle message for this object (TAIP-4).
533    ///
534    /// # Arguments
535    /// * `creator_did` - The DID of the agent creating this settlement
536    /// * `settlement_id` - CAIP-220 identifier of the underlying settlement transaction
537    /// * `amount` - Optional amount settled (must be <= original amount)
538    fn settle(
539        &self,
540        creator_did: &str,
541        settlement_id: &str,
542        amount: Option<&str>,
543    ) -> PlainMessage<Settle>;
544
545    /// Create a Revert message for this object (TAIP-4).
546    ///
547    /// # Arguments
548    /// * `creator_did` - The DID of the agent creating this reversal request
549    /// * `settlement_address` - CAIP-10 format address to return funds to
550    /// * `reason` - Reason for reversal request
551    fn revert(
552        &self,
553        creator_did: &str,
554        settlement_address: &str,
555        reason: &str,
556    ) -> PlainMessage<Revert>;
557
558    /// Create an AddAgents message for this object (TAIP-5).
559    ///
560    /// # Arguments
561    /// * `creator_did` - The DID of the agent creating this addition
562    /// * `agents` - List of agents to add
563    fn add_agents(&self, creator_did: &str, agents: Vec<Agent>) -> PlainMessage<AddAgents>;
564
565    /// Create a ReplaceAgent message for this object (TAIP-5).
566    ///
567    /// # Arguments
568    /// * `creator_did` - The DID of the agent creating this replacement
569    /// * `original_agent` - The agent DID to replace
570    /// * `replacement` - The replacement agent
571    fn replace_agent(
572        &self,
573        creator_did: &str,
574        original_agent: &str,
575        replacement: Agent,
576    ) -> PlainMessage<ReplaceAgent>;
577
578    /// Create a RemoveAgent message for this object (TAIP-5).
579    ///
580    /// # Arguments
581    /// * `creator_did` - The DID of the agent creating this removal
582    /// * `agent` - The agent DID to remove
583    fn remove_agent(&self, creator_did: &str, agent: &str) -> PlainMessage<RemoveAgent>;
584
585    /// Create an UpdateParty message for this object (TAIP-6).
586    ///
587    /// # Arguments
588    /// * `creator_did` - The DID of the agent creating this update
589    /// * `party_type` - The type of party ("originator", "beneficiary", etc.)
590    /// * `party` - The party information to update
591    fn update_party(
592        &self,
593        creator_did: &str,
594        party_type: &str,
595        party: Party,
596    ) -> PlainMessage<UpdateParty>;
597
598    /// Create an UpdatePolicies message for this object (TAIP-7).
599    ///
600    /// # Arguments
601    /// * `creator_did` - The DID of the agent creating this update
602    /// * `policies` - New policies to apply
603    fn update_policies(
604        &self,
605        creator_did: &str,
606        policies: Vec<Policy>,
607    ) -> PlainMessage<UpdatePolicies>;
608
609    /// Create a ConfirmRelationship message for this object (TAIP-9).
610    ///
611    /// # Arguments
612    /// * `creator_did` - The DID of the agent creating this confirmation
613    /// * `agent_did` - The agent DID confirming their relationship
614    /// * `for_entity` - The entity this agent is acting for
615    fn confirm_relationship(
616        &self,
617        creator_did: &str,
618        agent_did: &str,
619        for_entity: &str,
620    ) -> PlainMessage<ConfirmRelationship>;
621}