Redesigned EventBuilder (#53)

* Progress

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Builder finished

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Fixed all integrations

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Fmt'ed

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Fmt'ed part 2

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Fixed tests in reqwest integration

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* fmt'ed again

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
This commit is contained in:
Francesco Guardiani 2020-05-26 15:35:30 +02:00 committed by GitHub
parent 2b91ee8d7a
commit b832b6bcf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 669 additions and 275 deletions

View File

@ -35,14 +35,15 @@ cloudevents-sdk = "0.1.0"
Now you can start creating events:
```rust
use cloudevents::EventBuilder;
use cloudevents::{EventBuilder, EventBuilderV10};
use url::Url;
let event = EventBuilder::v03()
let event = EventBuilderV10::new()
.id("aaa")
.source(Url::parse("http://localhost").unwrap())
.ty("example.demo")
.build();
.build()
.unwrap();
```
Checkout the examples using our integrations with `actix-web` and `reqwest` to learn how to send and receive events:

View File

@ -17,7 +17,8 @@ actix-rt = "1"
lazy_static = "1.4.0"
bytes = "^0.5"
futures = "^0.3"
serde_json = "^1.0"
[dev-dependencies]
url = { version = "^2.1", features = ["serde"] }
url = { version = "^2.1", features = ["serde"] }
serde_json = "^1.0"
chrono = { version = "^0.4", features = ["serde"] }

View File

@ -117,25 +117,32 @@ mod tests {
use actix_web::test;
use url::Url;
use cloudevents::EventBuilder;
use chrono::Utc;
use cloudevents::{EventBuilder, EventBuilderV10};
use serde_json::json;
use std::str::FromStr;
#[actix_rt::test]
async fn test_request() {
let expected = EventBuilder::new()
let time = Utc::now();
let expected = EventBuilderV10::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost").unwrap())
.source("http://localhost/")
//TODO this is required now because the message deserializer implictly set default values
// As soon as this defaulting doesn't happen anymore, we can remove it (Issues #40/#41)
.time(time)
.extension("someint", "10")
.build();
.build()
.unwrap();
let (req, payload) = test::TestRequest::post()
.header("ce-specversion", "1.0")
.header("ce-id", "0001")
.header("ce-type", "example.test")
.header("ce-source", "http://localhost")
.header("ce-source", "http://localhost/")
.header("ce-someint", "10")
.header("ce-time", time.to_rfc3339())
.to_http_parts();
let resp = request_to_event(&req, web::Payload(payload)).await.unwrap();
@ -144,15 +151,20 @@ mod tests {
#[actix_rt::test]
async fn test_request_with_full_data() {
let time = Utc::now();
let j = json!({"hello": "world"});
let expected = EventBuilder::new()
let expected = EventBuilderV10::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost").unwrap())
//TODO this is required now because the message deserializer implictly set default values
// As soon as this defaulting doesn't happen anymore, we can remove it (Issues #40/#41)
.time(time)
.data("application/json", j.clone())
.extension("someint", "10")
.build();
.build()
.unwrap();
let (req, payload) = test::TestRequest::post()
.header("ce-specversion", "1.0")
@ -160,6 +172,7 @@ mod tests {
.header("ce-type", "example.test")
.header("ce-source", "http://localhost")
.header("ce-someint", "10")
.header("ce-time", time.to_rfc3339())
.header("content-type", "application/json")
.set_json(&j)
.to_http_parts();

View File

@ -82,19 +82,20 @@ mod tests {
use actix_web::http::StatusCode;
use actix_web::test;
use cloudevents::EventBuilder;
use cloudevents::{EventBuilder, EventBuilderV10};
use futures::TryStreamExt;
use serde_json::json;
use std::str::FromStr;
#[actix_rt::test]
async fn test_response() {
let input = EventBuilder::new()
let input = EventBuilderV10::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost/").unwrap())
.extension("someint", "10")
.build();
.build()
.unwrap();
let resp = event_to_response(input, HttpResponseBuilder::new(StatusCode::OK))
.await
@ -130,13 +131,14 @@ mod tests {
async fn test_response_with_full_data() {
let j = json!({"hello": "world"});
let input = EventBuilder::new()
let input = EventBuilderV10::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost").unwrap())
.data("application/json", j.clone())
.extension("someint", "10")
.build();
.build()
.unwrap();
let mut resp = event_to_response(input, HttpResponseBuilder::new(StatusCode::OK))
.await

View File

@ -14,7 +14,6 @@ repository = "https://github.com/cloudevents/sdk-rust"
cloudevents-sdk = { version = "0.1.0", path = ".." }
lazy_static = "1.4.0"
bytes = "^0.5"
serde_json = "^1.0"
[dependencies.reqwest]
version = "0.10.4"
@ -24,4 +23,6 @@ features = ["rustls-tls"]
[dev-dependencies]
mockito = "0.25.1"
tokio = { version = "^0.2", features = ["full"] }
url = { version = "^2.1" }
url = { version = "^2.1" }
serde_json = "^1.0"
chrono = { version = "^0.4", features = ["serde"] }

View File

@ -73,7 +73,7 @@ mod tests {
use mockito::{mock, Matcher};
use cloudevents::message::StructuredDeserializer;
use cloudevents::EventBuilder;
use cloudevents::{EventBuilder, EventBuilderV10};
use serde_json::json;
use url::Url;
@ -89,12 +89,13 @@ mod tests {
.match_body(Matcher::Missing)
.create();
let input = EventBuilder::new()
let input = EventBuilderV10::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost/").unwrap())
.extension("someint", "10")
.build();
.build()
.unwrap();
let client = reqwest::Client::new();
event_to_request(input, client.post(&url))
@ -121,13 +122,14 @@ mod tests {
.match_body(Matcher::Exact(j.to_string()))
.create();
let input = EventBuilder::new()
let input = EventBuilderV10::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost").unwrap())
.data("application/json", j.clone())
.extension("someint", "10")
.build();
.build()
.unwrap();
let client = reqwest::Client::new();
event_to_request(input, client.post(&url))
@ -143,13 +145,14 @@ mod tests {
async fn test_structured_request_with_full_data() {
let j = json!({"hello": "world"});
let input = EventBuilder::new()
let input = EventBuilderV10::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost").unwrap())
.data("application/json", j.clone())
.extension("someint", "10")
.build();
.build()
.unwrap();
let url = mockito::server_url();
let m = mock("POST", "/")

View File

@ -112,13 +112,15 @@ mod tests {
use super::*;
use mockito::mock;
use cloudevents::EventBuilder;
use chrono::Utc;
use cloudevents::{EventBuilder, EventBuilderV10};
use serde_json::json;
use std::str::FromStr;
use url::Url;
#[tokio::test]
async fn test_response() {
let time = Utc::now();
let url = mockito::server_url();
let _m = mock("GET", "/")
.with_status(200)
@ -127,14 +129,19 @@ mod tests {
.with_header("ce-type", "example.test")
.with_header("ce-source", "http://localhost")
.with_header("ce-someint", "10")
.with_header("ce-time", &time.to_rfc3339())
.create();
let expected = EventBuilder::new()
let expected = EventBuilderV10::new()
.id("0001")
.ty("example.test")
//TODO this is required now because the message deserializer implictly set default values
// As soon as this defaulting doesn't happen anymore, we can remove it (Issues #40/#41)
.time(time)
.source(Url::from_str("http://localhost").unwrap())
.extension("someint", "10")
.build();
.build()
.unwrap();
let client = reqwest::Client::new();
let res = client.get(&url).send().await.unwrap();
@ -145,6 +152,7 @@ mod tests {
#[tokio::test]
async fn test_response_with_full_data() {
let time = Utc::now();
let j = json!({"hello": "world"});
let url = mockito::server_url();
@ -156,16 +164,21 @@ mod tests {
.with_header("ce-source", "http://localhost/")
.with_header("content-type", "application/json")
.with_header("ce-someint", "10")
.with_header("ce-time", &time.to_rfc3339())
.with_body(j.to_string())
.create();
let expected = EventBuilder::new()
let expected = EventBuilderV10::new()
.id("0001")
.ty("example.test")
//TODO this is required now because the message deserializer implictly set default values
// As soon as this defaulting doesn't happen anymore, we can remove it (Issues #40/#41)
.time(time)
.source(Url::from_str("http://localhost").unwrap())
.data("application/json", j.clone())
.extension("someint", "10")
.build();
.build()
.unwrap();
let client = reqwest::Client::new();
let res = client.get(&url).send().await.unwrap();
@ -176,14 +189,20 @@ mod tests {
#[tokio::test]
async fn test_structured_response_with_full_data() {
let time = Utc::now();
let j = json!({"hello": "world"});
let expected = EventBuilder::new()
let expected = EventBuilderV10::new()
.id("0001")
.ty("example.test")
//TODO this is required now because the message deserializer implictly set default values
// As soon as this defaulting doesn't happen anymore, we can remove it (Issues #40/#41)
.time(time)
.source(Url::from_str("http://localhost").unwrap())
.data("application/json", j.clone())
.extension("someint", "10")
.build();
.build()
.unwrap();
let url = mockito::server_url();
let _m = mock("GET", "/")

View File

@ -1,8 +1,8 @@
use actix_web::{get, post, web, App, HttpRequest, HttpResponse, HttpServer};
use cloudevents::EventBuilder;
use url::Url;
use std::str::FromStr;
use cloudevents::{EventBuilder, EventBuilderV10};
use serde_json::json;
use std::str::FromStr;
use url::Url;
#[post("/")]
async fn post_event(req: HttpRequest, payload: web::Payload) -> Result<String, actix_web::Error> {
@ -16,15 +16,17 @@ async fn get_event() -> Result<HttpResponse, actix_web::Error> {
let payload = json!({"hello": "world"});
Ok(cloudevents_sdk_actix_web::event_to_response(
EventBuilder::new()
EventBuilderV10::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost/").unwrap())
.data("application/json", payload)
.extension("someint", "10")
.build(),
HttpResponse::Ok()
).await?)
.build()
.unwrap(),
HttpResponse::Ok(),
)
.await?)
}
#[actix_rt::main]
@ -39,8 +41,8 @@ async fn main() -> std::io::Result<()> {
.service(post_event)
.service(get_event)
})
.bind("127.0.0.1:9000")?
.workers(1)
.run()
.await
.bind("127.0.0.1:9000")?
.workers(1)
.run()
.await
}

View File

@ -1,11 +1,18 @@
use cloudevents::{EventBuilder, EventBuilderV10};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub async fn run(target: String, ty: String, datacontenttype: String, data: String) -> Result<(), String> {
let event = cloudevents::EventBuilder::new()
pub async fn run(
target: String,
ty: String,
datacontenttype: String,
data: String,
) -> Result<(), String> {
let event = EventBuilderV10::new()
.ty(ty)
.data(datacontenttype, data)
.build();
.build()
.unwrap();
println!("Going to send event: {:?}", event);
@ -17,4 +24,4 @@ pub async fn run(target: String, ty: String, datacontenttype: String, data: Stri
.map_err(|e| e.to_string())?;
Ok(())
}
}

View File

@ -1,32 +1,55 @@
use super::{EventBuilderV03, EventBuilderV10};
use super::Event;
use snafu::Snafu;
/// Builder to create [`super::Event`]:
/// Trait to implement a builder for [`Event`]:
/// ```
/// use cloudevents::EventBuilder;
/// use cloudevents::event::{EventBuilderV10, EventBuilder};
/// use chrono::Utc;
/// use url::Url;
///
/// let event = EventBuilder::v10()
/// let event = EventBuilderV10::new()
/// .id("my_event.my_application")
/// .source(Url::parse("http://localhost:8080").unwrap())
/// .source("http://localhost:8080")
/// .ty("example.demo")
/// .time(Utc::now())
/// .build();
/// .build()
/// .unwrap();
/// ```
pub struct EventBuilder {}
///
/// You can create an [`EventBuilder`] starting from an existing [`Event`] using the [`From`] trait.
/// You can create a default [`EventBuilder`] setting default values for some attributes.
pub trait EventBuilder
where
Self: Clone + Sized + From<Event> + Default,
{
/// Create a new empty builder
fn new() -> Self;
impl EventBuilder {
/// Creates a new builder for latest CloudEvents version
pub fn new() -> EventBuilderV10 {
return Self::v10();
}
/// Creates a new builder for CloudEvents V1.0
pub fn v10() -> EventBuilderV10 {
return EventBuilderV10::new();
}
/// Creates a new builder for CloudEvents V0.3
pub fn v03() -> EventBuilderV03 {
return EventBuilderV03::new();
}
/// Build [`super::Event`]
fn build(self) -> Result<super::Event, Error>;
}
/// Represents an error during build process
#[derive(Debug, Snafu, Clone)]
pub enum Error {
#[snafu(display("Missing required attribute {}", attribute_name))]
MissingRequiredAttribute { attribute_name: &'static str },
#[snafu(display(
"Error while setting attribute '{}' with timestamp type: {}",
attribute_name,
source
))]
ParseTimeError {
attribute_name: &'static str,
source: chrono::ParseError,
},
#[snafu(display(
"Error while setting attribute '{}' with uri/uriref type: {}",
attribute_name,
source
))]
ParseUrlError {
attribute_name: &'static str,
source: url::ParseError,
},
}

View File

@ -7,15 +7,18 @@ mod extensions;
mod format;
mod message;
mod spec_version;
mod types;
pub use attributes::Attributes;
pub use attributes::{AttributesReader, AttributesWriter};
pub use builder::Error as EventBuilderError;
pub use builder::EventBuilder;
pub use data::Data;
pub use event::Event;
pub use extensions::ExtensionValue;
pub use spec_version::InvalidSpecVersion;
pub use spec_version::SpecVersion;
pub use types::{TryIntoTime, TryIntoUrl};
mod v03;

48
src/event/types.rs Normal file
View File

@ -0,0 +1,48 @@
use chrono::{DateTime, Utc};
use url::Url;
/// Trait to define conversion to [`Url`]
pub trait TryIntoUrl {
fn into_url(self) -> Result<Url, url::ParseError>;
}
impl TryIntoUrl for Url {
fn into_url(self) -> Result<Url, url::ParseError> {
Ok(self)
}
}
impl TryIntoUrl for &str {
fn into_url(self) -> Result<Url, url::ParseError> {
Url::parse(self)
}
}
impl TryIntoUrl for String {
fn into_url(self) -> Result<Url, url::ParseError> {
self.as_str().into_url()
}
}
pub trait TryIntoTime {
fn into_time(self) -> Result<DateTime<Utc>, chrono::ParseError>;
}
impl TryIntoTime for DateTime<Utc> {
fn into_time(self) -> Result<DateTime<Utc>, chrono::ParseError> {
Ok(self)
}
}
/// Trait to define conversion to [`DateTime`]
impl TryIntoTime for &str {
fn into_time(self) -> Result<DateTime<Utc>, chrono::ParseError> {
Ok(DateTime::<Utc>::from(DateTime::parse_from_rfc3339(self)?))
}
}
impl TryIntoTime for String {
fn into_time(self) -> Result<DateTime<Utc>, chrono::ParseError> {
self.as_str().into_time()
}
}

View File

@ -159,7 +159,7 @@ impl Default for Attributes {
datacontenttype: None,
schemaurl: None,
subject: None,
time: None,
time: Some(Utc::now()),
}
}
}

View File

@ -1,58 +1,66 @@
use super::Attributes as AttributesV03;
use crate::event::{Attributes, AttributesWriter, Data, Event, ExtensionValue};
use crate::event::{
Attributes, Data, Event, EventBuilderError, ExtensionValue, TryIntoTime, TryIntoUrl,
};
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use url::Url;
/// Builder to create a CloudEvent V0.3
#[derive(Clone)]
pub struct EventBuilder {
event: Event,
id: Option<String>,
ty: Option<String>,
source: Option<Url>,
datacontenttype: Option<String>,
schemaurl: Option<Url>,
subject: Option<String>,
time: Option<DateTime<Utc>>,
data: Option<Data>,
extensions: HashMap<String, ExtensionValue>,
error: Option<EventBuilderError>,
}
impl EventBuilder {
pub fn from(event: Event) -> Self {
EventBuilder {
event: Event {
attributes: event.attributes.into_v03(),
data: event.data,
extensions: event.extensions,
},
}
}
pub fn new() -> Self {
EventBuilder {
event: Event {
attributes: Attributes::V03(AttributesV03::default()),
data: None,
extensions: HashMap::new(),
},
}
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.event.set_id(id);
return self;
self.id = Some(id.into());
self
}
pub fn source(mut self, source: impl Into<Url>) -> Self {
self.event.set_source(source);
return self;
pub fn source(mut self, source: impl TryIntoUrl) -> Self {
match source.into_url() {
Ok(u) => self.source = Some(u),
Err(e) => {
self.error = Some(EventBuilderError::ParseUrlError {
attribute_name: "source",
source: e,
})
}
};
self
}
pub fn ty(mut self, ty: impl Into<String>) -> Self {
self.event.set_type(ty);
return self;
self.ty = Some(ty.into());
self
}
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.event.set_subject(Some(subject));
return self;
self.subject = Some(subject.into());
self
}
pub fn time(mut self, time: impl Into<DateTime<Utc>>) -> Self {
self.event.set_time(Some(time));
return self;
pub fn time(mut self, time: impl TryIntoTime) -> Self {
match time.into_time() {
Ok(u) => self.time = Some(u),
Err(e) => {
self.error = Some(EventBuilderError::ParseTimeError {
attribute_name: "time",
source: e,
})
}
};
self
}
pub fn extension(
@ -60,75 +68,108 @@ impl EventBuilder {
extension_name: &str,
extension_value: impl Into<ExtensionValue>,
) -> Self {
self.event.set_extension(extension_name, extension_value);
return self;
self.extensions
.insert(extension_name.to_owned(), extension_value.into());
self
}
pub fn data(mut self, datacontenttype: impl Into<String>, data: impl Into<Data>) -> Self {
self.event.write_data(datacontenttype, data);
return self;
self.datacontenttype = Some(datacontenttype.into());
self.data = Some(data.into());
self
}
pub fn data_with_schema(
mut self,
datacontenttype: impl Into<String>,
schemaurl: impl Into<Url>,
schemaurl: impl TryIntoUrl,
data: impl Into<Data>,
) -> Self {
self.event
.write_data_with_schema(datacontenttype, schemaurl, data);
return self;
}
pub fn build(self) -> Event {
self.event
self.datacontenttype = Some(datacontenttype.into());
match schemaurl.into_url() {
Ok(u) => self.schemaurl = Some(u),
Err(e) => {
self.error = Some(EventBuilderError::ParseUrlError {
attribute_name: "schemaurl",
source: e,
})
}
};
self.data = Some(data.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{AttributesReader, SpecVersion};
impl From<Event> for EventBuilder {
fn from(event: Event) -> Self {
let attributes = match event.attributes.into_v03() {
Attributes::V03(attr) => attr,
// This branch is unreachable because into_v03() returns
// always a Attributes::V03
_ => unreachable!(),
};
#[test]
fn build_event() {
let id = "aaa";
let source = Url::parse("http://localhost:8080").unwrap();
let ty = "bbb";
let subject = "francesco";
let time: DateTime<Utc> = Utc::now();
let extension_name = "ext";
let extension_value = 10i64;
let content_type = "application/json";
let schema = Url::parse("http://localhost:8080/schema").unwrap();
let data = serde_json::json!({
"hello": "world"
});
let event = EventBuilder::new()
.id(id)
.source(source.clone())
.ty(ty)
.subject(subject)
.time(time)
.extension(extension_name, extension_value)
.data_with_schema(content_type, schema.clone(), data.clone())
.build();
assert_eq!(SpecVersion::V03, event.get_specversion());
assert_eq!(id, event.get_id());
assert_eq!(source, event.get_source().clone());
assert_eq!(ty, event.get_type());
assert_eq!(subject, event.get_subject().unwrap());
assert_eq!(time, event.get_time().unwrap().clone());
assert_eq!(
ExtensionValue::from(extension_value),
event.get_extension(extension_name).unwrap().clone()
);
assert_eq!(content_type, event.get_datacontenttype().unwrap());
assert_eq!(schema, event.get_dataschema().unwrap().clone());
let event_data: serde_json::Value = event.try_get_data().unwrap().unwrap();
assert_eq!(data, event_data);
EventBuilder {
id: Some(attributes.id),
ty: Some(attributes.ty),
source: Some(attributes.source),
datacontenttype: attributes.datacontenttype,
schemaurl: attributes.schemaurl,
subject: attributes.subject,
time: attributes.time,
data: event.data,
extensions: event.extensions,
error: None,
}
}
}
impl Default for EventBuilder {
fn default() -> Self {
Self::from(Event::default())
}
}
impl crate::event::builder::EventBuilder for EventBuilder {
fn new() -> Self {
EventBuilder {
id: None,
ty: None,
source: None,
datacontenttype: None,
schemaurl: None,
subject: None,
time: None,
data: None,
extensions: Default::default(),
error: None,
}
}
fn build(self) -> Result<Event, EventBuilderError> {
match self.error {
Some(e) => Err(e),
None => Ok(Event {
attributes: Attributes::V03(AttributesV03 {
id: self.id.ok_or(EventBuilderError::MissingRequiredAttribute {
attribute_name: "id",
})?,
ty: self.ty.ok_or(EventBuilderError::MissingRequiredAttribute {
attribute_name: "type",
})?,
source: self
.source
.ok_or(EventBuilderError::MissingRequiredAttribute {
attribute_name: "source",
})?,
datacontenttype: self.datacontenttype,
schemaurl: self.schemaurl,
subject: self.subject,
time: self.time,
}),
data: self.data,
extensions: self.extensions,
}),
}
}
}

View File

@ -3,8 +3,8 @@ mod builder;
mod format;
mod message;
pub(crate) use crate::event::v03::format::EventFormatDeserializer;
pub(crate) use crate::event::v03::format::EventFormatSerializer;
pub use attributes::Attributes;
pub(crate) use attributes::ATTRIBUTE_NAMES;
pub use builder::EventBuilder;
pub(crate) use format::EventFormatDeserializer;
pub(crate) use format::EventFormatSerializer;

View File

@ -158,7 +158,7 @@ impl Default for Attributes {
datacontenttype: None,
dataschema: None,
subject: None,
time: None,
time: Some(Utc::now()),
}
}
}

View File

@ -1,58 +1,66 @@
use super::Attributes as AttributesV10;
use crate::event::{Attributes, AttributesWriter, Data, Event, ExtensionValue};
use crate::event::{
Attributes, Data, Event, EventBuilderError, ExtensionValue, TryIntoTime, TryIntoUrl,
};
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use url::Url;
/// Builder to create a CloudEvent V1.0
#[derive(Clone)]
pub struct EventBuilder {
event: Event,
id: Option<String>,
ty: Option<String>,
source: Option<Url>,
datacontenttype: Option<String>,
dataschema: Option<Url>,
subject: Option<String>,
time: Option<DateTime<Utc>>,
data: Option<Data>,
extensions: HashMap<String, ExtensionValue>,
error: Option<EventBuilderError>,
}
impl EventBuilder {
pub fn from(event: Event) -> Self {
EventBuilder {
event: Event {
attributes: event.attributes.into_v10(),
data: event.data,
extensions: event.extensions,
},
}
}
pub fn new() -> Self {
EventBuilder {
event: Event {
attributes: Attributes::V10(AttributesV10::default()),
data: None,
extensions: HashMap::new(),
},
}
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.event.set_id(id);
return self;
self.id = Some(id.into());
self
}
pub fn source(mut self, source: impl Into<Url>) -> Self {
self.event.set_source(source);
return self;
pub fn source(mut self, source: impl TryIntoUrl) -> Self {
match source.into_url() {
Ok(u) => self.source = Some(u),
Err(e) => {
self.error = Some(EventBuilderError::ParseUrlError {
attribute_name: "source",
source: e,
})
}
};
self
}
pub fn ty(mut self, ty: impl Into<String>) -> Self {
self.event.set_type(ty);
return self;
self.ty = Some(ty.into());
self
}
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.event.set_subject(Some(subject));
return self;
self.subject = Some(subject.into());
self
}
pub fn time(mut self, time: impl Into<DateTime<Utc>>) -> Self {
self.event.set_time(Some(time));
return self;
pub fn time(mut self, time: impl TryIntoTime) -> Self {
match time.into_time() {
Ok(u) => self.time = Some(u),
Err(e) => {
self.error = Some(EventBuilderError::ParseTimeError {
attribute_name: "time",
source: e,
})
}
};
self
}
pub fn extension(
@ -60,75 +68,108 @@ impl EventBuilder {
extension_name: &str,
extension_value: impl Into<ExtensionValue>,
) -> Self {
self.event.set_extension(extension_name, extension_value);
return self;
self.extensions
.insert(extension_name.to_owned(), extension_value.into());
self
}
pub fn data(mut self, datacontenttype: impl Into<String>, data: impl Into<Data>) -> Self {
self.event.write_data(datacontenttype, data);
return self;
self.datacontenttype = Some(datacontenttype.into());
self.data = Some(data.into());
self
}
pub fn data_with_schema(
mut self,
datacontenttype: impl Into<String>,
dataschema: impl Into<Url>,
schemaurl: impl TryIntoUrl,
data: impl Into<Data>,
) -> Self {
self.event
.write_data_with_schema(datacontenttype, dataschema, data);
return self;
}
pub fn build(self) -> Event {
self.event
self.datacontenttype = Some(datacontenttype.into());
match schemaurl.into_url() {
Ok(u) => self.dataschema = Some(u),
Err(e) => {
self.error = Some(EventBuilderError::ParseUrlError {
attribute_name: "dataschema",
source: e,
})
}
};
self.data = Some(data.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{AttributesReader, SpecVersion};
impl From<Event> for EventBuilder {
fn from(event: Event) -> Self {
let attributes = match event.attributes.into_v10() {
Attributes::V10(attr) => attr,
// This branch is unreachable because into_v10() returns
// always a Attributes::V10
_ => unreachable!(),
};
#[test]
fn build_event() {
let id = "aaa";
let source = Url::parse("http://localhost:8080").unwrap();
let ty = "bbb";
let subject = "francesco";
let time: DateTime<Utc> = Utc::now();
let extension_name = "ext";
let extension_value = 10i64;
let content_type = "application/json";
let schema = Url::parse("http://localhost:8080/schema").unwrap();
let data = serde_json::json!({
"hello": "world"
});
let event = EventBuilder::new()
.id(id)
.source(source.clone())
.ty(ty)
.subject(subject)
.time(time)
.extension(extension_name, extension_value)
.data_with_schema(content_type, schema.clone(), data.clone())
.build();
assert_eq!(SpecVersion::V10, event.get_specversion());
assert_eq!(id, event.get_id());
assert_eq!(source, event.get_source().clone());
assert_eq!(ty, event.get_type());
assert_eq!(subject, event.get_subject().unwrap());
assert_eq!(time, event.get_time().unwrap().clone());
assert_eq!(
ExtensionValue::from(extension_value),
event.get_extension(extension_name).unwrap().clone()
);
assert_eq!(content_type, event.get_datacontenttype().unwrap());
assert_eq!(schema, event.get_dataschema().unwrap().clone());
let event_data: serde_json::Value = event.try_get_data().unwrap().unwrap();
assert_eq!(data, event_data);
EventBuilder {
id: Some(attributes.id),
ty: Some(attributes.ty),
source: Some(attributes.source),
datacontenttype: attributes.datacontenttype,
dataschema: attributes.dataschema,
subject: attributes.subject,
time: attributes.time,
data: event.data,
extensions: event.extensions,
error: None,
}
}
}
impl Default for EventBuilder {
fn default() -> Self {
Self::from(Event::default())
}
}
impl crate::event::builder::EventBuilder for EventBuilder {
fn new() -> Self {
EventBuilder {
id: None,
ty: None,
source: None,
datacontenttype: None,
dataschema: None,
subject: None,
time: None,
data: None,
extensions: Default::default(),
error: None,
}
}
fn build(self) -> Result<Event, EventBuilderError> {
match self.error {
Some(e) => Err(e),
None => Ok(Event {
attributes: Attributes::V10(AttributesV10 {
id: self.id.ok_or(EventBuilderError::MissingRequiredAttribute {
attribute_name: "id",
})?,
ty: self.ty.ok_or(EventBuilderError::MissingRequiredAttribute {
attribute_name: "type",
})?,
source: self
.source
.ok_or(EventBuilderError::MissingRequiredAttribute {
attribute_name: "source",
})?,
datacontenttype: self.datacontenttype,
dataschema: self.dataschema,
subject: self.subject,
time: self.time,
}),
data: self.data,
extensions: self.extensions,
}),
}
}
}

View File

@ -3,8 +3,8 @@ mod builder;
mod format;
mod message;
pub(crate) use crate::event::v10::format::EventFormatDeserializer;
pub(crate) use crate::event::v10::format::EventFormatSerializer;
pub use attributes::Attributes;
pub(crate) use attributes::ATTRIBUTE_NAMES;
pub use builder::EventBuilder;
pub(crate) use format::EventFormatDeserializer;
pub(crate) use format::EventFormatSerializer;

View File

@ -1,15 +1,17 @@
//! This crate implements the [CloudEvents](https://cloudevents.io/) Spec for Rust.
//!
//! ```
//! use cloudevents::{EventBuilder, AttributesReader};
//! use cloudevents::{EventBuilder, AttributesReader, EventBuilderV10};
//! use chrono::Utc;
//! use url::Url;
//!
//! let event = EventBuilder::v10()
//! let event = EventBuilderV10::new()
//! .id("my_event.my_application")
//! .source(Url::parse("http://localhost:8080").unwrap())
//! .source("http://localhost:8080")
//! .ty("example.demo")
//! .time(Utc::now())
//! .build();
//! .build()
//! .unwrap();
//!
//! println!("CloudEvent Id: {}", event.get_id());
//! println!("CloudEvent Time: {}", event.get_time().unwrap());
@ -32,5 +34,5 @@ pub mod event;
pub mod message;
pub use event::Event;
pub use event::EventBuilder;
pub use event::{AttributesReader, AttributesWriter};
pub use event::{EventBuilder, EventBuilderV03, EventBuilderV10};

83
tests/builder_v03.rs Normal file
View File

@ -0,0 +1,83 @@
#[macro_use]
mod util;
use chrono::{DateTime, Utc};
use cloudevents::event::{
AttributesReader, EventBuilder, EventBuilderError, ExtensionValue, SpecVersion,
};
use cloudevents::EventBuilderV03;
use url::Url;
#[test]
fn build_event() {
let id = "aaa";
let source = Url::parse("http://localhost:8080").unwrap();
let ty = "bbb";
let subject = "francesco";
let time: DateTime<Utc> = Utc::now();
let extension_name = "ext";
let extension_value = 10i64;
let content_type = "application/json";
let schema = Url::parse("http://localhost:8080/schema").unwrap();
let data = serde_json::json!({
"hello": "world"
});
let event = EventBuilderV03::new()
.id(id)
.source(source.clone())
.ty(ty)
.subject(subject)
.time(time)
.extension(extension_name, extension_value)
.data_with_schema(content_type, schema.clone(), data.clone())
.build()
.unwrap();
assert_eq!(SpecVersion::V03, event.get_specversion());
assert_eq!(id, event.get_id());
assert_eq!(source, event.get_source().clone());
assert_eq!(ty, event.get_type());
assert_eq!(subject, event.get_subject().unwrap());
assert_eq!(time, event.get_time().unwrap().clone());
assert_eq!(
ExtensionValue::from(extension_value),
event.get_extension(extension_name).unwrap().clone()
);
assert_eq!(content_type, event.get_datacontenttype().unwrap());
assert_eq!(schema, event.get_dataschema().unwrap().clone());
let event_data: serde_json::Value = event.try_get_data().unwrap().unwrap();
assert_eq!(data, event_data);
}
#[test]
fn build_missing_id() {
let res = EventBuilderV03::new()
.source("http://localhost:8080")
.build();
assert_match_pattern!(
res,
Err(EventBuilderError::MissingRequiredAttribute {
attribute_name: "id"
})
);
}
#[test]
fn source_invalid_url() {
let res = EventBuilderV03::new().source("").build();
assert_match_pattern!(
res,
Err(EventBuilderError::ParseUrlError {
attribute_name: "source",
..
})
);
}
#[test]
fn default_builds() {
let res = EventBuilderV03::default().build();
assert_match_pattern!(res, Ok(_));
}

83
tests/builder_v10.rs Normal file
View File

@ -0,0 +1,83 @@
#[macro_use]
mod util;
use chrono::{DateTime, Utc};
use cloudevents::event::{
AttributesReader, EventBuilder, EventBuilderError, ExtensionValue, SpecVersion,
};
use cloudevents::EventBuilderV10;
use url::Url;
#[test]
fn build_event() {
let id = "aaa";
let source = Url::parse("http://localhost:8080").unwrap();
let ty = "bbb";
let subject = "francesco";
let time: DateTime<Utc> = Utc::now();
let extension_name = "ext";
let extension_value = 10i64;
let content_type = "application/json";
let schema = Url::parse("http://localhost:8080/schema").unwrap();
let data = serde_json::json!({
"hello": "world"
});
let event = EventBuilderV10::new()
.id(id)
.source(source.clone())
.ty(ty)
.subject(subject)
.time(time)
.extension(extension_name, extension_value)
.data_with_schema(content_type, schema.clone(), data.clone())
.build()
.unwrap();
assert_eq!(SpecVersion::V10, event.get_specversion());
assert_eq!(id, event.get_id());
assert_eq!(source, event.get_source().clone());
assert_eq!(ty, event.get_type());
assert_eq!(subject, event.get_subject().unwrap());
assert_eq!(time, event.get_time().unwrap().clone());
assert_eq!(
ExtensionValue::from(extension_value),
event.get_extension(extension_name).unwrap().clone()
);
assert_eq!(content_type, event.get_datacontenttype().unwrap());
assert_eq!(schema, event.get_dataschema().unwrap().clone());
let event_data: serde_json::Value = event.try_get_data().unwrap().unwrap();
assert_eq!(data, event_data);
}
#[test]
fn build_missing_id() {
let res = EventBuilderV10::new()
.source("http://localhost:8080")
.build();
assert_match_pattern!(
res,
Err(EventBuilderError::MissingRequiredAttribute {
attribute_name: "id"
})
);
}
#[test]
fn source_invalid_url() {
let res = EventBuilderV10::new().source("").build();
assert_match_pattern!(
res,
Err(EventBuilderError::ParseUrlError {
attribute_name: "source",
..
})
);
}
#[test]
fn default_builds() {
let res = EventBuilderV10::default().build();
assert_match_pattern!(res, Ok(_));
}

View File

@ -1,15 +1,16 @@
use super::*;
use cloudevents::{Event, EventBuilder};
use cloudevents::{Event, EventBuilder, EventBuilderV03};
use serde_json::{json, Value};
use url::Url;
pub fn minimal() -> Event {
EventBuilder::v03()
EventBuilderV03::new()
.id(id())
.source(Url::parse(source().as_ref()).unwrap())
.ty(ty())
.build()
.unwrap()
}
pub fn minimal_json() -> Value {
@ -26,7 +27,7 @@ pub fn full_no_data() -> Event {
let (bool_ext_name, bool_ext_value) = bool_extension();
let (int_ext_name, int_ext_value) = int_extension();
EventBuilder::v03()
EventBuilderV03::new()
.id(id())
.source(Url::parse(source().as_ref()).unwrap())
.ty(ty())
@ -36,6 +37,7 @@ pub fn full_no_data() -> Event {
.extension(&bool_ext_name, bool_ext_value)
.extension(&int_ext_name, int_ext_value)
.build()
.unwrap()
}
pub fn full_no_data_json() -> Value {
@ -61,7 +63,7 @@ pub fn full_json_data() -> Event {
let (bool_ext_name, bool_ext_value) = bool_extension();
let (int_ext_name, int_ext_value) = int_extension();
EventBuilder::v03()
EventBuilderV03::new()
.id(id())
.source(Url::parse(source().as_ref()).unwrap())
.ty(ty())
@ -76,6 +78,7 @@ pub fn full_json_data() -> Event {
json_data(),
)
.build()
.unwrap()
}
pub fn full_json_data_json() -> Value {
@ -126,7 +129,7 @@ pub fn full_xml_string_data() -> Event {
let (bool_ext_name, bool_ext_value) = bool_extension();
let (int_ext_name, int_ext_value) = int_extension();
EventBuilder::v03()
EventBuilderV03::new()
.id(id())
.source(Url::parse(source().as_ref()).unwrap())
.ty(ty())
@ -137,6 +140,7 @@ pub fn full_xml_string_data() -> Event {
.extension(&int_ext_name, int_ext_value)
.data(xml_datacontenttype(), xml_data())
.build()
.unwrap()
}
pub fn full_xml_binary_data() -> Event {
@ -144,7 +148,7 @@ pub fn full_xml_binary_data() -> Event {
let (bool_ext_name, bool_ext_value) = bool_extension();
let (int_ext_name, int_ext_value) = int_extension();
EventBuilder::v03()
EventBuilderV03::new()
.id(id())
.source(Url::parse(source().as_ref()).unwrap())
.ty(ty())
@ -155,6 +159,7 @@ pub fn full_xml_binary_data() -> Event {
.extension(&int_ext_name, int_ext_value)
.data(xml_datacontenttype(), Vec::from(xml_data()))
.build()
.unwrap()
}
pub fn full_xml_string_data_json() -> Value {

View File

@ -1,14 +1,15 @@
use super::*;
use cloudevents::{Event, EventBuilder};
use cloudevents::{Event, EventBuilder, EventBuilderV10};
use serde_json::{json, Value};
use url::Url;
pub fn minimal() -> Event {
EventBuilder::v10()
EventBuilderV10::new()
.id(id())
.source(Url::parse(source().as_ref()).unwrap())
.ty(ty())
.build()
.unwrap()
}
pub fn minimal_json() -> Value {
@ -25,7 +26,7 @@ pub fn full_no_data() -> Event {
let (bool_ext_name, bool_ext_value) = bool_extension();
let (int_ext_name, int_ext_value) = int_extension();
EventBuilder::v10()
EventBuilderV10::new()
.id(id())
.source(Url::parse(source().as_ref()).unwrap())
.ty(ty())
@ -35,6 +36,7 @@ pub fn full_no_data() -> Event {
.extension(&bool_ext_name, bool_ext_value)
.extension(&int_ext_name, int_ext_value)
.build()
.unwrap()
}
pub fn full_no_data_json() -> Value {
@ -60,7 +62,7 @@ pub fn full_json_data() -> Event {
let (bool_ext_name, bool_ext_value) = bool_extension();
let (int_ext_name, int_ext_value) = int_extension();
EventBuilder::v10()
EventBuilderV10::new()
.id(id())
.source(Url::parse(source().as_ref()).unwrap())
.ty(ty())
@ -75,6 +77,7 @@ pub fn full_json_data() -> Event {
json_data(),
)
.build()
.unwrap()
}
pub fn full_json_data_json() -> Value {
@ -124,7 +127,7 @@ pub fn full_xml_string_data() -> Event {
let (bool_ext_name, bool_ext_value) = bool_extension();
let (int_ext_name, int_ext_value) = int_extension();
EventBuilder::v10()
EventBuilderV10::new()
.id(id())
.source(Url::parse(source().as_ref()).unwrap())
.ty(ty())
@ -135,6 +138,7 @@ pub fn full_xml_string_data() -> Event {
.extension(&int_ext_name, int_ext_value)
.data(xml_datacontenttype(), xml_data())
.build()
.unwrap()
}
pub fn full_xml_binary_data() -> Event {
@ -142,7 +146,7 @@ pub fn full_xml_binary_data() -> Event {
let (bool_ext_name, bool_ext_value) = bool_extension();
let (int_ext_name, int_ext_value) = int_extension();
EventBuilder::v10()
EventBuilderV10::new()
.id(id())
.source(Url::parse(source().as_ref()).unwrap())
.ty(ty())
@ -153,6 +157,7 @@ pub fn full_xml_binary_data() -> Event {
.extension(&int_ext_name, int_ext_value)
.data(xml_datacontenttype(), Vec::from(xml_data()))
.build()
.unwrap()
}
pub fn full_xml_string_data_json() -> Value {

10
tests/util/mod.rs Normal file
View File

@ -0,0 +1,10 @@
macro_rules! assert_match_pattern (
($e:expr, $p:pat) => (
match $e {
$p => (),
_ => panic!(r#"assertion failed (value doesn't match pattern):
value: `{:?}`,
pattern: `{}`"#, $e, stringify!($p))
}
)
);

View File

@ -1,17 +1,18 @@
mod test_data;
use cloudevents::event::{EventBuilderV03, EventBuilderV10};
use cloudevents::EventBuilder;
use test_data::*;
#[test]
fn v10_to_v03() {
let in_event = v10::full_json_data();
let out_event = EventBuilderV03::from(in_event).build();
let out_event = EventBuilderV03::from(in_event).build().unwrap();
assert_eq!(v03::full_json_data(), out_event)
}
#[test]
fn v03_to_v10() {
let in_event = v03::full_json_data();
let out_event = EventBuilderV10::from(in_event).build();
let out_event = EventBuilderV10::from(in_event).build().unwrap();
assert_eq!(v10::full_json_data(), out_event)
}