From b832b6bcf4f8fb84a5dfdf442f8876f41ef2a1bd Mon Sep 17 00:00:00 2001 From: Francesco Guardiani Date: Tue, 26 May 2020 15:35:30 +0200 Subject: [PATCH] Redesigned EventBuilder (#53) * Progress Signed-off-by: Francesco Guardiani * Builder finished Signed-off-by: Francesco Guardiani * Fixed all integrations Signed-off-by: Francesco Guardiani * Fmt'ed Signed-off-by: Francesco Guardiani * Fmt'ed part 2 Signed-off-by: Francesco Guardiani * Fixed tests in reqwest integration Signed-off-by: Francesco Guardiani * fmt'ed again Signed-off-by: Francesco Guardiani --- README.md | 7 +- cloudevents-sdk-actix-web/Cargo.toml | 5 +- .../src/server_request.rs | 27 ++- .../src/server_response.rs | 12 +- cloudevents-sdk-reqwest/Cargo.toml | 5 +- cloudevents-sdk-reqwest/src/client_request.rs | 17 +- .../src/client_response.rs | 33 ++- .../actix-web-example/src/main.rs | 24 +- .../reqwest-wasm-example/src/lib.rs | 15 +- src/event/builder.rs | 67 ++++-- src/event/mod.rs | 3 + src/event/types.rs | 48 ++++ src/event/v03/attributes.rs | 2 +- src/event/v03/builder.rs | 221 +++++++++++------- src/event/v03/mod.rs | 4 +- src/event/v10/attributes.rs | 2 +- src/event/v10/builder.rs | 221 +++++++++++------- src/event/v10/mod.rs | 4 +- src/lib.rs | 12 +- tests/builder_v03.rs | 83 +++++++ tests/builder_v10.rs | 83 +++++++ tests/test_data/v03.rs | 17 +- tests/test_data/v10.rs | 17 +- tests/util/mod.rs | 10 + tests/version_conversion.rs | 5 +- 25 files changed, 669 insertions(+), 275 deletions(-) create mode 100644 src/event/types.rs create mode 100644 tests/builder_v03.rs create mode 100644 tests/builder_v10.rs create mode 100644 tests/util/mod.rs diff --git a/README.md b/README.md index 7b2e4f0..413a7b7 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/cloudevents-sdk-actix-web/Cargo.toml b/cloudevents-sdk-actix-web/Cargo.toml index 57579a3..f87e0c8 100644 --- a/cloudevents-sdk-actix-web/Cargo.toml +++ b/cloudevents-sdk-actix-web/Cargo.toml @@ -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"] } \ No newline at end of file +url = { version = "^2.1", features = ["serde"] } +serde_json = "^1.0" +chrono = { version = "^0.4", features = ["serde"] } \ No newline at end of file diff --git a/cloudevents-sdk-actix-web/src/server_request.rs b/cloudevents-sdk-actix-web/src/server_request.rs index c5024cc..67c8edf 100644 --- a/cloudevents-sdk-actix-web/src/server_request.rs +++ b/cloudevents-sdk-actix-web/src/server_request.rs @@ -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(); diff --git a/cloudevents-sdk-actix-web/src/server_response.rs b/cloudevents-sdk-actix-web/src/server_response.rs index a1401cd..463019e 100644 --- a/cloudevents-sdk-actix-web/src/server_response.rs +++ b/cloudevents-sdk-actix-web/src/server_response.rs @@ -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 diff --git a/cloudevents-sdk-reqwest/Cargo.toml b/cloudevents-sdk-reqwest/Cargo.toml index a32ae10..db3fde8 100644 --- a/cloudevents-sdk-reqwest/Cargo.toml +++ b/cloudevents-sdk-reqwest/Cargo.toml @@ -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" } \ No newline at end of file +url = { version = "^2.1" } +serde_json = "^1.0" +chrono = { version = "^0.4", features = ["serde"] } \ No newline at end of file diff --git a/cloudevents-sdk-reqwest/src/client_request.rs b/cloudevents-sdk-reqwest/src/client_request.rs index d07ac63..fea062b 100644 --- a/cloudevents-sdk-reqwest/src/client_request.rs +++ b/cloudevents-sdk-reqwest/src/client_request.rs @@ -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", "/") diff --git a/cloudevents-sdk-reqwest/src/client_response.rs b/cloudevents-sdk-reqwest/src/client_response.rs index e854007..3c3e97e 100644 --- a/cloudevents-sdk-reqwest/src/client_response.rs +++ b/cloudevents-sdk-reqwest/src/client_response.rs @@ -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", "/") diff --git a/example-projects/actix-web-example/src/main.rs b/example-projects/actix-web-example/src/main.rs index 2bc476d..5cad849 100644 --- a/example-projects/actix-web-example/src/main.rs +++ b/example-projects/actix-web-example/src/main.rs @@ -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 { @@ -16,15 +16,17 @@ async fn get_event() -> Result { 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 } diff --git a/example-projects/reqwest-wasm-example/src/lib.rs b/example-projects/reqwest-wasm-example/src/lib.rs index 1c35c12..a599959 100644 --- a/example-projects/reqwest-wasm-example/src/lib.rs +++ b/example-projects/reqwest-wasm-example/src/lib.rs @@ -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(()) -} \ No newline at end of file +} diff --git a/src/event/builder.rs b/src/event/builder.rs index d627914..ac5927f 100644 --- a/src/event/builder.rs +++ b/src/event/builder.rs @@ -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 + 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; +} + +/// 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, + }, } diff --git a/src/event/mod.rs b/src/event/mod.rs index 79e2f2a..714ed0c 100644 --- a/src/event/mod.rs +++ b/src/event/mod.rs @@ -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; diff --git a/src/event/types.rs b/src/event/types.rs new file mode 100644 index 0000000..11436ca --- /dev/null +++ b/src/event/types.rs @@ -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; +} + +impl TryIntoUrl for Url { + fn into_url(self) -> Result { + Ok(self) + } +} + +impl TryIntoUrl for &str { + fn into_url(self) -> Result { + Url::parse(self) + } +} + +impl TryIntoUrl for String { + fn into_url(self) -> Result { + self.as_str().into_url() + } +} + +pub trait TryIntoTime { + fn into_time(self) -> Result, chrono::ParseError>; +} + +impl TryIntoTime for DateTime { + fn into_time(self) -> Result, chrono::ParseError> { + Ok(self) + } +} + +/// Trait to define conversion to [`DateTime`] +impl TryIntoTime for &str { + fn into_time(self) -> Result, chrono::ParseError> { + Ok(DateTime::::from(DateTime::parse_from_rfc3339(self)?)) + } +} + +impl TryIntoTime for String { + fn into_time(self) -> Result, chrono::ParseError> { + self.as_str().into_time() + } +} diff --git a/src/event/v03/attributes.rs b/src/event/v03/attributes.rs index 419ab8a..256b7af 100644 --- a/src/event/v03/attributes.rs +++ b/src/event/v03/attributes.rs @@ -159,7 +159,7 @@ impl Default for Attributes { datacontenttype: None, schemaurl: None, subject: None, - time: None, + time: Some(Utc::now()), } } } diff --git a/src/event/v03/builder.rs b/src/event/v03/builder.rs index e698b8f..94b64c1 100644 --- a/src/event/v03/builder.rs +++ b/src/event/v03/builder.rs @@ -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, + ty: Option, + source: Option, + datacontenttype: Option, + schemaurl: Option, + subject: Option, + time: Option>, + data: Option, + extensions: HashMap, + error: Option, } 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) -> Self { - self.event.set_id(id); - return self; + self.id = Some(id.into()); + self } - pub fn source(mut self, source: impl Into) -> 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) -> Self { - self.event.set_type(ty); - return self; + self.ty = Some(ty.into()); + self } pub fn subject(mut self, subject: impl Into) -> Self { - self.event.set_subject(Some(subject)); - return self; + self.subject = Some(subject.into()); + self } - pub fn time(mut self, time: impl Into>) -> 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, ) -> 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, data: impl Into) -> 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, - schemaurl: impl Into, + schemaurl: impl TryIntoUrl, data: impl Into, ) -> 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 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::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 { + 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, + }), + } } } diff --git a/src/event/v03/mod.rs b/src/event/v03/mod.rs index f9da195..0e791f1 100644 --- a/src/event/v03/mod.rs +++ b/src/event/v03/mod.rs @@ -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; diff --git a/src/event/v10/attributes.rs b/src/event/v10/attributes.rs index 99d864a..47fcfcb 100644 --- a/src/event/v10/attributes.rs +++ b/src/event/v10/attributes.rs @@ -158,7 +158,7 @@ impl Default for Attributes { datacontenttype: None, dataschema: None, subject: None, - time: None, + time: Some(Utc::now()), } } } diff --git a/src/event/v10/builder.rs b/src/event/v10/builder.rs index 1fc1cec..4d83ff4 100644 --- a/src/event/v10/builder.rs +++ b/src/event/v10/builder.rs @@ -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, + ty: Option, + source: Option, + datacontenttype: Option, + dataschema: Option, + subject: Option, + time: Option>, + data: Option, + extensions: HashMap, + error: Option, } 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) -> Self { - self.event.set_id(id); - return self; + self.id = Some(id.into()); + self } - pub fn source(mut self, source: impl Into) -> 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) -> Self { - self.event.set_type(ty); - return self; + self.ty = Some(ty.into()); + self } pub fn subject(mut self, subject: impl Into) -> Self { - self.event.set_subject(Some(subject)); - return self; + self.subject = Some(subject.into()); + self } - pub fn time(mut self, time: impl Into>) -> 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, ) -> 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, data: impl Into) -> 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, - dataschema: impl Into, + schemaurl: impl TryIntoUrl, data: impl Into, ) -> 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 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::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 { + 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, + }), + } } } diff --git a/src/event/v10/mod.rs b/src/event/v10/mod.rs index 6e9cd85..0e791f1 100644 --- a/src/event/v10/mod.rs +++ b/src/event/v10/mod.rs @@ -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; diff --git a/src/lib.rs b/src/lib.rs index 7d68835..eefb498 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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}; diff --git a/tests/builder_v03.rs b/tests/builder_v03.rs new file mode 100644 index 0000000..5d7bd22 --- /dev/null +++ b/tests/builder_v03.rs @@ -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::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(_)); +} diff --git a/tests/builder_v10.rs b/tests/builder_v10.rs new file mode 100644 index 0000000..5bdeb4b --- /dev/null +++ b/tests/builder_v10.rs @@ -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::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(_)); +} diff --git a/tests/test_data/v03.rs b/tests/test_data/v03.rs index be20180..2ff1c66 100644 --- a/tests/test_data/v03.rs +++ b/tests/test_data/v03.rs @@ -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 { diff --git a/tests/test_data/v10.rs b/tests/test_data/v10.rs index d7b4646..a4faa9a 100644 --- a/tests/test_data/v10.rs +++ b/tests/test_data/v10.rs @@ -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 { diff --git a/tests/util/mod.rs b/tests/util/mod.rs new file mode 100644 index 0000000..c898dfe --- /dev/null +++ b/tests/util/mod.rs @@ -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)) + } + ) +); diff --git a/tests/version_conversion.rs b/tests/version_conversion.rs index 5ba05e9..bbbb29b 100644 --- a/tests/version_conversion.rs +++ b/tests/version_conversion.rs @@ -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) }