use std::{collections::BTreeMap, error::Error, fmt};
use serde::Serialize;
pub const MAX_DOMAIN_EVENT_NAME_BYTES: usize = 128;
pub const MAX_DOMAIN_FIELDS: usize = 16;
pub const MAX_DOMAIN_FIELD_NAME_BYTES: usize = 64;
pub const MAX_DOMAIN_STRING_BYTES: usize = 512;
pub const MAX_DOMAIN_PAYLOAD_BYTES: usize = 4_096;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DomainEventError {
EmptyEventName,
EventNameTooLong,
EmptyFieldName,
FieldNameTooLong,
TooManyFields,
StringValueTooLong,
PayloadTooLarge,
}
impl fmt::Display for DomainEventError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::EmptyEventName => "domain event name must not be empty",
Self::EventNameTooLong => "domain event name exceeds its byte limit",
Self::EmptyFieldName => "domain event field name must not be empty",
Self::FieldNameTooLong => "domain event field name exceeds its byte limit",
Self::TooManyFields => "domain event has too many fields",
Self::StringValueTooLong => "domain event string value exceeds its byte limit",
Self::PayloadTooLarge => "domain event payload exceeds its encoded byte limit",
})
}
}
impl Error for DomainEventError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum EventLevel {
Debug,
Info,
Warn,
Error,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(untagged)]
pub enum DomainValue {
String(String),
Signed(i64),
Unsigned(u64),
Bool(bool),
}
impl From<&str> for DomainValue {
fn from(value: &str) -> Self {
Self::String(value.to_owned())
}
}
impl From<String> for DomainValue {
fn from(value: String) -> Self {
Self::String(value)
}
}
impl From<i64> for DomainValue {
fn from(value: i64) -> Self {
Self::Signed(value)
}
}
macro_rules! signed_domain_values {
($($type:ty),+ $(,)?) => {
$(
impl From<$type> for DomainValue {
fn from(value: $type) -> Self {
Self::Signed(i64::from(value))
}
}
)+
};
}
signed_domain_values!(i8, i16, i32);
impl From<u64> for DomainValue {
fn from(value: u64) -> Self {
Self::Unsigned(value)
}
}
macro_rules! unsigned_domain_values {
($($type:ty),+ $(,)?) => {
$(
impl From<$type> for DomainValue {
fn from(value: $type) -> Self {
Self::Unsigned(u64::from(value))
}
}
)+
};
}
unsigned_domain_values!(u8, u16, u32);
impl From<bool> for DomainValue {
fn from(value: bool) -> Self {
Self::Bool(value)
}
}
impl DomainValue {
pub(crate) fn into_json(self) -> serde_json::Value {
match self {
Self::String(value) => serde_json::Value::String(value),
Self::Signed(value) => serde_json::Value::from(value),
Self::Unsigned(value) => serde_json::Value::from(value),
Self::Bool(value) => serde_json::Value::from(value),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DomainEvent {
pub(crate) name: String,
pub(crate) level: EventLevel,
pub(crate) fields: BTreeMap<String, DomainValue>,
}
impl DomainEvent {
pub fn new(name: impl Into<String>) -> Result<Self, DomainEventError> {
let name = name.into();
if name.is_empty() {
return Err(DomainEventError::EmptyEventName);
}
if name.len() > MAX_DOMAIN_EVENT_NAME_BYTES {
return Err(DomainEventError::EventNameTooLong);
}
let event = Self {
name,
level: EventLevel::Info,
fields: BTreeMap::new(),
};
event.validate_payload_size()?;
Ok(event)
}
pub fn level(mut self, level: EventLevel) -> Self {
self.level = level;
self
}
pub fn field(
mut self,
name: impl Into<String>,
value: impl Into<DomainValue>,
) -> Result<Self, DomainEventError> {
let name = name.into();
let value = value.into();
if name.is_empty() {
return Err(DomainEventError::EmptyFieldName);
}
if name.len() > MAX_DOMAIN_FIELD_NAME_BYTES {
return Err(DomainEventError::FieldNameTooLong);
}
if matches!(&value, DomainValue::String(value) if value.len() > MAX_DOMAIN_STRING_BYTES) {
return Err(DomainEventError::StringValueTooLong);
}
if !self.fields.contains_key(&name) && self.fields.len() == MAX_DOMAIN_FIELDS {
return Err(DomainEventError::TooManyFields);
}
let previous = self.fields.insert(name.clone(), value);
if let Err(error) = self.validate_payload_size() {
match previous {
Some(previous) => {
self.fields.insert(name, previous);
}
None => {
self.fields.remove(&name);
}
}
return Err(error);
}
Ok(self)
}
fn validate_payload_size(&self) -> Result<(), DomainEventError> {
#[derive(Serialize)]
struct Payload<'a> {
name: &'a str,
fields: &'a BTreeMap<String, DomainValue>,
}
let size = serde_json::to_vec(&Payload {
name: &self.name,
fields: &self.fields,
})
.map_err(|_| DomainEventError::PayloadTooLarge)?
.len();
if size > MAX_DOMAIN_PAYLOAD_BYTES {
Err(DomainEventError::PayloadTooLarge)
} else {
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_oversized_names_values_and_field_counts() {
assert_eq!(DomainEvent::new(""), Err(DomainEventError::EmptyEventName));
assert_eq!(
DomainEvent::new("x".repeat(MAX_DOMAIN_EVENT_NAME_BYTES + 1)),
Err(DomainEventError::EventNameTooLong)
);
assert_eq!(
DomainEvent::new("event").unwrap().field("", true),
Err(DomainEventError::EmptyFieldName)
);
assert_eq!(
DomainEvent::new("event")
.unwrap()
.field("x".repeat(MAX_DOMAIN_FIELD_NAME_BYTES + 1), true),
Err(DomainEventError::FieldNameTooLong)
);
assert_eq!(
DomainEvent::new("event")
.unwrap()
.field("value", "x".repeat(MAX_DOMAIN_STRING_BYTES + 1)),
Err(DomainEventError::StringValueTooLong)
);
let mut event = DomainEvent::new("event").unwrap();
for index in 0..MAX_DOMAIN_FIELDS {
event = event.field(format!("field_{index}"), index as u64).unwrap();
}
assert_eq!(
event.field("one_too_many", true),
Err(DomainEventError::TooManyFields)
);
}
#[test]
fn encoded_payload_size_is_bounded() {
let mut event = DomainEvent::new("x".repeat(MAX_DOMAIN_EVENT_NAME_BYTES)).unwrap();
let mut index = 0;
let error = loop {
match event.field(
format!("field_{index}"),
"\\\"".repeat(MAX_DOMAIN_STRING_BYTES / 2),
) {
Ok(next) => {
event = next;
index += 1;
}
Err(error) => break error,
}
};
assert_eq!(error, DomainEventError::PayloadTooLarge);
}
}