use std::fmt::Display;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use time::OffsetDateTime;
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Message {
Identify(Identify),
Track(Track),
Page(Page),
Screen(Screen),
Group(Group),
Alias(Alias),
Batch(Batch),
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
pub struct Identify {
#[serde(flatten)]
pub user: User,
pub traits: Value,
#[serde(
skip_serializing_if = "Option::is_none",
with = "time::serde::rfc3339::option"
)]
pub timestamp: Option<OffsetDateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub integrations: Option<Value>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
pub struct Track {
#[serde(flatten)]
pub user: User,
pub event: String,
pub properties: Value,
#[serde(
skip_serializing_if = "Option::is_none",
with = "time::serde::rfc3339::option"
)]
pub timestamp: Option<OffsetDateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub integrations: Option<Value>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
pub struct Page {
#[serde(flatten)]
pub user: User,
pub name: String,
pub properties: Value,
#[serde(
skip_serializing_if = "Option::is_none",
with = "time::serde::rfc3339::option"
)]
pub timestamp: Option<OffsetDateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub integrations: Option<Value>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
pub struct Screen {
#[serde(flatten)]
pub user: User,
pub name: String,
pub properties: Value,
#[serde(
skip_serializing_if = "Option::is_none",
with = "time::serde::rfc3339::option"
)]
pub timestamp: Option<OffsetDateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub integrations: Option<Value>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
pub struct Group {
#[serde(flatten)]
pub user: User,
#[serde(rename = "groupId")]
pub group_id: String,
pub traits: Value,
#[serde(
skip_serializing_if = "Option::is_none",
with = "time::serde::rfc3339::option"
)]
pub timestamp: Option<OffsetDateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub integrations: Option<Value>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
pub struct Alias {
#[serde(flatten)]
pub user: User,
#[serde(rename = "previousId")]
pub previous_id: String,
#[serde(
skip_serializing_if = "Option::is_none",
with = "time::serde::rfc3339::option"
)]
pub timestamp: Option<OffsetDateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub integrations: Option<Value>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
pub struct Batch {
pub batch: Vec<BatchMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub integrations: Option<Value>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum BatchMessage {
#[serde(rename = "identify")]
Identify(Identify),
#[serde(rename = "track")]
Track(Track),
#[serde(rename = "page")]
Page(Page),
#[serde(rename = "screen")]
Screen(Screen),
#[serde(rename = "group")]
Group(Group),
#[serde(rename = "alias")]
Alias(Alias),
}
impl BatchMessage {
pub(crate) fn timestamp_mut(&mut self) -> &mut Option<OffsetDateTime> {
match self {
Self::Identify(identify) => &mut identify.timestamp,
Self::Track(track) => &mut track.timestamp,
Self::Page(page) => &mut page.timestamp,
Self::Screen(screen) => &mut screen.timestamp,
Self::Group(group) => &mut group.timestamp,
Self::Alias(alias) => &mut alias.timestamp,
}
}
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum User {
UserId {
#[serde(rename = "userId")]
user_id: String,
},
AnonymousId {
#[serde(rename = "anonymousId")]
anonymous_id: String,
},
Both {
#[serde(rename = "userId")]
user_id: String,
#[serde(rename = "anonymousId")]
anonymous_id: String,
},
}
impl Display for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
User::UserId { user_id } => write!(f, "{}", user_id),
User::AnonymousId { anonymous_id } => write!(f, "{}", anonymous_id),
User::Both { user_id, .. } => write!(f, "{}", user_id),
}
}
}
impl Default for User {
fn default() -> Self {
User::AnonymousId {
anonymous_id: "".to_owned(),
}
}
}
macro_rules! into {
(from $from:ident into $for:ident) => {
impl From<$from> for $for {
fn from(message: $from) -> Self {
Self::$from(message)
}
}
};
($(from $from:ident into $for:ident),+ $(,)?) => {
$(
into!{from $from into $for}
)+
};
}
into! {
from Identify into Message,
from Track into Message,
from Page into Message,
from Screen into Message,
from Group into Message,
from Alias into Message,
from Batch into Message,
from Identify into BatchMessage,
from Track into BatchMessage,
from Page into BatchMessage,
from Screen into BatchMessage,
from Group into BatchMessage,
from Alias into BatchMessage,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn serialize() {
assert_eq!(
serde_json::to_string(&Message::Identify(Identify {
user: User::UserId {
user_id: "foo".to_owned()
},
traits: json!({
"foo": "bar",
"baz": "quux",
}),
extra: [("messageId".to_owned(), json!("123"))]
.iter()
.cloned()
.collect(),
..Default::default()
}))
.unwrap(),
r#"{"userId":"foo","traits":{"baz":"quux","foo":"bar"},"messageId":"123"}"#.to_owned(),
);
assert_eq!(
serde_json::to_string(&Message::Track(Track {
user: User::AnonymousId {
anonymous_id: "foo".to_owned()
},
event: "Foo".to_owned(),
properties: json!({
"foo": "bar",
"baz": "quux",
}),
..Default::default()
}))
.unwrap(),
r#"{"anonymousId":"foo","event":"Foo","properties":{"baz":"quux","foo":"bar"}}"#
.to_owned(),
);
assert_eq!(
serde_json::to_string(&Message::Page(Page {
user: User::Both {
user_id: "foo".to_owned(),
anonymous_id: "bar".to_owned()
},
name: "Foo".to_owned(),
properties: json!({
"foo": "bar",
"baz": "quux",
}),
..Default::default()
}))
.unwrap(),
r#"{"userId":"foo","anonymousId":"bar","name":"Foo","properties":{"baz":"quux","foo":"bar"}}"#
.to_owned(),
);
assert_eq!(
serde_json::to_string(&Message::Screen(Screen {
user: User::Both {
user_id: "foo".to_owned(),
anonymous_id: "bar".to_owned()
},
name: "Foo".to_owned(),
properties: json!({
"foo": "bar",
"baz": "quux",
}),
..Default::default()
}))
.unwrap(),
r#"{"userId":"foo","anonymousId":"bar","name":"Foo","properties":{"baz":"quux","foo":"bar"}}"#
.to_owned(),
);
assert_eq!(
serde_json::to_string(&Message::Group(Group {
user: User::UserId {
user_id: "foo".to_owned()
},
group_id: "bar".to_owned(),
traits: json!({
"foo": "bar",
"baz": "quux",
}),
..Default::default()
}))
.unwrap(),
r#"{"userId":"foo","groupId":"bar","traits":{"baz":"quux","foo":"bar"}}"#.to_owned(),
);
assert_eq!(
serde_json::to_string(&Message::Alias(Alias {
user: User::UserId {
user_id: "foo".to_owned()
},
previous_id: "bar".to_owned(),
..Default::default()
}))
.unwrap(),
r#"{"userId":"foo","previousId":"bar"}"#.to_owned(),
);
assert_eq!(
serde_json::to_string(&Message::Batch(Batch {
batch: vec![
BatchMessage::Track(Track {
user: User::UserId {
user_id: "foo".to_owned()
},
event: "Foo".to_owned(),
properties: json!({}),
..Default::default()
}),
BatchMessage::Track(Track {
user: User::UserId {
user_id: "bar".to_owned()
},
event: "Bar".to_owned(),
properties: json!({}),
..Default::default()
}),
BatchMessage::Track(Track {
user: User::UserId {
user_id: "baz".to_owned()
},
event: "Baz".to_owned(),
properties: json!({}),
..Default::default()
})
],
context: Some(json!({
"foo": "bar",
})),
..Default::default()
}))
.unwrap(),
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"}}"#
.to_owned(),
);
}
}