use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SagaPayloadContract {
pub content_type: String,
pub schema_id: String,
}
impl Default for SagaPayloadContract {
fn default() -> Self {
Self {
content_type: "application/json".to_owned(),
schema_id: String::new(),
}
}
}
impl SagaPayloadContract {
pub fn validate(&self) -> Result<(), SagaPayloadError> {
let Some((kind, subtype)) = self.content_type.split_once('/') else {
return Err(SagaPayloadError);
};
if self.content_type.len() > 128
|| kind.is_empty()
|| subtype.is_empty()
|| !kind
.bytes()
.chain(subtype.bytes())
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b"!#$&^_.+-".contains(&b))
|| self.schema_id.len() > 256
|| self.schema_id.trim() != self.schema_id
|| self.schema_id.chars().any(char::is_control)
|| (self.content_type != "application/json" && self.schema_id.is_empty())
{
return Err(SagaPayloadError);
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SagaPayload {
pub content_type: String,
pub schema_id: String,
pub body: Vec<u8>,
}
impl SagaPayload {
pub fn new(
content_type: impl Into<String>,
schema_id: impl Into<String>,
body: Vec<u8>,
) -> Result<Self, SagaPayloadError> {
let payload = Self {
content_type: content_type.into(),
schema_id: schema_id.into(),
body,
};
payload.validate()?;
Ok(payload)
}
pub fn validate(&self) -> Result<(), SagaPayloadError> {
self.contract().validate()?;
if self.content_type == "application/json" {
serde_json::from_slice::<serde_json::Value>(&self.body)
.map_err(|_| SagaPayloadError)?;
}
Ok(())
}
pub fn contract(&self) -> SagaPayloadContract {
SagaPayloadContract {
content_type: self.content_type.clone(),
schema_id: self.schema_id.clone(),
}
}
pub fn json(value: serde_json::Value) -> Self {
Self {
content_type: "application/json".to_owned(),
schema_id: String::new(),
body: serde_json::to_vec(&value).expect("JSON value serialization"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SagaPayloadError;
impl std::fmt::Display for SagaPayloadError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("Saga payload contract is invalid")
}
}
impl std::error::Error for SagaPayloadError {}