use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::event::RawEvent;
pub type WireVersion = u8;
pub const CURRENT_WIRE_VERSION: WireVersion = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct BatchId(pub Uuid);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SessionId(pub Uuid);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct DeviceId(pub String);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OsKind {
Ios,
Android,
Linux,
Macos,
Windows,
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Batch {
pub batch_id: BatchId,
pub session_id: SessionId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub device_id: Option<DeviceId>,
pub wire_version: WireVersion,
pub app_version: String,
pub os: OsKind,
pub os_major: u16,
pub sdk_version: String,
pub events: Vec<RawEvent>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn batch_round_trip_preserves_fields() {
let batch = Batch {
batch_id: BatchId(Uuid::new_v4()),
session_id: SessionId(Uuid::new_v4()),
device_id: None,
wire_version: CURRENT_WIRE_VERSION,
app_version: "1.4.2".into(),
os: OsKind::Ios,
os_major: 18,
sdk_version: "0.11.0".into(),
events: vec![RawEvent {
event_type: "protocol.message.delivered".into(),
ts_ms: 1_747_000_000_000,
data: json!({ "latency_ms": 240, "hop_count": 1, "transport": "ble" }),
}],
};
let s = serde_json::to_string(&batch).unwrap();
let back: Batch = serde_json::from_str(&s).unwrap();
assert_eq!(back.events.len(), 1);
assert_eq!(back.events[0].event_type, "protocol.message.delivered");
assert_eq!(back.wire_version, CURRENT_WIRE_VERSION);
assert!(matches!(back.os, OsKind::Ios));
}
#[test]
fn os_lowercases_in_wire_form() {
let v = serde_json::to_value(OsKind::Android).unwrap();
assert_eq!(v, json!("android"));
}
#[test]
fn pre_device_id_wire_v1_json_still_deserializes() {
let legacy = json!({
"batch_id": "1f0e9a4c-3b6d-4e2f-9a1b-2c3d4e5f6a7b",
"session_id": "00000000-0000-0000-0000-000000000001",
"wire_version": 1,
"app_version": "1.4.2",
"os": "android",
"os_major": 15,
"sdk_version": "0.11.0",
"events": []
});
let batch: Batch = serde_json::from_value(legacy).unwrap();
assert_eq!(batch.device_id, None);
}
#[test]
fn device_id_round_trips_and_is_omitted_when_absent() {
let mut batch = Batch {
batch_id: BatchId(Uuid::new_v4()),
session_id: SessionId(Uuid::new_v4()),
device_id: None,
wire_version: CURRENT_WIRE_VERSION,
app_version: "1.4.2".into(),
os: OsKind::Android,
os_major: 15,
sdk_version: "0.11.0".into(),
events: vec![],
};
let v = serde_json::to_value(&batch).unwrap();
assert!(v.get("device_id").is_none());
batch.device_id = Some(DeviceId("a3f2b4c5d6e7f8091a2b3c4d5e6f7a8b".into()));
let v = serde_json::to_value(&batch).unwrap();
assert_eq!(v["device_id"], json!("a3f2b4c5d6e7f8091a2b3c4d5e6f7a8b"));
let back: Batch = serde_json::from_value(v).unwrap();
assert_eq!(back.device_id, batch.device_id);
}
}