use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
#[serde(bound = "T: Serialize + serde::de::DeserializeOwned")]
pub struct PlainMessage<T = Value> {
pub id: String,
#[serde(default = "default_typ")]
pub typ: String,
#[serde(rename = "type")]
pub type_: String,
pub body: T,
pub from: String,
#[serde(default)]
pub to: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pthid: Option<String>,
#[serde(flatten)]
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub extra_headers: HashMap<String, Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_time: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_time: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from_prior: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<Attachment>>,
}
pub type UntypedPlainMessage = PlainMessage<Value>;
const PLAINTEXT_TYP: &str = "application/didcomm-plain+json";
fn default_typ() -> String {
PLAINTEXT_TYP.to_string()
}
impl<T> PlainMessage<T>
where
T: serde::Serialize + serde::de::DeserializeOwned,
{
pub fn new(id: String, type_: String, body: T, from: String) -> Self {
Self {
id,
typ: default_typ(),
type_,
body,
from,
to: vec![],
thid: None,
pthid: None,
created_time: Some(chrono::Utc::now().timestamp() as u64),
expires_time: None,
from_prior: None,
attachments: None,
extra_headers: HashMap::new(),
}
}
pub fn with_recipients(mut self, to: Vec<String>) -> Self {
self.to = to;
self
}
pub fn with_recipient(mut self, recipient: &str) -> Self {
self.to.push(recipient.to_string());
self
}
pub fn with_thread_id(mut self, thid: Option<String>) -> Self {
self.thid = thid;
self
}
pub fn with_parent_thread_id(mut self, pthid: Option<String>) -> Self {
self.pthid = pthid;
self
}
pub fn with_expires_at(mut self, expires_time: u64) -> Self {
self.expires_time = Some(expires_time);
self
}
pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
self.attachments = Some(attachments);
self
}
pub fn with_header(mut self, key: String, value: Value) -> Self {
self.extra_headers.insert(key, value);
self
}
}
impl<T> PlainMessage<T>
where
T: crate::message::TapMessageBody + serde::Serialize + serde::de::DeserializeOwned,
{
pub fn new_typed(body: T, from: &str) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
typ: default_typ(),
type_: T::message_type().to_string(),
body,
from: from.to_string(),
to: vec![],
thid: None,
pthid: None,
created_time: Some(chrono::Utc::now().timestamp() as u64),
expires_time: None,
from_prior: None,
attachments: None,
extra_headers: HashMap::new(),
}
}
pub fn to_plain_message(self) -> crate::error::Result<PlainMessage<Value>> {
let mut body_value = serde_json::to_value(&self.body)?;
if let Some(body_obj) = body_value.as_object_mut() {
body_obj.insert(
"@type".to_string(),
Value::String(T::message_type().to_string()),
);
}
Ok(PlainMessage {
id: self.id,
typ: self.typ,
type_: self.type_,
body: body_value,
from: self.from,
to: self.to,
thid: self.thid,
pthid: self.pthid,
created_time: self.created_time,
expires_time: self.expires_time,
from_prior: self.from_prior,
attachments: self.attachments,
extra_headers: self.extra_headers,
})
}
pub fn extract_participants(&self) -> Vec<String> {
let mut participants = vec![];
if let Some(ctx_participants) = self.try_extract_from_context() {
participants = ctx_participants;
} else {
if let Ok(plain_msg) = self.body.to_didcomm(&self.from) {
participants = plain_msg.to;
}
}
for recipient in &self.to {
if !participants.contains(recipient) {
participants.push(recipient.clone());
}
}
participants
}
fn try_extract_from_context(&self) -> Option<Vec<String>> {
None
}
}
impl<T> PlainMessage<T>
where
T: crate::message::TapMessageBody
+ crate::message::MessageContext
+ serde::Serialize
+ serde::de::DeserializeOwned,
{
pub fn extract_participants_with_context(&self) -> Vec<String> {
self.body.participant_dids()
}
pub fn new_typed_with_context(body: T, from: &str) -> Self {
let participants = body.participant_dids();
Self {
id: uuid::Uuid::new_v4().to_string(),
typ: default_typ(),
type_: T::message_type().to_string(),
body,
from: from.to_string(),
to: participants.into_iter().filter(|did| did != from).collect(),
thid: None,
pthid: None,
created_time: Some(chrono::Utc::now().timestamp() as u64),
expires_time: None,
from_prior: None,
attachments: None,
extra_headers: HashMap::new(),
}
}
pub fn routing_hints(&self) -> crate::message::RoutingHints {
self.body.routing_hints()
}
pub fn transaction_context(&self) -> Option<crate::message::TransactionContext> {
self.body.transaction_context()
}
}
impl PlainMessage<Value> {
pub fn from_untyped(plain_msg: PlainMessage<Value>) -> Self {
plain_msg
}
pub fn parse_body<T: crate::message::TapMessageBody>(
self,
) -> crate::error::Result<PlainMessage<T>> {
if self.type_ != T::message_type() {
return Err(crate::error::Error::Validation(format!(
"Type mismatch: expected {}, got {}",
T::message_type(),
self.type_
)));
}
let typed_body: T = serde_json::from_value(self.body)?;
Ok(PlainMessage {
id: self.id,
typ: self.typ,
type_: self.type_,
body: typed_body,
from: self.from,
to: self.to,
thid: self.thid,
pthid: self.pthid,
created_time: self.created_time,
expires_time: self.expires_time,
from_prior: self.from_prior,
attachments: self.attachments,
extra_headers: self.extra_headers,
})
}
pub fn parse_tap_message(
&self,
) -> crate::error::Result<crate::message::tap_message_enum::TapMessage> {
crate::message::tap_message_enum::TapMessage::from_plain_message(self)
}
}
pub trait PlainMessageExt<T> {
fn into_typed(self) -> PlainMessage<T>;
fn parse_as<U: crate::message::TapMessageBody>(self) -> crate::error::Result<PlainMessage<U>>;
}
impl PlainMessageExt<Value> for PlainMessage<Value> {
fn into_typed(self) -> PlainMessage<Value> {
self
}
fn parse_as<U: crate::message::TapMessageBody>(self) -> crate::error::Result<PlainMessage<U>> {
self.parse_body()
}
}
impl<T: crate::message::TapMessageBody> TryFrom<PlainMessage<T>>
for crate::message::tap_message_enum::TapMessage
where
crate::message::tap_message_enum::TapMessage: From<T>,
{
type Error = crate::error::Error;
fn try_from(typed: PlainMessage<T>) -> crate::error::Result<Self> {
typed.to_plain_message()?.parse_tap_message()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutOfBand {
#[serde(rename = "goal_code")]
pub goal_code: String,
pub id: String,
pub label: String,
pub accept: Option<String>,
pub services: Vec<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SimpleAttachmentData {
#[serde(skip_serializing_if = "Option::is_none")]
pub base64: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub json: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct Attachment {
pub data: AttachmentData,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lastmod_time: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub byte_count: Option<u64>,
}
impl Attachment {
pub fn base64(base64: String) -> AttachmentBuilder {
AttachmentBuilder::new(AttachmentData::Base64 {
value: Base64AttachmentData { base64, jws: None },
})
}
pub fn json(json: Value) -> AttachmentBuilder {
AttachmentBuilder::new(AttachmentData::Json {
value: JsonAttachmentData { json, jws: None },
})
}
pub fn links(links: Vec<String>, hash: String) -> AttachmentBuilder {
AttachmentBuilder::new(AttachmentData::Links {
value: LinksAttachmentData {
links,
hash,
jws: None,
},
})
}
}
pub struct AttachmentBuilder {
data: AttachmentData,
id: Option<String>,
description: Option<String>,
filename: Option<String>,
media_type: Option<String>,
format: Option<String>,
lastmod_time: Option<u64>,
byte_count: Option<u64>,
}
impl AttachmentBuilder {
fn new(data: AttachmentData) -> Self {
AttachmentBuilder {
data,
id: None,
description: None,
filename: None,
media_type: None,
format: None,
lastmod_time: None,
byte_count: None,
}
}
pub fn id(mut self, id: String) -> Self {
self.id = Some(id);
self
}
pub fn description(mut self, description: String) -> Self {
self.description = Some(description);
self
}
pub fn filename(mut self, filename: String) -> Self {
self.filename = Some(filename);
self
}
pub fn media_type(mut self, media_type: String) -> Self {
self.media_type = Some(media_type);
self
}
pub fn format(mut self, format: String) -> Self {
self.format = Some(format);
self
}
pub fn lastmod_time(mut self, lastmod_time: u64) -> Self {
self.lastmod_time = Some(lastmod_time);
self
}
pub fn byte_count(mut self, byte_count: u64) -> Self {
self.byte_count = Some(byte_count);
self
}
pub fn jws(mut self, jws: String) -> Self {
match self.data {
AttachmentData::Base64 { ref mut value } => value.jws = Some(jws),
AttachmentData::Json { ref mut value } => value.jws = Some(jws),
AttachmentData::Links { ref mut value } => value.jws = Some(jws),
}
self
}
pub fn finalize(self) -> Attachment {
Attachment {
data: self.data,
id: self.id,
description: self.description,
filename: self.filename,
media_type: self.media_type,
format: self.format,
lastmod_time: self.lastmod_time,
byte_count: self.byte_count,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
#[serde(untagged)]
pub enum AttachmentData {
Base64 {
#[serde(flatten)]
value: Base64AttachmentData,
},
Json {
#[serde(flatten)]
value: JsonAttachmentData,
},
Links {
#[serde(flatten)]
value: LinksAttachmentData,
},
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct Base64AttachmentData {
pub base64: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub jws: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct JsonAttachmentData {
pub json: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub jws: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct LinksAttachmentData {
pub links: Vec<String>,
pub hash: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub jws: Option<String>,
}
#[cfg(test)]
mod tests {
use core::panic;
use serde_json::json;
use super::*;
#[test]
fn attachment_base64_works() {
let attachment = Attachment::base64("ZXhhbXBsZQ==".to_owned())
.id("example-1".to_owned())
.description("example-1-description".to_owned())
.filename("attachment-1".to_owned())
.media_type("message/example".to_owned())
.format("json".to_owned())
.lastmod_time(10000)
.byte_count(200)
.jws("jws".to_owned())
.finalize();
let data = match attachment.data {
AttachmentData::Base64 { ref value } => value,
_ => panic!("data isn't base64."),
};
assert_eq!(data.base64, "ZXhhbXBsZQ==");
assert_eq!(data.jws, Some("jws".to_owned()));
assert_eq!(attachment.id, Some("example-1".to_owned()));
assert_eq!(
attachment.description,
Some("example-1-description".to_owned())
);
assert_eq!(attachment.filename, Some("attachment-1".to_owned()));
assert_eq!(attachment.media_type, Some("message/example".to_owned()));
assert_eq!(attachment.format, Some("json".to_owned()));
assert_eq!(attachment.lastmod_time, Some(10000));
assert_eq!(attachment.byte_count, Some(200));
}
#[test]
fn attachment_json_works() {
let attachment = Attachment::json(json!("example"))
.id("example-1".to_owned())
.description("example-1-description".to_owned())
.filename("attachment-1".to_owned())
.media_type("message/example".to_owned())
.format("json".to_owned())
.lastmod_time(10000)
.byte_count(200)
.jws("jws".to_owned())
.finalize();
let data = match attachment.data {
AttachmentData::Json { ref value } => value,
_ => panic!("data isn't json."),
};
assert_eq!(data.json, json!("example"));
assert_eq!(data.jws, Some("jws".to_owned()));
assert_eq!(attachment.id, Some("example-1".to_owned()));
assert_eq!(
attachment.description,
Some("example-1-description".to_owned())
);
assert_eq!(attachment.filename, Some("attachment-1".to_owned()));
assert_eq!(attachment.media_type, Some("message/example".to_owned()));
assert_eq!(attachment.format, Some("json".to_owned()));
assert_eq!(attachment.lastmod_time, Some(10000));
assert_eq!(attachment.byte_count, Some(200));
}
#[test]
fn attachment_links_works() {
let attachment = Attachment::links(
vec!["http://example1".to_owned(), "https://example2".to_owned()],
"50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c".to_owned(),
)
.id("example-1".to_owned())
.description("example-1-description".to_owned())
.filename("attachment-1".to_owned())
.media_type("message/example".to_owned())
.format("json".to_owned())
.lastmod_time(10000)
.byte_count(200)
.jws("jws".to_owned())
.finalize();
let data = match attachment.data {
AttachmentData::Links { ref value } => value,
_ => panic!("data isn't links."),
};
assert_eq!(
data.links,
vec!["http://example1".to_owned(), "https://example2".to_owned()]
);
assert_eq!(
data.hash,
"50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c".to_owned()
);
assert_eq!(data.jws, Some("jws".to_owned()));
assert_eq!(attachment.id, Some("example-1".to_owned()));
assert_eq!(
attachment.description,
Some("example-1-description".to_owned())
);
assert_eq!(attachment.filename, Some("attachment-1".to_owned()));
assert_eq!(attachment.media_type, Some("message/example".to_owned()));
assert_eq!(attachment.format, Some("json".to_owned()));
assert_eq!(attachment.lastmod_time, Some(10000));
assert_eq!(attachment.byte_count, Some(200));
}
}