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// Comment
161// =============================================================================
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct ClickUpComment {
165    pub id: String,
166    pub comment_text: String,
167    #[serde(default)]
168    pub user: Option<ClickUpUser>,
169    #[serde(default)]
170    pub date: Option<String>,
171}
172
173/// Response from GET /task/{task_id}/comment.
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct ClickUpCommentList {
176    pub comments: Vec<ClickUpComment>,
177}
178
179// =============================================================================
180// List (for status resolution)
181// =============================================================================
182
183/// ClickUp list status (from GET /list/{list_id}).
184#[derive(Debug, Clone, Deserialize)]
185pub struct ClickUpListStatus {
186    pub status: String,
187    #[serde(default, rename = "type")]
188    pub status_type: Option<String>,
189    #[serde(default)]
190    pub color: Option<String>,
191    #[serde(default)]
192    pub orderindex: Option<u32>,
193}
194
195/// Partial response from GET /list/{list_id} (only statuses needed).
196#[derive(Debug, Clone, Deserialize)]
197pub struct ClickUpListInfo {
198    pub statuses: Vec<ClickUpListStatus>,
199}
200
201/// Response from POST /task/{task_id}/dependency.
202#[derive(Debug, Clone, Deserialize)]
203pub struct ClickUpDependencyResponse {
204    #[serde(default)]
205    pub dependency: Option<serde_json::Value>,
206}
207
208/// Response from POST /task/{task_id}/link/{other_task_id}.
209#[derive(Debug, Clone, Deserialize)]
210pub struct ClickUpLinkResponse {
211    #[serde(default)]
212    pub link: Option<serde_json::Value>,
213}
214
215// =============================================================================
216// Create/Update types
217// =============================================================================
218
219#[derive(Debug, Clone, Serialize)]
220pub struct CreateTaskRequest {
221    pub name: String,
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub description: Option<String>,
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub markdown_content: Option<String>,
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub parent: Option<String>,
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub status: Option<String>,
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub priority: Option<u8>,
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub tags: Option<Vec<String>>,
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub assignees: Option<Vec<u64>>,
236}
237
238#[derive(Debug, Clone, Serialize, Default)]
239pub struct UpdateTaskRequest {
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub name: Option<String>,
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub description: Option<String>,
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub markdown_content: Option<String>,
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub status: Option<String>,
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub priority: Option<u8>,
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub parent: Option<String>,
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub tags: Option<Vec<String>>,
254}
255
256#[derive(Debug, Clone, Serialize)]
257pub struct CreateCommentRequest {
258    pub comment_text: String,
259}
260
261/// Response from POST /task/{task_id}/comment.
262/// ClickUp returns a minimal response (no comment_text, id and date may be numbers).
263#[derive(Debug, Clone, Deserialize)]
264pub struct CreateCommentResponse {
265    #[serde(deserialize_with = "value_to_string")]
266    pub id: String,
267    #[serde(default, deserialize_with = "option_value_to_string")]
268    pub date: Option<String>,
269}
270
271/// Deserialize a value that may be a string or a number into String.
272fn value_to_string<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
273where
274    D: serde::Deserializer<'de>,
275{
276    let value = serde_json::Value::deserialize(deserializer)?;
277    match value {
278        serde_json::Value::String(s) => Ok(s),
279        serde_json::Value::Number(n) => Ok(n.to_string()),
280        other => Ok(other.to_string()),
281    }
282}
283
284/// Deserialize an optional value that may be a string or a number into Option<String>.
285fn option_value_to_string<'de, D>(deserializer: D) -> std::result::Result<Option<String>, D::Error>
286where
287    D: serde::Deserializer<'de>,
288{
289    let value = Option::<serde_json::Value>::deserialize(deserializer)?;
290    Ok(value.map(|v| match v {
291        serde_json::Value::String(s) => s,
292        serde_json::Value::Number(n) => n.to_string(),
293        other => other.to_string(),
294    }))
295}