use crate::types::permission::Ruleset;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Session {
pub id: String,
pub slug: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub directory: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary: Option<SessionSummary>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub share: Option<ShareInfo>,
#[serde(default)]
pub title: String,
#[serde(default)]
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub time: Option<SessionTime>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permission: Option<Ruleset>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revert: Option<RevertInfo>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionSummary {
pub additions: u64,
pub deletions: u64,
pub files: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub diffs: Option<Vec<FileDiffLite>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FileDiffLite {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub additions: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deletions: Option<u64>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShareInfo {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub secret: Option<String>,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionTime {
pub created: i64,
pub updated: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compacting: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub archived: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RevertInfo {
pub message_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub part_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub snapshot: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub diff: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateSessionRequest {
#[serde(default, skip_serializing_if = "Option::is_none", rename = "parentID")]
pub parent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permission: Option<Ruleset>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateSessionRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SummarizeRequest {
pub provider_id: String,
pub model_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RevertRequest {
pub message_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub part_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionStatus {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_session_id: Option<String>,
#[serde(default)]
pub busy: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum SessionStatusInfo {
Idle,
Busy,
Retry {
attempt: u64,
message: String,
next: u64,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionFileDiff {
pub file: String,
pub before: String,
pub after: String,
pub additions: u64,
pub deletions: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<SessionDiffStatus>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SessionDiffStatus {
Added,
Deleted,
Modified,
}
pub type SessionDiff = Vec<SessionFileDiff>;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TodoItem {
pub id: String,
pub content: String,
#[serde(default)]
pub completed: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub priority: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_session_deserialize() {
let json = r#"{
"id": "s1",
"slug": "s1",
"projectId": "p1",
"directory": "/path/to/project",
"title": "Test Session",
"version": "1.0",
"time": {"created": 1234567890, "updated": 1234567890}
}"#;
let session: Session = serde_json::from_str(json).unwrap();
assert_eq!(session.id, "s1");
assert_eq!(session.slug, "s1");
assert_eq!(session.title, "Test Session");
}
#[test]
fn test_session_minimal_upstream() {
let json = r#"{"id": "s1", "slug": "s1"}"#;
let session: Session = serde_json::from_str(json).unwrap();
assert_eq!(session.id, "s1");
assert_eq!(session.slug, "s1");
assert!(session.project_id.is_none());
}
#[test]
fn test_session_missing_slug_fails() {
let json = r#"{"id": "s1"}"#;
assert!(serde_json::from_str::<Session>(json).is_err());
}
#[test]
fn test_session_with_optional_fields() {
let json = r#"{
"id": "s1",
"slug": "s1",
"projectId": "p1",
"directory": "/path",
"title": "Test",
"version": "1.0",
"time": {"created": 1234567890, "updated": 1234567890},
"parentId": "s0",
"share": {"url": "https://example.com/share/s1"}
}"#;
let session: Session = serde_json::from_str(json).unwrap();
assert_eq!(session.slug, "s1");
assert_eq!(session.parent_id, Some("s0".to_string()));
assert!(session.share.is_some());
}
#[test]
fn parse_legacy_status_shape_is_rejected() {
let json = r#"{"busy": true, "activeSessionId": "s1"}"#;
let resp: Result<HashMap<String, SessionStatusInfo>, _> = serde_json::from_str(json);
assert!(resp.is_err());
}
#[test]
fn parse_map_status() {
let json = r#"{"s1": {"type": "busy"}, "s2": {"type": "retry", "attempt": 2, "message": "rate limited", "next": 12345}}"#;
let resp: HashMap<String, SessionStatusInfo> = serde_json::from_str(json).unwrap();
assert!(matches!(resp.get("s1"), Some(SessionStatusInfo::Busy)));
assert!(matches!(
resp.get("s2"),
Some(SessionStatusInfo::Retry { attempt: 2, .. })
));
assert!(!resp.contains_key("s3"));
}
#[test]
fn parse_empty_map_status() {
let json = r"{}";
let resp: HashMap<String, SessionStatusInfo> = serde_json::from_str(json).unwrap();
assert!(resp.is_empty());
}
#[test]
fn parse_session_file_diff() {
let json = r#"{
"file": "src/main.rs",
"before": "fn main() {}",
"after": "fn main() { println!(\"hello\"); }",
"additions": 1,
"deletions": 0,
"status": "modified"
}"#;
let diff: SessionFileDiff = serde_json::from_str(json).unwrap();
assert_eq!(diff.file, "src/main.rs");
assert_eq!(diff.additions, 1);
assert_eq!(diff.deletions, 0);
assert_eq!(diff.status, Some(SessionDiffStatus::Modified));
}
#[test]
fn parse_session_diff_array() {
let json = r#"[
{"file": "a.rs", "before": "", "after": "new", "additions": 1, "deletions": 0, "status": "added"},
{"file": "b.rs", "before": "old", "after": "", "additions": 0, "deletions": 1, "status": "deleted"}
]"#;
let diff: SessionDiff = serde_json::from_str(json).unwrap();
assert_eq!(diff.len(), 2);
assert_eq!(diff[0].status, Some(SessionDiffStatus::Added));
assert_eq!(diff[1].status, Some(SessionDiffStatus::Deleted));
}
#[test]
fn test_create_session_request_parent_id_serializes_as_uppercase() {
let req = CreateSessionRequest {
parent_id: Some("ses-123".to_string()),
title: None,
permission: None,
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains(r#""parentID""#));
assert!(!json.contains(r#""parentId""#));
}
#[test]
fn test_create_session_request_without_parent_id() {
let req = CreateSessionRequest {
parent_id: None,
title: Some("Test Session".to_string()),
permission: None,
};
let json = serde_json::to_string(&req).unwrap();
assert!(!json.contains("parentID"));
assert!(json.contains(r#""title":"Test Session""#));
}
}