1use serde::{Deserialize, Serialize};
9
10use crate::wire::{lossy_opt_u64, lossy_u64};
11
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
19pub struct Activity {
20 #[serde(deserialize_with = "lossy_u64")]
22 pub activity_id: u64,
23
24 pub app: String,
26
27 #[serde(rename = "type", alias = "activity_type")]
29 pub activity_type: String,
30
31 #[serde(default)]
33 pub user: String,
34
35 pub subject: String,
37
38 #[serde(default)]
40 pub message: Option<String>,
41
42 pub datetime: String,
44
45 #[serde(default)]
47 pub link: Option<String>,
48
49 #[serde(default)]
51 pub object_type: Option<String>,
52
53 #[serde(default, deserialize_with = "lossy_opt_u64")]
55 pub object_id: Option<u64>,
56
57 #[serde(default)]
59 pub object_name: Option<String>,
60}
61
62impl Activity {
63 pub fn new(
65 activity_id: u64,
66 app: impl Into<String>,
67 activity_type: impl Into<String>,
68 user: impl Into<String>,
69 subject: impl Into<String>,
70 datetime: impl Into<String>,
71 ) -> Self {
72 Self {
73 activity_id,
74 app: app.into(),
75 activity_type: activity_type.into(),
76 user: user.into(),
77 subject: subject.into(),
78 message: None,
79 datetime: datetime.into(),
80 link: None,
81 object_type: None,
82 object_id: None,
83 object_name: None,
84 }
85 }
86
87 pub const fn activity_id(&self) -> u64 {
89 self.activity_id
90 }
91
92 pub fn app(&self) -> &str {
94 &self.app
95 }
96
97 pub fn activity_type(&self) -> &str {
99 &self.activity_type
100 }
101
102 pub fn user(&self) -> &str {
104 &self.user
105 }
106
107 pub fn subject(&self) -> &str {
109 &self.subject
110 }
111
112 pub fn message(&self) -> Option<&str> {
114 self.message.as_deref()
115 }
116
117 pub fn datetime(&self) -> &str {
119 &self.datetime
120 }
121
122 pub fn link(&self) -> Option<&str> {
124 self.link.as_deref()
125 }
126
127 pub fn object_type(&self) -> Option<&str> {
129 self.object_type.as_deref()
130 }
131
132 pub const fn object_id(&self) -> Option<u64> {
134 self.object_id
135 }
136
137 pub fn object_name(&self) -> Option<&str> {
139 self.object_name.as_deref()
140 }
141
142 #[must_use]
144 pub fn with_message(mut self, message: impl Into<String>) -> Self {
145 self.message = Some(message.into());
146 self
147 }
148
149 #[must_use]
151 pub fn with_link(mut self, link: impl Into<String>) -> Self {
152 self.link = Some(link.into());
153 self
154 }
155
156 #[must_use]
158 pub fn with_object(
159 mut self,
160 object_type: impl Into<String>,
161 object_id: u64,
162 object_name: impl Into<String>,
163 ) -> Self {
164 self.object_type = Some(object_type.into());
165 self.object_id = Some(object_id);
166 self.object_name = Some(object_name.into());
167 self
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn parses_official_doc_example_json() {
177 let json = r#"{
178 "activity_id": 1,
179 "datetime": "2015-11-20T12:49:31+00:00",
180 "app": "files",
181 "type": "file_created",
182 "user": "test1",
183 "subject": "test1 created hello.txt",
184 "subject_rich": {
185 "0": "{user1} created {file1}",
186 "1": {
187 "user1": {
188 "type": "user",
189 "id": "test1",
190 "name": "Test User"
191 },
192 "file1": {
193 "type": "file",
194 "id": 23,
195 "name": "hello.txt",
196 "path": "/test/hello.txt"
197 }
198 }
199 },
200 "icon": "https://cloud.example.com/apps/files/img/add-color.svg",
201 "link": "https://cloud.example.com/apps/files/?dir=/test",
202 "object_type": "files",
203 "object_id": 23,
204 "object_name": "/test/hello.txt",
205 "previews": [
206 {
207 "source": "https://cloud.example.com/core/preview.png?file=/hello.txt&x=150&y=150",
208 "link": "https://cloud.example.com/apps/files/?dir=/test&scrollto=hello.txt",
209 "mimeType": "text/plain",
210 "fileId": 23,
211 "view": "files",
212 "isMimeTypeIcon": false,
213 "filename": "hello.txt"
214 }
215 ]
216 }"#;
217
218 let activity: Activity = serde_json::from_str(json).expect("valid activity json");
219 assert_eq!(activity.activity_id(), 1);
220 assert_eq!(activity.datetime(), "2015-11-20T12:49:31+00:00");
221 assert_eq!(activity.app(), "files");
222 assert_eq!(activity.activity_type(), "file_created");
223 assert_eq!(activity.user(), "test1");
224 assert_eq!(activity.subject(), "test1 created hello.txt");
225 assert_eq!(activity.message(), None);
226 assert_eq!(
227 activity.link(),
228 Some("https://cloud.example.com/apps/files/?dir=/test")
229 );
230 assert_eq!(activity.object_type(), Some("files"));
231 assert_eq!(activity.object_id(), Some(23));
232 assert_eq!(activity.object_name(), Some("/test/hello.txt"));
233 }
234
235 #[test]
236 fn parses_string_ids_and_optional_message() {
237 let json = r#"{
238 "activity_id": "42",
239 "datetime": "2026-10-24T15:00:00+00:00",
240 "app": "comments",
241 "type": "comment_added",
242 "user": "alice",
243 "subject": "alice commented on report.pdf",
244 "message": "Looks great to me!",
245 "link": null,
246 "object_type": "files",
247 "object_id": "100",
248 "object_name": "/report.pdf"
249 }"#;
250
251 let activity: Activity = serde_json::from_str(json).expect("valid activity json");
252 assert_eq!(activity.activity_id(), 42);
253 assert_eq!(activity.activity_type(), "comment_added");
254 assert_eq!(activity.user(), "alice");
255 assert_eq!(activity.message(), Some("Looks great to me!"));
256 assert_eq!(activity.link(), None);
257 assert_eq!(activity.object_id(), Some(100));
258 }
259
260 #[test]
261 fn constructor_and_builder_methods() {
262 let activity = Activity::new(
263 7,
264 "deck",
265 "card_created",
266 "bob",
267 "bob created card",
268 "2026-01-01T00:00:00Z",
269 )
270 .with_message("Card in Todo list")
271 .with_link("https://cloud.example.com/apps/deck/#/boards/1")
272 .with_object("deck_card", 55, "New Task");
273
274 assert_eq!(activity.activity_id, 7);
275 assert_eq!(activity.app, "deck");
276 assert_eq!(activity.activity_type, "card_created");
277 assert_eq!(activity.user, "bob");
278 assert_eq!(activity.subject, "bob created card");
279 assert_eq!(activity.datetime, "2026-01-01T00:00:00Z");
280 assert_eq!(activity.message.as_deref(), Some("Card in Todo list"));
281 assert_eq!(
282 activity.link.as_deref(),
283 Some("https://cloud.example.com/apps/deck/#/boards/1")
284 );
285 assert_eq!(activity.object_type.as_deref(), Some("deck_card"));
286 assert_eq!(activity.object_id, Some(55));
287 assert_eq!(activity.object_name.as_deref(), Some("New Task"));
288 }
289}