use base64;
use chrono::prelude::{DateTime, FixedOffset};
use failure::{Error, format_err};
use serde::ser::Serialize;
use serde_derive::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use url::{ParseError, Url};
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum ExtensionValue {
String(String),
Object(Value),
}
impl ExtensionValue {
pub fn from_string<S>(s: S) -> Self
where
S: Into<String>,
{
ExtensionValue::String(s.into())
}
pub fn from_serializable<S>(s: S) -> Result<Self, Error>
where
S: Serialize,
{
Ok(ExtensionValue::Object(serde_json::to_value(s)?))
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum Data {
StringOrBinary(String),
Object(Value),
}
impl Data {
pub fn from_string<S>(s: S) -> Self
where
S: Into<String>,
{
Data::StringOrBinary(s.into())
}
pub fn from_binary<I>(i: I) -> Self
where
I: AsRef<[u8]>,
{
Data::StringOrBinary(base64::encode(&i))
}
pub fn from_serializable<T>(v: T) -> Result<Self, Error>
where
T: Serialize,
{
Ok(Data::Object(serde_json::to_value(v)?))
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct CloudEvent {
#[serde(rename = "type")]
event_type: String,
specversion: &'static str,
source: String,
id: String,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
time: Option<DateTime<FixedOffset>>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
schemaurl: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
contenttype: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<Data>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
extensions: Option<HashMap<String, ExtensionValue>>,
}
impl CloudEvent {
pub fn event_type(&self) -> &str {
self.event_type.as_ref()
}
pub fn source(&self) -> &str {
self.source.as_ref()
}
pub fn event_id(&self) -> &str {
self.id.as_ref()
}
pub fn event_time(&self) -> Option<&DateTime<FixedOffset>> {
self.time.as_ref()
}
pub fn schema_url(&self) -> Option<&str> {
self.schemaurl.as_ref().map(|x| x.as_ref())
}
pub fn data(&self) -> Option<&Data> {
self.data.as_ref()
}
pub fn extensions(&self) -> Option<&HashMap<String, ExtensionValue>> {
self.extensions.as_ref()
}
}
#[derive(Debug)]
pub struct CloudEventBuilder {
event_type: Option<String>,
source: Option<String>,
id: Option<String>,
time: Option<String>,
schemaurl: Option<String>,
contenttype: Option<String>,
data: Option<Data>,
extensions: Option<HashMap<String, ExtensionValue>>,
}
impl CloudEventBuilder {
pub fn event_type<S: Into<String>>(mut self, s: S) -> Self {
self.event_type = Some(s.into());
self
}
pub fn source<S: Into<String>>(mut self, s: S) -> Self {
self.source = Some(s.into());
self
}
pub fn event_id<S: Into<String>>(mut self, s: S) -> Self {
self.id = Some(s.into());
self
}
pub fn time<S: Into<String>>(mut self, s: S) -> Self {
self.time = Some(s.into());
self
}
pub fn schemaurl<S: Into<String>>(mut self, s: S) -> Self {
self.schemaurl = Some(s.into());
self
}
pub fn contenttype<S: Into<String>>(mut self, s: S) -> Self {
self.contenttype = Some(s.into());
self
}
pub fn data(mut self, d: Data) -> Self {
self.data = Some(d);
self
}
pub fn extensions(mut self, e: HashMap<String, ExtensionValue>) -> Self {
self.extensions = Some(e);
self
}
pub fn build(self) -> Result<CloudEvent, Error> {
Ok(CloudEvent {
event_type: self.event_type.ok_or(format_err!("Event type is required"))?,
source: {
if let Some(x) = self.source {
let source = x;
match Url::parse(&source) {
Ok(_) | Err(ParseError::RelativeUrlWithoutBase) => source,
Err(e) => return Err(format_err!("{}", e)),
}
} else {
return Err(format_err!("Source is required"));
}
},
id: self.id.ok_or(format_err!("Event id is required"))?,
time: {
if let Some(t) = self.time {
Some(DateTime::parse_from_rfc3339(&t)?)
} else {
None
}
},
contenttype: self.contenttype,
data: self.data,
extensions: self.extensions,
schemaurl: {
if let Some(x) = self.schemaurl {
let schemaurl = x;
match Url::parse(&schemaurl) {
Ok(_) | Err(ParseError::RelativeUrlWithoutBase) => Some(schemaurl),
Err(e) => return Err(format_err!("{}", e)),
}
} else {
None
}
},
specversion: "0.2",
})
}
}
impl Default for CloudEventBuilder {
fn default() -> Self {
CloudEventBuilder {
event_type: None,
id: None,
schemaurl: None,
source: None,
extensions: None,
data: None,
contenttype: None,
time: None,
}
}
}
#[macro_export]
macro_rules! cloudevent_v02 {
($( $name:ident: $value:expr $(,)* )+) => {
$crate::v02::CloudEventBuilder::default()
$(
.$name($value)
)*
.build()
};
}
#[cfg(test)]
mod test {
use super::*;
use serde_json::json;
#[test]
fn string_data_can_be_created_from_str() {
let content = "string content";
let data = Data::from_string(content);
assert_eq!(data, Data::StringOrBinary(content.to_owned()));
}
#[test]
fn binary_data_can_be_created_from_slice() {
let data = Data::from_binary(b"this is binary");
assert_eq!(
data,
Data::StringOrBinary("dGhpcyBpcyBiaW5hcnk=".to_owned())
)
}
#[test]
fn object_data_can_be_created_from_serializable() {
#[derive(Serialize)]
struct SerializableStruct {
content: String,
}
let object = SerializableStruct {
content: "content".to_owned(),
};
let data = Data::from_serializable(object).unwrap();
let expected = json!({
"content": "content",
});
assert_eq!(data, Data::Object(expected));
}
#[test]
fn extension_string_data_can_be_created_from_str() {
let content = "string content";
let data = ExtensionValue::from_string(content);
assert_eq!(data, ExtensionValue::String(content.to_owned()));
}
#[test]
fn extension_object_data_can_be_created_from_serializable() {
#[derive(Serialize)]
struct SerializableStruct {
content: String,
}
let object = SerializableStruct {
content: "content".to_owned(),
};
let data = ExtensionValue::from_serializable(object).unwrap();
let expected = json!({
"content": "content",
});
assert_eq!(data, ExtensionValue::Object(expected));
}
#[test]
fn builder_works() {
let event = CloudEventBuilder::default()
.event_id("id")
.source("http://www.google.com")
.event_type("test type")
.contenttype("application/json")
.build()
.unwrap();
assert_eq!(event.event_type, "test type");
assert_eq!(event.source, "http://www.google.com");
assert_eq!(event.id, "id");
assert_eq!(event.specversion, "0.2");
assert_eq!(event.extensions, None);
assert_eq!(event.data, None);
assert_eq!(event.time, None);
assert_eq!(event.contenttype, Some("application/json".to_owned()));
assert_eq!(event.schemaurl, None);
}
#[test]
fn builder_macro_works() {
let event = cloudevent_v02!(
event_type: "test type",
source: "http://www.google.com",
event_id: "id",
contenttype: "application/json",
data: Data::from_string("test"),
)
.unwrap();
assert_eq!(event.event_type, "test type");
assert_eq!(event.source, "http://www.google.com");
assert_eq!(event.id, "id");
assert_eq!(event.specversion, "0.2");
assert_eq!(event.extensions, None);
assert_eq!(event.data, Some(Data::StringOrBinary("test".to_owned())));
assert_eq!(event.time, None);
assert_eq!(event.contenttype, Some("application/json".to_owned()));
assert_eq!(event.schemaurl, None);
}
#[test]
fn source_is_allowed_to_be_a_relative_uri() {
let event = CloudEventBuilder::default()
.event_id("id")
.source("/cloudevents/spec/pull/123")
.event_type("test type")
.build()
.unwrap();
assert_eq!(event.source, "/cloudevents/spec/pull/123");
}
#[test]
fn source_is_allowed_to_be_a_urn() {
let event = CloudEventBuilder::default()
.event_id("id")
.source("urn:event:from:myapi/resourse/123")
.event_type("test type")
.build()
.unwrap();
assert_eq!(event.source, "urn:event:from:myapi/resourse/123");
}
#[test]
fn source_is_allowed_to_be_a_mailto() {
let event = CloudEventBuilder::default()
.event_id("id")
.source("mailto:cncf-wg-serverless@lists.cncf.io")
.event_type("test type")
.build()
.unwrap();
assert_eq!(
event.source,
"mailto:cncf-wg-serverless@lists.cncf.io"
);
}
}