Skip to main content

co_didcomm/messages/
attachment.rs

1use std::convert::TryFrom;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{Error, Message, Result};
6
7/// Attachment holding structure
8///
9#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
10pub struct Attachment {
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub id: Option<String>,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub description: Option<String>,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub filename: Option<String>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub media_type: Option<String>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub format: Option<String>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub lastmod_time: Option<String>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub byte_count: Option<usize>,
25    pub data: AttachmentData,
26}
27
28/// Attachment Data holding structure
29#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
30pub struct AttachmentData {
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub jws: Option<String>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub hash: Option<String>,
35    #[serde(default)]
36    #[serde(skip_serializing_if = "Vec::is_empty")]
37    pub links: Vec<String>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub base64: Option<String>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub json: Option<String>,
42}
43
44/// Builder for `AttachmentData`
45pub struct AttachmentDataBuilder {
46    inner: AttachmentData,
47}
48
49impl AttachmentDataBuilder {
50    /// Constructor for default and empty data
51    ///
52    pub fn new() -> Self {
53        Self {
54            inner: AttachmentData::default(),
55        }
56    }
57
58    /// Attach `jws` stringified property.
59    ///
60    /// # Parameters
61    ///
62    /// * `jws` - JSON Web Signature serialized into String
63    ///
64    pub fn with_jws(mut self, jws: &str) -> Self {
65        self.inner.jws = Some(jws.into());
66        self
67    }
68
69    /// [optional] The hash of the content encoded in multi-hash format.
70    /// Used as an integrity check for the attachment,
71    ///  and MUST be used if the data is referenced via the links data attribute.
72    ///
73    /// # Parameters
74    ///
75    /// * `hash` - String of hash to be attached
76    ///
77    pub fn with_hash(mut self, hash: &str) -> Self {
78        self.inner.hash = Some(hash.into());
79        self
80    }
81
82    /// [optional] A list of zero or more locations at which the content may be fetched.
83    /// Adds one link into list of links. No uniqueness is guarranteed.
84    ///
85    /// # Parameters
86    ///
87    /// * `link` - String representation of where to fetch the attachment
88    ///
89    pub fn with_link(mut self, link: &str) -> Self {
90        self.inner.links.push(link.into());
91        self
92    }
93
94    /// Raw bytes of the payload to be attached - will be BASE64URL encoded
95    ///  before attaching.
96    ///
97    /// # Parameters
98    ///
99    /// * `payload` - set of bytes to be attached as payload
100    ///
101    pub fn with_raw_payload(mut self, payload: impl AsRef<[u8]>) -> Self {
102        self.inner.base64 = Some(base64_url::encode(payload.as_ref()));
103        self
104    }
105
106    /// Same as `with_raw_payload`, but data is already encoded
107    ///
108    /// # Parameters
109    ///
110    /// * `payload` - BASE64URL encoded bytes of payload
111    ///
112    pub fn with_encoded_payload(mut self, payload: &str) -> Self {
113        self.inner.base64 = Some(payload.into());
114        self
115    }
116
117    /// Attach stringified JSON object
118    ///
119    /// # Parameters
120    ///
121    /// * `stringified` - String of JSON object
122    ///
123    pub fn with_json(mut self, stringified: &str) -> Self {
124        self.inner.json = Some(stringified.into());
125        self
126    }
127
128    fn finalize(self) -> AttachmentData {
129        self.inner
130    }
131}
132
133impl Default for AttachmentDataBuilder {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139/// Builder of attachment metadata and payload.
140/// Used to construct and inject Attachment into `Message`
141///
142pub struct AttachmentBuilder {
143    inner: Attachment,
144    timed: bool,
145}
146
147impl AttachmentBuilder {
148    /// Constructor of new instance of the builder.
149    ///
150    /// # Parameters
151    ///
152    /// * `included_mod_time` - `bool` value indicating
153    /// if this attachment should be timestamped on attaching.
154    /// If `true` - will update `lastmod_time` property on
155    /// builder consumption.
156    ///
157    pub fn new(include_mod_time: bool) -> Self {
158        Self {
159            inner: Attachment::default(),
160            timed: include_mod_time,
161        }
162    }
163
164    /// Optional, but recommended identifier of attachment content.
165    ///
166    /// # Parameters
167    ///
168    /// * `id` - String of `Attachment` identifier
169    ///
170    pub fn with_id(mut self, id: &str) -> Self {
171        self.inner.id = Some(id.into());
172        self
173    }
174
175    /// Human redable description string
176    ///
177    /// # Parameters
178    ///
179    /// * `description` - String of description for this `Attachment`
180    ///
181    pub fn with_description(mut self, description: &str) -> Self {
182        self.inner.description = Some(description.into());
183        self
184    }
185
186    /// Attachment file name specifier.
187    ///
188    /// # Parameters
189    ///
190    /// * `filename` - name of the file attached
191    ///
192    pub fn with_filename(mut self, filename: &str) -> Self {
193        self.inner.filename = Some(filename.into());
194        self
195    }
196
197    /// Describes the media (MIME) type of the attached content
198    ///
199    /// # Parameters
200    ///
201    /// * `media_type` - String of media type description
202    ///
203    pub fn with_media_type(mut self, media_type: &str) -> Self {
204        self.inner.media_type = Some(media_type.into());
205        self
206    }
207
208    /// Describes the format of the attachment if the `media_type` is not sufficient.
209    ///
210    /// # Parameters
211    ///
212    /// * `format` - String format identifier
213    ///
214    pub fn with_format(mut self, format: &str) -> Self {
215        self.inner.format = Some(format.into());
216        self
217    }
218
219    /// mostly relevant when content is included by reference instead of by value.
220    /// Lets the receiver guess how expensive it will be, in time, bandwidth, and storage, to fully fetch the attachment.
221    ///
222    /// # Parameters
223    ///
224    /// * `bytes` - usize of bytes.
225    ///
226    pub fn external_size(mut self, bytes: usize) -> Self {
227        self.inner.byte_count = Some(bytes);
228        self
229    }
230
231    /// Attach actual payload in form of `AttachmentData`
232    /// Consumes `AttachmentDataBuilder` to do so.
233    ///
234    /// # Parameters
235    ///
236    /// * `attachment_data` - 'AttachmentDataBuilder' instance, prepopulated.
237    ///
238    pub fn with_data(mut self, attachment_data: AttachmentDataBuilder) -> Self {
239        self.inner.data = attachment_data.finalize();
240        self
241    }
242
243    fn timestamp(&mut self) {
244        if self.timed {
245            self.inner.lastmod_time = Some(chrono::Utc::now().to_string());
246        }
247    }
248
249    fn finalize(mut self) -> Attachment {
250        self.timestamp();
251        self.inner
252    }
253}
254
255impl<T> TryFrom<(&str, T)> for AttachmentBuilder
256where
257    T: Serialize,
258{
259    type Error = Error;
260    fn try_from((format, data): (&str, T)) -> std::result::Result<Self, Self::Error> {
261        let serialized = serde_json::to_string(&data)?;
262        let builder = AttachmentBuilder::new(true)
263            .with_media_type("application/json")
264            .with_format(format)
265            .with_data(AttachmentDataBuilder::new().with_json(&serialized));
266        Ok(builder)
267    }
268}
269
270impl Message {
271    /// Appends attachment into `attachments` field.
272    /// Consumes instance of `AttachmentBuilder` to do so.
273    ///
274    /// # Parameters
275    ///
276    /// * `builder` - prepopulated instance of `AttachmentBuilder`
277    ///
278    pub fn append_attachment(&mut self, builder: AttachmentBuilder) {
279        self.attachments.push(builder.finalize());
280    }
281
282    /// Returns iterator of all attachments.
283    pub fn attachment_iter(&self) -> impl DoubleEndedIterator<Item = &Attachment> {
284        self.attachments.iter()
285    }
286
287    /// Deserializes a the attachements with media-type `fmt` into `Vec<T>`.
288    ///
289    /// # Error:
290    /// It returns an error if media type is not `application/json` or if the media is invalid JSON document.
291    pub fn deserialize_attachments<'de, T>(&'de self, fmt: &str) -> Result<Vec<T>>
292    where
293        T: Deserialize<'de>,
294    {
295        if fmt != "application/json" {
296            return Err(Error::AttachmentError("unsupported media type".into()));
297        }
298
299        self.attachments
300            .iter()
301            .filter(|&att| att.format == Some(fmt.into()))
302            .map(|attachment| match attachment.media_type {
303                Some(ref media_type) if media_type == "application/json" => {
304                    match &attachment.data.json {
305                        Some(json) => serde_json::from_str(json).map_err(Error::SerdeError),
306                        None if attachment.id.is_some() => Err(Error::AttachmentError(format!(
307                            "attachment with id {} contains invalid JSON data",
308                            attachment.id.clone().unwrap()
309                        ))),
310                        _ => Err(Error::AttachmentError("NO ATTACHMENT ID".into())),
311                    }
312                }
313                _ => Err(Error::AttachmentError("unsupported media type".into()))?,
314            })
315            .collect()
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use serde::{Deserialize, Serialize};
322
323    use super::Message;
324    use super::*;
325
326    #[derive(Serialize, Deserialize, Debug)]
327    struct Data;
328
329    #[test]
330    fn try_from_successfully_creates_builder() {
331        for (&format, data) in [
332            "dif/presentation-exchange/definitions@v1.0",
333            "dif/presentation-exchange/submission@v1.0",
334        ]
335        .iter()
336        .zip([Data, Data])
337        {
338            let builder = AttachmentBuilder::try_from((format, data));
339            assert!(builder.is_ok(), "failed to create builder");
340        }
341    }
342
343    #[test]
344    fn deserialize_json_formatteed_attachments_successfully() {
345        let mut message = Message::new();
346        let builder = AttachmentBuilder::try_from(("application/json", Data))
347            .expect("failed to create builder");
348        message.append_attachment(builder);
349        let data: Vec<Data> = message
350            .deserialize_attachments("application/json")
351            .expect("failed to get attachments");
352        assert_eq!(data.len(), 1)
353    }
354
355    #[test]
356    #[should_panic(expected = "unsupported media type")]
357    fn cannot_deserialize_attachments_with_invalid_format() {
358        let mut message = Message::new();
359        let builder = AttachmentBuilder::try_from(("application/json", Data))
360            .expect("failed to create builder");
361        message.append_attachment(builder);
362        message
363            .deserialize_attachments::<Data>("application/yaml")
364            .unwrap();
365    }
366}