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