1use serde::{Deserialize, Serialize};
4use std::error::Error;
5use std::fmt;
6
7#[derive(Debug)]
9pub struct ActivityBuildError(String);
10
11impl fmt::Display for ActivityBuildError {
12 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13 write!(f, "Activity build error: {}", self.0)
14 }
15}
16
17impl Error for ActivityBuildError {}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
21#[serde(rename_all = "camelCase")]
22pub struct PlanStep {
23 #[serde(skip_serializing_if = "Option::is_none")]
24 id: Option<String>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 title: Option<String>,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 description: Option<String>,
29}
30
31impl PlanStep {
32 #[must_use]
34 pub fn id(&self) -> Option<&str> {
35 self.id.as_deref()
36 }
37
38 #[must_use]
40 pub fn title(&self) -> Option<&str> {
41 self.title.as_deref()
42 }
43
44 #[must_use]
46 pub fn description(&self) -> Option<&str> {
47 self.description.as_deref()
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
53#[serde(rename_all = "camelCase")]
54pub struct Plan {
55 #[serde(skip_serializing_if = "Option::is_none")]
56 id: Option<String>,
57 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 steps: Vec<PlanStep>,
59}
60
61impl Plan {
62 #[must_use]
64 pub fn id(&self) -> Option<&str> {
65 self.id.as_deref()
66 }
67
68 #[must_use]
70 pub fn steps(&self) -> &[PlanStep] {
71 &self.steps
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
77#[serde(rename_all = "camelCase")]
78pub struct PlanGenerated {
79 #[serde(skip_serializing_if = "Option::is_none")]
80 plan: Option<Plan>,
81}
82
83impl PlanGenerated {
84 #[must_use]
86 pub fn plan(&self) -> Option<&Plan> {
87 self.plan.as_ref()
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
100#[serde(rename_all = "camelCase")]
101pub struct Activity {
102 #[serde(skip_serializing_if = "Option::is_none")]
103 id: Option<String>,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 name: Option<String>,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 create_time: Option<String>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 originator: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 plan_generated: Option<PlanGenerated>,
112 #[serde(flatten)]
115 extra: serde_json::Map<String, serde_json::Value>,
116}
117
118impl Activity {
119 #[must_use]
121 pub fn builder() -> ActivityBuilder {
122 ActivityBuilder::default()
123 }
124
125 #[must_use]
127 pub fn id(&self) -> Option<&str> {
128 self.id.as_deref()
129 }
130
131 #[must_use]
133 pub fn name(&self) -> Option<&str> {
134 self.name.as_deref()
135 }
136
137 #[must_use]
139 pub fn create_time(&self) -> Option<&str> {
140 self.create_time.as_deref()
141 }
142
143 #[must_use]
145 pub fn originator(&self) -> Option<&str> {
146 self.originator.as_deref()
147 }
148
149 #[must_use]
151 pub fn plan_generated(&self) -> Option<&PlanGenerated> {
152 self.plan_generated.as_ref()
153 }
154
155 #[must_use]
158 pub fn extra(&self) -> &serde_json::Map<String, serde_json::Value> {
159 &self.extra
160 }
161}
162
163#[derive(Debug, Default)]
165pub struct ActivityBuilder {
166 id: Option<String>,
167 name: Option<String>,
168 create_time: Option<String>,
169 originator: Option<String>,
170 plan_generated: Option<PlanGenerated>,
171}
172
173impl ActivityBuilder {
174 #[must_use]
176 pub fn id(mut self, id: impl Into<String>) -> Self {
177 self.id = Some(id.into());
178 self
179 }
180
181 #[must_use]
183 pub fn name(mut self, name: impl Into<String>) -> Self {
184 self.name = Some(name.into());
185 self
186 }
187
188 #[must_use]
190 pub fn create_time(mut self, create_time: impl Into<String>) -> Self {
191 self.create_time = Some(create_time.into());
192 self
193 }
194
195 #[must_use]
197 pub fn originator(mut self, originator: impl Into<String>) -> Self {
198 self.originator = Some(originator.into());
199 self
200 }
201
202 pub fn build(self) -> Result<Activity, ActivityBuildError> {
208 Ok(Activity {
209 id: self.id,
210 name: self.name,
211 create_time: self.create_time,
212 originator: self.originator,
213 plan_generated: self.plan_generated,
214 extra: serde_json::Map::new(),
215 })
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn test_activity_builder_with_fields() {
225 let activity = Activity::builder()
226 .id("act-1")
227 .name("My Activity")
228 .build()
229 .unwrap();
230 assert_eq!(activity.id(), Some("act-1"));
231 assert_eq!(activity.name(), Some("My Activity"));
232 }
233
234 #[test]
235 fn test_activity_builder_without_fields() {
236 let activity = Activity::builder().build().unwrap();
237 assert_eq!(activity.id(), None);
238 assert_eq!(activity.name(), None);
239 }
240
241 #[test]
244 fn test_activity_deserializes_real_api_shape() {
245 let json = r#"{
246 "name": "sessions/000/activities/001",
247 "createTime": "2026-08-08T12:42:12Z",
248 "originator": "AGENT",
249 "planGenerated": {
250 "plan": {
251 "id": "plan-1",
252 "steps": [
253 {"id": "step-1", "title": "Investigate", "description": "Look around"}
254 ]
255 }
256 },
257 "id": "001"
258 }"#;
259
260 let activity: Activity = serde_json::from_str(json).unwrap();
261 assert_eq!(activity.originator(), Some("AGENT"));
262 let plan = activity
263 .plan_generated()
264 .and_then(PlanGenerated::plan)
265 .unwrap();
266 assert_eq!(plan.steps().len(), 1);
267 assert_eq!(plan.steps()[0].title(), Some("Investigate"));
268 }
269
270 #[test]
273 fn test_activity_preserves_unknown_kind_via_extra() {
274 let json = r#"{
275 "name": "sessions/000/activities/002",
276 "createTime": "2026-08-08T12:42:12Z",
277 "originator": "USER",
278 "id": "002",
279 "userMessaged": {
280 "message": "hello"
281 }
282 }"#;
283
284 let activity: Activity = serde_json::from_str(json).unwrap();
285 assert!(activity.plan_generated().is_none());
286 assert!(activity.extra().contains_key("userMessaged"));
287 assert_eq!(
288 activity.extra()["userMessaged"]["message"],
289 serde_json::Value::String("hello".to_string())
290 );
291 }
292}