june_analytics/
message.rs

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    /// The user associated with this message.
22    #[serde(flatten)]
23    pub user: User,
24
25    /// The traits to assign to the user.
26    pub traits: Value,
27
28    /// The timestamp associated with this message.
29    #[serde(
30        skip_serializing_if = "Option::is_none",
31        with = "time::serde::rfc3339::option"
32    )]
33    pub timestamp: Option<OffsetDateTime>,
34
35    /// Context associated with this message.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub context: Option<Value>,
38
39    /// Integrations to route this message to.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub integrations: Option<Value>,
42
43    /// Extra fields to put at the top level of this message.
44    #[serde(flatten)]
45    pub extra: Map<String, Value>,
46}
47
48#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
49pub struct Track {
50    /// The user associated with this message.
51    #[serde(flatten)]
52    pub user: User,
53
54    /// The name of the event being tracked.
55    pub event: String,
56
57    /// The properties associated with the event.
58    pub properties: Value,
59
60    /// The timestamp associated with this message.
61    #[serde(
62        skip_serializing_if = "Option::is_none",
63        with = "time::serde::rfc3339::option"
64    )]
65    pub timestamp: Option<OffsetDateTime>,
66
67    /// Context associated with this message.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub context: Option<Value>,
70
71    /// Integrations to route this message to.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub integrations: Option<Value>,
74
75    /// Extra fields to put at the top level of this message.
76    #[serde(flatten)]
77    pub extra: Map<String, Value>,
78}
79
80#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
81pub struct Page {
82    /// The user associated with this message.
83    #[serde(flatten)]
84    pub user: User,
85
86    /// The name of the page being tracked.
87    pub name: String,
88
89    /// The properties associated with the event.
90    pub properties: Value,
91
92    /// The timestamp associated with this message.
93    #[serde(
94        skip_serializing_if = "Option::is_none",
95        with = "time::serde::rfc3339::option"
96    )]
97    pub timestamp: Option<OffsetDateTime>,
98
99    /// Context associated with this message.
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub context: Option<Value>,
102
103    /// Integrations to route this message to.
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub integrations: Option<Value>,
106
107    /// Extra fields to put at the top level of this message.
108    #[serde(flatten)]
109    pub extra: Map<String, Value>,
110}
111
112#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
113pub struct Screen {
114    /// The user associated with this message.
115    #[serde(flatten)]
116    pub user: User,
117
118    /// The name of the screen being tracked.
119    pub name: String,
120
121    /// The properties associated with the event.
122    pub properties: Value,
123
124    /// The timestamp associated with this message.
125    #[serde(
126        skip_serializing_if = "Option::is_none",
127        with = "time::serde::rfc3339::option"
128    )]
129    pub timestamp: Option<OffsetDateTime>,
130
131    /// Context associated with this message.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub context: Option<Value>,
134
135    /// Integrations to route this message to.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub integrations: Option<Value>,
138
139    /// Extra fields to put at the top level of this message.
140    #[serde(flatten)]
141    pub extra: Map<String, Value>,
142}
143
144#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
145pub struct Group {
146    /// The user associated with this message.
147    #[serde(flatten)]
148    pub user: User,
149
150    /// The group the user is being associated with.
151    #[serde(rename = "groupId")]
152    pub group_id: String,
153
154    /// The traits to assign to the group.
155    pub traits: Value,
156
157    /// The timestamp associated with this message.
158    #[serde(
159        skip_serializing_if = "Option::is_none",
160        with = "time::serde::rfc3339::option"
161    )]
162    pub timestamp: Option<OffsetDateTime>,
163
164    /// Context associated with this message.
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub context: Option<Value>,
167
168    /// Integrations to route this message to.
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub integrations: Option<Value>,
171
172    /// Extra fields to put at the top level of this message.
173    #[serde(flatten)]
174    pub extra: Map<String, Value>,
175}
176
177#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
178pub struct Alias {
179    /// The user associated with this message.
180    #[serde(flatten)]
181    pub user: User,
182
183    /// The user's previous ID.
184    #[serde(rename = "previousId")]
185    pub previous_id: String,
186
187    /// The timestamp associated with this message.
188    #[serde(
189        skip_serializing_if = "Option::is_none",
190        with = "time::serde::rfc3339::option"
191    )]
192    pub timestamp: Option<OffsetDateTime>,
193
194    /// Context associated with this message.
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub context: Option<Value>,
197
198    /// Integrations to route this message to.
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub integrations: Option<Value>,
201
202    /// Extra fields to put at the top level of this message.
203    #[serde(flatten)]
204    pub extra: Map<String, Value>,
205}
206
207#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
208pub struct Batch {
209    /// The batch of messages to send.
210    pub batch: Vec<BatchMessage>,
211
212    /// Context associated with this message.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub context: Option<Value>,
215
216    /// Integrations to route this message to.
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub integrations: Option<Value>,
219
220    /// Extra fields to put at the top level of this message.
221    #[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    /// The user is identified only by a user ID.
259    UserId {
260        #[serde(rename = "userId")]
261        user_id: String,
262    },
263
264    /// The user is identified only by an anonymous ID.
265    AnonymousId {
266        #[serde(rename = "anonymousId")]
267        anonymous_id: String,
268    },
269
270    /// The user is identified by both a user ID and an anonymous ID.
271    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    /// Display a `UserId`. If he has both an `anonymous_id` and a `user_id` we display the `user_id`
282    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}