Serde (#18)
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> Co-authored-by: Fabrizio Lazzaretti <fabrizio@lazzaretti.me>
This commit is contained in:
parent
7b73db2ebf
commit
493db3448d
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<impl Into<String>>);
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "specversion")]
|
||||
pub enum Attributes {
|
||||
#[serde(rename = "1.0")]
|
||||
V10(AttributesV10),
|
||||
}
|
||||
|
||||
|
|
|
@ -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<u8>),
|
||||
#[serde(rename = "data")]
|
||||
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 {
|
||||
fn into(self) -> Data {
|
||||
Data::Json(self)
|
||||
|
@ -44,7 +82,7 @@ impl Into<Data> for Vec<u8> {
|
|||
|
||||
impl Into<Data> for String {
|
||||
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> {
|
||||
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<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 {
|
||||
type Error = std::string::FromUtf8Error;
|
||||
|
||||
fn try_from(value: Data) -> Result<Self, Self::Error> {
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Data>,
|
||||
#[serde(flatten)]
|
||||
pub attributes: Attributes,
|
||||
}
|
||||
|
||||
impl AttributesReader for Event {
|
||||
|
|
|
@ -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<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 {
|
||||
pub fn from_string<S>(s: S) -> Self
|
||||
where
|
||||
|
@ -59,11 +52,4 @@ impl ExtensionValue {
|
|||
{
|
||||
ExtensionValue::from(s.into())
|
||||
}
|
||||
|
||||
pub fn from_json_value<S>(s: S) -> Self
|
||||
where
|
||||
S: Into<serde_json::Value>,
|
||||
{
|
||||
ExtensionValue::from(s.into())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
dataschema: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
subject: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
time: Option<DateTime<Utc>>,
|
||||
#[serde(flatten)]
|
||||
extensions: HashMap<String, ExtensionValue>,
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue