1use serde::{Deserialize, Serialize};
9use std::fmt::{Debug, Display, Formatter};
10use uuid::Uuid;
11
12#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
17#[cfg_attr(feature = "sqlx", sqlx(transparent))]
18#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
19pub struct EventId(Uuid);
20impl Default for EventId {
21 fn default() -> Self {
23 Self(Uuid::new_v4())
24 }
25}
26impl EventId {
27 #[must_use]
30 pub fn load(id: Uuid) -> Self {
31 Self(id)
32 }
33 #[must_use]
36 pub fn as_uuid(&self) -> Uuid {
37 self.0
38 }
39}
40
41#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
49#[cfg_attr(feature = "sqlx", sqlx(transparent))]
50#[derive(Debug, Clone)]
51pub struct IdempotencyToken(pub String);
52impl IdempotencyToken {
53 #[must_use]
55 pub fn new(token: String) -> Self {
56 Self(token)
57 }
58 #[must_use]
60 pub fn as_str(&self) -> &str {
61 &self.0
62 }
63 #[must_use]
66 pub fn as_bytes(&self) -> &[u8] {
67 self.0.as_bytes()
68 }
69}
70
71#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
74#[cfg_attr(feature = "sqlx", sqlx(transparent))]
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct EventType(String);
77impl EventType {
78 #[must_use]
82 pub fn new(event_type: &str) -> Self {
83 Self(event_type.to_string())
84 }
85 #[must_use]
89 pub fn load(value: &str) -> Self {
90 Self(value.to_owned())
91 }
92 #[must_use]
94 pub fn as_str(&self) -> &str {
95 &self.0
96 }
97}
98impl Display for EventType {
99 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
100 f.write_str(&self.0)
101 }
102}
103
104#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
110#[cfg_attr(feature = "sqlx", sqlx(transparent))]
111#[derive(Debug, Clone, Serialize, Deserialize)]
112#[serde(transparent)]
113pub struct Payload<T>(T);
114impl<T> Payload<T>
115where
116 T: Debug + Clone + Serialize + Send + Sync,
117{
118 #[must_use]
120 pub fn new(payload: T) -> Self {
121 Self(payload)
122 }
123 #[must_use]
128 pub fn from_ref(value: &T) -> Self {
129 Self(value.clone())
130 }
131 #[must_use]
133 pub fn as_value(&self) -> &T {
134 &self.0
135 }
136}
137
138#[cfg(test)]
139#[allow(clippy::unwrap_used)]
140mod tests {
141 use super::*;
142 use rstest::rstest;
143
144 #[rstest]
147 fn event_id_default_generates_unique_uuids_across_calls() {
148 let a = EventId::default();
149 let b = EventId::default();
150 assert_ne!(a, b);
151 assert_ne!(a.as_uuid(), b.as_uuid());
152 }
153
154 #[rstest]
155 fn event_id_load_preserves_inner_uuid() {
156 let uuid = Uuid::new_v4();
157 let id = EventId::load(uuid);
158 assert_eq!(id.as_uuid(), uuid);
159 }
160
161 #[rstest]
162 fn event_id_equality_reflects_inner_uuid() {
163 let uuid = Uuid::new_v4();
164 let a = EventId::load(uuid);
165 let b = EventId::load(uuid);
166 assert_eq!(a, b);
167 let copied = a;
169 assert_eq!(copied, a);
170 }
171
172 #[rstest]
173 fn event_id_default_is_v4() {
174 let id = EventId::default();
175 assert_eq!(id.as_uuid().get_version_num(), 4);
176 }
177
178 #[rstest]
181 #[case("abc")]
182 #[case("")]
183 #[case("with spaces and 🦀")]
184 fn idempotency_token_new_preserves_string(#[case] raw: &str) {
185 let tok = IdempotencyToken::new(raw.to_string());
186 assert_eq!(tok.as_str(), raw);
187 }
188
189 #[rstest]
190 fn idempotency_token_as_bytes_matches_as_str_bytes() {
191 let tok = IdempotencyToken::new("hello".into());
192 assert_eq!(tok.as_bytes(), "hello".as_bytes());
193 assert_eq!(tok.as_bytes(), tok.as_str().as_bytes());
194 }
195
196 #[rstest]
199 fn event_type_new_preserves_str() {
200 let et = EventType::new("order.created");
201 assert_eq!(et.as_str(), "order.created");
202 }
203
204 #[rstest]
205 fn event_type_load_preserves_str() {
206 let et = EventType::load("order.created");
207 assert_eq!(et.as_str(), "order.created");
208 }
209
210 #[rstest]
211 fn event_type_new_and_load_produce_equal_string_views() {
212 let a = EventType::new("x");
213 let b = EventType::load("x");
214 assert_eq!(a.as_str(), b.as_str());
215 }
216
217 #[rstest]
218 fn event_type_display_matches_as_str() {
219 let et = EventType::new("payment.settled");
220 assert_eq!(format!("{et}"), et.as_str());
221 }
222
223 #[rstest]
226 fn payload_new_preserves_value() {
227 let p = Payload::new(42i32);
228 assert_eq!(*p.as_value(), 42);
229 }
230
231 #[rstest]
232 fn payload_from_ref_clones_without_consuming_source() {
233 let source = String::from("keep-me");
234 let p = Payload::from_ref(&source);
235 assert_eq!(p.as_value(), &source);
236 assert_eq!(source, "keep-me");
238 }
239
240 #[rstest]
241 fn payload_serde_is_transparent_over_inner() {
242 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
243 struct Inner {
244 a: u32,
245 b: String,
246 }
247
248 let inner = Inner {
249 a: 7,
250 b: "x".into(),
251 };
252 let wrapped = Payload::new(inner.clone());
253
254 let inner_json = serde_json::to_string(&inner).unwrap();
255 let wrapped_json = serde_json::to_string(&wrapped).unwrap();
256 assert_eq!(inner_json, wrapped_json);
257 }
258
259 #[rstest]
260 fn payload_deserialize_is_transparent_over_inner() {
261 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
262 struct Inner {
263 a: u32,
264 }
265
266 let json = r#"{"a":9}"#;
267 let p: Payload<Inner> = serde_json::from_str(json).unwrap();
268 assert_eq!(*p.as_value(), Inner { a: 9 });
269 }
270}