offline_protocol_telemetry_wire/
batch.rs1use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6use crate::event::RawEvent;
7
8pub type WireVersion = u8;
12
13pub const CURRENT_WIRE_VERSION: WireVersion = 1;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(transparent)]
19pub struct BatchId(pub Uuid);
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(transparent)]
29pub struct SessionId(pub Uuid);
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(transparent)]
41pub struct DeviceId(pub String);
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "lowercase")]
52pub enum OsKind {
53 Ios,
55 Android,
57 Linux,
59 Macos,
61 Windows,
63 Other,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct Batch {
69 pub batch_id: BatchId,
70 pub session_id: SessionId,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub device_id: Option<DeviceId>,
76 pub wire_version: WireVersion,
77 pub app_version: String,
78 pub os: OsKind,
79 pub os_major: u16,
80 pub sdk_version: String,
81 pub events: Vec<RawEvent>,
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use serde_json::json;
88
89 #[test]
90 fn batch_round_trip_preserves_fields() {
91 let batch = Batch {
92 batch_id: BatchId(Uuid::new_v4()),
93 session_id: SessionId(Uuid::new_v4()),
94 device_id: None,
95 wire_version: CURRENT_WIRE_VERSION,
96 app_version: "1.4.2".into(),
97 os: OsKind::Ios,
98 os_major: 18,
99 sdk_version: "0.11.0".into(),
100 events: vec![RawEvent {
101 event_type: "protocol.message.delivered".into(),
102 ts_ms: 1_747_000_000_000,
103 data: json!({ "latency_ms": 240, "hop_count": 1, "transport": "ble" }),
104 }],
105 };
106
107 let s = serde_json::to_string(&batch).unwrap();
108 let back: Batch = serde_json::from_str(&s).unwrap();
109
110 assert_eq!(back.events.len(), 1);
111 assert_eq!(back.events[0].event_type, "protocol.message.delivered");
112 assert_eq!(back.wire_version, CURRENT_WIRE_VERSION);
113 assert!(matches!(back.os, OsKind::Ios));
114 }
115
116 #[test]
117 fn os_lowercases_in_wire_form() {
118 let v = serde_json::to_value(OsKind::Android).unwrap();
119 assert_eq!(v, json!("android"));
120 }
121
122 #[test]
123 fn pre_device_id_wire_v1_json_still_deserializes() {
124 let legacy = json!({
126 "batch_id": "1f0e9a4c-3b6d-4e2f-9a1b-2c3d4e5f6a7b",
127 "session_id": "00000000-0000-0000-0000-000000000001",
128 "wire_version": 1,
129 "app_version": "1.4.2",
130 "os": "android",
131 "os_major": 15,
132 "sdk_version": "0.11.0",
133 "events": []
134 });
135 let batch: Batch = serde_json::from_value(legacy).unwrap();
136 assert_eq!(batch.device_id, None);
137 }
138
139 #[test]
140 fn device_id_round_trips_and_is_omitted_when_absent() {
141 let mut batch = Batch {
142 batch_id: BatchId(Uuid::new_v4()),
143 session_id: SessionId(Uuid::new_v4()),
144 device_id: None,
145 wire_version: CURRENT_WIRE_VERSION,
146 app_version: "1.4.2".into(),
147 os: OsKind::Android,
148 os_major: 15,
149 sdk_version: "0.11.0".into(),
150 events: vec![],
151 };
152
153 let v = serde_json::to_value(&batch).unwrap();
154 assert!(v.get("device_id").is_none());
155
156 batch.device_id = Some(DeviceId("a3f2b4c5d6e7f8091a2b3c4d5e6f7a8b".into()));
157 let v = serde_json::to_value(&batch).unwrap();
158 assert_eq!(v["device_id"], json!("a3f2b4c5d6e7f8091a2b3c4d5e6f7a8b"));
159 let back: Batch = serde_json::from_value(v).unwrap();
160 assert_eq!(back.device_id, batch.device_id);
161 }
162}