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}