Skip to main content

devboy_clickup/
types.rs

1//! ClickUp API response types.
2//!
3//! These types represent the raw JSON responses from ClickUp API v2.
4//! They are deserialized and then mapped to unified types.
5
6use serde::{Deserialize, Serialize};
7
8// =============================================================================
9// User
10// =============================================================================
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ClickUpUser {
14    pub id: u64,
15    pub username: String,
16    #[serde(default)]
17    pub email: Option<String>,
18    #[serde(default, rename = "profilePicture")]
19    pub profile_picture: Option<String>,
20}
21
22// =============================================================================
23// Task (Issue)
24// =============================================================================
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ClickUpTask {
28    pub id: String,
29    #[serde(default)]
30    pub custom_id: Option<String>,
31    pub name: String,
32    #[serde(default)]
33    pub description: Option<String>,
34    #[serde(default)]
35    pub text_content: Option<String>,
36    pub status: ClickUpStatus,
37    #[serde(default)]
38    pub priority: Option<ClickUpPriority>,
39    #[serde(default)]
40    pub tags: Vec<ClickUpTag>,
41    #[serde(default)]
42    pub assignees: Vec<ClickUpUser>,
43    #[serde(default)]
44    pub creator: Option<ClickUpUser>,
45    pub url: String,
46    #[serde(default)]
47    pub date_created: Option<String>,
48    #[serde(default)]
49    pub date_updated: Option<String>,
50    #[serde(default)]
51    pub parent: Option<String>,
52    #[serde(default)]
53    pub subtasks: Option<Vec<ClickUpTask>>,
54    /// Dependencies (blocking/waiting relationships).
55    /// Uses `serde_json::Value` for flexible parsing of undocumented API shape.
56    #[serde(default)]
57    pub dependencies: Option<Vec<serde_json::Value>>,
58    /// Linked tasks (non-dependency relationships).
59    #[serde(default)]
60    pub linked_tasks: Option<Vec<ClickUpLinkedTask>>,
61    /// Attachments uploaded to the task.
62    ///
63    /// The ClickUp API returns this under `attachments` on the task object.
64    /// It may be absent for older tasks or tasks without uploads.
65    #[serde(default)]
66    pub attachments: Vec<ClickUpAttachment>,
67}
68
69/// ClickUp task attachment entry as returned on the task payload.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ClickUpAttachment {
72    pub id: String,
73    #[serde(default)]
74    pub title: Option<String>,
75    /// Direct download URL.
76    #[serde(default)]
77    pub url: Option<String>,
78    /// Size in bytes (API sometimes returns a string).
79    #[serde(default)]
80    pub size: Option<serde_json::Value>,
81    /// File extension (e.g. "png").
82    #[serde(default)]
83    pub extension: Option<String>,
84    #[serde(default)]
85    pub mimetype: Option<String>,
86    /// Creation timestamp (epoch ms as string in ClickUp's responses).
87    #[serde(default)]
88    pub date: Option<String>,
89    /// Author display info, if present.
90    #[serde(default)]
91    pub user: Option<ClickUpUser>,
92}
93
94/// ClickUp task status.
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ClickUpStatus {
97    pub status: String,
98    #[serde(default, rename = "type")]
99    pub status_type: Option<String>,
100}
101
102/// ClickUp task priority.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct ClickUpPriority {
105    pub id: String,
106    pub priority: String,
107    #[serde(default)]
108    pub color: Option<String>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ClickUpTag {
113    pub name: String,
114}
115
116/// ClickUp linked task (non-dependency relationship).
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct ClickUpLinkedTask {
119    pub task_id: String,
120    pub link_id: String,
121    /// Dependency type: "blocked_by", "blocking", or null (plain link).
122    #[serde(default)]
123    pub link_type: Option<String>,
124}
125
126// =============================================================================
127// Task List Response
128// =============================================================================
129
130/// Response from GET /list/{list_id}/task.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ClickUpTaskList {
133    pub tasks: Vec<ClickUpTask>,
134}
135
136// =============================================================================
137// Comment
138// =============================================================================
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ClickUpComment {
142    pub id: String,
143    pub comment_text: String,
144    #[serde(default)]
145    pub user: Option<ClickUpUser>,
146    #[serde(default)]
147    pub date: Option<String>,
148}
149
150/// Response from GET /task/{task_id}/comment.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct ClickUpCommentList {
153    pub comments: Vec<ClickUpComment>,
154}
155
156// =============================================================================
157// List (for status resolution)
158// =============================================================================
159
160/// ClickUp list status (from GET /list/{list_id}).
161#[derive(Debug, Clone, Deserialize)]
162pub struct ClickUpListStatus {
163    pub status: String,
164    #[serde(default, rename = "type")]
165    pub status_type: Option<String>,
166    #[serde(default)]
167    pub color: Option<String>,
168    #[serde(default)]
169    pub orderindex: Option<u32>,
170}
171
172/// Partial response from GET /list/{list_id} (only statuses needed).
173#[derive(Debug, Clone, Deserialize)]
174pub struct ClickUpListInfo {
175    pub statuses: Vec<ClickUpListStatus>,
176}
177
178/// Response from POST /task/{task_id}/dependency.
179#[derive(Debug, Clone, Deserialize)]
180pub struct ClickUpDependencyResponse {
181    #[serde(default)]
182    pub dependency: Option<serde_json::Value>,
183}
184
185/// Response from POST /task/{task_id}/link/{other_task_id}.
186#[derive(Debug, Clone, Deserialize)]
187pub struct ClickUpLinkResponse {
188    #[serde(default)]
189    pub link: Option<serde_json::Value>,
190}
191
192// =============================================================================
193// Create/Update types
194// =============================================================================
195
196#[derive(Debug, Clone, Serialize)]
197pub struct CreateTaskRequest {
198    pub name: String,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub description: Option<String>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub markdown_content: Option<String>,
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub parent: Option<String>,
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub status: Option<String>,
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub priority: Option<u8>,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub tags: Option<Vec<String>>,
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub assignees: Option<Vec<u64>>,
213}
214
215#[derive(Debug, Clone, Serialize, Default)]
216pub struct UpdateTaskRequest {
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub name: Option<String>,
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub description: Option<String>,
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub markdown_content: Option<String>,
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub status: Option<String>,
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub priority: Option<u8>,
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub parent: Option<String>,
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub tags: Option<Vec<String>>,
231}
232
233#[derive(Debug, Clone, Serialize)]
234pub struct CreateCommentRequest {
235    pub comment_text: String,
236}
237
238/// Response from POST /task/{task_id}/comment.
239/// ClickUp returns a minimal response (no comment_text, id and date may be numbers).
240#[derive(Debug, Clone, Deserialize)]
241pub struct CreateCommentResponse {
242    #[serde(deserialize_with = "value_to_string")]
243    pub id: String,
244    #[serde(default, deserialize_with = "option_value_to_string")]
245    pub date: Option<String>,
246}
247
248/// Deserialize a value that may be a string or a number into String.
249fn value_to_string<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
250where
251    D: serde::Deserializer<'de>,
252{
253    let value = serde_json::Value::deserialize(deserializer)?;
254    match value {
255        serde_json::Value::String(s) => Ok(s),
256        serde_json::Value::Number(n) => Ok(n.to_string()),
257        other => Ok(other.to_string()),
258    }
259}
260
261/// Deserialize an optional value that may be a string or a number into Option<String>.
262fn option_value_to_string<'de, D>(deserializer: D) -> std::result::Result<Option<String>, D::Error>
263where
264    D: serde::Deserializer<'de>,
265{
266    let value = Option::<serde_json::Value>::deserialize(deserializer)?;
267    Ok(value.map(|v| match v {
268        serde_json::Value::String(s) => s,
269        serde_json::Value::Number(n) => n.to_string(),
270        other => other.to_string(),
271    }))
272}