use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Project {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub directory: Option<String>,
#[serde(default)]
pub current: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub settings: Option<ProjectSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worktree: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vcs: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icon: Option<ProjectIcon>,
#[serde(default)]
pub commands: ProjectCommands,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub time: Option<ProjectTime>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sandboxes: Vec<String>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectIcon {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub r#type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectCommands {
#[serde(flatten)]
pub commands: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectTime {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub updated: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accessed: Option<u64>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectSettings {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_model: Option<ModelRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_agent: Option<String>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelRef {
#[serde(
rename = "providerID",
default,
skip_serializing_if = "Option::is_none"
)]
pub provider_id: Option<String>,
#[serde(rename = "modelID", default, skip_serializing_if = "Option::is_none")]
pub model_id: Option<String>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateProjectRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub settings: Option<ProjectSettings>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GitInitRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_branch: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GitInitResponse {
pub success: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_project_minimal() {
let json = r#"{"id": "proj-123"}"#;
let project: Project = serde_json::from_str(json).unwrap();
assert_eq!(project.id, "proj-123");
assert!(project.name.is_none());
assert!(!project.current);
assert!(project.sandboxes.is_empty());
}
#[test]
fn test_project_with_new_fields() {
let json = r#"{
"id": "proj-123",
"name": "My Project",
"worktree": "/path/to/worktree",
"vcs": "git",
"icon": {
"type": "emoji",
"value": "rocket"
},
"time": {
"created": 1234567890,
"updated": 1234567891,
"accessed": 1234567892
},
"sandboxes": ["sandbox-1", "sandbox-2"]
}"#;
let project: Project = serde_json::from_str(json).unwrap();
assert_eq!(project.id, "proj-123");
assert_eq!(project.name, Some("My Project".to_string()));
assert_eq!(project.worktree, Some("/path/to/worktree".to_string()));
assert_eq!(project.vcs, Some("git".to_string()));
let icon = project.icon.unwrap();
assert_eq!(icon.r#type, Some("emoji".to_string()));
assert_eq!(icon.value, Some("rocket".to_string()));
let time = project.time.unwrap();
assert_eq!(time.created, Some(1_234_567_890));
assert_eq!(time.updated, Some(1_234_567_891));
assert_eq!(time.accessed, Some(1_234_567_892));
assert_eq!(project.sandboxes, vec!["sandbox-1", "sandbox-2"]);
}
#[test]
fn test_project_icon() {
let json = r#"{"type": "url", "value": "https://example.com/icon.png"}"#;
let icon: ProjectIcon = serde_json::from_str(json).unwrap();
assert_eq!(icon.r#type, Some("url".to_string()));
assert_eq!(icon.value, Some("https://example.com/icon.png".to_string()));
}
#[test]
fn test_project_time() {
let json = r#"{"created": 1000, "updated": 2000}"#;
let time: ProjectTime = serde_json::from_str(json).unwrap();
assert_eq!(time.created, Some(1000));
assert_eq!(time.updated, Some(2000));
assert!(time.accessed.is_none());
}
#[test]
fn test_project_commands_empty() {
let json = r"{}";
let commands: ProjectCommands = serde_json::from_str(json).unwrap();
assert!(commands.commands.is_empty());
}
#[test]
fn test_project_commands_with_data() {
let json = r#"{"build": {"script": "cargo build"}, "test": {"script": "cargo test"}}"#;
let commands: ProjectCommands = serde_json::from_str(json).unwrap();
assert_eq!(commands.commands.len(), 2);
assert!(commands.commands.contains_key("build"));
assert!(commands.commands.contains_key("test"));
}
#[test]
fn test_project_extra_fields_preserved() {
let json = r#"{
"id": "proj-123",
"futureField": "some value",
"anotherFuture": 42
}"#;
let project: Project = serde_json::from_str(json).unwrap();
assert_eq!(project.id, "proj-123");
assert_eq!(project.extra["futureField"], "some value");
assert_eq!(project.extra["anotherFuture"], 42);
}
#[test]
fn test_model_ref() {
let json = r#"{"providerID": "anthropic", "modelID": "claude-3"}"#;
let model_ref: ModelRef = serde_json::from_str(json).unwrap();
assert_eq!(model_ref.provider_id, Some("anthropic".to_string()));
assert_eq!(model_ref.model_id, Some("claude-3".to_string()));
}
#[test]
fn test_model_ref_partial() {
let json = r"{}";
let model_ref: ModelRef = serde_json::from_str(json).unwrap();
assert!(model_ref.provider_id.is_none());
assert!(model_ref.model_id.is_none());
}
#[test]
fn test_model_ref_serializes_correct_casing() {
let model_ref = ModelRef {
provider_id: Some("openai".to_string()),
model_id: Some("gpt-4".to_string()),
extra: serde_json::Value::Null,
};
let json = serde_json::to_string(&model_ref).unwrap();
assert!(json.contains("providerID"));
assert!(json.contains("modelID"));
assert!(!json.contains("providerId"));
assert!(!json.contains("modelId"));
}
}