Skip to content

Commit

Permalink
✨ enforce mutation to only create coherente cdevents (type match subj…
Browse files Browse the repository at this point in the history
…ect.content)

- hide context
- provide accessor and mutator to restrict access

Signed-off-by: David Bernard <david.bernard.31@gmail.com>
  • Loading branch information
davidB committed Jan 23, 2024
1 parent 7b5eca8 commit 886e00c
Show file tree
Hide file tree
Showing 15 changed files with 526 additions and 146 deletions.
40 changes: 15 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,23 @@ To send a CDEvent as CloudEvent:
// from examples/pipelinerun_finished.rs
use std::error::Error;

use cdevents_sdk::{CDEvent, Context, Subject, pipelinerun_finished, Content};
use cdevents_sdk::{CDEvent, Subject, pipelinerun_finished, Content};
use cloudevents::{Event, AttributesReader};
use time::OffsetDateTime;

fn main() -> Result<(), Box<dyn Error>> {
let cdevent = CDEvent {
context: Context {
version: "0.3.0".to_string(),
id: "271069a8-fc18-44f1-b38f-9d70a1695819".to_string(),
r#type: "dev.cdevents.pipelinerun.finished.0.1.1".to_string(),
source: "/event/source/123".try_into()?,
timestamp: OffsetDateTime::now_utc(),
},
subject: Subject {
id: "/dev/pipeline/run/1".to_string(),
source: Some("https://dev.pipeline.run/source".try_into()?),
r#type: "build".to_string(),
content: Content::PipelinerunFinished(pipelinerun_finished::Content{
errors: Some("pipelineErrors".into()),
outcome: Some("success".into()),
pipeline_name: Some("testPipeline".into()),
url: Some("https://dev.pipeline.run/source".into())
})
},
custom_data: None,
custom_data_content_type: None,
};
let cdevent = CDEvent::from(
Subject::from(pipelinerun_finished::Content{
errors: Some("pipelineErrors".into()),
outcome: Some("success".into()),
pipeline_name: Some("testPipeline".into()),
url: Some("https://dev.pipeline.run/url".into())
})
.with_id("/dev/pipeline/run/1")
.with_source("https://dev.pipeline.run/source".try_into()?)
)
.with_id("271069a8-fc18-44f1-b38f-9d70a1695819")
.with_source("https://dev.cdevents".try_into()?)
;

let cdevent_expected = cdevent.clone();

Expand All @@ -62,7 +52,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let cloudevent_received: Event = cloudevent.clone();
let cdevent_extracted: CDEvent = cloudevent_received.try_into()?;

assert_eq!(cloudevent.id(), cdevent_extracted.context.id);
assert_eq!(cloudevent.id(), cdevent_extracted.id());
assert_eq!(cdevent_expected, cdevent_extracted);
Ok(())
}
Expand Down
41 changes: 16 additions & 25 deletions cdevents-sdk/examples/pipelinerun_finished.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,22 @@
use std::error::Error;

use cdevents_sdk::{CDEvent, Context, Subject, pipelinerun_finished, Content};
use cdevents_sdk::{CDEvent, Subject, pipelinerun_finished};
use cloudevents::{Event, AttributesReader};
use time::OffsetDateTime;

