1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ClickUpMetadata {
11 #[serde(default)]
12 pub statuses: Vec<ClickUpStatus>,
13 #[serde(default, alias = "customFields")]
15 pub custom_fields: Vec<ClickUpCustomField>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ClickUpStatus {
20 pub name: String,
21 #[serde(default)]
23 pub r#type: Option<String>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ClickUpCustomField {
29 pub id: String,
30 pub name: String,
32 #[serde(alias = "type")]
33 pub field_type: ClickUpFieldType,
34 #[serde(default)]
36 pub required: bool,
37 #[serde(default)]
39 pub options: Vec<ClickUpFieldOption>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
44#[serde(rename_all = "snake_case")]
45pub enum ClickUpFieldType {
46 #[serde(alias = "drop_down")]
48 Dropdown,
49 Labels,
51 Number,
53 Currency,
55 Checkbox,
57 Date,
59 Text,
61 Email,
63 Url,
65 Phone,
67 #[serde(other)]
69 Unknown,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ClickUpFieldOption {
75 pub id: String,
76 pub name: String,
78 #[serde(default)]
80 pub orderindex: Option<u32>,
81}
82
83impl ClickUpCustomField {
84 pub fn transform_value(&self, value: &serde_json::Value) -> serde_json::Value {
90 match self.field_type {
91 ClickUpFieldType::Dropdown => {
92 if let Some(name) = value.as_str() {
93 for (idx, opt) in self.options.iter().enumerate() {
95 if opt.name.eq_ignore_ascii_case(name) {
96 return serde_json::json!(opt.orderindex.unwrap_or(idx as u32));
97 }
98 }
99 }
100 value.clone()
101 }
102 ClickUpFieldType::Labels => {
103 if let Some(names) = value.as_array() {
104 let ids: Vec<serde_json::Value> = names
105 .iter()
106 .filter_map(|n| {
107 let name = n.as_str()?;
108 self.options
109 .iter()
110 .find(|o| o.name.eq_ignore_ascii_case(name))
111 .map(|o| serde_json::json!(o.id))
112 })
113 .collect();
114 return serde_json::json!(ids);
115 }
116 value.clone()
117 }
118 ClickUpFieldType::Checkbox => {
119 if let Some(b) = value.as_bool() {
121 serde_json::json!(b)
122 } else if let Some(s) = value.as_str() {
123 serde_json::json!(s == "true" || s == "1")
124 } else {
125 value.clone()
126 }
127 }
128 _ => value.clone(),
130 }
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use serde_json::json;
138
139 fn sample_dropdown_field() -> ClickUpCustomField {
140 ClickUpCustomField {
141 id: "uuid-1".into(),
142 name: "Risk Level".into(),
143 field_type: ClickUpFieldType::Dropdown,
144 required: false,
145 options: vec![
146 ClickUpFieldOption {
147 id: "opt-1".into(),
148 name: "Low".into(),
149 orderindex: Some(0),
150 },
151 ClickUpFieldOption {
152 id: "opt-2".into(),
153 name: "Medium".into(),
154 orderindex: Some(1),
155 },
156 ClickUpFieldOption {
157 id: "opt-3".into(),
158 name: "High".into(),
159 orderindex: Some(2),
160 },
161 ],
162 }
163 }
164
165 fn sample_labels_field() -> ClickUpCustomField {
166 ClickUpCustomField {
167 id: "uuid-2".into(),
168 name: "Tags".into(),
169 field_type: ClickUpFieldType::Labels,
170 required: false,
171 options: vec![
172 ClickUpFieldOption {
173 id: "label-1".into(),
174 name: "Frontend".into(),
175 orderindex: None,
176 },
177 ClickUpFieldOption {
178 id: "label-2".into(),
179 name: "Backend".into(),
180 orderindex: None,
181 },
182 ],
183 }
184 }
185
186 #[test]
187 fn test_dropdown_transform_by_name() {
188 let field = sample_dropdown_field();
189 assert_eq!(field.transform_value(&json!("Medium")), json!(1));
190 assert_eq!(field.transform_value(&json!("High")), json!(2));
191 }
192
193 #[test]
194 fn test_dropdown_case_insensitive() {
195 let field = sample_dropdown_field();
196 assert_eq!(field.transform_value(&json!("low")), json!(0));
197 }
198
199 #[test]
200 fn test_labels_transform_names_to_ids() {
201 let field = sample_labels_field();
202 let result = field.transform_value(&json!(["Frontend", "Backend"]));
203 assert_eq!(result, json!(["label-1", "label-2"]));
204 }
205
206 #[test]
207 fn test_checkbox_transform() {
208 let field = ClickUpCustomField {
209 id: "uuid-3".into(),
210 name: "Done".into(),
211 field_type: ClickUpFieldType::Checkbox,
212 required: false,
213 options: vec![],
214 };
215 assert_eq!(field.transform_value(&json!(true)), json!(true));
216 assert_eq!(field.transform_value(&json!("true")), json!(true));
217 assert_eq!(field.transform_value(&json!("false")), json!(false));
218 }
219
220 #[test]
221 fn test_number_passthrough() {
222 let field = ClickUpCustomField {
223 id: "uuid-4".into(),
224 name: "Story Points".into(),
225 field_type: ClickUpFieldType::Number,
226 required: false,
227 options: vec![],
228 };
229 assert_eq!(field.transform_value(&json!(5)), json!(5));
230 }
231
232 #[test]
233 fn test_metadata_deserialization() {
234 let json = json!({
235 "statuses": [
236 { "name": "To Do", "type": "open" },
237 { "name": "In Progress", "type": "custom" },
238 { "name": "Done", "type": "closed" },
239 ],
240 "custom_fields": [
241 {
242 "id": "uuid-1",
243 "name": "Story Points",
244 "field_type": "number",
245 "required": false,
246 "options": []
247 }
248 ]
249 });
250 let meta: ClickUpMetadata = serde_json::from_value(json).unwrap();
251 assert_eq!(meta.statuses.len(), 3);
252 assert_eq!(meta.custom_fields.len(), 1);
253 assert_eq!(meta.custom_fields[0].field_type, ClickUpFieldType::Number);
254 }
255}