tap_msg/message/
did_presentation.rs

1//! DIDComm Presentation types for TAP messages.
2//!
3//! This module defines the structure of DIDComm presentation messages used in TAP.
4
5use serde::{Deserialize, Serialize};
6
7use crate::didcomm::Attachment;
8use crate::didcomm::PlainMessage;
9use crate::error::{Error, Result};
10use crate::message::tap_message_trait::TapMessageBody;
11use chrono::Utc;
12
13/// DIDComm Presentation message body.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DIDCommPresentation {
16    /// The format of the presentation (simplified from AttachmentFormat).
17    pub formats: Vec<String>,
18
19    /// Attachments containing the presentation data.
20    pub attachments: Vec<Attachment>,
21
22    /// Thread ID for this presentation.
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub thid: Option<String>,
25}
26
27impl TapMessageBody for DIDCommPresentation {
28    fn message_type() -> &'static str {
29        "https://didcomm.org/present-proof/3.0/presentation"
30    }
31
32    fn validate(&self) -> Result<()> {
33        // Basic validation - ensure we have attachments
34        if self.attachments.is_empty() {
35            return Err(Error::Validation(
36                "Presentation must have at least one attachment".to_string(),
37            ));
38        }
39
40        // Validate that attachment ids are not empty
41        for (i, attachment) in self.attachments.iter().enumerate() {
42            if let Some(id) = &attachment.id {
43                if id.is_empty() {
44                    return Err(Error::Validation(format!(
45                        "Attachment {} has an empty ID",
46                        i
47                    )));
48                }
49            }
50        }
51
52        // Ensure formats are present and not empty
53        if self.formats.is_empty() {
54            return Err(Error::Validation(
55                "Presentation must have at least one format specified".to_string(),
56            ));
57        }
58
59        // Check attachments for required format field
60        for (i, attachment) in self.attachments.iter().enumerate() {
61            if attachment.format.is_none() {
62                return Err(Error::Validation(format!(
63                    "Attachment {} is missing the 'format' field",
64                    i
65                )));
66            }
67        }
68
69        Ok(())
70    }
71
72    fn to_didcomm(&self, from: &str) -> Result<PlainMessage> {
73        // Serialize the presentation to a JSON value
74        let body_json =
75            serde_json::to_value(self).map_err(|e| Error::SerializationError(e.to_string()))?;
76
77        let now = Utc::now().timestamp() as u64;
78
79        // Create a new Message with required fields
80        let message = PlainMessage {
81            id: uuid::Uuid::new_v4().to_string(),
82            typ: "application/didcomm-plain+json".to_string(),
83            type_: Self::message_type().to_string(),
84            body: body_json,
85            from: from.to_string(),
86            to: Vec::new(), // Recipients will be set separately
87            thid: self.thid.clone(),
88            pthid: None,
89            created_time: Some(now),
90            expires_time: None,
91            extra_headers: std::collections::HashMap::new(),
92            from_prior: None,
93            attachments: None,
94        };
95
96        Ok(message)
97    }
98
99    fn from_didcomm(message: &PlainMessage) -> Result<Self> {
100        // Validate message type
101        if message.type_ != Self::message_type() {
102            return Err(Error::InvalidMessageType(format!(
103                "Expected {} but got {}",
104                Self::message_type(),
105                message.type_
106            )));
107        }
108
109        // Extract fields from message body as Value
110        let body = message.body.clone();
111        let mut body_obj = body
112            .as_object()
113            .ok_or_else(|| Error::SerializationError("Body is not a JSON object".to_string()))?
114            .clone();
115
116        // First make sure any message-level attachments are included
117        let mut attachments_in_body = if body_obj.contains_key("attachments") {
118            match &body_obj["attachments"] {
119                serde_json::Value::Array(arr) => arr.clone(),
120                _ => Vec::new(),
121            }
122        } else {
123            Vec::new()
124        };
125
126        // Then add any top-level message attachments
127        if let Some(msg_attachments) = &message.attachments {
128            // Convert message attachments to value and combine with body attachments
129            if let Ok(serde_json::Value::Array(arr)) = serde_json::to_value(msg_attachments) {
130                attachments_in_body.extend(arr);
131            }
132        }
133
134        // Update the body with combined attachments
135        body_obj.insert(
136            "attachments".to_string(),
137            serde_json::Value::Array(attachments_in_body.clone()),
138        );
139
140        // Handle missing formats field for backwards compatibility with test vectors
141        if !body_obj.contains_key("formats") {
142            // Extract formats from attachments if possible
143            let mut formats = Vec::new();
144            for attachment in &attachments_in_body {
145                if let Some(format) = attachment.get("format") {
146                    if let Some(format_str) = format.as_str() {
147                        formats.push(format_str.to_string());
148                    }
149                }
150            }
151
152            // If we couldn't extract formats, use a default
153            if formats.is_empty() {
154                formats.push("dif/presentation-exchange/submission@v1.0".to_string());
155            }
156
157            body_obj.insert(
158                "formats".to_string(),
159                serde_json::to_value(formats).unwrap(),
160            );
161        }
162
163        // Convert the updated body to DIDCommPresentation
164        let mut presentation: DIDCommPresentation =
165            serde_json::from_value(serde_json::Value::Object(body_obj))
166                .map_err(|e| Error::SerializationError(e.to_string()))?;
167
168        // Set thid from message if it's not already set in the presentation
169        if presentation.thid.is_none() {
170            presentation.thid = message.thid.clone();
171        }
172
173        Ok(presentation)
174    }
175}
176
177impl DIDCommPresentation {
178    /// Create a new DIDCommPresentation message.
179    pub fn new(formats: Vec<String>, attachments: Vec<Attachment>, thid: Option<String>) -> Self {
180        Self {
181            formats,
182            attachments,
183            thid,
184        }
185    }
186
187    // Import implementation already provided by the TapMessageBody trait
188}