1use std::fmt::Display;
2
3use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value};
5use time::OffsetDateTime;
6
7#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
8#[serde(untagged)]
9pub enum Message {
10 Identify(Identify),
11 Track(Track),
12 Page(Page),
13 Screen(Screen),
14 Group(Group),
15 Alias(Alias),
16 Batch(Batch),
17}
18
19#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
20pub struct Identify {
21 #[serde(flatten)]
23 pub user: User,
24
25 pub traits: Value,
27
28 #[serde(
30 skip_serializing_if = "Option::is_none",
31 with = "time::serde::rfc3339::option"
32 )]
33 pub timestamp: Option<OffsetDateTime>,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub context: Option<Value>,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub integrations: Option<Value>,
42
43 #[serde(flatten)]
45 pub extra: Map<String, Value>,
46}
47
48#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
49pub struct Track {
50 #[serde(flatten)]
52 pub user: User,
53
54 pub event: String,
56
57 pub properties: Value,
59
60 #[serde(
62 skip_serializing_if = "Option::is_none",
63 with = "time::serde::rfc3339::option"
64 )]
65 pub timestamp: Option<OffsetDateTime>,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub context: Option<Value>,
70
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub integrations: Option<Value>,
74
75 #[serde(flatten)]
77 pub extra: Map<String, Value>,
78}
79
80#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
81pub struct Page {
82 #[serde(flatten)]
84 pub user: User,
85
86 pub name: String,
88
89 pub properties: Value,
91
92 #[serde(
94 skip_serializing_if = "Option::is_none",
95 with = "time::serde::rfc3339::option"
96 )]
97 pub timestamp: Option<OffsetDateTime>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub context: Option<Value>,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub integrations: Option<Value>,
106
107 #[serde(flatten)]
109 pub extra: Map<String, Value>,
110}
111
112#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
113pub struct Screen {
114 #[serde(flatten)]
116 pub user: User,
117
118 pub name: String,
120
121 pub properties: Value,
123
124 #[serde(
126 skip_serializing_if = "Option::is_none",
127 with = "time::serde::rfc3339::option"
128 )]
129 pub timestamp: Option<OffsetDateTime>,
130
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub context: Option<Value>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub integrations: Option<Value>,
138
139 #[serde(flatten)]
141 pub extra: Map<String, Value>,
142}
143
144#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
145pub struct Group {
146 #[serde(flatten)]
148 pub user: User,
149
150 #[serde(rename = "groupId")]
152 pub group_id: String,
153
154 pub traits: Value,
156
157 #[serde(
159 skip_serializing_if = "Option::is_none",
160 with = "time::serde::rfc3339::option"
161 )]
162 pub timestamp: Option<OffsetDateTime>,
163
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub context: Option<Value>,
167
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub integrations: Option<Value>,
171
172 #[serde(flatten)]
174 pub extra: Map<String, Value>,
175}
176
177#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
178pub struct Alias {
179 #[serde(flatten)]
181 pub user: User,
182
183 #[serde(rename = "previousId")]
185 pub previous_id: String,
186
187 #[serde(
189 skip_serializing_if = "Option::is_none",
190 with = "time::serde::rfc3339::option"
191 )]
192 pub timestamp: Option<OffsetDateTime>,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub context: Option<Value>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub integrations: Option<Value>,
201
202 #[serde(flatten)]
204 pub extra: Map<String, Value>,
205}
206
207#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
208pub struct Batch {
209 pub batch: Vec<BatchMessage>,
211
212 #[serde(skip_serializing_if = "Option::is_none")]
214 pub context: Option<Value>,
215
216 #[serde(skip_serializing_if = "Option::is_none")]
218 pub integrations: Option<Value>,
219
220 #[serde(flatten)]
222 pub extra: Map<String, Value>,
223}
224
225#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
226#[serde(tag = "type")]
227pub enum BatchMessage {
228 #[serde(rename = "identify")]
229 Identify(Identify),
230 #[serde(rename = "track")]
231 Track(Track),
232 #[serde(rename = "page")]
233 Page(Page),
234 #[serde(rename = "screen")]
235 Screen(Screen),
236 #[serde(rename = "group")]
237 Group(Group),
238 #[serde(rename = "alias")]
239 Alias(Alias),
240}
241
242impl BatchMessage {
243 pub(crate) fn timestamp_mut(&mut self) -> &mut Option<OffsetDateTime> {
244 match self {
245 Self::Identify(identify) => &mut identify.timestamp,
246 Self::Track(track) => &mut track.timestamp,
247 Self::Page(page) => &mut page.timestamp,
248 Self::Screen(screen) => &mut screen.timestamp,
249 Self::Group(group) => &mut group.timestamp,
250 Self::Alias(alias) => &mut alias.timestamp,
251 }
252 }
253}
254
255#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
256#[serde(untagged)]
257pub enum User {
258 UserId {
260 #[serde(rename = "userId")]
261 user_id: String,
262 },
263
264 AnonymousId {
266 #[serde(rename = "anonymousId")]
267 anonymous_id: String,
268 },
269
270 Both {
272 #[serde(rename = "userId")]
273 user_id: String,
274
275 #[serde(rename = "anonymousId")]
276 anonymous_id: String,
277 },
278}
279
280impl Display for User {
281 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 match self {
284 User::UserId { user_id } => write!(f, "{}", user_id),
285 User::AnonymousId { anonymous_id } => write!(f, "{}", anonymous_id),
286 User::Both { user_id, .. } => write!(f, "{}", user_id),
287 }
288 }
289}
290
291impl Default for User {
292 fn default() -> Self {
293 User::AnonymousId {
294 anonymous_id: "".to_owned(),
295 }
296 }
297}
298
299macro_rules! into {
300 (from $from:ident into $for:ident) => {
301 impl From<$from> for $for {
302 fn from(message: $from) -> Self {
303 Self::$from(message)
304 }
305 }
306 };
307 ($(from $from:ident into $for:ident),+ $(,)?) => {
308 $(
309 into!{from $from into $for}
310 )+
311 };
312}
313
314into! {
315 from Identify into Message,
316 from Track into Message,
317 from Page into Message,
318 from Screen into Message,
319 from Group into Message,
320 from Alias into Message,
321 from Batch into Message,
322
323 from Identify into BatchMessage,
324 from Track into BatchMessage,
325 from Page into BatchMessage,
326 from Screen into BatchMessage,
327 from Group into BatchMessage,
328 from Alias into BatchMessage,
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use serde_json::json;
335
336 #[test]
337 fn serialize() {
338 assert_eq!(
339 serde_json::to_string(&Message::Identify(Identify {
340 user: User::UserId {
341 user_id: "foo".to_owned()
342 },
343 traits: json!({
344 "foo": "bar",
345 "baz": "quux",
346 }),
347 extra: [("messageId".to_owned(), json!("123"))]
348 .iter()
349 .cloned()
350 .collect(),
351 ..Default::default()
352 }))
353 .unwrap(),
354 r#"{"userId":"foo","traits":{"baz":"quux","foo":"bar"},"messageId":"123"}"#.to_owned(),
355 );
356
357 assert_eq!(
358 serde_json::to_string(&Message::Track(Track {
359 user: User::AnonymousId {
360 anonymous_id: "foo".to_owned()
361 },
362 event: "Foo".to_owned(),
363 properties: json!({
364 "foo": "bar",
365 "baz": "quux",
366 }),
367 ..Default::default()
368 }))
369 .unwrap(),
370 r#"{"anonymousId":"foo","event":"Foo","properties":{"baz":"quux","foo":"bar"}}"#
371 .to_owned(),
372 );
373
374 assert_eq!(
375 serde_json::to_string(&Message::Page(Page {
376 user: User::Both {
377 user_id: "foo".to_owned(),
378 anonymous_id: "bar".to_owned()
379 },
380 name: "Foo".to_owned(),
381 properties: json!({
382 "foo": "bar",
383 "baz": "quux",
384 }),
385 ..Default::default()
386 }))
387 .unwrap(),
388 r#"{"userId":"foo","anonymousId":"bar","name":"Foo","properties":{"baz":"quux","foo":"bar"}}"#
389 .to_owned(),
390 );
391
392 assert_eq!(
393 serde_json::to_string(&Message::Screen(Screen {
394 user: User::Both {
395 user_id: "foo".to_owned(),
396 anonymous_id: "bar".to_owned()
397 },
398 name: "Foo".to_owned(),
399 properties: json!({
400 "foo": "bar",
401 "baz": "quux",
402 }),
403 ..Default::default()
404 }))
405 .unwrap(),
406 r#"{"userId":"foo","anonymousId":"bar","name":"Foo","properties":{"baz":"quux","foo":"bar"}}"#
407 .to_owned(),
408 );
409
410 assert_eq!(
411 serde_json::to_string(&Message::Group(Group {
412 user: User::UserId {
413 user_id: "foo".to_owned()
414 },
415 group_id: "bar".to_owned(),
416 traits: json!({
417 "foo": "bar",
418 "baz": "quux",
419 }),
420 ..Default::default()
421 }))
422 .unwrap(),
423 r#"{"userId":"foo","groupId":"bar","traits":{"baz":"quux","foo":"bar"}}"#.to_owned(),
424 );
425
426 assert_eq!(
427 serde_json::to_string(&Message::Alias(Alias {
428 user: User::UserId {
429 user_id: "foo".to_owned()
430 },
431 previous_id: "bar".to_owned(),
432 ..Default::default()
433 }))
434 .unwrap(),
435 r#"{"userId":"foo","previousId":"bar"}"#.to_owned(),
436 );
437
438 assert_eq!(
439 serde_json::to_string(&Message::Batch(Batch {
440 batch: vec![
441 BatchMessage::Track(Track {
442 user: User::UserId {
443 user_id: "foo".to_owned()
444 },
445 event: "Foo".to_owned(),
446 properties: json!({}),
447 ..Default::default()
448 }),
449 BatchMessage::Track(Track {
450 user: User::UserId {
451 user_id: "bar".to_owned()
452 },
453 event: "Bar".to_owned(),
454 properties: json!({}),
455 ..Default::default()
456 }),
457 BatchMessage::Track(Track {
458 user: User::UserId {
459 user_id: "baz".to_owned()
460 },
461 event: "Baz".to_owned(),
462 properties: json!({}),
463 ..Default::default()
464 })
465 ],
466 context: Some(json!({
467 "foo": "bar",
468 })),
469 ..Default::default()
470 }))
471 .unwrap(),
472 r#"{"batch":[{"type":"track","userId":"foo","event":"Foo","properties":{}},{"type":"track","userId":"bar","event":"Bar","properties":{}},{"type":"track","userId":"baz","event":"Baz","properties":{}}],"context":{"foo":"bar"}}"#
473 .to_owned(),
474 );
475 }
476}