didcomm/message/
message.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4
5use super::Attachment;
6use crate::error::{err_msg, ErrorKind, Result, ToResult};
7
8/// Wrapper for plain message. Provides helpers for message building and packing/unpacking.
9#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
10pub struct Message {
11    /// Message id. Must be unique to the sender.
12    pub id: String,
13
14    /// Optional, if present it must be "application/didcomm-plain+json"
15    #[serde(default = "default_typ")]
16    pub typ: String,
17
18    /// Message type attribute value MUST be a valid Message Type URI,
19    /// that when resolved gives human readable information about the message.
20    /// The attribute’s value also informs the content of the message,
21    /// or example the presence of other attributes and how they should be processed.
22    #[serde(rename = "type")]
23    pub type_: String,
24
25    /// Message body.
26    pub body: Value,
27
28    /// Sender identifier. The from attribute MUST be a string that is a valid DID
29    /// or DID URL (without the fragment component) which identifies the sender of the message.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub from: Option<String>,
32
33    /// Identifier(s) for recipients. MUST be an array of strings where each element
34    /// is a valid DID or DID URL (without the fragment component) that identifies a member
35    /// of the message’s intended audience.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub to: Option<Vec<String>>,
38
39    /// Uniquely identifies the thread that the message belongs to.
40    /// If not included the id property of the message MUST be treated as the value of the `thid`.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub thid: Option<String>,
43
44    /// If the message is a child of a thread the `pthid`
45    /// will uniquely identify which thread is the parent.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub pthid: Option<String>,
48
49    /// Custom message headers.
50    #[serde(flatten)]
51    #[serde(skip_serializing_if = "HashMap::is_empty")]
52    pub extra_headers: HashMap<String, Value>,
53
54    /// The attribute is used for the sender
55    /// to express when they created the message, expressed in
56    /// UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z UTC).
57    /// This attribute is informative to the recipient, and may be relied on by protocols.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub created_time: Option<u64>,
60
61    /// The expires_time attribute is used for the sender to express when they consider
62    /// the message to be expired, expressed in UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z UTC).
63    /// This attribute signals when the message is considered no longer valid by the sender.
64    /// When omitted, the message is considered to have no expiration by the sender.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub expires_time: Option<u64>,
67
68    /// from_prior is a compactly serialized signed JWT containing FromPrior value
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub from_prior: Option<String>,
71
72    /// Message attachments
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub attachments: Option<Vec<Attachment>>,
75}
76
77const PLAINTEXT_TYP: &str = "application/didcomm-plain+json";
78
79fn default_typ() -> String {
80    PLAINTEXT_TYP.to_string()
81}
82
83impl Message {
84    pub fn build(id: String, type_: String, body: Value) -> MessageBuilder {
85        MessageBuilder::new(id, type_, body)
86    }
87
88    pub(crate) fn from_str(s: &str) -> Result<Message> {
89        serde_json::from_str(s).to_didcomm("Unable deserialize jwm")
90    }
91
92    pub(crate) fn validate(self) -> Result<Self> {
93        if self.typ != PLAINTEXT_TYP {
94            Err(err_msg(
95                ErrorKind::Malformed,
96                format!("`typ` must be \"{}\"", PLAINTEXT_TYP),
97            ))?;
98        }
99        Ok(self)
100    }
101}
102
103pub struct MessageBuilder {
104    id: String,
105    type_: String,
106    body: Value,
107    from: Option<String>,
108    to: Option<Vec<String>>,
109    thid: Option<String>,
110    pthid: Option<String>,
111    extra_headers: HashMap<String, Value>,
112    created_time: Option<u64>,
113    expires_time: Option<u64>,
114    from_prior: Option<String>,
115    attachments: Option<Vec<Attachment>>,
116}
117
118impl MessageBuilder {
119    fn new(id: String, type_: String, body: Value) -> Self {
120        MessageBuilder {
121            id,
122            type_,
123            body,
124            from: None,
125            to: None,
126            thid: None,
127            pthid: None,
128            extra_headers: HashMap::new(),
129            created_time: None,
130            expires_time: None,
131            from_prior: None,
132            attachments: None,
133        }
134    }
135
136    pub fn to(mut self, to: String) -> Self {
137        if let Some(ref mut sto) = self.to {
138            sto.push(to);
139            self
140        } else {
141            self.to = Some(vec![to]);
142            self
143        }
144    }
145
146    pub fn to_many(mut self, to: Vec<String>) -> Self {
147        if let Some(ref mut sto) = self.to {
148            let mut to = to;
149            sto.append(&mut to);
150            self
151        } else {
152            self.to = Some(to);
153            self
154        }
155    }
156
157    pub fn from(mut self, from: String) -> Self {
158        self.from = Some(from);
159        self
160    }
161
162    pub fn thid(mut self, thid: String) -> Self {
163        self.thid = Some(thid);
164        self
165    }
166
167    pub fn pthid(mut self, pthid: String) -> Self {
168        self.pthid = Some(pthid);
169        self
170    }
171
172    pub fn header(mut self, key: String, value: Value) -> Self {
173        self.extra_headers.insert(key, value);
174        self
175    }
176
177    pub fn created_time(mut self, created_time: u64) -> Self {
178        self.created_time = Some(created_time);
179        self
180    }
181
182    pub fn expires_time(mut self, expires_time: u64) -> Self {
183        self.expires_time = Some(expires_time);
184        self
185    }
186
187    pub fn from_prior(mut self, from_prior: String) -> Self {
188        self.from_prior = Some(from_prior);
189        self
190    }
191
192    pub fn attachment(mut self, attachment: Attachment) -> Self {
193        if let Some(ref mut attachments) = self.attachments {
194            attachments.push(attachment);
195            self
196        } else {
197            self.attachments = Some(vec![attachment]);
198            self
199        }
200    }
201
202    pub fn attachments(mut self, attachments: Vec<Attachment>) -> Self {
203        if let Some(ref mut sattachments) = self.attachments {
204            let mut attachments = attachments;
205            sattachments.append(&mut attachments);
206            self
207        } else {
208            self.attachments = Some(attachments);
209            self
210        }
211    }
212
213    pub fn finalize(self) -> Message {
214        Message {
215            id: self.id,
216            typ: PLAINTEXT_TYP.to_owned(),
217            type_: self.type_,
218            body: self.body,
219            to: self.to,
220            thid: self.thid,
221            pthid: self.pthid,
222            from: self.from,
223            extra_headers: self.extra_headers,
224            created_time: self.created_time,
225            expires_time: self.expires_time,
226            from_prior: self.from_prior,
227            attachments: self.attachments,
228        }
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use serde_json::json;
235
236    use super::*;
237
238    #[test]
239    fn message_build_works() {
240        let message = Message::build(
241            "example-1".into(),
242            "example/v1".into(),
243            json!("example-body"),
244        )
245        .to("did:example:1".into())
246        .to_many(vec!["did:example:2".into(), "did:example:3".into()])
247        .from("did:example:4".into())
248        .thid("example-thread-1".into())
249        .pthid("example-parent-thread-1".into())
250        .header("example-header-1".into(), json!("example-header-1-value"))
251        .header("example-header-2".into(), json!("example-header-2-value"))
252        .created_time(10000)
253        .expires_time(20000)
254        .attachment(
255            Attachment::base64("ZXhhbXBsZQ==".into())
256                .id("attachment1".into())
257                .finalize(),
258        )
259        .attachments(vec![
260            Attachment::json(json!("example"))
261                .id("attachment2".into())
262                .finalize(),
263            Attachment::json(json!("example"))
264                .id("attachment3".into())
265                .finalize(),
266        ])
267        .finalize();
268
269        assert_eq!(message.id, "example-1");
270        assert_eq!(message.typ, "application/didcomm-plain+json");
271        assert_eq!(message.type_, "example/v1");
272        assert_eq!(message.body, json!("example-body"));
273        assert_eq!(message.from, Some("did:example:4".into()));
274        assert_eq!(message.thid, Some("example-thread-1".into()));
275        assert_eq!(message.pthid, Some("example-parent-thread-1".into()));
276        assert_eq!(message.created_time, Some(10000));
277        assert_eq!(message.expires_time, Some(20000));
278
279        assert_eq!(
280            message.to,
281            Some(vec![
282                "did:example:1".into(),
283                "did:example:2".into(),
284                "did:example:3".into()
285            ])
286        );
287
288        let extra_headers = message.extra_headers;
289        assert_eq!(extra_headers.len(), 2);
290
291        assert!(extra_headers.contains_key(&"example-header-1".to_owned()));
292
293        assert_eq!(
294            extra_headers[&"example-header-1".to_owned()],
295            "example-header-1-value"
296        );
297
298        assert!(extra_headers.contains_key(&"example-header-2".to_owned()));
299
300        assert_eq!(
301            extra_headers[&"example-header-2".to_owned()],
302            "example-header-2-value"
303        );
304
305        let attachments = message.attachments.expect("attachments is some.");
306        assert_eq!(attachments.len(), 3);
307        assert_eq!(attachments[0].id, Some("attachment1".into()));
308        assert_eq!(attachments[1].id, Some("attachment2".into()));
309        assert_eq!(attachments[2].id, Some("attachment3".into()));
310    }
311}