use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CustomField {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub type_: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_config: Option<TypeConfig>,
pub value: CustomFieldValue,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TypeConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<DropdownOption>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_time: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DropdownOption {
pub id: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub orderindex: Option<i32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CustomFieldValue {
Text(String),
Number(f64),
Currency(i64),
Checkbox(String),
Dropdown(String),
Labels(Vec<String>),
Date(i64),
Users(Vec<u32>),
Email(String),
Phone(String),
Url(String),
Location(LocationValue),
Rating(i32),
Files(Vec<FileValue>),
Automatic(JsonValue),
Null,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LocationValue {
#[serde(skip_serializing_if = "Option::is_none")]
pub formatted_address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lat: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lng: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FileValue {
pub id: String,
pub name: String,
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mimetype: Option<String>,
}
impl CustomField {
pub fn text(id: impl Into<String>, value: impl Into<String>) -> Self {
Self {
id: id.into(),
name: None,
type_: Some("text".to_string()),
type_config: None,
value: CustomFieldValue::Text(value.into()),
}
}
pub fn number(id: impl Into<String>, value: f64) -> Self {
Self {
id: id.into(),
name: None,
type_: Some("number".to_string()),
type_config: None,
value: CustomFieldValue::Number(value),
}
}
pub fn checkbox(id: impl Into<String>, checked: bool) -> Self {
Self {
id: id.into(),
name: None,
type_: Some("checkbox".to_string()),
type_config: None,
value: CustomFieldValue::Checkbox(if checked {
"true".to_string()
} else {
"false".to_string()
}),
}
}
pub fn dropdown(id: impl Into<String>, option_id: impl Into<String>) -> Self {
Self {
id: id.into(),
name: None,
type_: Some("drop_down".to_string()),
type_config: None,
value: CustomFieldValue::Dropdown(option_id.into()),
}
}
pub fn labels(id: impl Into<String>, option_ids: Vec<String>) -> Self {
Self {
id: id.into(),
name: None,
type_: Some("labels".to_string()),
type_config: None,
value: CustomFieldValue::Labels(option_ids),
}
}
pub fn date(id: impl Into<String>, timestamp_ms: i64) -> Self {
Self {
id: id.into(),
name: None,
type_: Some("date".to_string()),
type_config: None,
value: CustomFieldValue::Date(timestamp_ms),
}
}
pub fn users(id: impl Into<String>, user_ids: Vec<u32>) -> Self {
Self {
id: id.into(),
name: None,
type_: Some("users".to_string()),
type_config: None,
value: CustomFieldValue::Users(user_ids),
}
}
pub fn email(id: impl Into<String>, email: impl Into<String>) -> Self {
Self {
id: id.into(),
name: None,
type_: Some("email".to_string()),
type_config: None,
value: CustomFieldValue::Email(email.into()),
}
}
pub fn url(id: impl Into<String>, url: impl Into<String>) -> Self {
Self {
id: id.into(),
name: None,
type_: Some("url".to_string()),
type_config: None,
value: CustomFieldValue::Url(url.into()),
}
}
pub fn phone(id: impl Into<String>, phone: impl Into<String>) -> Self {
Self {
id: id.into(),
name: None,
type_: Some("phone".to_string()),
type_config: None,
value: CustomFieldValue::Phone(phone.into()),
}
}
pub fn currency(id: impl Into<String>, cents: i64) -> Self {
Self {
id: id.into(),
name: None,
type_: Some("currency".to_string()),
type_config: None,
value: CustomFieldValue::Currency(cents),
}
}
pub fn rating(id: impl Into<String>, rating: i32) -> Self {
Self {
id: id.into(),
name: None,
type_: Some("rating".to_string()),
type_config: None,
value: CustomFieldValue::Rating(rating.clamp(0, 5)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_checkbox_uses_string() {
let field_true = CustomField::checkbox("test-id", true);
match field_true.value {
CustomFieldValue::Checkbox(ref s) => assert_eq!(s, "true"),
_ => panic!("Expected Checkbox variant"),
}
let field_false = CustomField::checkbox("test-id", false);
match field_false.value {
CustomFieldValue::Checkbox(ref s) => assert_eq!(s, "false"),
_ => panic!("Expected Checkbox variant"),
}
}
#[test]
fn test_date_uses_milliseconds() {
let field = CustomField::date("test-id", 1672531200000);
match field.value {
CustomFieldValue::Date(ms) => assert_eq!(ms, 1672531200000),
_ => panic!("Expected Date variant"),
}
}
#[test]
fn test_rating_clamps() {
let field = CustomField::rating("test-id", 10);
match field.value {
CustomFieldValue::Rating(r) => assert_eq!(r, 5), _ => panic!("Expected Rating variant"),
}
}
#[test]
fn test_custom_field_constructors() {
let text = CustomField::text("1", "hello");
assert_eq!(text.id, "1");
assert!(matches!(text.value, CustomFieldValue::Text(_)));
let number = CustomField::number("2", 42.5);
assert!(matches!(number.value, CustomFieldValue::Number(_)));
let dropdown = CustomField::dropdown("3", "option-1");
assert!(matches!(dropdown.value, CustomFieldValue::Dropdown(_)));
let labels = CustomField::labels("4", vec!["opt1".to_string(), "opt2".to_string()]);
assert!(matches!(labels.value, CustomFieldValue::Labels(_)));
}
}