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) unsafe fn from_validated_bytes(bytes: Bytes) -> Self {
216 debug_assert!(
217 bytes.len() <= MAX_PAYLOAD_LEN,
218 "from_validated_bytes invariant: length ≤ MAX_PAYLOAD_LEN"
219 );
220 Self { inner: bytes }
221 }
222
223 #[must_use]
225 pub fn as_slice(&self) -> &[u8] {
226 &self.inner
227 }
228
229 #[must_use]
231 pub fn into_bytes(self) -> Bytes {
232 self.inner
233 }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Hash)]
250pub struct Metadata {
251 inner: Bytes,
252}
253
254impl Metadata {
255 pub fn from_bytes(bytes: Bytes) -> Result<Self, ValueError> {
262 if bytes.is_empty() {
263 return Err(ValueError::MetadataEmpty);
264 }
265 if bytes.len() > MAX_METADATA_LEN {
266 return Err(ValueError::MetadataTooLong {
267 actual: bytes.len(),
268 });
269 }
270 Ok(Self { inner: bytes })
271 }
272
273 pub(crate) unsafe fn from_validated_bytes(bytes: Bytes) -> Self {
294 debug_assert!(
295 !bytes.is_empty(),
296 "from_validated_bytes invariant: non-empty"
297 );
298 debug_assert!(
299 bytes.len() <= MAX_METADATA_LEN,
300 "from_validated_bytes invariant: length ≤ MAX_METADATA_LEN"
301 );
302 Self { inner: bytes }
303 }
304
305 #[must_use]
307 pub fn as_slice(&self) -> &[u8] {
308 &self.inner
309 }
310
311 #[must_use]
313 pub fn into_bytes(self) -> Bytes {
314 self.inner
315 }
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
329pub struct SchemaVersion(NonZeroU32);
330
331impl SchemaVersion {
332 pub const INITIAL: Self = Self(NonZeroU32::MIN);
334
335 #[must_use]
337 pub const fn new(value: NonZeroU32) -> Self {
338 Self(value)
339 }
340
341 pub fn from_u32(value: u32) -> Result<Self, ValueError> {
347 NonZeroU32::new(value)
348 .map(Self)
349 .ok_or(ValueError::SchemaVersionZero)
350 }
351
352 #[must_use]
354 pub const fn get(self) -> u32 {
355 self.0.get()
356 }
357}
358
359impl From<SchemaVersion> for mnesis::Version {
360 #[allow(
363 clippy::expect_used,
364 reason = "NonZeroU32 widened to u64 is structurally nonzero; the None arm of Version::new is unreachable"
365 )]
366 fn from(sv: SchemaVersion) -> Self {
367 Self::new(u64::from(sv.get())).expect("NonZeroU32 widened to u64 is nonzero")
368 }
369}
370
371#[cfg(test)]
372mod event_type_tests {
373 use super::*;
374 use bytes::Bytes;
375
376 #[test]
377 fn from_static_str_accepts_literal() {
378 let et = EventType::from_static_str("UserCreated");
379 assert_eq!(et.as_str(), "UserCreated");
380 }
381
382 #[test]
383 fn from_bytes_accepts_valid_utf8_within_cap() {
384 let et = EventType::from_bytes(Bytes::from_static(b"OrderPlaced"))
385 .expect("valid utf-8 within cap");
386 assert_eq!(et.as_str(), "OrderPlaced");
387 }
388
389 #[test]
390 fn from_bytes_rejects_invalid_utf8() {
391 let err = EventType::from_bytes(Bytes::from_static(&[0xFFu8, 0xFE]))
392 .expect_err("must reject invalid utf-8");
393 assert!(matches!(err, ValueError::EventTypeInvalidUtf8 { .. }));
394 }
395
396 #[test]
397 fn from_bytes_rejects_oversize() {
398 let too_big = Bytes::from(vec![b'a'; MAX_EVENT_TYPE_LEN + 1]);
399 let err =
400 EventType::from_bytes(too_big).expect_err("must reject length > MAX_EVENT_TYPE_LEN");
401 assert!(
402 matches!(err, ValueError::EventTypeTooLong { actual } if actual == MAX_EVENT_TYPE_LEN + 1)
403 );
404 }
405
406 #[test]
407 fn at_max_cap_accepted() {
408 let at_cap = Bytes::from(vec![b'a'; MAX_EVENT_TYPE_LEN]);
409 let et = EventType::from_bytes(at_cap).expect("length at cap is valid");
410 assert_eq!(et.as_bytes().len(), MAX_EVENT_TYPE_LEN);
411 }
412
413 #[test]
414 fn into_bytes_returns_inner_arc() {
415 let et = EventType::from_static_str("Foo");
416 let bytes = et.into_bytes();
417 assert_eq!(bytes.as_ref(), b"Foo");
418 }
419
420 #[test]
421 fn debug_redacts_nothing_short_names_show_in_full() {
422 let et = EventType::from_static_str("Foo");
423 let dbg = format!("{et:?}");
424 assert!(dbg.contains("Foo"), "Debug should include event type name");
425 assert!(
426 dbg.len() < 40,
427 "Debug should be tight (no padding/extra fields): got {} chars: {dbg}",
428 dbg.len(),
429 );
430 }
431
432 #[test]
433 #[should_panic(expected = "exceeds MAX_EVENT_TYPE_LEN")]
434 fn from_static_str_panics_on_leaked_oversize_string() {
435 let big = "a".repeat(MAX_EVENT_TYPE_LEN + 1);
441 let leaked: &'static str = Box::leak(big.into_boxed_str());
442 let _ = EventType::from_static_str(leaked);
443 }
444}
445
446#[cfg(test)]
447mod payload_tests {
448 use super::*;
449
450 #[test]
451 fn from_bytes_accepts_empty() {
452 let p = Payload::from_bytes(Bytes::new()).expect("empty payload is valid");
453 assert!(p.as_slice().is_empty());
454 }
455
456 #[test]
457 fn from_bytes_accepts_small() {
458 let p = Payload::from_bytes(Bytes::from_static(b"hello")).expect("valid");
459 assert_eq!(p.as_slice(), b"hello");
460 }
461
462 #[test]
463 fn into_bytes_returns_inner_arc() {
464 let p = Payload::from_bytes(Bytes::from_static(b"payload")).expect("valid");
465 let bytes = p.into_bytes();
466 assert_eq!(bytes.as_ref(), b"payload");
467 }
468
469 }
473
474#[cfg(test)]
475mod metadata_tests {
476 use super::*;
477
478 #[test]
479 fn from_bytes_accepts_small() {
480 let m = Metadata::from_bytes(Bytes::from_static(b"meta")).expect("valid");
481 assert_eq!(m.as_slice(), b"meta");
482 }
483
484 #[test]
485 fn from_bytes_rejects_empty() {
486 let err = Metadata::from_bytes(Bytes::new())
487 .expect_err("empty metadata must be rejected — use Option::None for absent");
488 assert!(matches!(err, ValueError::MetadataEmpty));
489 }
490
491 #[test]
492 fn into_bytes_returns_inner_arc() {
493 let m = Metadata::from_bytes(Bytes::from_static(b"m")).expect("valid");
494 let bytes = m.into_bytes();
495 assert_eq!(bytes.as_ref(), b"m");
496 }
497}
498
499#[cfg(test)]
500mod schema_version_tests {
501 use super::*;
502 use std::num::NonZeroU32;
503
504 #[test]
505 fn new_accepts_nonzero() {
506 let nz = NonZeroU32::new(1).expect("nonzero");
507 let sv = SchemaVersion::new(nz);
508 assert_eq!(sv.get(), 1);
509 }
510
511 #[test]
512 fn initial_is_one() {
513 assert_eq!(SchemaVersion::INITIAL.get(), 1);
514 }
515
516 #[test]
517 fn from_u32_accepts_nonzero() {
518 let sv = SchemaVersion::from_u32(42).expect("nonzero");
519 assert_eq!(sv.get(), 42);
520 }
521
522 #[test]
523 fn from_u32_rejects_zero() {
524 let err = SchemaVersion::from_u32(0).expect_err("zero rejected");
525 assert!(matches!(err, ValueError::SchemaVersionZero));
526 }
527
528 #[test]
529 fn into_version_widens_to_nonzero_u64() {
530 let sv = SchemaVersion::from_u32(7).expect("nonzero");
531 let v: mnesis::Version = sv.into();
532 assert_eq!(v.as_u64(), 7);
533 }
534}
535
536#[cfg(test)]
537mod property_tests {
538 use super::*;
539 use bytes::Bytes;
540 use proptest::prelude::*;
541 use std::num::NonZeroU32;
542
543 proptest! {
544 #[test]
545 fn event_type_from_bytes_roundtrips_valid_utf8(s in "[a-zA-Z][a-zA-Z0-9_]{0,63}") {
546 let bytes = Bytes::from(s.clone().into_bytes());
547 let et = EventType::from_bytes(bytes).expect("valid");
548 prop_assert_eq!(et.as_str(), &s);
549 }
550
551 #[test]
552 fn payload_arbitrary_bytes_within_cap_accepted(
553 buf in proptest::collection::vec(any::<u8>(), 0..4096),
554 ) {
555 let p = Payload::from_bytes(Bytes::from(buf.clone())).expect("under cap");
556 prop_assert_eq!(p.as_slice(), buf.as_slice());
557 }
558
559 #[test]
560 fn metadata_nonempty_within_cap_accepted(
561 buf in proptest::collection::vec(any::<u8>(), 1..4096),
562 ) {
563 let m = Metadata::from_bytes(Bytes::from(buf.clone())).expect("nonempty, under cap");
564 prop_assert_eq!(m.as_slice(), buf.as_slice());
565 }
566
567 #[test]
568 fn event_type_invalid_utf8_always_rejected(
569 bytes in proptest::collection::vec(any::<u8>(), 1..256),
570 ) {
571 prop_assume!(bytes.iter().any(|&b| b == 0xFEu8 || b == 0xFFu8));
574 let result = EventType::from_bytes(Bytes::from(bytes));
575 prop_assert!(result.is_err());
576 }
577
578 #[test]
579 fn schema_version_from_u32_roundtrips_nonzero(value in 1u32..) {
580 let sv = SchemaVersion::from_u32(value).expect("nonzero");
581 prop_assert_eq!(sv.get(), value);
582 }
583
584 #[test]
585 fn schema_version_widens_to_version(value in 1u32..) {
586 let sv = SchemaVersion::from_u32(value).expect("nonzero");
587 let v: mnesis::Version = sv.into();
588 prop_assert_eq!(v.as_u64(), u64::from(value));
589 }
590
591 #[test]
592 fn schema_version_ord_agrees_with_inner_u32_ord(
593 a in 1u32..,
594 b in 1u32..,
595 ) {
596 let sa = SchemaVersion::from_u32(a).expect("nonzero");
597 let sb = SchemaVersion::from_u32(b).expect("nonzero");
598 prop_assert_eq!(sa.cmp(&sb), a.cmp(&b));
599 }
600 }
601
602 #[test]
605 fn metadata_empty_always_rejected() {
606 let err = Metadata::from_bytes(Bytes::new()).expect_err("empty metadata always rejected");
607 assert!(matches!(err, ValueError::MetadataEmpty));
608 }
609
610 #[test]
611 fn schema_version_zero_rejected() {
612 let err = SchemaVersion::from_u32(0).expect_err("zero rejected");
613 assert!(matches!(err, ValueError::SchemaVersionZero));
614 }
615
616 #[test]
617 fn schema_version_initial_equals_one() {
618 assert_eq!(SchemaVersion::INITIAL.get(), 1);
619 }
620
621 #[test]
622 fn schema_version_initial_widens_to_version_one() {
623 let v: mnesis::Version = SchemaVersion::INITIAL.into();
624 assert_eq!(v.as_u64(), 1);
625 }
626
627 #[test]
628 fn schema_version_new_from_nonzero_construction() {
629 let nz = NonZeroU32::new(99).expect("nonzero");
630 let sv = SchemaVersion::new(nz);
631 assert_eq!(sv.get(), 99);
632 }
633}