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    /// Custom fields configured on the list this task lives in. Each
68    /// entry has a stable id, a human-readable name, and an arbitrary
69    /// JSON value (string for text, number for numeric, object for
70    /// dropdown / labels). Empty for tasks in lists without custom
71    /// fields.
72    #[serde(default)]
73    pub custom_fields: Vec<ClickUpCustomField>,
74}
75
76/// ClickUp custom-field entry as returned on the task payload.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ClickUpCustomField {
79    pub id: String,
80    #[serde(default)]
81    pub name: Option<String>,
82    /// Field type — `"text"`, `"number"`, `"drop_down"`, `"labels"`,
83    /// `"users"`, `"date"`, …
84    #[serde(default, rename = "type")]
85    pub field_type: Option<String>,
86    /// Raw value as returned by ClickUp. Shape varies by `field_type`.
87    /// Absent when the user hasn't set the field.
88    #[serde(default)]
89    pub value: Option<serde_json::Value>,
90}
91
92/// ClickUp task attachment entry as returned on the task payload.
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ClickUpAttachment {
95    pub id: String,
96    #[serde(default)]
97    pub title: Option<String>,
98    /// Direct download URL.
99    #[serde(default)]
100    pub url: Option<String>,
101    /// Size in bytes (API sometimes returns a string).
102    #[serde(default)]
103    pub size: Option<serde_json::Value>,
104    /// File extension (e.g. "png").
105    #[serde(default)]
106    pub extension: Option<String>,
107    #[serde(default)]
108    pub mimetype: Option<String>,
109    /// Creation timestamp (epoch ms as string in ClickUp's responses).
110    #[serde(default)]
111    pub date: Option<String>,
112    /// Author display info, if present.
113    #[serde(default)]
114    pub user: Option<ClickUpUser>,
115}
116
117/// ClickUp task status.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ClickUpStatus {
120    pub status: String,
121    #[serde(default, rename = "type")]
122    pub status_type: Option<String>,
123}
124
125/// ClickUp task priority.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct ClickUpPriority {
128    pub id: String,
129    pub priority: String,
130    #[serde(default)]
131    pub color: Option<String>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct ClickUpTag {
136    pub name: String,
137}
138
139/// ClickUp linked task (non-dependency relationship).
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ClickUpLinkedTask {
142    pub task_id: String,
143    pub link_id: String,
144    /// Dependency type: "blocked_by", "blocking", or null (plain link).
145    #[serde(default)]
146    pub link_type: Option<String>,
147}
148
149// =============================================================================
150// Task List Response
151// =============================================================================
152
153/// Response from GET /list/{list_id}/task.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct ClickUpTaskList {
156    pub tasks: Vec<ClickUpTask>,
157}
158
159// =============================================================================
160// Team (workspace) — used to look up assignees by email/username.
161// =============================================================================
162
163/// Response from GET /api/v2/team — list workspaces the auth user is in,
164/// each with embedded members.
165#[derive(Debug, Clone, Deserialize)]
166pub struct ClickUpTeamsResponse {
167    #[serde(default)]
168    pub teams: Vec<ClickUpTeam>,
169}
170
171#[derive(Debug, Clone, Deserialize)]
172pub struct ClickUpTeam {
173    pub id: String,
174    #[serde(default)]
175    pub members: Vec<ClickUpTeamMember>,
176}
177
178#[derive(Debug, Clone, Deserialize)]
179pub struct ClickUpTeamMember {
180    pub user: ClickUpUser,
181}
182
183// =============================================================================
184// Comment
185// =============================================================================
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct ClickUpComment {
189    pub id: String,
190    pub comment_text: String,
191    #[serde(default)]
192    pub user: Option<ClickUpUser>,
193    #[serde(default)]
194    pub date: Option<String>,
195}
196
197/// Response from GET /task/{task_id}/comment.
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct ClickUpCommentList {
200    pub comments: Vec<ClickUpComment>,
201}
202
203// =============================================================================
204// List (for status resolution)
205// =============================================================================
206
207/// ClickUp list status (from GET /list/{list_id}).
208#[derive(Debug, Clone, Deserialize)]
209pub struct ClickUpListStatus {
210    pub status: String,
211    #[serde(default, rename = "type")]
212    pub status_type: Option<String>,
213    #[serde(default)]
214    pub color: Option<String>,
215    #[serde(default)]
216    pub orderindex: Option<u32>,
217}
218
219/// Partial response from GET /list/{list_id} (only statuses needed).
220#[derive(Debug, Clone, Deserialize)]
221pub struct ClickUpListInfo {
222    pub statuses: Vec<ClickUpListStatus>,
223}
224
225/// Response from POST /task/{task_id}/dependency.
226#[derive(Debug, Clone, Deserialize)]
227pub struct ClickUpDependencyResponse {
228    #[serde(default)]
229    pub dependency: Option<serde_json::Value>,
230}
231
232/// Response from POST /task/{task_id}/link/{other_task_id}.
233#[derive(Debug, Clone, Deserialize)]
234pub struct ClickUpLinkResponse {
235    #[serde(default)]
236    pub link: Option<serde_json::Value>,
237}
238
239// =============================================================================
240// Create/Update types
241// =============================================================================
242
243#[derive(Debug, Clone, Serialize)]
244pub struct CreateTaskRequest {
245    pub name: String,
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub description: Option<String>,
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub markdown_content: Option<String>,
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub parent: Option<String>,
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub status: Option<String>,
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub priority: Option<u8>,
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub tags: Option<Vec<String>>,
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub assignees: Option<Vec<u64>>,
260}
261
262#[derive(Debug, Clone, Serialize, Default)]
263pub struct UpdateTaskRequest {
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub name: Option<String>,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub description: Option<String>,
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub markdown_content: Option<String>,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub status: Option<String>,
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub priority: Option<u8>,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub parent: Option<String>,
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub tags: Option<Vec<String>>,
278    /// Assignee diff for PUT /task/:id. ClickUp does NOT accept a flat
279    /// `assignees: [...]` array on update — it silently 200's and drops
280    /// the field. The supported shape is `{ add: [u64], rem: [u64] }`.
281    /// `None` leaves assignees untouched.
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub assignees: Option<AssigneeDiff>,
284}
285
286/// Diff envelope for PUT /task/:id `assignees` field.
287/// Both arrays are sent only when non-empty so ClickUp doesn't see noise.
288#[derive(Debug, Clone, Default, Serialize)]
289pub struct AssigneeDiff {
290    #[serde(skip_serializing_if = "Vec::is_empty")]
291    pub add: Vec<u64>,
292    #[serde(skip_serializing_if = "Vec::is_empty")]
293    pub rem: Vec<u64>,
294}
295
296#[derive(Debug, Clone, Serialize)]
297pub struct CreateCommentRequest {
298    pub comment_text: String,
299}
300
301/// Response from POST /task/{task_id}/comment.
302/// ClickUp returns a minimal response (no comment_text, id and date may be numbers).
303#[derive(Debug, Clone, Deserialize)]
304pub struct CreateCommentResponse {
305    #[serde(deserialize_with = "value_to_string")]
306    pub id: String,
307    #[serde(default, deserialize_with = "option_value_to_string")]
308    pub date: Option<String>,
309}
310
311/// Deserialize a value that may be a string or a number into String.
312fn value_to_string<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
313where
314    D: serde::Deserializer<'de>,
315{
316    let value = serde_json::Value::deserialize(deserializer)?;
317    match value {
318        serde_json::Value::String(s) => Ok(s),
319        serde_json::Value::Number(n) => Ok(n.to_string()),
320        other => Ok(other.to_string()),
321    }
322}
323
324/// Deserialize an optional value that may be a string or a number into Option<String>.
325fn option_value_to_string<'de, D>(deserializer: D) -> std::result::Result<Option<String>, D::Error>
326where
327    D: serde::Deserializer<'de>,
328{
329    let value = Option::<serde_json::Value>::deserialize(deserializer)?;
330    Ok(value.map(|v| match v {
331        serde_json::Value::String(s) => s,
332        serde_json::Value::Number(n) => n.to_string(),
333        other => other.to_string(),
334    }))
335}