use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "config", rename_all = "snake_case")]
pub enum FieldType {
Text,
TextArea,
Number {
#[serde(default)]
min: Option<f64>,
#[serde(default)]
max: Option<f64>,
},
Boolean,
DateTime,
Select {
options: Vec<String>,
#[serde(default)]
allow_multiple: bool,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
#[serde(try_from = "FieldBuilder")]
pub struct FieldDefinition {
#[validate(length(min = 1, max = 64))]
id: String,
#[validate(length(min = 1, max = 100))]
label: String,
field_type: FieldType,
required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
}
impl FieldDefinition {
pub fn id(&self) -> &str {
&self.id
}
pub fn label(&self) -> &str {
&self.label
}
pub fn field_type(&self) -> &FieldType {
&self.field_type
}
pub fn is_required(&self) -> bool {
self.required
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
}
impl TryFrom<FieldBuilder> for FieldDefinition {
type Error = validator::ValidationErrors;
fn try_from(builder: FieldBuilder) -> Result<Self, Self::Error> {
let def = FieldDefinition {
id: builder.id,
label: builder.label,
field_type: builder.field_type,
required: builder.required,
description: builder.description,
};
def.validate()?;
Ok(def)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldBuilder {
id: String,
label: String,
field_type: FieldType,
#[serde(default)]
required: bool,
description: Option<String>,
}
impl FieldBuilder {
pub fn new(id: &str, label: &str, field_type: FieldType) -> Self {
Self {
id: id.to_string(),
label: label.to_string(),
field_type,
required: false,
description: None,
}
}
pub fn required(mut self, is_required: bool) -> Self {
self.required = is_required;
self
}
pub fn with_description(mut self, description: &str) -> Self {
self.description = Some(description.to_string());
self
}
pub fn build(self) -> Result<FieldDefinition, validator::ValidationErrors> {
FieldDefinition::try_from(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_field_builder() {
let field = FieldBuilder::new("test_id", "Test Label", FieldType::Text)
.required(true)
.with_description("A test field")
.build()
.expect("Field builder should produce a valid FieldDefinition here.");
assert_eq!(field.id, "test_id");
assert_eq!(field.required, true);
assert_eq!(field.description, Some("A test field".to_string()));
assert!(matches!(field.field_type, FieldType::Text));
}
#[test]
fn test_serialization_text() {
let field_type = FieldType::Text;
let json = serde_json::to_value(&field_type).unwrap();
assert_eq!(json, json!({ "kind": "text" }));
}
#[test]
fn test_serialization_number_config() {
let field_type = FieldType::Number {
min: Some(0.0),
max: Some(100.0),
};
let json = serde_json::to_value(&field_type).unwrap();
assert_eq!(
json,
json!({
"kind": "number",
"config": {
"min": 0.0,
"max": 100.0
}
})
);
}
#[test]
fn test_deserialization_select() {
let json_input = json!({
"id": "status",
"label": "Status",
"required": true,
"field_type": {
"kind": "select",
"config": {
"options": ["Open", "Closed"],
"allow_multiple": true
}
}
});
let field: FieldDefinition = serde_json::from_value(json_input).unwrap();
assert_eq!(field.id, "status");
match field.field_type {
FieldType::Select {
options,
allow_multiple,
} => {
assert_eq!(options, vec!["Open", "Closed"]);
assert_eq!(allow_multiple, true);
}
_ => panic!("Wrong field type deserialized"),
}
}
#[test]
fn test_deserialization_defaults() {
let json_input = json!({
"id": "score",
"label": "Score",
"field_type": {
"kind": "number",
"config": {}
}
});
let field: FieldDefinition =
serde_json::from_value(json_input).expect("Field definition should be valid");
assert_eq!(field.required, false);
match field.field_type {
FieldType::Number { min, max } => {
assert_eq!(min, None);
assert_eq!(max, None);
}
_ => panic!("Wrong field type"),
}
}
#[test]
fn test_serde_validation_integration() {
let valid_json = json!({
"id": "short_id",
"label": "Valid Label",
"field_type": { "kind": "text" }
});
let result: Result<FieldDefinition, _> = serde_json::from_value(valid_json);
assert!(result.is_ok());
let long_id =
"this_id_is_way_too_long_to_pass_validation_rules_that_we_set_at_sixty_four_characters";
let invalid_json = json!({
"id": long_id,
"label": "Invalid ID",
"field_type": { "kind": "text" }
});
let result: Result<FieldDefinition, _> = serde_json::from_value(invalid_json);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.is_data());
let err_string = err.to_string();
assert!(err_string.contains("length"));
let long_label = "a".repeat(101);
let invalid_json = json!({
"id": "this_id_is_an_ok_length",
"label": long_label,
"field_type": { "kind": "text" }
});
let result: Result<FieldDefinition, _> = serde_json::from_value(invalid_json);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.is_data());
let err_string = err.to_string();
assert!(err_string.contains("length"));
}
}