use serde::{Deserialize, Serialize};
use serde_json::Map;
pub type Gid = String;
#[derive(Debug, Clone, Deserialize)]
pub struct DataWrapper<T> {
pub data: T,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ListWrapper<T> {
pub data: Vec<T>,
pub next_page: Option<NextPage>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct NextPage {
pub offset: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Resource {
pub gid: Gid,
#[serde(default)]
pub resource_type: Option<String>,
#[serde(flatten)]
pub fields: Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PortfolioItem {
pub gid: Gid,
pub resource_type: String,
#[serde(default)]
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskRef {
pub gid: Gid,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub completed: bool,
#[serde(default)]
pub num_subtasks: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserRef {
pub gid: Gid,
#[serde(default)]
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FavoriteItem {
pub gid: Gid,
pub resource_type: String,
#[serde(default)]
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Story {
pub gid: Gid,
#[serde(default)]
pub resource_subtype: Option<String>,
#[serde(default)]
pub text: Option<String>,
#[serde(default)]
pub html_text: Option<String>,
#[serde(flatten)]
pub fields: Map<String, serde_json::Value>,
}
impl Story {
pub fn is_comment(&self) -> bool {
self.resource_subtype.as_deref() == Some("comment_added")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskDependency {
pub gid: Gid,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub resource_type: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PortfolioWithItems {
#[serde(flatten)]
pub portfolio: Resource,
pub items: Vec<PortfolioItemExpanded>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "resource_type", rename_all = "snake_case")]
pub enum PortfolioItemExpanded {
Project(Box<Resource>),
Portfolio(Box<PortfolioWithItems>),
}
#[derive(Debug, Clone, Serialize)]
pub struct TaskWithContext {
#[serde(flatten)]
pub task: Resource,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub subtasks: Vec<TaskRef>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub dependencies: Vec<TaskDependency>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub dependents: Vec<TaskDependency>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub comments: Vec<Story>,
}
#[derive(Debug, Serialize)]
pub struct FavoritesResponse {
pub projects: Vec<Resource>,
pub portfolios: Vec<PortfolioWithItems>,
pub errors: Vec<FavoriteError>,
}
#[derive(Debug, Serialize)]
pub struct FavoriteError {
pub item: FavoriteItem,
pub error: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Job {
pub gid: Gid,
#[serde(default)]
pub status: Option<String>,
#[serde(default)]
pub new_project: Option<Resource>,
#[serde(flatten)]
pub fields: Map<String, serde_json::Value>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resource_deserialization() {
let json = r#"{"gid": "123", "name": "Test", "custom_field": "value"}"#;
let resource: Resource = serde_json::from_str(json).unwrap();
assert_eq!(resource.gid, "123");
assert_eq!(resource.fields.get("name").unwrap(), "Test");
assert_eq!(resource.fields.get("custom_field").unwrap(), "value");
}
#[test]
fn test_portfolio_item_deserialization() {
let json = r#"{"gid": "456", "resource_type": "project", "name": "My Project"}"#;
let item: PortfolioItem = serde_json::from_str(json).unwrap();
assert_eq!(item.gid, "456");
assert_eq!(item.resource_type, "project");
assert_eq!(item.name, Some("My Project".to_string()));
}
#[test]
fn test_story_is_comment() {
let comment = Story {
gid: "1".to_string(),
resource_subtype: Some("comment_added".to_string()),
text: Some("Hello".to_string()),
html_text: None,
fields: Map::new(),
};
assert!(comment.is_comment());
let system = Story {
gid: "2".to_string(),
resource_subtype: Some("added_to_project".to_string()),
text: None,
html_text: None,
fields: Map::new(),
};
assert!(!system.is_comment());
}
#[test]
fn test_data_wrapper() {
let json = r#"{"data": {"gid": "789", "name": "Wrapped"}}"#;
let wrapper: DataWrapper<Resource> = serde_json::from_str(json).unwrap();
assert_eq!(wrapper.data.gid, "789");
}
#[test]
fn test_list_wrapper_with_pagination() {
let json = r#"{
"data": [{"gid": "1"}, {"gid": "2"}],
"next_page": {"offset": "abc123"}
}"#;
let wrapper: ListWrapper<Resource> = serde_json::from_str(json).unwrap();
assert_eq!(wrapper.data.len(), 2);
assert_eq!(wrapper.next_page.unwrap().offset, "abc123");
}
}