1use core::num::NonZeroU32;
14
15use bytes::Bytes;
16use thiserror::Error;
17
18#[allow(
20 clippy::as_conversions,
21 reason = "const-context u16→usize widening; lossless on all targets"
22)]
23pub const MAX_EVENT_TYPE_LEN: usize = u16::MAX as usize;
24
25#[allow(
28 clippy::as_conversions,
29 reason = "const-context u32→usize widening; lossless on 32+ bit targets"
30)]
31pub const MAX_METADATA_LEN: usize = (u32::MAX - 1) as usize;
32
33#[allow(
35 clippy::as_conversions,
36 reason = "const-context u32→usize widening; lossless on 32+ bit targets"
37)]
38pub const MAX_PAYLOAD_LEN: usize = u32::MAX as usize;
39
40#[derive(Debug, Error)]
42#[non_exhaustive]
43pub enum ValueError {
44 #[error("event_type length {actual} exceeds maximum {MAX_EVENT_TYPE_LEN}")]
45 EventTypeTooLong { actual: usize },
46 #[error("invalid UTF-8 in event_type bytes (at byte {valid_up_to})")]
47 EventTypeInvalidUtf8 {
48 valid_up_to: usize,
49 #[source]
50 source: core::str::Utf8Error,
51 },
52 #[error("payload length {actual} exceeds maximum {MAX_PAYLOAD_LEN}")]
53 PayloadTooLong { actual: usize },
54 #[error("metadata length {actual} exceeds maximum {MAX_METADATA_LEN}")]
55 MetadataTooLong { actual: usize },
56 #[error("metadata is empty; use Option::None to represent absent metadata")]
57 MetadataEmpty,
58 #[error("schema_version must be > 0 (got 0)")]
59 SchemaVersionZero,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Hash)]
71pub struct EventType {
72 inner: Bytes,
73}
74
75impl EventType {
76 #[must_use]
87 pub fn from_static_str(s: &'static str) -> Self {
88 assert!(
89 s.len() <= MAX_EVENT_TYPE_LEN,
90 "event_type length {} exceeds MAX_EVENT_TYPE_LEN ({})",
91 s.len(),
92 MAX_EVENT_TYPE_LEN,
93 );
94 Self {
95 inner: Bytes::from_static(s.as_bytes()),
96 }
97 }
98
99 pub fn from_bytes(bytes: Bytes) -> Result<Self, ValueError> {
106 if bytes.len() > MAX_EVENT_TYPE_LEN {
107 return Err(ValueError::EventTypeTooLong {
108 actual: bytes.len(),
109 });
110 }
111 core::str::from_utf8(&bytes).map_err(|e| ValueError::EventTypeInvalidUtf8 {
112 valid_up_to: e.valid_up_to(),
113 source: e,
114 })?;
115 Ok(Self { inner: bytes })
116 }
117
118 pub(crate) unsafe fn from_validated_bytes(bytes: Bytes) -> Self {
133 debug_assert!(
134 bytes.len() <= MAX_EVENT_TYPE_LEN,
135 "from_validated_bytes invariant: length ≤ MAX_EVENT_TYPE_LEN"
136 );
137 debug_assert!(
138 core::str::from_utf8(&bytes).is_ok(),
139 "from_validated_bytes invariant: valid UTF-8"
140 );
141 Self { inner: bytes }
142 }
143
144 #[must_use]
146 pub fn as_str(&self) -> &str {
147 #[allow(
150 unsafe_code,
151 reason = "UTF-8 invariant established by every constructor"
152 )]
153 unsafe {
154 core::str::from_utf8_unchecked(&self.inner)
155 }
156 }
157
158 #[must_use]
160 pub fn as_bytes(&self) -> &[u8] {
161 &self.inner
162 }
163
164 #[must_use]
166 pub fn into_bytes(self) -> Bytes {
167 self.inner
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Hash)]
179pub struct Payload {
180 inner: Bytes,
181}
182
183impl Payload {
184 pub fn from_bytes(bytes: Bytes) -> Result<Self, ValueError> {
190 if bytes.len() > MAX_PAYLOAD_LEN {
191 return Err(ValueError::PayloadTooLong {
192 actual: bytes.len(),
193 });
194 }
195 Ok(Self { inner: bytes })
196 }
197
198 pub(crate) fn from_validated_bytes(bytes: Bytes) -> Self {
212 debug_assert!(
213 bytes.len() <= MAX_PAYLOAD_LEN,
214 "from_validated_bytes invariant: length ≤ MAX_PAYLOAD_LEN"
215 );
216 Self { inner: bytes }
217 }
218
219 #[must_use]
221 pub fn as_slice(&self) -> &[u8] {
222 &self.inner
223 }
224
225 #[must_use]
227 pub fn into_bytes(self) -> Bytes {
228 self.inner
229 }
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Hash)]
246pub struct Metadata {
247 inner: Bytes,
248}
249
250impl Metadata {
251 pub fn from_bytes(bytes: Bytes) -> Result<Self, ValueError> {
258 if bytes.is_empty() {
259 return Err(ValueError::MetadataEmpty);
260 }
261 if bytes.len() > MAX_METADATA_LEN {
262 return Err(ValueError::MetadataTooLong {
263 actual: bytes.len(),
264 });
265 }
266 Ok(Self { inner: bytes })
267 }
268
269 pub(crate) fn from_validated_bytes(bytes: Bytes) -> Self {
288 debug_assert!(
289 !bytes.is_empty(),
290 "from_validated_bytes invariant: non-empty"
291 );
292 debug_assert!(
293 bytes.len() <= MAX_METADATA_LEN,
294 "from_validated_bytes invariant: length ≤ MAX_METADATA_LEN"
295 );
296 Self { inner: bytes }
297 }
298
299 #[must_use]
301 pub fn as_slice(&self) -> &[u8] {
302 &self.inner
303 }
304
305 #[must_use]
307 pub fn into_bytes(self) -> Bytes {
308 self.inner
309 }
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
323pub struct SchemaVersion(NonZeroU32);
324
325impl SchemaVersion {
326 pub const INITIAL: Self = Self(NonZeroU32::MIN);
328
329 #[must_use]
331 pub const fn new(value: NonZeroU32) -> Self {
332 Self(value)
333 }
334
335 pub fn from_u32(value: u32) -> Result<Self, ValueError> {
341 NonZeroU32::new(value)
342 .map(Self)
343 .ok_or(ValueError::SchemaVersionZero)
344 }
345
346 #[must_use]
348 pub const fn get(self) -> u32 {
349 self.0.get()
350 }
351}
352
353impl From<SchemaVersion> for mnesis::Version {
354 #[allow(
357 clippy::expect_used,
358 reason = "NonZeroU32 widened to u64 is structurally nonzero; the None arm of Version::new is unreachable"
359 )]
360 fn from(sv: SchemaVersion) -> Self {
361 Self::new(u64::from(sv.get())).expect("NonZeroU32 widened to u64 is nonzero")
362 }
363}
364
365#[cfg(test)]
366mod event_type_tests {
367 use super::*;
368 use bytes::Bytes;
369
370 #[test]
371 fn from_static_str_accepts_literal() {
372 let et = EventType::from_static_str("UserCreated");
373 assert_eq!(et.as_str(), "UserCreated");
374 }
375
376 #[test]
377 fn from_bytes_accepts_valid_utf8_within_cap() {
378 let et = EventType::from_bytes(Bytes::from_static(b"OrderPlaced"))
379 .expect("valid utf-8 within cap");
380 assert_eq!(et.as_str(), "OrderPlaced");
381 }
382
383 #[test]
384 fn from_bytes_rejects_invalid_utf8() {
385 let err = EventType::from_bytes(Bytes::from_static(&[0xFFu8, 0xFE]))
386 .expect_err("must reject invalid utf-8");
387 assert!(matches!(err, ValueError::EventTypeInvalidUtf8 { .. }));
388 }
389
390 #[test]
391 fn from_bytes_rejects_oversize() {
392 let too_big = Bytes::from(vec![b'a'; MAX_EVENT_TYPE_LEN + 1]);
393 let err =
394 EventType::from_bytes(too_big).expect_err("must reject length > MAX_EVENT_TYPE_LEN");
395 assert!(
396 matches!(err, ValueError::EventTypeTooLong { actual } if actual == MAX_EVENT_TYPE_LEN + 1)
397 );
398 }
399
400 #[test]
401 fn at_max_cap_accepted() {
402 let at_cap = Bytes::from(vec![b'a'; MAX_EVENT_TYPE_LEN]);
403 let et = EventType::from_bytes(at_cap).expect("length at cap is valid");
404 assert_eq!(et.as_bytes().len(), MAX_EVENT_TYPE_LEN);
405 }
406
407 #[test]
408 fn into_bytes_returns_inner_arc() {
409 let et = EventType::from_static_str("Foo");
410 let bytes = et.into_bytes();
411 assert_eq!(bytes.as_ref(), b"Foo");
412 }
413
414 #[test]
415 fn debug_redacts_nothing_short_names_show_in_full() {
416 let et = EventType::from_static_str("Foo");
417 let dbg = format!("{et:?}");
418 assert!(dbg.contains("Foo"), "Debug should include event type name");
419 assert!(
420 dbg.len() < 40,
421 "Debug should be tight (no padding/extra fields): got {} chars: {dbg}",
422 dbg.len(),
423 );
424 }
425
426 #[test]
427 #[should_panic(expected = "exceeds MAX_EVENT_TYPE_LEN")]
428 fn from_static_str_panics_on_leaked_oversize_string() {
429 let big = "a".repeat(MAX_EVENT_TYPE_LEN + 1);
435 let leaked: &'static str = Box::leak(big.into_boxed_str());
436 let _ = EventType::from_static_str(leaked);
437 }
438}
439
440#[cfg(test)]
441mod payload_tests {
442 use super::*;
443
444 #[test]
445 fn from_bytes_accepts_empty() {
446 let p = Payload::from_bytes(Bytes::new()).expect("empty payload is valid");
447 assert!(p.as_slice().is_empty());
448 }
449
450 #[test]
451 fn from_bytes_accepts_small() {
452 let p = Payload::from_bytes(Bytes::from_static(b"hello")).expect("valid");
453 assert_eq!(p.as_slice(), b"hello");
454 }
455
456 #[test]
457 fn into_bytes_returns_inner_arc() {
458 let p = Payload::from_bytes(Bytes::from_static(b"payload")).expect("valid");
459 let bytes = p.into_bytes();
460 assert_eq!(bytes.as_ref(), b"payload");
461 }
462
463 }
467
468#[cfg(test)]
469mod metadata_tests {
470 use super::*;
471
472 #[test]
473 fn from_bytes_accepts_small() {
474 let m = Metadata::from_bytes(Bytes::from_static(b"meta")).expect("valid");
475 assert_eq!(m.as_slice(), b"meta");
476 }
477
478 #[test]
479 fn from_bytes_rejects_empty() {
480 let err = Metadata::from_bytes(Bytes::new())
481 .expect_err("empty metadata must be rejected — use Option::None for absent");
482 assert!(matches!(err, ValueError::MetadataEmpty));
483 }
484
485 #[test]
486 fn into_bytes_returns_inner_arc() {
487 let m = Metadata::from_bytes(Bytes::from_static(b"m")).expect("valid");
488 let bytes = m.into_bytes();
489 assert_eq!(bytes.as_ref(), b"m");
490 }
491}
492
493#[cfg(test)]
494mod schema_version_tests {
495 use super::*;
496 use std::num::NonZeroU32;
497
498 #[test]
499 fn new_accepts_nonzero() {
500 let nz = NonZeroU32::new(1).expect("nonzero");
501 let sv = SchemaVersion::new(nz);
502 assert_eq!(sv.get(), 1);
503 }
504
505 #[test]
506 fn initial_is_one() {
507 assert_eq!(SchemaVersion::INITIAL.get(), 1);
508 }
509
510 #[test]
511 fn from_u32_accepts_nonzero() {
512 let sv = SchemaVersion::from_u32(42).expect("nonzero");
513 assert_eq!(sv.get(), 42);
514 }
515
516 #[test]
517 fn from_u32_rejects_zero() {
518 let err = SchemaVersion::from_u32(0).expect_err("zero rejected");
519 assert!(matches!(err, ValueError::SchemaVersionZero));
520 }
521
522 #[test]
523 fn into_version_widens_to_nonzero_u64() {
524 let sv = SchemaVersion::from_u32(7).expect("nonzero");
525 let v: mnesis::Version = sv.into();
526 assert_eq!(v.as_u64(), 7);
527 }
528}
529
530#[cfg(test)]
531mod property_tests {
532 use super::*;
533 use bytes::Bytes;
534 use proptest::prelude::*;
535 use std::num::NonZeroU32;
536
537 proptest! {
538 #[test]
539 fn event_type_from_bytes_roundtrips_valid_utf8(s in "[a-zA-Z][a-zA-Z0-9_]{0,63}") {
540 let bytes = Bytes::from(s.clone().into_bytes());
541 let et = EventType::from_bytes(bytes).expect("valid");
542 prop_assert_eq!(et.as_str(), &s);
543 }
544
545 #[test]
546 fn payload_arbitrary_bytes_within_cap_accepted(
547 buf in proptest::collection::vec(any::<u8>(), 0..4096),
548 ) {
549 let p = Payload::from_bytes(Bytes::from(buf.clone())).expect("under cap");
550 prop_assert_eq!(p.as_slice(), buf.as_slice());
551 }
552
553 #[test]
554 fn metadata_nonempty_within_cap_accepted(
555 buf in proptest::collection::vec(any::<u8>(), 1..4096),
556 ) {
557 let m = Metadata::from_bytes(Bytes::from(buf.clone())).expect("nonempty, under cap");
558 prop_assert_eq!(m.as_slice(), buf.as_slice());
559 }
560
561 #[test]
562 fn event_type_invalid_utf8_always_rejected(
563 bytes in proptest::collection::vec(any::<u8>(), 1..256),
564 ) {
565 prop_assume!(bytes.iter().any(|&b| b == 0xFEu8 || b == 0xFFu8));
568 let result = EventType::from_bytes(Bytes::from(bytes));
569 prop_assert!(result.is_err());
570 }
571
572 #[test]
573 fn schema_version_from_u32_roundtrips_nonzero(value in 1u32..) {
574 let sv = SchemaVersion::from_u32(value).expect("nonzero");
575 prop_assert_eq!(sv.get(), value);
576 }
577
578 #[test]
579 fn schema_version_widens_to_version(value in 1u32..) {
580 let sv = SchemaVersion::from_u32(value).expect("nonzero");
581 let v: mnesis::Version = sv.into();
582 prop_assert_eq!(v.as_u64(), u64::from(value));
583 }
584
585 #[test]
586 fn schema_version_ord_agrees_with_inner_u32_ord(
587 a in 1u32..,
588 b in 1u32..,
589 ) {
590 let sa = SchemaVersion::from_u32(a).expect("nonzero");
591 let sb = SchemaVersion::from_u32(b).expect("nonzero");
592 prop_assert_eq!(sa.cmp(&sb), a.cmp(&b));
593 }
594 }
595
596 #[test]
599 fn metadata_empty_always_rejected() {
600 let err = Metadata::from_bytes(Bytes::new()).expect_err("empty metadata always rejected");
601 assert!(matches!(err, ValueError::MetadataEmpty));
602 }
603
604 #[test]
605 fn schema_version_zero_rejected() {
606 let err = SchemaVersion::from_u32(0).expect_err("zero rejected");
607 assert!(matches!(err, ValueError::SchemaVersionZero));
608 }
609
610 #[test]
611 fn schema_version_initial_equals_one() {
612 assert_eq!(SchemaVersion::INITIAL.get(), 1);
613 }
614
615 #[test]
616 fn schema_version_initial_widens_to_version_one() {
617 let v: mnesis::Version = SchemaVersion::INITIAL.into();
618 assert_eq!(v.as_u64(), 1);
619 }
620
621 #[test]
622 fn schema_version_new_from_nonzero_construction() {
623 let nz = NonZeroU32::new(99).expect("nonzero");
624 let sv = SchemaVersion::new(nz);
625 assert_eq!(sv.get(), 99);
626 }
627}