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

Co-authored-by: Fabrizio Lazzaretti <fabrizio@lazzaretti.me>
This commit is contained in:
Francesco Guardiani 2020-03-19 08:26:30 +01:00 committed by GitHub
parent 7b73db2ebf
commit 493db3448d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 350 additions and 32 deletions

48
Cargo.lock generated
View File

@ -30,14 +30,25 @@ dependencies = [
"time", "time",
] ]
[[package]]
name = "claim"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2e893ee68bf12771457cceea72497bc9cb7da404ec8a5311226d354b895ba4"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "cloudevents-sdk" name = "cloudevents-sdk"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"base64", "base64",
"chrono", "chrono",
"claim",
"delegate", "delegate",
"hostname", "hostname",
"rstest",
"serde", "serde",
"serde_json", "serde_json",
"uuid", "uuid",
@ -177,12 +188,49 @@ version = "0.1.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 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]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.2" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" 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]] [[package]]
name = "serde" name = "serde"
version = "1.0.104" version = "1.0.104"

View File

@ -19,5 +19,9 @@ uuid = { version = "^0.8", features = ["serde", "v4"] }
hostname = "^0.1" hostname = "^0.1"
base64 = "^0.12" base64 = "^0.12"
[dev-dependencies]
rstest = "0.6"
claim = "0.3.1"
[lib] [lib]
name = "cloudevents" name = "cloudevents"

View File

@ -1,6 +1,7 @@
use super::SpecVersion; use super::SpecVersion;
use crate::event::{AttributesV10, ExtensionValue}; use crate::event::{AttributesV10, ExtensionValue};
use chrono::{DateTime, Utc}; 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). /// Trait to get [CloudEvents Context attributes](https://github.com/cloudevents/spec/blob/master/spec.md#context-attributes).
pub trait AttributesReader { pub trait AttributesReader {
@ -48,8 +49,10 @@ pub(crate) trait DataAttributesWriter {
fn set_dataschema(&mut self, dataschema: Option<impl Into<String>>); fn set_dataschema(&mut self, dataschema: Option<impl Into<String>>);
} }
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "specversion")]
pub enum Attributes { pub enum Attributes {
#[serde(rename = "1.0")]
V10(AttributesV10), V10(AttributesV10),
} }

View File

