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    /// Field configuration embedded in the task payload. For
91    /// `drop_down` / `labels` fields this carries the `options` list,
92    /// which lets us resolve the opaque `value` (an order index or
93    /// option id) back to its human-readable label inline — no extra
94    /// metadata fetch required.
95    #[serde(default)]
96    pub type_config: Option<ClickUpFieldTypeConfig>,
97}
98
99/// `type_config` block embedded per custom field on a ClickUp task.
100#[derive(Debug, Clone, Default, Serialize, Deserialize)]
101pub struct ClickUpFieldTypeConfig {
102    /// Selectable options for `drop_down` / `labels` fields.
103    #[serde(default)]
104    pub options: Vec<ClickUpFieldOptionInline>,
105}
106
107/// A single `drop_down`/`labels` option as embedded in a task payload.
108/// `drop_down` options carry `name`; `labels` options carry `label` —
109/// both are accepted so either field type resolves.
110#[derive(Debug, Clone, Default, Serialize, Deserialize)]
111pub struct ClickUpFieldOptionInline {
112    #[serde(default)]
113    pub id: Option<String>,
114    #[serde(default, alias = "label")]
115    pub name: Option<String>,
116    #[serde(default)]
117    pub orderindex: Option<u32>,
118}
119
120/// ClickUp task attachment entry as returned on the task payload.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ClickUpAttachment {
123    pub id: String,
124    #[serde(default)]
125    pub title: Option<String>,
126    /// Direct download URL.
127    #[serde(default)]
128    pub url: Option<String>,
129    /// Size in bytes (API sometimes returns a string).
130    #[serde(default)]
131    pub size: Option<serde_json::Value>,
132    /// File extension (e.g. "png").
133    #[serde(default)]
134    pub extension: Option<String>,
135    #[serde(default)]
136    pub mimetype: Option<String>,
137    /// Creation timestamp (epoch ms as string in ClickUp's responses).
138    #[serde(default)]
139    pub date: Option<String>,
140    /// Author display info, if present.
141    #[serde(default)]
142    pub user: Option<ClickUpUser>,
143}
144
145/// ClickUp task status.
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct ClickUpStatus {
148    pub status: String,
149    #[serde(default, rename = "type")]
150    pub status_type: Option<String>,
151}
152
153/// ClickUp task priority.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct ClickUpPriority {
156    pub id: String,
157    pub priority: String,
158    #[serde(default)]
159    pub color: Option<String>,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct ClickUpTag {
164    pub name: String,
165}
166
167/// ClickUp linked task (non-dependency relationship).
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct ClickUpLinkedTask {
170    pub task_id: String,
171    pub link_id: String,
172    /// Dependency type: "blocked_by", "blocking", or null (plain link).
173    #[serde(default)]
174    pub link_type: Option<String>,
175}
176
177// =============================================================================
178// Task List Response
179// =============================================================================
180
181/// Response from GET /list/{list_id}/task.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct ClickUpTaskList {
184    pub tasks: Vec<ClickUpTask>,
185}
186
187// =============================================================================
188// Team (workspace) — used to look up assignees by email/username.
189// =============================================================================
190
191/// Response from GET /api/v2/team — list workspaces the auth user is in,
192/// each with embedded members.
193#[derive(Debug, Clone, Deserialize)]
194pub struct ClickUpTeamsResponse {
195    #[serde(default)]
196    pub teams: Vec<ClickUpTeam>,
197}
198
199#[derive(Debug, Clone, Deserialize)]
200pub struct ClickUpTeam {
201    pub id: String,
202    #[serde(default)]
203    pub members: Vec<ClickUpTeamMember>,
204}
205
206#[derive(Debug, Clone, Deserialize)]
207pub struct ClickUpTeamMember {
208    pub user: ClickUpUser,
209}
210
211// =============================================================================
212// Comment
213// =============================================================================
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct ClickUpComment {
217    pub id: String,
218    pub comment_text: String,
219    #[serde(default)]
220    pub user: Option<ClickUpUser>,
221    #[serde(default)]
222    pub date: Option<String>,
223}
224
225/// Response from GET /task/{task_id}/comment.
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct ClickUpCommentList {
228    pub comments: Vec<ClickUpComment>,
229}
230
231// =============================================================================
232// List (for status resolution)
233// =============================================================================
234
235/// ClickUp list status (from GET /list/{list_id}).
236#[derive(Debug, Clone, Deserialize)]
237pub struct ClickUpListStatus {
238    pub status: String,
239    #[serde(default, rename = "type")]
240    pub status_type: Option<String>,
241    #[serde(default)]
242    pub color: Option<String>,
243    #[serde(default)]
244    pub orderindex: Option<u32>,
245}
246
247/// Partial response from GET /list/{list_id} (only statuses needed).
248#[derive(Debug, Clone, Deserialize)]
249pub struct ClickUpListInfo {
250    pub statuses: Vec<ClickUpListStatus>,
251}
252
253/// Response from POST /task/{task_id}/dependency.
254#[derive(Debug, Clone, Deserialize)]
255pub struct ClickUpDependencyResponse {
256    #[serde(default)]
257    pub dependency: Option<serde_json::Value>,
258}
259
260/// Response from POST /task/{task_id}/link/{other_task_id}.
261#[derive(Debug, Clone, Deserialize)]
262pub struct ClickUpLinkResponse {
263    #[serde(default)]
264    pub link: Option<serde_json::Value>,
265}
266
267// =============================================================================
268// Create/Update types
269// =============================================================================
270
271#[derive(Debug, Clone, Serialize)]
272pub struct CreateTaskRequest {
273    pub name: String,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub description: Option<String>,
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub markdown_content: Option<String>,
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub parent: Option<String>,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub status: Option<String>,
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub priority: Option<u8>,
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub tags: Option<Vec<String>>,
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub assignees: Option<Vec<u64>>,
288}
289
290#[derive(Debug, Clone, Serialize, Default)]
291pub struct UpdateTaskRequest {
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub name: Option<String>,
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub description: Option<String>,
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub markdown_content: Option<String>,
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub status: Option<String>,
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub priority: Option<u8>,
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub parent: Option<String>,
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub tags: Option<Vec<String>>,
306    /// Assignee diff for PUT /task/:id. ClickUp does NOT accept a flat
307    /// `assignees: [...]` array on update — it silently 200's and drops
308    /// the field. The supported shape is `{ add: [u64], rem: [u64] }`.
309    /// `None` leaves assignees untouched.
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub assignees: Option<AssigneeDiff>,
312}
313
314/// Diff envelope for PUT /task/:id `assignees` field.
315/// Both arrays are sent only when non-empty so ClickUp doesn't see noise.
316#[derive(Debug, Clone, Default, Serialize)]
317pub struct AssigneeDiff {
318    #[serde(skip_serializing_if = "Vec::is_empty")]
319    pub add: Vec<u64>,
320    #[serde(skip_serializing_if = "Vec::is_empty")]
321    pub rem: Vec<u64>,
322}
323
324#[derive(Debug, Clone, Serialize)]
325pub struct CreateCommentRequest {
326    /// Plain-text body. ClickUp requires this field and uses it as the
327    /// fallback / notification text. It is run through a lossy auto-formatter
328    /// for rendering, so the structured `comment` array below is what actually
329    /// drives clean rich-text display.
330    pub comment_text: String,
331    /// Structured rich-text runs (Quill Delta shape). When present, ClickUp
332    /// renders these instead of auto-formatting `comment_text`, so inline
333    /// code, code blocks and lists display correctly without fragmenting
334    /// prose. Built from the markdown body by
335    /// [`crate::comment_format::markdown_to_comment_blocks`].
336    #[serde(skip_serializing_if = "Vec::is_empty")]
337    pub comment: Vec<crate::comment_format::CommentBlock>,
338}
339
340/// Response from POST /task/{task_id}/comment.
341/// ClickUp returns a minimal response (no comment_text, id and date may be numbers).
342#[derive(Debug, Clone, Deserialize)]
343pub struct CreateCommentResponse {
344    #[serde(deserialize_with = "value_to_string")]
345    pub id: String,
346    #[serde(default, deserialize_with = "option_value_to_string")]
347    pub date: Option<String>,
348}
349
350/// Deserialize a value that may be a string or a number into String.
351fn value_to_string<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
352where
353    D: serde::Deserializer<'de>,
354{
355    let value = serde_json::Value::deserialize(deserializer)?;
356    match value {
357        serde_json::Value::String(s) => Ok(s),
358        serde_json::Value::Number(n) => Ok(n.to_string()),
359        other => Ok(other.to_string()),
360    }
361}
362
363/// Deserialize an optional value that may be a string or a number into Option<String>.
364fn option_value_to_string<'de, D>(deserializer: D) -> std::result::Result<Option<String>, D::Error>
365where
366    D: serde::Deserializer<'de>,
367{
368    let value = Option::<serde_json::Value>::deserialize(deserializer)?;
369    Ok(value.map(|v| match v {
370        serde_json::Value::String(s) => s,
371        serde_json::Value::Number(n) => n.to_string(),
372        other => other.to_string(),
373    }))
374}