diff --git a/Cargo.lock b/Cargo.lock index 8516a3c..40f12fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,14 +30,25 @@ dependencies = [ "time", ] +[[package]] +name = "claim" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2e893ee68bf12771457cceea72497bc9cb7da404ec8a5311226d354b895ba4" +dependencies = [ + "autocfg", +] + [[package]] name = "cloudevents-sdk" version = "0.0.1" dependencies = [ "base64", "chrono", + "claim", "delegate", "hostname", + "rstest", "serde", "serde_json", "uuid", @@ -177,12 +188,49 @@ version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" +[[package]] +name = "rstest" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d5f9396fa6a44e2aa2068340b17208794515e2501c5bf3e680a0c3422a5971" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + [[package]] name = "ryu" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "serde" version = "1.0.104" diff --git a/Cargo.toml b/Cargo.toml index d678c6f..f7d244a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,5 +19,9 @@ uuid = { version = "^0.8", features = ["serde", "v4"] } hostname = "^0.1" base64 = "^0.12" +[dev-dependencies] +rstest = "0.6" +claim = "0.3.1" + [lib] name = "cloudevents" diff --git a/src/event/attributes.rs b/src/event/attributes.rs index da5a323..df6916c 100644 --- a/src/event/attributes.rs +++ b/src/event/attributes.rs @@ -1,6 +1,7 @@ use super::SpecVersion; use crate::event::{AttributesV10, ExtensionValue}; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; /// Trait to get [CloudEvents Context attributes](https://github.com/cloudevents/spec/blob/master/spec.md#context-attributes). pub trait AttributesReader { @@ -48,8 +49,10 @@ pub(crate) trait DataAttributesWriter { fn set_dataschema(&mut self, dataschema: Option>); } -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "specversion")] pub enum Attributes { + #[serde(rename = "1.0")] V10(AttributesV10), } diff --git a/src/event/data.rs b/src/event/data.rs index 1811dd3..96aed58 100644 --- a/src/event/data.rs +++ b/src/event/data.rs @@ -1,10 +1,17 @@ +use serde::de::Visitor; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::convert::{Into, TryFrom}; +use std::fmt::{self, Formatter}; -#[derive(Debug, PartialEq, Clone)] -/// Possible data values +/// Event [data attribute](https://github.com/cloudevents/spec/blob/master/spec.md#event-data) representation +/// +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub enum Data { - String(String), + #[serde(rename = "data_base64")] + #[serde(serialize_with = "serialize_base64")] + #[serde(deserialize_with = "deserialize_base64")] Binary(Vec), + #[serde(rename = "data")] Json(serde_json::Value), } @@ -30,6 +37,37 @@ impl Data { } } +fn serialize_base64(data: &Vec, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&base64::encode(&data)) +} + +struct Base64Visitor; + +impl<'de> Visitor<'de> for Base64Visitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str("a Base64 encoded string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + base64::decode(v).map_err(|e| serde::de::Error::custom(e.to_string())) + } +} + +fn deserialize_base64<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_str(Base64Visitor) +} + impl Into for serde_json::Value { fn into(self) -> Data { Data::Json(self) @@ -44,7 +82,7 @@ impl Into for Vec { impl Into for String { fn into(self) -> Data { - Data::String(self) + Data::Json(self.into()) } } @@ -53,21 +91,31 @@ impl TryFrom for serde_json::Value { fn try_from(value: Data) -> Result { match value { - Data::String(s) => Ok(serde_json::from_str(&s)?), Data::Binary(v) => Ok(serde_json::from_slice(&v)?), Data::Json(v) => Ok(v), } } } +impl TryFrom for Vec { + type Error = serde_json::Error; + + fn try_from(value: Data) -> Result { + match value { + Data::Binary(v) => Ok(serde_json::from_slice(&v)?), + Data::Json(v) => Ok(serde_json::to_vec(&v)?), + } + } +} + impl TryFrom for String { type Error = std::string::FromUtf8Error; fn try_from(value: Data) -> Result { match value { - Data::String(s) => Ok(s), Data::Binary(v) => Ok(String::from_utf8(v)?), - Data::Json(s) => Ok(s.to_string()), + Data::Json(serde_json::Value::String(s)) => Ok(s), // Return the string without quotes + Data::Json(v) => Ok(v.to_string()), } } } diff --git a/src/event/event.rs b/src/event/event.rs index 1f33dc3..4d6a063 100644 --- a/src/event/event.rs +++ b/src/event/event.rs @@ -5,6 +5,7 @@ use super::{ use crate::event::attributes::DataAttributesWriter; use chrono::{DateTime, Utc}; use delegate::delegate; +use serde::{Deserialize, Serialize}; use std::convert::TryFrom; /// Data structure that represents a [CloudEvent](https://github.com/cloudevents/spec/blob/master/spec.md). @@ -31,10 +32,15 @@ use std::convert::TryFrom; /// let data: serde_json::Value = e.try_get_data().unwrap().unwrap(); /// println!("Event data: {}", data) /// ``` -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] pub struct Event { - pub attributes: Attributes, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(alias = "data_base64")] + #[serde(alias = "data")] + #[serde(flatten)] pub data: Option, + #[serde(flatten)] + pub attributes: Attributes, } impl AttributesReader for Event { diff --git a/src/event/extensions.rs b/src/event/extensions.rs index ac8f001..3abca5c 100644 --- a/src/event/extensions.rs +++ b/src/event/extensions.rs @@ -1,7 +1,8 @@ -use serde_json::Value; +use serde::{Deserialize, Serialize}; use std::convert::From; -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(untagged)] /// Represents all the possible [CloudEvents extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) values pub enum ExtensionValue { /// Represents a [`String`](std::string::String) value. @@ -10,8 +11,6 @@ pub enum ExtensionValue { Boolean(bool), /// Represents an integer [`i64`](i64) value. Integer(i64), - /// Represents a [Json `Value`](serde_json::value::Value). - Json(Value), } impl From for ExtensionValue { @@ -32,12 +31,6 @@ impl From for ExtensionValue { } } -impl From for ExtensionValue { - fn from(s: Value) -> Self { - ExtensionValue::Json(s) - } -} - impl ExtensionValue { pub fn from_string(s: S) -> Self where @@ -59,11 +52,4 @@ impl ExtensionValue { { ExtensionValue::from(s.into()) } - - pub fn from_json_value(s: S) -> Self - where - S: Into, - { - ExtensionValue::from(s.into()) - } } diff --git a/src/event/spec_version.rs b/src/event/spec_version.rs index 113fd90..d14cb08 100644 --- a/src/event/spec_version.rs +++ b/src/event/spec_version.rs @@ -1,12 +1,9 @@ -use serde::{Deserialize, Serialize}; use std::convert::TryFrom; use std::fmt; -#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[derive(PartialEq, Debug, Clone)] pub enum SpecVersion { - #[serde(rename = "0.3")] V03, - #[serde(rename = "1.0")] V10, } diff --git a/src/event/v10/attributes.rs b/src/event/v10/attributes.rs index e035cbb..6aa9d0d 100644 --- a/src/event/v10/attributes.rs +++ b/src/event/v10/attributes.rs @@ -2,18 +2,25 @@ use crate::event::attributes::DataAttributesWriter; use crate::event::{AttributesReader, AttributesWriter, ExtensionValue, SpecVersion}; use chrono::{DateTime, Utc}; use hostname::get_hostname; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use uuid::Uuid; -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] pub struct Attributes { id: String, + #[serde(rename = "type")] ty: String, source: String, + #[serde(skip_serializing_if = "Option::is_none")] datacontenttype: Option, + #[serde(skip_serializing_if = "Option::is_none")] dataschema: Option, + #[serde(skip_serializing_if = "Option::is_none")] subject: Option, + #[serde(skip_serializing_if = "Option::is_none")] time: Option>, + #[serde(flatten)] extensions: HashMap, } diff --git a/tests/serde_json.rs b/tests/serde_json.rs new file mode 100644 index 0000000..ac54cf0 --- /dev/null +++ b/tests/serde_json.rs @@ -0,0 +1,36 @@ +use claim::*; +use cloudevents::Event; +use rstest::rstest; +use serde_json::Value; + +mod test_data; +use test_data::*; + +/// This test is a parametrized test that uses data from tests/test_data +/// The test follows the flow Event -> serde_json::Value -> String -> Event +#[rstest( + event, + expected_json, + case::minimal_v1(minimal_v1(), minimal_v1_json()), + case::full_v1_no_data(full_v1_no_data(), full_v1_no_data_json()), + case::full_v1_with_json_data(full_v1_json_data(), full_v1_json_data_json()), + case::full_v1_with_base64_data(full_v1_binary_data(), full_v1_base64_data_json()) +)] +fn serialize_deserialize_should_succeed(event: Event, expected_json: Value) { + // Event -> serde_json::Value + let serialize_result = serde_json::to_value(event.clone()); + assert_ok!(&serialize_result); + let actual_json = serialize_result.unwrap(); + assert_eq!(&actual_json, &expected_json); + + // serde_json::Value -> String + let actual_json_serialized = actual_json.to_string(); + assert_eq!(actual_json_serialized, expected_json.to_string()); + + // String -> Event + let deserialize_result: Result = + serde_json::from_str(&actual_json_serialized); + assert_ok!(&deserialize_result); + let deserialize_json = deserialize_result.unwrap(); + assert_eq!(deserialize_json, event) +} diff --git a/tests/test_data/mod.rs b/tests/test_data/mod.rs new file mode 100644 index 0000000..cd582d9 --- /dev/null +++ b/tests/test_data/mod.rs @@ -0,0 +1,183 @@ +use chrono::{DateTime, TimeZone, Utc}; +use cloudevents::{Event, EventBuilder}; +use serde_json::{json, Value}; + +pub fn id() -> String { + "0001".to_string() +} + +pub fn ty() -> String { + "test_event.test_application".to_string() +} + +pub fn source() -> String { + "http://localhost".to_string() +} + +pub fn datacontenttype() -> String { + "application/json".to_string() +} + +pub fn dataschema() -> String { + "http://localhost/schema".to_string() +} + +pub fn data() -> Value { + json!({"hello": "world"}) +} + +pub fn data_base_64() -> Vec { + serde_json::to_vec(&json!({"hello": "world"})).unwrap() +} + +pub fn subject() -> String { + "cloudevents-sdk".to_string() +} + +pub fn time() -> DateTime { + Utc.ymd(2020, 3, 16).and_hms(11, 50, 00) +} + +pub fn string_extension() -> (String, String) { + ("string_ex".to_string(), "val".to_string()) +} + +pub fn bool_extension() -> (String, bool) { + ("bool_ex".to_string(), true) +} + +pub fn int_extension() -> (String, i64) { + ("int_ex".to_string(), 10) +} + +pub fn minimal_v1() -> Event { + EventBuilder::v10() + .id(id()) + .source(source()) + .ty(ty()) + .build() +} + +pub fn minimal_v1_json() -> Value { + json!({ + "specversion": "1.0", + "id": id(), + "type": ty(), + "source": source(), + }) +} + +pub fn full_v1_no_data() -> Event { + let (string_ext_name, string_ext_value) = string_extension(); + let (bool_ext_name, bool_ext_value) = bool_extension(); + let (int_ext_name, int_ext_value) = int_extension(); + + EventBuilder::v10() + .id(id()) + .source(source()) + .ty(ty()) + .subject(subject()) + .time(time()) + .extension(&string_ext_name, string_ext_value) + .extension(&bool_ext_name, bool_ext_value) + .extension(&int_ext_name, int_ext_value) + .build() +} + +pub fn full_v1_no_data_json() -> Value { + let (string_ext_name, string_ext_value) = string_extension(); + let (bool_ext_name, bool_ext_value) = bool_extension(); + let (int_ext_name, int_ext_value) = int_extension(); + + json!({ + "specversion": "1.0", + "id": id(), + "type": ty(), + "source": source(), + "subject": subject(), + "time": time(), + string_ext_name: string_ext_value, + bool_ext_name: bool_ext_value, + int_ext_name: int_ext_value + }) +} + +pub fn full_v1_json_data() -> Event { + let (string_ext_name, string_ext_value) = string_extension(); + let (bool_ext_name, bool_ext_value) = bool_extension(); + let (int_ext_name, int_ext_value) = int_extension(); + + EventBuilder::v10() + .id(id()) + .source(source()) + .ty(ty()) + .subject(subject()) + .time(time()) + .extension(&string_ext_name, string_ext_value) + .extension(&bool_ext_name, bool_ext_value) + .extension(&int_ext_name, int_ext_value) + .data_with_schema(datacontenttype(), dataschema(), data()) + .build() +} + +pub fn full_v1_json_data_json() -> Value { + let (string_ext_name, string_ext_value) = string_extension(); + let (bool_ext_name, bool_ext_value) = bool_extension(); + let (int_ext_name, int_ext_value) = int_extension(); + + json!({ + "specversion": "1.0", + "id": id(), + "type": ty(), + "source": source(), + "subject": subject(), + "time": time(), + string_ext_name: string_ext_value, + bool_ext_name: bool_ext_value, + int_ext_name: int_ext_value, + "datacontenttype": datacontenttype(), + "dataschema": dataschema(), + "data": data() + }) +} + +pub fn full_v1_binary_data() -> Event { + let (string_ext_name, string_ext_value) = string_extension(); + let (bool_ext_name, bool_ext_value) = bool_extension(); + let (int_ext_name, int_ext_value) = int_extension(); + + EventBuilder::v10() + .id(id()) + .source(source()) + .ty(ty()) + .subject(subject()) + .time(time()) + .extension(&string_ext_name, string_ext_value) + .extension(&bool_ext_name, bool_ext_value) + .extension(&int_ext_name, int_ext_value) + .data_with_schema(datacontenttype(), dataschema(), data_base_64()) + .build() +} + +pub fn full_v1_base64_data_json() -> Value { + let (string_ext_name, string_ext_value) = string_extension(); + let (bool_ext_name, bool_ext_value) = bool_extension(); + let (int_ext_name, int_ext_value) = int_extension(); + + let d = base64::encode(&data_base_64()); + + json!({ + "specversion": "1.0", + "id": id(), + "type": ty(), + "source": source(), + "subject": subject(), + "time": time(), + string_ext_name: string_ext_value, + bool_ext_name: bool_ext_value, + int_ext_name: int_ext_value, + "datacontenttype": datacontenttype(), + "dataschema": dataschema(), + "data_base64": d + }) +}