Skip to main content

asana_cli/models/
section.rs

1//! Section domain models and request payload helpers.
2
3use serde::{Deserialize, Serialize};
4
5/// Compact section reference used in task memberships and other contexts.
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd)]
7#[serde(rename_all = "snake_case")]
8pub struct SectionReference {
9    /// Globally unique identifier.
10    pub gid: String,
11    /// Section display name.
12    #[serde(default)]
13    pub name: Option<String>,
14    /// Resource type marker.
15    #[serde(default)]
16    pub resource_type: Option<String>,
17}
18
19impl SectionReference {
20    /// Human readable label.
21    #[must_use]
22    pub fn label(&self) -> String {
23        self.name.clone().unwrap_or_else(|| self.gid.clone())
24    }
25}
26
27/// Full section payload returned from Asana.
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29#[serde(rename_all = "snake_case")]
30pub struct Section {
31    /// Section identifier.
32    pub gid: String,
33    /// Display name (the text displayed as the section header).
34    pub name: String,
35    /// Resource type marker.
36    #[serde(default)]
37    pub resource_type: Option<String>,
38    /// Creation timestamp.
39    #[serde(default)]
40    pub created_at: Option<String>,
41    /// Parent project reference.
42    #[serde(default)]
43    pub project: Option<SectionProjectReference>,
44    /// Deprecated field - use project instead.
45    #[serde(default)]
46    pub projects: Vec<SectionProjectReference>,
47}
48
49/// Compact project reference used within section payloads.
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd)]
51#[serde(rename_all = "snake_case")]
52pub struct SectionProjectReference {
53    /// Globally unique identifier.
54    pub gid: String,
55    /// Project name.
56    #[serde(default)]
57    pub name: Option<String>,
58    /// Resource type marker.
59    #[serde(default)]
60    pub resource_type: Option<String>,
61}
62
63impl SectionProjectReference {
64    /// Human readable label.
65    #[must_use]
66    pub fn label(&self) -> String {
67        self.name.clone().unwrap_or_else(|| self.gid.clone())
68    }
69}
70
71/// Payload for creating a section in a project.
72#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
73#[serde(rename_all = "snake_case")]
74pub struct SectionCreateData {
75    /// Section name (required).
76    pub name: String,
77    /// Optional positioning parameter: insert before this section gid.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub insert_before: Option<String>,
80    /// Optional positioning parameter: insert after this section gid.
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub insert_after: Option<String>,
83}
84
85/// API envelope for section create requests.
86#[derive(Debug, Clone, Serialize)]
87pub struct SectionCreateRequest {
88    /// Wrapped data payload.
89    pub data: SectionCreateData,
90}
91
92/// Payload for updating a section.
93#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
94#[serde(rename_all = "snake_case")]
95pub struct SectionUpdateData {
96    /// New section name.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub name: Option<String>,
99}
100
101/// API envelope for section update requests.
102#[derive(Debug, Clone, Serialize)]
103pub struct SectionUpdateRequest {
104    /// Wrapped data payload.
105    pub data: SectionUpdateData,
106}
107
108/// Payload for adding a task to a section.
109#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
110#[serde(rename_all = "snake_case")]
111pub struct AddTaskToSectionData {
112    /// Task gid to add to the section.
113    pub task: String,
114    /// Optional: insert task before this task gid.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub insert_before: Option<String>,
117    /// Optional: insert task after this task gid.
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub insert_after: Option<String>,
120}
121
122/// API envelope for add task to section requests.
123#[derive(Debug, Clone, Serialize)]
124pub struct AddTaskToSectionRequest {
125    /// Wrapped data payload.
126    pub data: AddTaskToSectionData,
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn section_reference_label_uses_name() {
135        let section = SectionReference {
136            gid: "123".to_string(),
137            name: Some("In Progress".to_string()),
138            resource_type: Some("section".to_string()),
139        };
140        assert_eq!(section.label(), "In Progress");
141    }
142
143    #[test]
144    fn section_reference_label_fallback_to_gid() {
145        let section = SectionReference {
146            gid: "456".to_string(),
147            name: None,
148            resource_type: Some("section".to_string()),
149        };
150        assert_eq!(section.label(), "456");
151    }
152
153    #[test]
154    fn create_request_serializes_correctly() {
155        let request = SectionCreateRequest {
156            data: SectionCreateData {
157                name: "New Section".to_string(),
158                insert_before: None,
159                insert_after: Some("789".to_string()),
160            },
161        };
162        let json = serde_json::to_string(&request).unwrap();
163        assert!(json.contains("\"name\":\"New Section\""));
164        assert!(json.contains("\"insert_after\":\"789\""));
165        assert!(!json.contains("insert_before"));
166    }
167
168    #[test]
169    fn add_task_request_serializes_correctly() {
170        let request = AddTaskToSectionRequest {
171            data: AddTaskToSectionData {
172                task: "task123".to_string(),
173                insert_before: Some("task456".to_string()),
174                insert_after: None,
175            },
176        };
177        let json = serde_json::to_string(&request).unwrap();
178        assert!(json.contains("\"task\":\"task123\""));
179        assert!(json.contains("\"insert_before\":\"task456\""));
180        assert!(!json.contains("insert_after"));
181    }
182}