use serde::{Deserialize, Serialize};
use crate::wire::{lossy_opt_u64, lossy_u64};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Activity {
#[serde(deserialize_with = "lossy_u64")]
pub activity_id: u64,
pub app: String,
#[serde(rename = "type", alias = "activity_type")]
pub activity_type: String,
#[serde(default)]
pub user: String,
pub subject: String,
#[serde(default)]
pub message: Option<String>,
pub datetime: String,
#[serde(default)]
pub link: Option<String>,
#[serde(default)]
pub object_type: Option<String>,
#[serde(default, deserialize_with = "lossy_opt_u64")]
pub object_id: Option<u64>,
#[serde(default)]
pub object_name: Option<String>,
}
impl Activity {
pub fn new(
activity_id: u64,
app: impl Into<String>,
activity_type: impl Into<String>,
user: impl Into<String>,
subject: impl Into<String>,
datetime: impl Into<String>,
) -> Self {
Self {
activity_id,
app: app.into(),
activity_type: activity_type.into(),
user: user.into(),
subject: subject.into(),
message: None,
datetime: datetime.into(),
link: None,
object_type: None,
object_id: None,
object_name: None,
}
}
pub const fn activity_id(&self) -> u64 {
self.activity_id
}
pub fn app(&self) -> &str {
&self.app
}
pub fn activity_type(&self) -> &str {
&self.activity_type
}
pub fn user(&self) -> &str {
&self.user
}
pub fn subject(&self) -> &str {
&self.subject
}
pub fn message(&self) -> Option<&str> {
self.message.as_deref()
}
pub fn datetime(&self) -> &str {
&self.datetime
}
pub fn link(&self) -> Option<&str> {
self.link.as_deref()
}
pub fn object_type(&self) -> Option<&str> {
self.object_type.as_deref()
}
pub const fn object_id(&self) -> Option<u64> {
self.object_id
}
pub fn object_name(&self) -> Option<&str> {
self.object_name.as_deref()
}
#[must_use]
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
#[must_use]
pub fn with_link(mut self, link: impl Into<String>) -> Self {
self.link = Some(link.into());
self
}
#[must_use]
pub fn with_object(
mut self,
object_type: impl Into<String>,
object_id: u64,
object_name: impl Into<String>,
) -> Self {
self.object_type = Some(object_type.into());
self.object_id = Some(object_id);
self.object_name = Some(object_name.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_official_doc_example_json() {
let json = r#"{
"activity_id": 1,
"datetime": "2015-11-20T12:49:31+00:00",
"app": "files",
"type": "file_created",
"user": "test1",
"subject": "test1 created hello.txt",
"subject_rich": {
"0": "{user1} created {file1}",
"1": {
"user1": {
"type": "user",
"id": "test1",
"name": "Test User"
},
"file1": {
"type": "file",
"id": 23,
"name": "hello.txt",
"path": "/test/hello.txt"
}
}
},
"icon": "https://cloud.example.com/apps/files/img/add-color.svg",
"link": "https://cloud.example.com/apps/files/?dir=/test",
"object_type": "files",
"object_id": 23,
"object_name": "/test/hello.txt",
"previews": [
{
"source": "https://cloud.example.com/core/preview.png?file=/hello.txt&x=150&y=150",
"link": "https://cloud.example.com/apps/files/?dir=/test&scrollto=hello.txt",
"mimeType": "text/plain",
"fileId": 23,
"view": "files",
"isMimeTypeIcon": false,
"filename": "hello.txt"
}
]
}"#;
let activity: Activity = serde_json::from_str(json).expect("valid activity json");
assert_eq!(activity.activity_id(), 1);
assert_eq!(activity.datetime(), "2015-11-20T12:49:31+00:00");
assert_eq!(activity.app(), "files");
assert_eq!(activity.activity_type(), "file_created");
assert_eq!(activity.user(), "test1");
assert_eq!(activity.subject(), "test1 created hello.txt");
assert_eq!(activity.message(), None);
assert_eq!(
activity.link(),
Some("https://cloud.example.com/apps/files/?dir=/test")
);
assert_eq!(activity.object_type(), Some("files"));
assert_eq!(activity.object_id(), Some(23));
assert_eq!(activity.object_name(), Some("/test/hello.txt"));
}
#[test]
fn parses_string_ids_and_optional_message() {
let json = r#"{
"activity_id": "42",
"datetime": "2026-10-24T15:00:00+00:00",
"app": "comments",
"type": "comment_added",
"user": "alice",
"subject": "alice commented on report.pdf",
"message": "Looks great to me!",
"link": null,
"object_type": "files",
"object_id": "100",
"object_name": "/report.pdf"
}"#;
let activity: Activity = serde_json::from_str(json).expect("valid activity json");
assert_eq!(activity.activity_id(), 42);
assert_eq!(activity.activity_type(), "comment_added");
assert_eq!(activity.user(), "alice");
assert_eq!(activity.message(), Some("Looks great to me!"));
assert_eq!(activity.link(), None);
assert_eq!(activity.object_id(), Some(100));
}
#[test]
fn constructor_and_builder_methods() {
let activity = Activity::new(
7,
"deck",
"card_created",
"bob",
"bob created card",
"2026-01-01T00:00:00Z",
)
.with_message("Card in Todo list")
.with_link("https://cloud.example.com/apps/deck/#/boards/1")
.with_object("deck_card", 55, "New Task");
assert_eq!(activity.activity_id, 7);
assert_eq!(activity.app, "deck");
assert_eq!(activity.activity_type, "card_created");
assert_eq!(activity.user, "bob");
assert_eq!(activity.subject, "bob created card");
assert_eq!(activity.datetime, "2026-01-01T00:00:00Z");
assert_eq!(activity.message.as_deref(), Some("Card in Todo list"));
assert_eq!(
activity.link.as_deref(),
Some("https://cloud.example.com/apps/deck/#/boards/1")
);
assert_eq!(activity.object_type.as_deref(), Some("deck_card"));
assert_eq!(activity.object_id, Some(55));
assert_eq!(activity.object_name.as_deref(), Some("New Task"));
}
}