use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub struct ActivityBuildError(String);
impl fmt::Display for ActivityBuildError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Activity build error: {}", self.0)
}
}
impl Error for ActivityBuildError {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct PlanStep {
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
}
impl PlanStep {
#[must_use]
pub fn id(&self) -> Option<&str> {
self.id.as_deref()
}
#[must_use]
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
#[must_use]
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Plan {
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
steps: Vec<PlanStep>,
}
impl Plan {
#[must_use]
pub fn id(&self) -> Option<&str> {
self.id.as_deref()
}
#[must_use]
pub fn steps(&self) -> &[PlanStep] {
&self.steps
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct PlanGenerated {
#[serde(skip_serializing_if = "Option::is_none")]
plan: Option<Plan>,
}
impl PlanGenerated {
#[must_use]
pub fn plan(&self) -> Option<&Plan> {
self.plan.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Activity {
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
create_time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
originator: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
plan_generated: Option<PlanGenerated>,
#[serde(flatten)]
extra: serde_json::Map<String, serde_json::Value>,
}
impl Activity {
#[must_use]
pub fn builder() -> ActivityBuilder {
ActivityBuilder::default()
}
#[must_use]
pub fn id(&self) -> Option<&str> {
self.id.as_deref()
}
#[must_use]
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
#[must_use]
pub fn create_time(&self) -> Option<&str> {
self.create_time.as_deref()
}
#[must_use]
pub fn originator(&self) -> Option<&str> {
self.originator.as_deref()
}
#[must_use]
pub fn plan_generated(&self) -> Option<&PlanGenerated> {
self.plan_generated.as_ref()
}
#[must_use]
pub fn extra(&self) -> &serde_json::Map<String, serde_json::Value> {
&self.extra
}
}
#[derive(Debug, Default)]
pub struct ActivityBuilder {
id: Option<String>,
name: Option<String>,
create_time: Option<String>,
originator: Option<String>,
plan_generated: Option<PlanGenerated>,
}
impl ActivityBuilder {
#[must_use]
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn create_time(mut self, create_time: impl Into<String>) -> Self {
self.create_time = Some(create_time.into());
self
}
#[must_use]
pub fn originator(mut self, originator: impl Into<String>) -> Self {
self.originator = Some(originator.into());
self
}
pub fn build(self) -> Result<Activity, ActivityBuildError> {
Ok(Activity {
id: self.id,
name: self.name,
create_time: self.create_time,
originator: self.originator,
plan_generated: self.plan_generated,
extra: serde_json::Map::new(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_activity_builder_with_fields() {
let activity = Activity::builder()
.id("act-1")
.name("My Activity")
.build()
.unwrap();
assert_eq!(activity.id(), Some("act-1"));
assert_eq!(activity.name(), Some("My Activity"));
}
#[test]
fn test_activity_builder_without_fields() {
let activity = Activity::builder().build().unwrap();
assert_eq!(activity.id(), None);
assert_eq!(activity.name(), None);
}
#[test]
fn test_activity_deserializes_real_api_shape() {
let json = r#"{
"name": "sessions/000/activities/001",
"createTime": "2026-08-08T12:42:12Z",
"originator": "AGENT",
"planGenerated": {
"plan": {
"id": "plan-1",
"steps": [
{"id": "step-1", "title": "Investigate", "description": "Look around"}
]
}
},
"id": "001"
}"#;
let activity: Activity = serde_json::from_str(json).unwrap();
assert_eq!(activity.originator(), Some("AGENT"));
let plan = activity
.plan_generated()
.and_then(PlanGenerated::plan)
.unwrap();
assert_eq!(plan.steps().len(), 1);
assert_eq!(plan.steps()[0].title(), Some("Investigate"));
}
#[test]
fn test_activity_preserves_unknown_kind_via_extra() {
let json = r#"{
"name": "sessions/000/activities/002",
"createTime": "2026-08-08T12:42:12Z",
"originator": "USER",
"id": "002",
"userMessaged": {
"message": "hello"
}
}"#;
let activity: Activity = serde_json::from_str(json).unwrap();
assert!(activity.plan_generated().is_none());
assert!(activity.extra().contains_key("userMessaged"));
assert_eq!(
activity.extra()["userMessaged"]["message"],
serde_json::Value::String("hello".to_string())
);
}
}