@ -1,10 +1,17 @@
use serde::de::Visitor;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::convert::{Into, TryFrom}; use std::convert::{Into, TryFrom};
use std::fmt::{self, Formatter};
#[derive(Debug, PartialEq, Clone)] /// Event [data attribute](https://github.com/cloudevents/spec/blob/master/spec.md#event-data) representation
/// Possible data values ///
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub enum Data { pub enum Data {
String(String), #[serde(rename = "data_base64")]
#[serde(serialize_with = "serialize_base64")]
#[serde(deserialize_with = "deserialize_base64")]
Binary(Vec<u8>), Binary(Vec<u8>),
#[serde(rename = "data")]
Json(serde_json::Value), Json(serde_json::Value),
} }
@ -30,6 +37,37 @@ impl Data {
} }
} }
fn serialize_base64<S>(data: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&base64::encode(&data))
}
struct Base64Visitor;
impl<'de> Visitor<'de> for Base64Visitor {
type Value = Vec<u8>;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
formatter.write_str("a Base64 encoded string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
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<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(Base64Visitor)
}
impl Into<Data> for serde_json::Value { impl Into<Data> for serde_json::Value {
fn into(self) -> Data { fn into(self) -> Data {
Data::Json(self) Data::Json(self)
@ -44,7 +82,7 @@ impl Into<Data> for Vec<u8> {
impl Into<Data> for String { impl Into<Data> for String {
fn into(self) -> Data { fn into(self) -> Data {
Data::String(self) Data::Json(self.into())
} }
} }
@ -53,21 +91,31 @@ impl TryFrom<Data> for serde_json::Value {
fn try_from(value: Data) -> Result<Self, Self::Error> { fn try_from(value: Data) -> Result<Self, Self::Error> {
match value { match value {
Data::String(s) => Ok(serde_json::from_str(&s)?),
Data::Binary(v) => Ok(serde_json::from_slice(&v)?), Data::Binary(v) => Ok(serde_json::from_slice(&v)?),
Data::Json(v) => Ok(v), Data::Json(v) => Ok(v),
} }
} }
} }
impl TryFrom<Data> for Vec<u8> {
type Error = serde_json::Error;
fn try_from(value: Data) -> Result<Self, Self::Error> {
match value {
Data::Binary(v) => Ok(serde_json::from_slice(&v)?),
Data::Json(v) => Ok(serde_json::to_vec(&v)?),
}
}
}
impl TryFrom<Data> for String { impl TryFrom<Data> for String {
type Error = std::string::FromUtf8Error; type Error = std::string::FromUtf8Error;
fn try_from(value: Data) -> Result<Self, Self::Error> { fn try_from(value: Data) -> Result<Self, Self::Error> {
match value { match value {
Data::String(s) => Ok(s),
Data::Binary(v) => Ok(String::from_utf8(v)?), 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()),
} }
} }
} }

View File

@ -5,6 +5,7 @@ use super::{
use crate::event::attributes::DataAttributesWriter; use crate::event::attributes::DataAttributesWriter;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use delegate::delegate; use delegate::delegate;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom; use std::convert::TryFrom;
/// Data structure that represents a [CloudEvent](https://github.com/cloudevents/spec/blob/master/spec.md). /// 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(); /// let data: serde_json::Value = e.try_get_data().unwrap().unwrap();
/// println!("Event data: {}", data) /// println!("Event data: {}", data)
/// ``` /// ```
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
pub struct Event { 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<Data>, pub data: Option<Data>,
#[serde(flatten)]
pub attributes: Attributes,
} }
impl AttributesReader for Event { impl AttributesReader for Event {

View File

@ -1,7 +1,8 @@
use serde_json::Value; use serde::{Deserialize, Serialize};
use std::convert::From; 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 /// Represents all the possible [CloudEvents extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) values
pub enum ExtensionValue { pub enum ExtensionValue {
/// Represents a [`String`](std::string::String) value. /// Represents a [`String`](std::string::String) value.
@ -10,8 +11,6 @@ pub enum ExtensionValue {
Boolean(bool), Boolean(bool),
/// Represents an integer [`i64`](i64) value. /// Represents an integer [`i64`](i64) value.
Integer(i64), Integer(i64),
/// Represents a [Json `Value`](serde_json::value::Value).
Json(Value),
} }
impl From<String> for ExtensionValue { impl From<String> for ExtensionValue {
@ -32,12 +31,6 @@ impl From<i64> for ExtensionValue {
} }
} }
impl From<Value> for ExtensionValue {
fn from(s: Value) -> Self {
ExtensionValue::Json(s)
}
}
impl ExtensionValue { impl ExtensionValue {
pub fn from_string<S>(s: S) -> Self pub fn from_string<S>(s: S) -> Self
where where
@ -59,11 +52,4 @@ impl ExtensionValue {
{ {
ExtensionValue::from(s.into()) ExtensionValue::from(s.into())
} }
pub fn from_json_value<S>(s: S) -> Self
where
S: Into<serde_json::Value>,
{
ExtensionValue::from(s.into())
}
} }

View File

@ -1,12 +1,9 @@
use serde::{Deserialize, Serialize};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fmt; use std::fmt;
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub enum SpecVersion { pub enum SpecVersion {
#[serde(rename = "0.3")]
V03, V03,
#[serde(rename = "1.0")]
V10, V10,
} }

View File

@ -2,18 +2,25 @@ use crate::event::attributes::DataAttributesWriter;
use crate::event::{AttributesReader, AttributesWriter, ExtensionValue, SpecVersion}; use crate::event::{AttributesReader, AttributesWriter, ExtensionValue, SpecVersion};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use hostname::get_hostname; use hostname::get_hostname;
use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use uuid::Uuid; use uuid::Uuid;
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
pub struct Attributes { pub struct Attributes {
id: String, id: String,
#[serde(rename = "type")]
ty: String, ty: String,
source: String, source: String,
#[serde(skip_serializing_if = "Option::is_none")]
datacontenttype: Option<String>, datacontenttype: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
dataschema: Option<String>, dataschema: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
subject: Option<String>, subject: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
time: Option<DateTime<Utc>>, time: Option<DateTime<Utc>>,
#[serde(flatten)]
extensions: HashMap<String, ExtensionValue>, extensions: HashMap<String, ExtensionValue>,
} }

36
tests/serde_json.rs Normal file
View File

@ -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<Event, serde_json::Error> =
serde_json::from_str(&actual_json_serialized);
assert_ok!(&deserialize_result);
let deserialize_json = deserialize_result.unwrap();
assert_eq!(deserialize_json, event)
}

183
tests/test_data/mod.rs Normal file
View File

@ -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<u8> {
serde_json::to_vec(&json!({"hello": "world"})).unwrap()
}
pub fn subject() -> String {
"cloudevents-sdk".to_string()
}
pub fn time() -> DateTime<Utc> {
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
})
}