1use std::{collections::BTreeMap, error::Error, fmt};
2
3use serde::Serialize;
4
5pub const MAX_DOMAIN_EVENT_NAME_BYTES: usize = 128;
7pub const MAX_DOMAIN_FIELDS: usize = 16;
9pub const MAX_DOMAIN_FIELD_NAME_BYTES: usize = 64;
11pub const MAX_DOMAIN_STRING_BYTES: usize = 512;
13pub const MAX_DOMAIN_PAYLOAD_BYTES: usize = 4_096;
15
16#[derive(Clone, Debug, Eq, PartialEq)]
18pub enum DomainEventError {
19 EmptyEventName,
20 EventNameTooLong,
21 EmptyFieldName,
22 FieldNameTooLong,
23 TooManyFields,
24 StringValueTooLong,
25 PayloadTooLarge,
26}
27
28impl fmt::Display for DomainEventError {
29 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30 formatter.write_str(match self {
31 Self::EmptyEventName => "domain event name must not be empty",
32 Self::EventNameTooLong => "domain event name exceeds its byte limit",
33 Self::EmptyFieldName => "domain event field name must not be empty",
34 Self::FieldNameTooLong => "domain event field name exceeds its byte limit",
35 Self::TooManyFields => "domain event has too many fields",
36 Self::StringValueTooLong => "domain event string value exceeds its byte limit",
37 Self::PayloadTooLarge => "domain event payload exceeds its encoded byte limit",
38 })
39 }
40}
41
42impl Error for DomainEventError {}
43
44#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
46#[serde(rename_all = "lowercase")]
47pub enum EventLevel {
48 Debug,
49 Info,
50 Warn,
51 Error,
52}
53
54#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
59#[serde(untagged)]
60pub enum DomainValue {
61 String(String),
62 Signed(i64),
63 Unsigned(u64),
64 Bool(bool),
65}
66
67impl From<&str> for DomainValue {
68 fn from(value: &str) -> Self {
69 Self::String(value.to_owned())
70 }
71}
72
73impl From<String> for DomainValue {
74 fn from(value: String) -> Self {
75 Self::String(value)
76 }
77}
78
79impl From<i64> for DomainValue {
80 fn from(value: i64) -> Self {
81 Self::Signed(value)
82 }
83}
84
85macro_rules! signed_domain_values {
86 ($($type:ty),+ $(,)?) => {
87 $(
88 impl From<$type> for DomainValue {
89 fn from(value: $type) -> Self {
90 Self::Signed(i64::from(value))
91 }
92 }
93 )+
94 };
95}
96
97signed_domain_values!(i8, i16, i32);
98
99impl From<u64> for DomainValue {
100 fn from(value: u64) -> Self {
101 Self::Unsigned(value)
102 }
103}
104
105macro_rules! unsigned_domain_values {
106 ($($type:ty),+ $(,)?) => {
107 $(
108 impl From<$type> for DomainValue {
109 fn from(value: $type) -> Self {
110 Self::Unsigned(u64::from(value))
111 }
112 }
113 )+
114 };
115}
116
117unsigned_domain_values!(u8, u16, u32);
118
119impl From<bool> for DomainValue {
120 fn from(value: bool) -> Self {
121 Self::Bool(value)
122 }
123}
124
125impl DomainValue {
126 pub(crate) fn into_json(self) -> serde_json::Value {
127 match self {
128 Self::String(value) => serde_json::Value::String(value),
129 Self::Signed(value) => serde_json::Value::from(value),
130 Self::Unsigned(value) => serde_json::Value::from(value),
131 Self::Bool(value) => serde_json::Value::from(value),
132 }
133 }
134}
135
136#[derive(Clone, Debug, Eq, PartialEq)]
138pub struct DomainEvent {
139 pub(crate) name: String,
140 pub(crate) level: EventLevel,
141 pub(crate) fields: BTreeMap<String, DomainValue>,
142}
143
144impl DomainEvent {
145 pub fn new(name: impl Into<String>) -> Result<Self, DomainEventError> {
146 let name = name.into();
147 if name.is_empty() {
148 return Err(DomainEventError::EmptyEventName);
149 }
150 if name.len() > MAX_DOMAIN_EVENT_NAME_BYTES {
151 return Err(DomainEventError::EventNameTooLong);
152 }
153 let event = Self {
154 name,
155 level: EventLevel::Info,
156 fields: BTreeMap::new(),
157 };
158 event.validate_payload_size()?;
159 Ok(event)
160 }
161
162 pub fn level(mut self, level: EventLevel) -> Self {
163 self.level = level;
164 self
165 }
166
167 pub fn field(
168 mut self,
169 name: impl Into<String>,
170 value: impl Into<DomainValue>,
171 ) -> Result<Self, DomainEventError> {
172 let name = name.into();
173 let value = value.into();
174 if name.is_empty() {
175 return Err(DomainEventError::EmptyFieldName);
176 }
177 if name.len() > MAX_DOMAIN_FIELD_NAME_BYTES {
178 return Err(DomainEventError::FieldNameTooLong);
179 }
180 if matches!(&value, DomainValue::String(value) if value.len() > MAX_DOMAIN_STRING_BYTES) {
181 return Err(DomainEventError::StringValueTooLong);
182 }
183 if !self.fields.contains_key(&name) && self.fields.len() == MAX_DOMAIN_FIELDS {
184 return Err(DomainEventError::TooManyFields);
185 }
186 let previous = self.fields.insert(name.clone(), value);
187 if let Err(error) = self.validate_payload_size() {
188 match previous {
189 Some(previous) => {
190 self.fields.insert(name, previous);
191 }
192 None => {
193 self.fields.remove(&name);
194 }
195 }
196 return Err(error);
197 }
198 Ok(self)
199 }
200
201 fn validate_payload_size(&self) -> Result<(), DomainEventError> {
202 #[derive(Serialize)]
203 struct Payload<'a> {
204 name: &'a str,
205 fields: &'a BTreeMap<String, DomainValue>,
206 }
207
208 let size = serde_json::to_vec(&Payload {
209 name: &self.name,
210 fields: &self.fields,
211 })
212 .map_err(|_| DomainEventError::PayloadTooLarge)?
213 .len();
214 if size > MAX_DOMAIN_PAYLOAD_BYTES {
215 Err(DomainEventError::PayloadTooLarge)
216 } else {
217 Ok(())
218 }
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn rejects_oversized_names_values_and_field_counts() {
228 assert_eq!(DomainEvent::new(""), Err(DomainEventError::EmptyEventName));
229 assert_eq!(
230 DomainEvent::new("x".repeat(MAX_DOMAIN_EVENT_NAME_BYTES + 1)),
231 Err(DomainEventError::EventNameTooLong)
232 );
233 assert_eq!(
234 DomainEvent::new("event").unwrap().field("", true),
235 Err(DomainEventError::EmptyFieldName)
236 );
237 assert_eq!(
238 DomainEvent::new("event")
239 .unwrap()
240 .field("x".repeat(MAX_DOMAIN_FIELD_NAME_BYTES + 1), true),
241 Err(DomainEventError::FieldNameTooLong)
242 );
243 assert_eq!(
244 DomainEvent::new("event")
245 .unwrap()
246 .field("value", "x".repeat(MAX_DOMAIN_STRING_BYTES + 1)),
247 Err(DomainEventError::StringValueTooLong)
248 );
249
250 let mut event = DomainEvent::new("event").unwrap();
251 for index in 0..MAX_DOMAIN_FIELDS {
252 event = event.field(format!("field_{index}"), index as u64).unwrap();
253 }
254 assert_eq!(
255 event.field("one_too_many", true),
256 Err(DomainEventError::TooManyFields)
257 );
258 }
259
260 #[test]
261 fn encoded_payload_size_is_bounded() {
262 let mut event = DomainEvent::new("x".repeat(MAX_DOMAIN_EVENT_NAME_BYTES)).unwrap();
263 let mut index = 0;
264 let error = loop {
265 match event.field(
266 format!("field_{index}"),
267 "\\\"".repeat(MAX_DOMAIN_STRING_BYTES / 2),
268 ) {
269 Ok(next) => {
270 event = next;
271 index += 1;
272 }
273 Err(error) => break error,
274 }
275 };
276 assert_eq!(error, DomainEventError::PayloadTooLarge);
277 }
278}