1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! Codec roundtrip tests using `serde_json`.
//!
//! These tests demonstrate the encode/decode identity property: for any event `e`,
//! `decode(encode(e)) == e`.
//!
//! # Design note on the `Codec` trait
//!
//! The `Codec` trait's methods are generic over `E: DomainEvent`, but `DomainEvent`
//! does not require `Serialize + DeserializeOwned`. This means a generic JSON codec
//! cannot call `serde_json::to_vec(event)` inside the trait implementation because
//! the trait bound is insufficient.
//!
//! Real-world codecs will either:
//! - Use `Any` downcasting (complex, fragile)
//! - Require a wrapper trait that adds `Serialize + DeserializeOwned` bounds
//! - Work with a concrete event type
//!
//! These tests use standalone encode/decode functions that mirror the pattern a
//! real codec implementation would follow, testing the serde roundtrip identity
//! property that underpins any JSON-based `Codec` implementation.
#![allow(
clippy::unwrap_used,
reason = "tests use unwrap for clarity and brevity"
)]
#![allow(
clippy::expect_used,
reason = "tests use expect for clarity and brevity"
)]
#![allow(clippy::str_to_string, reason = "tests use to_string/to_owned freely")]
#![allow(
clippy::shadow_reuse,
reason = "tests shadow variables for readability"
)]
#![allow(
clippy::shadow_unrelated,
reason = "tests shadow variables for readability"
)]
#![allow(
clippy::float_cmp,
reason = "tests compare exact f64 values through serde roundtrip"
)]
use mnesis::{DomainEvent, Message};
use serde::{Deserialize, Serialize};
/// A test event enum with three variant shapes to exercise serde's enum representation.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[allow(
clippy::enum_variant_names,
reason = "test enum uses Variant suffix for clarity"
)]
enum TestEvent {
/// Unit variant — no payload.
UnitVariant,
/// Tuple variant — positional fields.
TupleVariant(String, u64),
/// Struct variant — named fields.
StructVariant { name: String, value: f64 },
}
impl Message for TestEvent {}
impl DomainEvent for TestEvent {
fn name(&self) -> &'static str {
match self {
Self::UnitVariant => "UnitVariant",
Self::TupleVariant(..) => "TupleVariant",
Self::StructVariant { .. } => "StructVariant",
}
}
}
/// Encode a `TestEvent` to JSON bytes, mirroring what a JSON `Codec::encode` would do.
fn encode_test_event(event: &TestEvent) -> Vec<u8> {
serde_json::to_vec(event).expect("TestEvent should always serialize to JSON")
}
/// Decode JSON bytes back to a `TestEvent`, mirroring what a JSON `Codec::decode` would do.
///
/// # Errors
///
/// Returns `serde_json::Error` if the payload is not valid JSON or does not match
/// the `TestEvent` schema.
fn decode_test_event(payload: &[u8]) -> Result<TestEvent, serde_json::Error> {
serde_json::from_slice(payload)
}
#[test]
fn roundtrip_unit_variant() {
let original = TestEvent::UnitVariant;
let bytes = encode_test_event(&original);
let decoded = decode_test_event(&bytes).unwrap();
assert_eq!(original, decoded);
assert_eq!(original.name(), "UnitVariant");
}
#[test]
fn roundtrip_tuple_variant() {
let original = TestEvent::TupleVariant("hello".to_owned(), 42);
let bytes = encode_test_event(&original);
let decoded = decode_test_event(&bytes).unwrap();
assert_eq!(original, decoded);
assert_eq!(original.name(), "TupleVariant");
}
#[test]
fn roundtrip_struct_variant() {
let original = TestEvent::StructVariant {
name: "temperature".to_owned(),
value: 98.6,
};
let bytes = encode_test_event(&original);
let decoded = decode_test_event(&bytes).unwrap();
assert_eq!(original, decoded);
assert_eq!(original.name(), "StructVariant");
}
#[test]
fn decode_corrupted_payload() {
let garbage: &[u8] = &[0xFF, 0xFE, 0x00, 0x01, 0xDE, 0xAD];
let result = decode_test_event(garbage);
assert!(result.is_err(), "corrupted payload should fail to decode");
}
#[test]
fn decode_wrong_format() {
// Encode a StructVariant, then try to decode the raw JSON.
// serde_json decodes based on the JSON content, not an external type hint.
// So passing a "wrong" event_type string wouldn't matter to serde_json —
// it deserializes purely from the payload structure.
//
// However, if we fabricate JSON that is valid JSON but does not match any
// TestEvent variant, serde will reject it.
let original = TestEvent::StructVariant {
name: "test".to_owned(),
value: 1.0,
};
let bytes = encode_test_event(&original);
// The encoded bytes are valid JSON for a StructVariant. Decoding succeeds
// regardless of what "event_type" hint we might pass, because serde_json
// determines the variant from the JSON structure itself.
let decoded = decode_test_event(&bytes).unwrap();
assert_eq!(original, decoded);
// Now test with JSON that is valid but represents an unknown shape.
// For example, an object with a key that is not a valid TestEvent variant.
let wrong_json = br#"{"UnknownVariant": {"x": 1}}"#;
let result = decode_test_event(wrong_json);
assert!(
result.is_err(),
"JSON with an unknown variant key should fail to deserialize into TestEvent. \
serde_json uses the externally-tagged enum representation by default, so \
the variant name in JSON must match a variant of TestEvent."
);
}