use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClickUpMetadata {
#[serde(default)]
pub statuses: Vec<ClickUpStatus>,
#[serde(default, alias = "customFields")]
pub custom_fields: Vec<ClickUpCustomField>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClickUpStatus {
pub name: String,
#[serde(default)]
pub r#type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClickUpCustomField {
pub id: String,
pub name: String,
#[serde(alias = "type")]
pub field_type: ClickUpFieldType,
#[serde(default)]
pub required: bool,
#[serde(default)]
pub options: Vec<ClickUpFieldOption>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ClickUpFieldType {
#[serde(alias = "drop_down")]
Dropdown,
Labels,
Number,
Currency,
Checkbox,
Date,
Text,
Email,
Url,
Phone,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClickUpFieldOption {
pub id: String,
pub name: String,
#[serde(default)]
pub orderindex: Option<u32>,
}
impl ClickUpCustomField {
pub fn transform_value(&self, value: &serde_json::Value) -> serde_json::Value {
match self.field_type {
ClickUpFieldType::Dropdown => {
if let Some(name) = value.as_str() {
for (idx, opt) in self.options.iter().enumerate() {
if opt.name.eq_ignore_ascii_case(name) {
return serde_json::json!(opt.orderindex.unwrap_or(idx as u32));
}
}
}
value.clone()
}
ClickUpFieldType::Labels => {
if let Some(names) = value.as_array() {
let ids: Vec<serde_json::Value> = names
.iter()
.filter_map(|n| {
let name = n.as_str()?;
self.options
.iter()
.find(|o| o.name.eq_ignore_ascii_case(name))
.map(|o| serde_json::json!(o.id))
})
.collect();
return serde_json::json!(ids);
}
value.clone()
}
ClickUpFieldType::Checkbox => {
if let Some(b) = value.as_bool() {
serde_json::json!(b)
} else if let Some(s) = value.as_str() {
serde_json::json!(s == "true" || s == "1")
} else {
value.clone()
}
}
_ => value.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn sample_dropdown_field() -> ClickUpCustomField {
ClickUpCustomField {
id: "uuid-1".into(),
name: "Risk Level".into(),
field_type: ClickUpFieldType::Dropdown,
required: false,
options: vec![
ClickUpFieldOption {
id: "opt-1".into(),
name: "Low".into(),
orderindex: Some(0),
},
ClickUpFieldOption {
id: "opt-2".into(),
name: "Medium".into(),
orderindex: Some(1),
},
ClickUpFieldOption {
id: "opt-3".into(),
name: "High".into(),
orderindex: Some(2),
},
],
}
}
fn sample_labels_field() -> ClickUpCustomField {
ClickUpCustomField {
id: "uuid-2".into(),
name: "Tags".into(),
field_type: ClickUpFieldType::Labels,
required: false,
options: vec![
ClickUpFieldOption {
id: "label-1".into(),
name: "Frontend".into(),
orderindex: None,
},
ClickUpFieldOption {
id: "label-2".into(),
name: "Backend".into(),
orderindex: None,
},
],
}
}
#[test]
fn test_dropdown_transform_by_name() {
let field = sample_dropdown_field();
assert_eq!(field.transform_value(&json!("Medium")), json!(1));
assert_eq!(field.transform_value(&json!("High")), json!(2));
}
#[test]
fn test_dropdown_case_insensitive() {
let field = sample_dropdown_field();
assert_eq!(field.transform_value(&json!("low")), json!(0));
}
#[test]
fn test_labels_transform_names_to_ids() {
let field = sample_labels_field();
let result = field.transform_value(&json!(["Frontend", "Backend"]));
assert_eq!(result, json!(["label-1", "label-2"]));
}
#[test]
fn test_checkbox_transform() {
let field = ClickUpCustomField {
id: "uuid-3".into(),
name: "Done".into(),
field_type: ClickUpFieldType::Checkbox,
required: false,
options: vec![],
};
assert_eq!(field.transform_value(&json!(true)), json!(true));
assert_eq!(field.transform_value(&json!("true")), json!(true));
assert_eq!(field.transform_value(&json!("false")), json!(false));
}
#[test]
fn test_number_passthrough() {
let field = ClickUpCustomField {
id: "uuid-4".into(),
name: "Story Points".into(),
field_type: ClickUpFieldType::Number,
required: false,
options: vec![],
};
assert_eq!(field.transform_value(&json!(5)), json!(5));
}
#[test]
fn test_metadata_deserialization() {
let json = json!({
"statuses": [
{ "name": "To Do", "type": "open" },
{ "name": "In Progress", "type": "custom" },
{ "name": "Done", "type": "closed" },
],
"custom_fields": [
{
"id": "uuid-1",
"name": "Story Points",
"field_type": "number",
"required": false,
"options": []
}
]
});
let meta: ClickUpMetadata = serde_json::from_value(json).unwrap();
assert_eq!(meta.statuses.len(), 3);
assert_eq!(meta.custom_fields.len(), 1);
assert_eq!(meta.custom_fields[0].field_type, ClickUpFieldType::Number);
}
}