fn main() -> Result<(), Box<dyn Error>> {
let cdevent = CDEvent {
context: Context {
version: "0.3.0".to_string(),
id: "271069a8-fc18-44f1-b38f-9d70a1695819".to_string(),
r#type: "dev.cdevents.pipelinerun .finished.0.1.1".to_string(),
source: "/event/source/123".try_into()?,
timestamp: OffsetDateTime::now_utc(),
},
subject: Subject {
id: "/dev/pipeline/run/1".to_string(),
source: Some("https://dev.pipeline.run/source".try_into()?),
r#type: "build".to_string(),
content: Content::PipelinerunFinished(pipelinerun_finished::Content{
errors: Some("pipelineErrors".into()),
outcome: Some("success".into()),
pipeline_name: Some("testPipeline".into()),
url: Some("https://dev.pipeline.run/source".into())
})
},
custom_data: None,
custom_data_content_type: None,
};
let cdevent = CDEvent::from(
Subject::from(pipelinerun_finished::Content{
errors: Some("pipelineErrors".into()),
outcome: Some("success".into()),
pipeline_name: Some("testPipeline".into()),
url: Some("https://dev.pipeline.run/url".into())
})
.with_id("/dev/pipeline/run/1")
.with_source("https://dev.pipeline.run/source".try_into()?)
)
.with_id("271069a8-fc18-44f1-b38f-9d70a1695819")
.with_source("https://dev.cdevents".try_into()?)
;

let cdevent_expected = cdevent.clone();

Expand All @@ -44,7 +34,8 @@ fn main() -> Result<(), Box<dyn Error>> {
let cloudevent_received: Event = cloudevent.clone();
let cdevent_extracted: CDEvent = cloudevent_received.try_into()?;

assert_eq!(cloudevent.id(), cdevent_extracted.context.id);
assert_eq!(cloudevent.id(), cdevent_extracted.id());
assert_eq!(cdevent_expected, cdevent_extracted);
Ok(())
}

104 changes: 98 additions & 6 deletions cdevents-sdk/src/cdevent.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{Context, Subject};
use crate::{Context, Subject, UriReference};
use serde::{
de::{self, Deserializer, MapAccess, Visitor},
Deserialize, Serialize,
Expand All @@ -8,15 +8,107 @@ use std::fmt;
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct CDEvent {
pub context: Context,
pub subject: Subject,
context: Context,
subject: Subject,
#[serde(rename = "customData", skip_serializing_if = "Option::is_none")]
pub custom_data: Option<serde_json::Value>,
custom_data: Option<serde_json::Value>,
#[serde(
rename = "customDataContentType",
skip_serializing_if = "Option::is_none"
)]
pub custom_data_content_type: Option<String>,
custom_data_content_type: Option<String>,
}

impl From<Subject> for CDEvent {
fn from(subject: Subject) -> Self {
let context = Context {
ty: subject.ty().into(),
..Default::default()
};
Self {
context,
subject,
custom_data: None,
custom_data_content_type: None,
}
}
}

impl CDEvent {
/// see <https://github.com/cdevents/spec/blob/main/spec.md#version>
pub fn version(&self) -> &str {
self.context.version.as_str()
}

pub fn with_version<T>(mut self, v: T) -> Self where T: Into<String> {
self.context.version = v.into();
self
}

/// see <https://github.com/cdevents/spec/blob/main/spec.md#id-context>
pub fn id(&self) -> &str {
self.context.id.as_str()
}

pub fn with_id<T>(mut self, v: T) -> Self where T: Into<String> {
self.context.id = v.into();
self
}

/// see <https://github.com/cdevents/spec/blob/main/spec.md#source-context>
pub fn source(&self) -> &UriReference {
&self.context.source
}

pub fn with_source(mut self, v: UriReference) -> Self {
self.context.source = v;
self
}

/// see <https://github.com/cdevents/spec/blob/main/spec.md#timestamp>
pub fn timestamp(&self) -> &time::OffsetDateTime {
&self.context.timestamp
}

pub fn with_timestamp(mut self, v: time::OffsetDateTime) -> Self {
self.context.timestamp = v;
self
}

/// see <https://github.com/cdevents/spec/blob/main/spec.md#cdevent-subject>
pub fn subject(&self) -> &Subject {
&self.subject
}

/// see <https://github.com/cdevents/spec/blob/main/spec.md#type-context>
/// derived from subject.content
pub fn ty(&self) -> &str {
//self.context.ty()
self.subject.ty()
}

/// see <https://github.com/cdevents/spec/blob/main/spec.md#customdata>
pub fn custom_data(&self) -> &Option<serde_json::Value> {
&self.custom_data
}

pub fn with_custom_data(mut self, custom_data: serde_json::Value) -> Self {
self.custom_data = Some(custom_data);
self
}

/// see <https://github.com/cdevents/spec/blob/main/spec.md#customdatacontenttype>
pub fn custom_data_content_type(&self) -> &Option<String> {
&self.custom_data_content_type
}

pub fn with_custom_data_content_type(
mut self,
custom_data_content_type: String,
) -> Self {
self.custom_data_content_type = Some(custom_data_content_type);
self
}
}

