use serde::{Deserialize, Serialize};
use std::borrow::Cow;
pub const EVENT_SCHEMA_JSON: &str = r#"[
{
"type": "record",
"name": "OrderPlaced",
"namespace": "spate.datagen",
"fields": [
{"name": "order_id", "type": "long"},
{"name": "customer_id", "type": "int"},
{"name": "region", "type": "string"},
{"name": "placed_at", "type": {"type": "long", "logicalType": "timestamp-millis"}},
{"name": "lines", "type": {"type": "array", "items": {
"type": "record",
"name": "OrderLine",
"fields": [
{"name": "sku", "type": "string"},
{"name": "qty", "type": "int"},
{"name": "unit_cents", "type": "int"}
]
}}}
]
},
{
"type": "record",
"name": "PaymentCaptured",
"namespace": "spate.datagen",
"fields": [
{"name": "order_id", "type": "long"},
{"name": "amount_cents", "type": "long"}
]
},
{
"type": "record",
"name": "RefundIssued",
"namespace": "spate.datagen",
"fields": [
{"name": "order_id", "type": "long"},
{"name": "amount_cents", "type": "long"},
{"name": "reason", "type": "string"}
]
}
]"#;
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum StorefrontEvent {
OrderPlaced(OrderPlaced),
PaymentCaptured(PaymentCaptured),
RefundIssued(RefundIssued),
}
impl StorefrontEvent {
#[must_use]
pub fn order_id(&self) -> u64 {
match self {
StorefrontEvent::OrderPlaced(e) => e.order_id,
StorefrontEvent::PaymentCaptured(e) => e.order_id,
StorefrontEvent::RefundIssued(e) => e.order_id,
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct OrderPlaced {
pub order_id: u64,
pub customer_id: u32,
pub region: Cow<'static, str>,
pub placed_at: i64,
pub lines: Vec<OrderLine>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct OrderLine {
pub sku: Cow<'static, str>,
pub qty: u32,
pub unit_cents: u32,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct PaymentCaptured {
pub order_id: u64,
pub amount_cents: u64,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct RefundIssued {
pub order_id: u64,
pub amount_cents: u64,
pub reason: Cow<'static, str>,
}
#[cfg(test)]
mod tests {
use super::*;
fn placed() -> StorefrontEvent {
StorefrontEvent::OrderPlaced(OrderPlaced {
order_id: 12,
customer_id: 7,
region: Cow::Borrowed("eu-west"),
placed_at: 1_767_225_600_000,
lines: vec![OrderLine {
sku: Cow::Borrowed("KBD-01"),
qty: 2,
unit_cents: 7_900,
}],
})
}
#[test]
fn the_json_encoding_is_internally_tagged_snake_case() {
let json = serde_json::to_string(&placed()).unwrap();
assert!(
json.starts_with(r#"{"type":"order_placed","order_id":12"#),
"{json}"
);
let refund = StorefrontEvent::RefundIssued(RefundIssued {
order_id: 12,
amount_cents: 500,
reason: Cow::Borrowed("damaged"),
});
assert_eq!(
serde_json::to_string(&refund).unwrap(),
r#"{"type":"refund_issued","order_id":12,"amount_cents":500,"reason":"damaged"}"#
);
}
#[test]
fn every_variant_round_trips_through_json() {
for event in [
placed(),
StorefrontEvent::PaymentCaptured(PaymentCaptured {
order_id: 12,
amount_cents: 15_800,
}),
StorefrontEvent::RefundIssued(RefundIssued {
order_id: 12,
amount_cents: 500,
reason: Cow::Borrowed("damaged"),
}),
] {
let bytes = serde_json::to_vec(&event).unwrap();
let back: StorefrontEvent = serde_json::from_slice(&bytes).unwrap();
assert_eq!(back, event, "round trip changed the value");
assert_eq!(back.order_id(), 12);
}
}
#[test]
fn an_unknown_tag_is_a_decode_error_rather_than_a_silent_drop() {
let err = serde_json::from_str::<StorefrontEvent>(r#"{"type":"order_shipped"}"#)
.expect_err("an unmodelled event type must not decode");
assert!(err.to_string().contains("order_shipped"), "{err}");
}
}