1use std::fmt::Display;
30
31use serde::{Deserialize, Serialize};
32use serde_json::{Map, Value};
33use time::OffsetDateTime;
34
35#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
37#[serde(untagged)]
38pub enum Message {
39 Identify(Identify),
40 Track(Track),
41 Page(Page),
42 Screen(Screen),
43 Group(Group),
44 Alias(Alias),
45 Batch(Batch),
46}
47
48#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
53pub struct Identify {
54 #[serde(flatten)]
56 pub user: User,
57
58 pub traits: Value,
60
61 #[serde(
63 skip_serializing_if = "Option::is_none",
64 with = "time::serde::rfc3339::option"
65 )]
66 pub timestamp: Option<OffsetDateTime>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub context: Option<Value>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub integrations: Option<Value>,
75
76 #[serde(flatten)]
78 pub extra: Map<String, Value>,
79}
80
81#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
86pub struct Track {
87 #[serde(flatten)]
89 pub user: User,
90
91 pub event: String,
93
94 pub properties: Value,
96
97 #[serde(
99 skip_serializing_if = "Option::is_none",
100 with = "time::serde::rfc3339::option"
101 )]
102 pub timestamp: Option<OffsetDateTime>,
103
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub context: Option<Value>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub integrations: Option<Value>,
111
112 #[serde(flatten)]
114 pub extra: Map<String, Value>,
115}
116
117#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
122pub struct Page {
123 #[serde(flatten)]
125 pub user: User,
126
127 pub name: String,
129
130 pub properties: Value,
132
133 #[serde(
135 skip_serializing_if = "Option::is_none",
136 with = "time::serde::rfc3339::option"
137 )]
138 pub timestamp: Option<OffsetDateTime>,
139
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub context: Option<Value>,
143
144 #[serde(skip_serializing_if = "Option::is_none")]
146 pub integrations: Option<Value>,
147
148 #[serde(flatten)]
150 pub extra: Map<String, Value>,
151}
152
153#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
158pub struct Screen {
159 #[serde(flatten)]
161 pub user: User,
162
163 pub name: String,
165
166 pub properties: Value,
168
169 #[serde(
171 skip_serializing_if = "Option::is_none",
172 with = "time::serde::rfc3339::option"
173 )]
174 pub timestamp: Option<OffsetDateTime>,
175
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub context: Option<Value>,
179
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub integrations: Option<Value>,
183
184 #[serde(flatten)]
186 pub extra: Map<String, Value>,
187}
188
189#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
194pub struct Group {
195 #[serde(flatten)]
197 pub user: User,
198
199 #[serde(rename = "groupId")]
201 pub group_id: String,
202
203 pub traits: Value,
205
206 #[serde(
208 skip_serializing_if = "Option::is_none",
209 with = "time::serde::rfc3339::option"
210 )]
211 pub timestamp: Option<OffsetDateTime>,
212
213 #[serde(skip_serializing_if = "Option::is_none")]
215 pub context: Option<Value>,
216
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub integrations: Option<Value>,
220
221 #[serde(flatten)]
223 pub extra: Map<String, Value>,
224}
225
226#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
231pub struct Alias {
232 #[serde(flatten)]
234 pub user: User,
235
236 #[serde(rename = "previousId")]
238 pub previous_id: String,
239
240 #[serde(
242 skip_serializing_if = "Option::is_none",
243 with = "time::serde::rfc3339::option"
244 )]
245 pub timestamp: Option<OffsetDateTime>,
246
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub context: Option<Value>,
250
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub integrations: Option<Value>,
254
255 #[serde(flatten)]
257 pub extra: Map<String, Value>,
258}
259
260#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
266pub struct Batch {
267 pub batch: Vec<BatchMessage>,
269
270 #[serde(skip_serializing_if = "Option::is_none")]
272 pub context: Option<Value>,
273
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub integrations: Option<Value>,
277
278 #[serde(flatten)]
280 pub extra: Map<String, Value>,
281}
282
283#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
285#[serde(tag = "type")]
286pub enum BatchMessage {
287 #[serde(rename = "identify")]
288 Identify(Identify),
289 #[serde(rename = "track")]
290 Track(Track),
291 #[serde(rename = "page")]
292 Page(Page),
293 #[serde(rename = "screen")]
294 Screen(Screen),
295 #[serde(rename = "group")]
296 Group(Group),
297 #[serde(rename = "alias")]
298 Alias(Alias),
299}
300
301impl BatchMessage {
302 pub(crate) fn timestamp_mut(&mut self) -> &mut Option<OffsetDateTime> {
303 match self {
304 Self::Identify(identify) => &mut identify.timestamp,
305 Self::Track(track) => &mut track.timestamp,
306 Self::Page(page) => &mut page.timestamp,
307 Self::Screen(screen) => &mut screen.timestamp,
308 Self::Group(group) => &mut group.timestamp,
309 Self::Alias(alias) => &mut alias.timestamp,
310 }
311 }
312}
313
314#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
321#[serde(untagged)]
322pub enum User {
323 UserId {
325 #[serde(rename = "userId")]
326 user_id: String,
327 },
328
329 AnonymousId {
331 #[serde(rename = "anonymousId")]
332 anonymous_id: String,
333 },
334
335 Both {
337 #[serde(rename = "userId")]
338 user_id: String,
339
340 #[serde(rename = "anonymousId")]
341 anonymous_id: String,
342 },
343}
344
345impl Display for User {
346 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348 match self {
349 User::UserId { user_id } => write!(f, "{}", user_id),
350 User::AnonymousId { anonymous_id } => write!(f, "{}", anonymous_id),
351 User::Both { user_id, .. } => write!(f, "{}", user_id),
352 }
353 }
354}
355
356impl Default for User {
357 fn default() -> Self {
358 User::AnonymousId {
359 anonymous_id: "".to_owned(),
360 }
361 }
362}
363
364macro_rules! into {
365 (from $from:ident into $for:ident) => {
366 impl From<$from> for $for {
367 fn from(message: $from) -> Self {
368 Self::$from(message)
369 }
370 }
371 };
372 ($(from $from:ident into $for:ident),+ $(,)?) => {
373 $(
374 into!{from $from into $for}
375 )+
376 };
377}
378
379into! {
380 from Identify into Message,
381 from Track into Message,
382 from Page into Message,
383 from Screen into Message,
384 from Group into Message,
385 from Alias into Message,
386 from Batch into Message,
387
388 from Identify into BatchMessage,
389 from Track into BatchMessage,
390 from Page into BatchMessage,
391 from Screen into BatchMessage,
392 from Group into BatchMessage,
393 from Alias into BatchMessage,
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399 use serde_json::json;
400
401 #[test]
402 fn serialize() {
403 assert_eq!(
404 serde_json::to_string(&Message::Identify(Identify {
405 user: User::UserId {
406 user_id: "foo".to_owned()
407 },
408 traits: json!({
409 "foo": "bar",
410 "baz": "quux",
411 }),
412 extra: [("messageId".to_owned(), json!("123"))]
413 .iter()
414 .cloned()
415 .collect(),
416 ..Default::default()
417 }))
418 .unwrap(),
419 r#"{"userId":"foo","traits":{"baz":"quux","foo":"bar"},"messageId":"123"}"#.to_owned(),
420 );
421
422 assert_eq!(
423 serde_json::to_string(&Message::Track(Track {
424 user: User::AnonymousId {
425 anonymous_id: "foo".to_owned()
426 },
427 event: "Foo".to_owned(),
428 properties: json!({
429 "foo": "bar",
430 "baz": "quux",
431 }),
432 ..Default::default()
433 }))
434 .unwrap(),
435 r#"{"anonymousId":"foo","event":"Foo","properties":{"baz":"quux","foo":"bar"}}"#
436 .to_owned(),
437 );
438
439 assert_eq!(
440 serde_json::to_string(&Message::Page(Page {
441 user: User::Both {
442 user_id: "foo".to_owned(),
443 anonymous_id: "bar".to_owned()
444 },
445 name: "Foo".to_owned(),
446 properties: json!({
447 "foo": "bar",
448 "baz": "quux",
449 }),
450 ..Default::default()
451 }))
452 .unwrap(),
453 r#"{"userId":"foo","anonymousId":"bar","name":"Foo","properties":{"baz":"quux","foo":"bar"}}"#
454 .to_owned(),
455 );
456
457 assert_eq!(
458 serde_json::to_string(&Message::Screen(Screen {
459 user: User::Both {
460 user_id: "foo".to_owned(),
461 anonymous_id: "bar".to_owned()
462 },
463 name: "Foo".to_owned(),
464 properties: json!({
465 "foo": "bar",
466 "baz": "quux",
467 }),
468 ..Default::default()
469 }))
470 .unwrap(),
471 r#"{"userId":"foo","anonymousId":"bar","name":"Foo","properties":{"baz":"quux","foo":"bar"}}"#
472 .to_owned(),
473 );
474
475 assert_eq!(
476 serde_json::to_string(&Message::Group(Group {
477 user: User::UserId {
478 user_id: "foo".to_owned()
479 },
480 group_id: "bar".to_owned(),
481 traits: json!({
482 "foo": "bar",
483 "baz": "quux",
484 }),
485 ..Default::default()
486 }))
487 .unwrap(),
488 r#"{"userId":"foo","groupId":"bar","traits":{"baz":"quux","foo":"bar"}}"#.to_owned(),
489 );
490
491 assert_eq!(
492 serde_json::to_string(&Message::Alias(Alias {
493 user: User::UserId {
494 user_id: "foo".to_owned()
495 },
496 previous_id: "bar".to_owned(),
497 ..Default::default()
498 }))
499 .unwrap(),
500 r#"{"userId":"foo","previousId":"bar"}"#.to_owned(),
501 );
502
503 assert_eq!(
504 serde_json::to_string(&Message::Batch(Batch {
505 batch: vec![
506 BatchMessage::Track(Track {
507 user: User::UserId {
508 user_id: "foo".to_owned()
509 },
510 event: "Foo".to_owned(),
511 properties: json!({}),
512 ..Default::default()
513 }),
514 BatchMessage::Track(Track {
515 user: User::UserId {
516 user_id: "bar".to_owned()
517 },
518 event: "Bar".to_owned(),
519 properties: json!({}),
520 ..Default::default()
521 }),
522 BatchMessage::Track(Track {
523 user: User::UserId {
524 user_id: "baz".to_owned()
525 },
526 event: "Baz".to_owned(),
527 properties: json!({}),
528 ..Default::default()
529 })
530 ],
531 context: Some(json!({
532 "foo": "bar",
533 })),
534 ..Default::default()
535 }))
536 .unwrap(),
537 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"}}"#
538 .to_owned(),
539 );
540 }
541}