impl<'de> Deserialize<'de> for CDEvent {
Expand Down Expand Up @@ -82,7 +174,7 @@ impl<'de> Deserialize<'de> for CDEvent {
let subject_json =
subject_json.ok_or_else(|| de::Error::missing_field("subject"))?;
let subject =
Subject::from_json(&context.r#type, subject_json).map_err(de::Error::custom)?;
Subject::from_json(&context.ty, subject_json).map_err(de::Error::custom)?;

Ok(CDEvent {
context,
Expand Down
45 changes: 16 additions & 29 deletions cdevents-sdk/src/cloudevents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ impl BuilderExt for cloudevents::EventBuilderV10 {

fn with_cdevent(self, cdevent: CDEvent) -> Result<Self, Self::Error> {
Ok(
self.id(&cdevent.context.id)
.ty(&cdevent.context.r#type)
.source(cdevent.context.source.as_str())
.subject(&cdevent.subject.id)
.time(cdevent.context.timestamp.format(&Rfc3339).map_err(|e| Self::Error::Other{source: Box::new(e)})?)
self.id(cdevent.id())
.ty(cdevent.ty())
.source(cdevent.source().as_str())
.subject(cdevent.subject().id())
.time(cdevent.timestamp().format(&Rfc3339).map_err(|e| Self::Error::Other{source: Box::new(e)})?)
.data("application/json", serde_json::to_value(cdevent).map_err(Self::Error::from)?)
)
}
Expand Down Expand Up @@ -61,32 +61,19 @@ impl TryFrom<Event> for CDEvent {
#[cfg(test)]
mod tests {
use ::cloudevents::{AttributesReader, EventBuilder, EventBuilderV10};
use ::time::OffsetDateTime;

use crate::*;

use super::*;

#[test]
fn test_true() -> Result<(), Box<dyn std::error::Error>> {
let cdevent = CDEvent {
context: Context {
version: "0.3.0".to_string(),
id: "271069a8-fc18-44f1-b38f-9d70a1695819".to_string(),
r#type: "dev.cdevents.build.queued.0.1.1".to_string(),
source: "/event/source/123".try_into()?,
timestamp: OffsetDateTime::now_utc(),
},
subject: Subject {
id: "subject123".to_string(),
source: Some("/event/source/123".try_into()?),
r#type: "build".to_string(),
content: Content::BuildQueued(build_queued::Content{})
},
custom_data: None,
custom_data_content_type: None,
};

fn test_into_cloudevent() -> Result<(), Box<dyn std::error::Error>> {
let cdevent = CDEvent::from(
Subject::from(build_queued::Content{})
.with_id("subject123")
.with_source("/event/source/123".try_into()?)
)
.with_id("271069a8-fc18-44f1-b38f-9d70a1695819")
.with_source("https://dev.cdevents".try_into()?)
;

let cloudevent_via_builder = EventBuilderV10::new()
.with_cdevent(cdevent.clone())?
Expand All @@ -95,11 +82,11 @@ mod tests {
assert_eq!(cloudevent_via_builder, cloudevent);

assert_eq!(cloudevent.id(), "271069a8-fc18-44f1-b38f-9d70a1695819");
assert_eq!(cloudevent.id(), cdevent.context.id);
assert_eq!(cloudevent.id(), cdevent.id());

let (_, _, data) = cloudevent.take_data();
let cdevent_extracted: CDEvent = data.ok_or(Error::DataNotFoundInCloudEvent)?.try_into()?;
assert_eq!(cloudevent.id(), cdevent_extracted.context.id);
assert_eq!(cloudevent.id(), cdevent_extracted.id());
assert_eq!(cdevent, cdevent_extracted);
Ok(())
}
Expand Down
14 changes: 7 additions & 7 deletions cdevents-sdk/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ use crate::UriReference;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Context {
pub version: String,
pub id: String,
pub source: UriReference,
pub(crate) struct Context {
pub(crate) version: String,
pub(crate) id: String,
pub(crate) source: UriReference,
#[serde(rename = "type")]
pub r#type: String,
pub(crate) ty: String,
#[serde(with = "crate::serde::datetime")]
pub timestamp: time::OffsetDateTime,
pub(crate) timestamp: time::OffsetDateTime,
}

impl Default for Context {
Expand All @@ -20,7 +20,7 @@ impl Default for Context {
version: "0.3.0".into(),
id: "00000000-0000-0000-0000-000000000000".into(),
source: UriReference::default(),
r#type: "dev.cdevents.undef.undef.0.0.0".into(),
ty: "dev.cdevents.undef.undef.0.0.0".into(),
timestamp: time::OffsetDateTime::now_utc(),
}
}
Expand Down
Loading

0 comments on commit 886e00c

Please sign in to comment.