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