1use serde::{Deserialize, Serialize};
19use std::borrow::Cow;
20
21pub const EVENT_SCHEMA_JSON: &str = r#"[
29 {
30 "type": "record",
31 "name": "OrderPlaced",
32 "namespace": "spate.datagen",
33 "fields": [
34 {"name": "order_id", "type": "long"},
35 {"name": "customer_id", "type": "int"},
36 {"name": "region", "type": "string"},
37 {"name": "placed_at", "type": {"type": "long", "logicalType": "timestamp-millis"}},
38 {"name": "lines", "type": {"type": "array", "items": {
39 "type": "record",
40 "name": "OrderLine",
41 "fields": [
42 {"name": "sku", "type": "string"},
43 {"name": "qty", "type": "int"},
44 {"name": "unit_cents", "type": "int"}
45 ]
46 }}}
47 ]
48 },
49 {
50 "type": "record",
51 "name": "PaymentCaptured",
52 "namespace": "spate.datagen",
53 "fields": [
54 {"name": "order_id", "type": "long"},
55 {"name": "amount_cents", "type": "long"}
56 ]
57 },
58 {
59 "type": "record",
60 "name": "RefundIssued",
61 "namespace": "spate.datagen",
62 "fields": [
63 {"name": "order_id", "type": "long"},
64 {"name": "amount_cents", "type": "long"},
65 {"name": "reason", "type": "string"}
66 ]
67 }
68]"#;
69
70#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
73#[serde(tag = "type", rename_all = "snake_case")]
74#[non_exhaustive]
75pub enum StorefrontEvent {
76 OrderPlaced(OrderPlaced),
78 PaymentCaptured(PaymentCaptured),
80 RefundIssued(RefundIssued),
82}
83
84impl StorefrontEvent {
85 #[must_use]
88 pub fn order_id(&self) -> u64 {
89 match self {
90 StorefrontEvent::OrderPlaced(e) => e.order_id,
91 StorefrontEvent::PaymentCaptured(e) => e.order_id,
92 StorefrontEvent::RefundIssued(e) => e.order_id,
93 }
94 }
95}
96
97#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
99#[non_exhaustive]
100pub struct OrderPlaced {
101 pub order_id: u64,
104 pub customer_id: u32,
106 pub region: Cow<'static, str>,
108 pub placed_at: i64,
110 pub lines: Vec<OrderLine>,
112}
113
114#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
116#[non_exhaustive]
117pub struct OrderLine {
118 pub sku: Cow<'static, str>,
120 pub qty: u32,
122 pub unit_cents: u32,
124}
125
126#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
129#[non_exhaustive]
130pub struct PaymentCaptured {
131 pub order_id: u64,
133 pub amount_cents: u64,
136}
137
138#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
140#[non_exhaustive]
141pub struct RefundIssued {
142 pub order_id: u64,
144 pub amount_cents: u64,
146 pub reason: Cow<'static, str>,
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 fn placed() -> StorefrontEvent {
155 StorefrontEvent::OrderPlaced(OrderPlaced {
156 order_id: 12,
157 customer_id: 7,
158 region: Cow::Borrowed("eu-west"),
159 placed_at: 1_767_225_600_000,
160 lines: vec![OrderLine {
161 sku: Cow::Borrowed("KBD-01"),
162 qty: 2,
163 unit_cents: 7_900,
164 }],
165 })
166 }
167
168 #[test]
171 fn the_json_encoding_is_internally_tagged_snake_case() {
172 let json = serde_json::to_string(&placed()).unwrap();
173 assert!(
174 json.starts_with(r#"{"type":"order_placed","order_id":12"#),
175 "{json}"
176 );
177
178 let refund = StorefrontEvent::RefundIssued(RefundIssued {
179 order_id: 12,
180 amount_cents: 500,
181 reason: Cow::Borrowed("damaged"),
182 });
183 assert_eq!(
184 serde_json::to_string(&refund).unwrap(),
185 r#"{"type":"refund_issued","order_id":12,"amount_cents":500,"reason":"damaged"}"#
186 );
187 }
188
189 #[test]
193 fn every_variant_round_trips_through_json() {
194 for event in [
195 placed(),
196 StorefrontEvent::PaymentCaptured(PaymentCaptured {
197 order_id: 12,
198 amount_cents: 15_800,
199 }),
200 StorefrontEvent::RefundIssued(RefundIssued {
201 order_id: 12,
202 amount_cents: 500,
203 reason: Cow::Borrowed("damaged"),
204 }),
205 ] {
206 let bytes = serde_json::to_vec(&event).unwrap();
207 let back: StorefrontEvent = serde_json::from_slice(&bytes).unwrap();
208 assert_eq!(back, event, "round trip changed the value");
209 assert_eq!(back.order_id(), 12);
210 }
211 }
212
213 #[test]
214 fn an_unknown_tag_is_a_decode_error_rather_than_a_silent_drop() {
215 let err = serde_json::from_str::<StorefrontEvent>(r#"{"type":"order_shipped"}"#)
216 .expect_err("an unmodelled event type must not decode");
217 assert!(err.to_string().contains("order_shipped"), "{err}");
218 }
219}