tap_msg/
didcomm.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4
5/// Wrapper for plain message. Provides helpers for message building and packing/unpacking.
6/// Adapted from https://github.com/sicpa-dlab/didcomm-rust/blob/main/src/message/message.rs
7#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
8pub struct PlainMessage {
9    /// Message id. Must be unique to the sender.
10    pub id: String,
11
12    /// Optional, if present it must be "application/didcomm-plain+json"
13    #[serde(default = "default_typ")]
14    pub typ: String,
15
16    /// Message type attribute value MUST be a valid Message Type URI,
17    /// that when resolved gives human readable information about the message.
18    /// The attribute’s value also informs the content of the message,
19    /// or example the presence of other attributes and how they should be processed.
20    #[serde(rename = "type")]
21    pub type_: String,
22
23    /// Message body.
24    pub body: Value,
25
26    /// Sender identifier. The from attribute MUST be a string that is a valid DID
27    /// or DID URL (without the fragment component) which identifies the sender of the message.
28    pub from: String,
29
30    /// Identifier(s) for recipients. MUST be an array of strings where each element
31    /// is a valid DID or DID URL (without the fragment component) that identifies a member
32    /// of the message’s intended audience.
33    pub to: Vec<String>,
34
35    /// Uniquely identifies the thread that the message belongs to.
36    /// If not included the id property of the message MUST be treated as the value of the `thid`.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub thid: Option<String>,
39
40    /// If the message is a child of a thread the `pthid`
41    /// will uniquely identify which thread is the parent.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub pthid: Option<String>,
44
45    /// Custom message headers.
46    #[serde(flatten)]
47    #[serde(skip_serializing_if = "HashMap::is_empty")]
48    pub extra_headers: HashMap<String, Value>,
49
50    /// The attribute is used for the sender
51    /// to express when they created the message, expressed in
52    /// UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z UTC).
53    /// This attribute is informative to the recipient, and may be relied on by protocols.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub created_time: Option<u64>,
56
57    /// The expires_time attribute is used for the sender to express when they consider
58    /// the message to be expired, expressed in UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z UTC).
59    /// This attribute signals when the message is considered no longer valid by the sender.
60    /// When omitted, the message is considered to have no expiration by the sender.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub expires_time: Option<u64>,
63
64    /// from_prior is a compactly serialized signed JWT containing FromPrior value
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub from_prior: Option<String>,
67
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub attachments: Option<Vec<Attachment>>,
70}
71
72const PLAINTEXT_TYP: &str = "application/didcomm-plain+json";
73
74fn default_typ() -> String {
75    PLAINTEXT_TYP.to_string()
76}
77
78/// Message for out-of-band invitations (TAIP-2).
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct OutOfBand {
81    /// Goal code for the invitation.
82    #[serde(rename = "goal_code")]
83    pub goal_code: String,
84
85    /// Invitation message ID.
86    pub id: String,
87
88    /// Label for the invitation.
89    pub label: String,
90
91    /// Accept option for the invitation.
92    pub accept: Option<String>,
93
94    /// The DIDComm services to connect to.
95    pub services: Vec<serde_json::Value>,
96}
97
98/// Simple attachment data for a TAP message.
99///
100/// This structure represents a simplified version of attachment data
101/// that directly contains base64 or JSON without the complexity of the
102/// full AttachmentData enum.
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
104pub struct SimpleAttachmentData {
105    /// Base64-encoded data.
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub base64: Option<String>,
108
109    /// JSON data.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub json: Option<serde_json::Value>,
112}
113
114/// Attachment for a TAP message.
115#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
116pub struct Attachment {
117    /// A JSON object that gives access to the actual content of the attachment.
118    /// Can be based on base64, json or external links.
119    pub data: AttachmentData,
120
121    /// Identifies attached content within the scope of a given message.
122    ///  Recommended on appended attachment descriptors. Possible but generally unused
123    ///  on embedded attachment descriptors. Never required if no references to the attachment
124    ///  exist; if omitted, then there is no way to refer to the attachment later in the thread,
125    ///  in error messages, and so forth. Because id is used to compose URIs, it is recommended
126    ///  that this name be brief and avoid spaces and other characters that require URI escaping.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub id: Option<String>,
129
130    /// A human-readable description of the content.
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub description: Option<String>,
133
134    /// A hint about the name that might be used if this attachment is persisted as a file.
135    /// It is not required, and need not be unique. If this field is present and mime-type is not,
136    /// the extension on the filename may be used to infer a MIME type.
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub filename: Option<String>,
139
140    /// Describes the MIME type of the attached content.
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub media_type: Option<String>,
143
144    /// Describes the format of the attachment if the mime_type is not sufficient.
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub format: Option<String>,
147
148    /// A hint about when the content in this attachment was last modified
149    /// in UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z UTC).
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub lastmod_time: Option<u64>,
152
153    /// Mostly relevant when content is included by reference instead of by value.
154    /// Lets the receiver guess how expensive it will be, in time, bandwidth, and storage,
155    /// to fully fetch the attachment.
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub byte_count: Option<u64>,
158}
159
160impl Attachment {
161    pub fn base64(base64: String) -> AttachmentBuilder {
162        AttachmentBuilder::new(AttachmentData::Base64 {
163            value: Base64AttachmentData { base64, jws: None },
164        })
165    }
166
167    pub fn json(json: Value) -> AttachmentBuilder {
168        AttachmentBuilder::new(AttachmentData::Json {
169            value: JsonAttachmentData { json, jws: None },
170        })
171    }
172
173    pub fn links(links: Vec<String>, hash: String) -> AttachmentBuilder {
174        AttachmentBuilder::new(AttachmentData::Links {
175            value: LinksAttachmentData {
176                links,
177                hash,
178                jws: None,
179            },
180        })
181    }
182}
183
184pub struct AttachmentBuilder {
185    data: AttachmentData,
186    id: Option<String>,
187    description: Option<String>,
188    filename: Option<String>,
189    media_type: Option<String>,
190    format: Option<String>,
191    lastmod_time: Option<u64>,
192    byte_count: Option<u64>,
193}
194
195impl AttachmentBuilder {
196    fn new(data: AttachmentData) -> Self {
197        AttachmentBuilder {
198            data,
199            id: None,
200            description: None,
201            filename: None,
202            media_type: None,
203            format: None,
204            lastmod_time: None,
205            byte_count: None,
206        }
207    }
208
209    pub fn id(mut self, id: String) -> Self {
210        self.id = Some(id);
211        self
212    }
213
214    pub fn description(mut self, description: String) -> Self {
215        self.description = Some(description);
216        self
217    }
218
219    pub fn filename(mut self, filename: String) -> Self {
220        self.filename = Some(filename);
221        self
222    }
223
224    pub fn media_type(mut self, media_type: String) -> Self {
225        self.media_type = Some(media_type);
226        self
227    }
228
229    pub fn format(mut self, format: String) -> Self {
230        self.format = Some(format);
231        self
232    }
233
234    pub fn lastmod_time(mut self, lastmod_time: u64) -> Self {
235        self.lastmod_time = Some(lastmod_time);
236        self
237    }
238
239    pub fn byte_count(mut self, byte_count: u64) -> Self {
240        self.byte_count = Some(byte_count);
241        self
242    }
243
244    pub fn jws(mut self, jws: String) -> Self {
245        match self.data {
246            AttachmentData::Base64 { ref mut value } => value.jws = Some(jws),
247            AttachmentData::Json { ref mut value } => value.jws = Some(jws),
248            AttachmentData::Links { ref mut value } => value.jws = Some(jws),
249        }
250
251        self
252    }
253
254    pub fn finalize(self) -> Attachment {
255        Attachment {
256            data: self.data,
257            id: self.id,
258            description: self.description,
259            filename: self.filename,
260            media_type: self.media_type,
261            format: self.format,
262            lastmod_time: self.lastmod_time,
263            byte_count: self.byte_count,
264        }
265    }
266}
267
268// Attention: we are using untagged enum serialization variant.
269// Serde will try to match the data against each variant in order and the
270// first one that deserializes successfully is the one returned.
271// It should work as we always have discrimination here.
272
273/// Represents attachment data in Base64, embedded Json or Links form.
274#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
275#[serde(untagged)]
276pub enum AttachmentData {
277    Base64 {
278        #[serde(flatten)]
279        value: Base64AttachmentData,
280    },
281    Json {
282        #[serde(flatten)]
283        value: JsonAttachmentData,
284    },
285    Links {
286        #[serde(flatten)]
287        value: LinksAttachmentData,
288    },
289}
290
291#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
292pub struct Base64AttachmentData {
293    /// Base64-encoded data, when representing arbitrary content inline.
294    pub base64: String,
295
296    /// A JSON Web Signature over the content of the attachment.
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub jws: Option<String>,
299}
300
301#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
302pub struct JsonAttachmentData {
303    /// Directly embedded JSON data.
304    pub json: Value,
305
306    /// A JSON Web Signature over the content of the attachment.
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub jws: Option<String>,
309}
310
311#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
312pub struct LinksAttachmentData {
313    /// A list of one or more locations at which the content may be fetched.
314    pub links: Vec<String>,
315
316    /// The hash of the content encoded in multi-hash format. Used as an integrity check for the attachment.
317    pub hash: String,
318
319    /// A JSON Web Signature over the content of the attachment.
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub jws: Option<String>,
322}
323
324#[cfg(test)]
325mod tests {
326    use core::panic;
327    use serde_json::json;
328
329    use super::*;
330
331    #[test]
332    fn attachment_base64_works() {
333        let attachment = Attachment::base64("ZXhhbXBsZQ==".to_owned())
334            .id("example-1".to_owned())
335            .description("example-1-description".to_owned())
336            .filename("attachment-1".to_owned())
337            .media_type("message/example".to_owned())
338            .format("json".to_owned())
339            .lastmod_time(10000)
340            .byte_count(200)
341            .jws("jws".to_owned())
342            .finalize();
343
344        let data = match attachment.data {
345            AttachmentData::Base64 { ref value } => value,
346            _ => panic!("data isn't base64."),
347        };
348
349        assert_eq!(data.base64, "ZXhhbXBsZQ==");
350        assert_eq!(data.jws, Some("jws".to_owned()));
351        assert_eq!(attachment.id, Some("example-1".to_owned()));
352
353        assert_eq!(
354            attachment.description,
355            Some("example-1-description".to_owned())
356        );
357
358        assert_eq!(attachment.filename, Some("attachment-1".to_owned()));
359        assert_eq!(attachment.media_type, Some("message/example".to_owned()));
360        assert_eq!(attachment.format, Some("json".to_owned()));
361        assert_eq!(attachment.lastmod_time, Some(10000));
362        assert_eq!(attachment.byte_count, Some(200));
363    }
364
365    #[test]
366    fn attachment_json_works() {
367        let attachment = Attachment::json(json!("example"))
368            .id("example-1".to_owned())
369            .description("example-1-description".to_owned())
370            .filename("attachment-1".to_owned())
371            .media_type("message/example".to_owned())
372            .format("json".to_owned())
373            .lastmod_time(10000)
374            .byte_count(200)
375            .jws("jws".to_owned())
376            .finalize();
377
378        let data = match attachment.data {
379            AttachmentData::Json { ref value } => value,
380            _ => panic!("data isn't json."),
381        };
382
383        assert_eq!(data.json, json!("example"));
384        assert_eq!(data.jws, Some("jws".to_owned()));
385        assert_eq!(attachment.id, Some("example-1".to_owned()));
386
387        assert_eq!(
388            attachment.description,
389            Some("example-1-description".to_owned())
390        );
391
392        assert_eq!(attachment.filename, Some("attachment-1".to_owned()));
393        assert_eq!(attachment.media_type, Some("message/example".to_owned()));
394        assert_eq!(attachment.format, Some("json".to_owned()));
395        assert_eq!(attachment.lastmod_time, Some(10000));
396        assert_eq!(attachment.byte_count, Some(200));
397    }
398
399    #[test]
400    fn attachment_links_works() {
401        let attachment = Attachment::links(
402            vec!["http://example1".to_owned(), "https://example2".to_owned()],
403            "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c".to_owned(),
404        )
405        .id("example-1".to_owned())
406        .description("example-1-description".to_owned())
407        .filename("attachment-1".to_owned())
408        .media_type("message/example".to_owned())
409        .format("json".to_owned())
410        .lastmod_time(10000)
411        .byte_count(200)
412        .jws("jws".to_owned())
413        .finalize();
414
415        let data = match attachment.data {
416            AttachmentData::Links { ref value } => value,
417            _ => panic!("data isn't links."),
418        };
419
420        assert_eq!(
421            data.links,
422            vec!["http://example1".to_owned(), "https://example2".to_owned()]
423        );
424
425        assert_eq!(
426            data.hash,
427            "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c".to_owned()
428        );
429
430        assert_eq!(data.jws, Some("jws".to_owned()));
431        assert_eq!(attachment.id, Some("example-1".to_owned()));
432
433        assert_eq!(
434            attachment.description,
435            Some("example-1-description".to_owned())
436        );
437
438        assert_eq!(attachment.filename, Some("attachment-1".to_owned()));
439        assert_eq!(attachment.media_type, Some("message/example".to_owned()));
440        assert_eq!(attachment.format, Some("json".to_owned()));
441        assert_eq!(attachment.lastmod_time, Some(10000));
442        assert_eq!(attachment.byte_count, Some(200));
443    }
444}