use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::ExtensionBlock;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "fieldType", rename_all = "camelCase")]
pub enum FormField {
TextInput(TextInputField),
TextArea(TextAreaField),
Checkbox(CheckboxField),
RadioGroup(RadioGroupField),
Dropdown(DropdownField),
DatePicker(DatePickerField),
Signature(SignatureField),
}
impl FormField {
#[must_use]
pub fn from_extension(ext: &ExtensionBlock) -> Option<Self> {
if ext.namespace != "forms" {
return None;
}
match ext.block_type.as_str() {
"textInput" => serde_json::from_value(ext.attributes.clone())
.ok()
.map(FormField::TextInput),
"textArea" => serde_json::from_value(ext.attributes.clone())
.ok()
.map(FormField::TextArea),
"checkbox" => serde_json::from_value(ext.attributes.clone())
.ok()
.map(FormField::Checkbox),
"radioGroup" => serde_json::from_value(ext.attributes.clone())
.ok()
.map(FormField::RadioGroup),
"dropdown" => serde_json::from_value(ext.attributes.clone())
.ok()
.map(FormField::Dropdown),
"datePicker" => serde_json::from_value(ext.attributes.clone())
.ok()
.map(FormField::DatePicker),
"signature" => serde_json::from_value(ext.attributes.clone())
.ok()
.map(FormField::Signature),
_ => None,
}
}
#[must_use]
pub fn id(&self) -> Option<&str> {
match self {
Self::TextInput(f) => f.id.as_deref(),
Self::TextArea(f) => f.id.as_deref(),
Self::Checkbox(f) => f.id.as_deref(),
Self::RadioGroup(f) => f.id.as_deref(),
Self::Dropdown(f) => f.id.as_deref(),
Self::DatePicker(f) => f.id.as_deref(),
Self::Signature(f) => f.id.as_deref(),
}
}
#[must_use]
pub fn label(&self) -> &str {
match self {
Self::TextInput(f) => &f.label,
Self::TextArea(f) => &f.label,
Self::Checkbox(f) => &f.label,
Self::RadioGroup(f) => &f.label,
Self::Dropdown(f) => &f.label,
Self::DatePicker(f) => &f.label,
Self::Signature(f) => &f.label,
}
}
#[must_use]
pub fn is_required(&self) -> bool {
match self {
Self::TextInput(f) => f.required,
Self::TextArea(f) => f.required,
Self::Checkbox(f) => f.required,
Self::RadioGroup(f) => f.required,
Self::Dropdown(f) => f.required,
Self::DatePicker(f) => f.required,
Self::Signature(f) => f.required,
}
}
#[must_use]
pub fn validation(&self) -> Option<&FormValidation> {
match self {
Self::TextInput(f) => f.validation.as_ref(),
Self::TextArea(f) => f.validation.as_ref(),
Self::DatePicker(f) => f.validation.as_ref(),
Self::Checkbox(_) | Self::RadioGroup(_) | Self::Dropdown(_) | Self::Signature(_) => {
None
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextInputField {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_value: Option<String>,
#[serde(default)]
pub required: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub readonly: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub validation: Option<FormValidation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conditional_validation: Option<ConditionalValidation>,
}
impl TextInputField {
#[must_use]
pub fn new(label: impl Into<String>) -> Self {
Self {
id: None,
label: label.into(),
placeholder: None,
default_value: None,
required: false,
readonly: false,
input_type: None,
validation: None,
conditional_validation: None,
}
}
#[must_use]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = Some(placeholder.into());
self
}
#[must_use]
pub fn with_default(mut self, value: impl Into<String>) -> Self {
self.default_value = Some(value.into());
self
}
#[must_use]
pub const fn required(mut self) -> Self {
self.required = true;
self
}
#[must_use]
pub const fn readonly(mut self) -> Self {
self.readonly = true;
self
}
#[must_use]
pub fn with_input_type(mut self, input_type: impl Into<String>) -> Self {
self.input_type = Some(input_type.into());
self
}
#[must_use]
pub fn with_validation(mut self, validation: FormValidation) -> Self {
self.validation = Some(validation);
self
}
#[must_use]
pub fn with_conditional_validation(mut self, cv: ConditionalValidation) -> Self {
self.conditional_validation = Some(cv);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextAreaField {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_value: Option<String>,
#[serde(default)]
pub required: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub readonly: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rows: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_length: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub validation: Option<FormValidation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conditional_validation: Option<ConditionalValidation>,
}
impl TextAreaField {
#[must_use]
pub fn new(label: impl Into<String>) -> Self {
Self {
id: None,
label: label.into(),
placeholder: None,
default_value: None,
required: false,
readonly: false,
rows: None,
max_length: None,
validation: None,
conditional_validation: None,
}
}
#[must_use]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub const fn with_rows(mut self, rows: u32) -> Self {
self.rows = Some(rows);
self
}
#[must_use]
pub const fn with_max_length(mut self, max_length: usize) -> Self {
self.max_length = Some(max_length);
self
}
#[must_use]
pub const fn required(mut self) -> Self {
self.required = true;
self
}
#[must_use]
pub fn with_conditional_validation(mut self, cv: ConditionalValidation) -> Self {
self.conditional_validation = Some(cv);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckboxField {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub label: String,
#[serde(default)]
pub default_checked: bool,
#[serde(default)]
pub required: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub readonly: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conditional_validation: Option<ConditionalValidation>,
}
impl CheckboxField {
#[must_use]
pub fn new(label: impl Into<String>) -> Self {
Self {
id: None,
label: label.into(),
default_checked: false,
required: false,
readonly: false,
conditional_validation: None,
}
}
#[must_use]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub const fn checked(mut self) -> Self {
self.default_checked = true;
self
}
#[must_use]
pub const fn required(mut self) -> Self {
self.required = true;
self
}
#[must_use]
pub fn with_conditional_validation(mut self, cv: ConditionalValidation) -> Self {
self.conditional_validation = Some(cv);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RadioOption {
pub value: String,
pub label: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub disabled: bool,
}
impl RadioOption {
#[must_use]
pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
Self {
value: value.into(),
label: label.into(),
disabled: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RadioGroupField {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub label: String,
pub options: Vec<RadioOption>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_value: Option<String>,
#[serde(default)]
pub required: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub readonly: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conditional_validation: Option<ConditionalValidation>,
}
impl RadioGroupField {
#[must_use]
pub fn new(label: impl Into<String>, options: Vec<RadioOption>) -> Self {
Self {
id: None,
label: label.into(),
options,
default_value: None,
required: false,
readonly: false,
conditional_validation: None,
}
}
#[must_use]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub fn with_default(mut self, value: impl Into<String>) -> Self {
self.default_value = Some(value.into());
self
}
#[must_use]
pub const fn required(mut self) -> Self {
self.required = true;
self
}
#[must_use]
pub fn with_conditional_validation(mut self, cv: ConditionalValidation) -> Self {
self.conditional_validation = Some(cv);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DropdownOption {
pub value: String,
pub label: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub disabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
}
impl DropdownOption {
#[must_use]
pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
Self {
value: value.into(),
label: label.into(),
disabled: false,
group: None,
}
}
#[must_use]
pub fn with_group(mut self, group: impl Into<String>) -> Self {
self.group = Some(group.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DropdownField {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub label: String,
pub options: Vec<DropdownOption>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_value: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
#[serde(default)]
pub required: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub readonly: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub multiple: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conditional_validation: Option<ConditionalValidation>,
}
impl DropdownField {
#[must_use]
pub fn new(label: impl Into<String>, options: Vec<DropdownOption>) -> Self {
Self {
id: None,
label: label.into(),
options,
default_value: None,
placeholder: None,
required: false,
readonly: false,
multiple: false,
conditional_validation: None,
}
}
#[must_use]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = Some(placeholder.into());
self
}
#[must_use]
pub fn with_default(mut self, value: impl Into<String>) -> Self {
self.default_value = Some(value.into());
self
}
#[must_use]
pub const fn required(mut self) -> Self {
self.required = true;
self
}
#[must_use]
pub const fn multiple(mut self) -> Self {
self.multiple = true;
self
}
#[must_use]
pub fn with_conditional_validation(mut self, cv: ConditionalValidation) -> Self {
self.conditional_validation = Some(cv);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DatePickerField {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub label: String,
#[serde(default)]
pub mode: DatePickerMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_value: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max: Option<String>,
#[serde(default)]
pub required: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub readonly: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub validation: Option<FormValidation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conditional_validation: Option<ConditionalValidation>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DatePickerMode {
#[default]
Date,
Time,
Datetime,
}
impl DatePickerField {
#[must_use]
pub fn new(label: impl Into<String>) -> Self {
Self {
id: None,
label: label.into(),
mode: DatePickerMode::Date,
default_value: None,
min: None,
max: None,
required: false,
readonly: false,
validation: None,
conditional_validation: None,
}
}
#[must_use]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub const fn with_mode(mut self, mode: DatePickerMode) -> Self {
self.mode = mode;
self
}
#[must_use]
pub fn with_min(mut self, min: impl Into<String>) -> Self {
self.min = Some(min.into());
self
}
#[must_use]
pub fn with_max(mut self, max: impl Into<String>) -> Self {
self.max = Some(max.into());
self
}
#[must_use]
pub const fn required(mut self) -> Self {
self.required = true;
self
}
#[must_use]
pub fn with_conditional_validation(mut self, cv: ConditionalValidation) -> Self {
self.conditional_validation = Some(cv);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignatureField {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub label: String,
#[serde(default)]
pub required: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub readonly: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub legal_text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conditional_validation: Option<ConditionalValidation>,
}
impl SignatureField {
#[must_use]
pub fn new(label: impl Into<String>) -> Self {
Self {
id: None,
label: label.into(),
required: false,
readonly: false,
legal_text: None,
conditional_validation: None,
}
}
#[must_use]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub fn with_legal_text(mut self, text: impl Into<String>) -> Self {
self.legal_text = Some(text.into());
self
}
#[must_use]
pub const fn required(mut self) -> Self {
self.required = true;
self
}
#[must_use]
pub fn with_conditional_validation(mut self, cv: ConditionalValidation) -> Self {
self.conditional_validation = Some(cv);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FormValidation {
pub rules: Vec<ValidationRule>,
}
impl FormValidation {
#[must_use]
pub fn new(rules: Vec<ValidationRule>) -> Self {
Self { rules }
}
#[must_use]
pub fn with_rule(rule: ValidationRule) -> Self {
Self { rules: vec![rule] }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConditionalValidation {
pub when: Condition,
pub then: ConditionalAction,
}
impl ConditionalValidation {
#[must_use]
pub fn new(when: Condition, then: ConditionalAction) -> Self {
Self { when, then }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Condition {
pub field: String,
#[serde(flatten)]
pub operator: ConditionOperator,
}
impl Condition {
#[must_use]
pub fn equals(field: impl Into<String>, value: Value) -> Self {
Self {
field: field.into(),
operator: ConditionOperator {
equals: Some(value),
not_equals: None,
is_empty: None,
is_not_empty: None,
},
}
}
#[must_use]
pub fn not_equals(field: impl Into<String>, value: Value) -> Self {
Self {
field: field.into(),
operator: ConditionOperator {
equals: None,
not_equals: Some(value),
is_empty: None,
is_not_empty: None,
},
}
}
#[must_use]
pub fn is_empty(field: impl Into<String>) -> Self {
Self {
field: field.into(),
operator: ConditionOperator {
equals: None,
not_equals: None,
is_empty: Some(true),
is_not_empty: None,
},
}
}
#[must_use]
pub fn is_not_empty(field: impl Into<String>) -> Self {
Self {
field: field.into(),
operator: ConditionOperator {
equals: None,
not_equals: None,
is_empty: None,
is_not_empty: Some(true),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConditionOperator {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub equals: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub not_equals: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_empty: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_not_empty: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConditionalAction {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub validation: Option<FormValidation>,
}
impl ConditionalAction {
#[must_use]
pub fn require() -> Self {
Self {
required: Some(true),
validation: None,
}
}
#[must_use]
pub fn with_validation(validation: FormValidation) -> Self {
Self {
required: None,
validation: Some(validation),
}
}
#[must_use]
pub fn require_with_validation(validation: FormValidation) -> Self {
Self {
required: Some(true),
validation: Some(validation),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ValidationRule {
Required {
#[serde(default, skip_serializing_if = "Option::is_none")]
message: Option<String>,
},
MinLength {
value: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
message: Option<String>,
},
MaxLength {
value: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
message: Option<String>,
},
Pattern {
pattern: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
message: Option<String>,
},
Email {
#[serde(default, skip_serializing_if = "Option::is_none")]
message: Option<String>,
},
Url {
#[serde(default, skip_serializing_if = "Option::is_none")]
message: Option<String>,
},
Min {
value: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
message: Option<String>,
},
Max {
value: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
message: Option<String>,
},
ContainsUppercase {
#[serde(default, skip_serializing_if = "Option::is_none")]
message: Option<String>,
},
ContainsLowercase {
#[serde(default, skip_serializing_if = "Option::is_none")]
message: Option<String>,
},
ContainsDigit {
#[serde(default, skip_serializing_if = "Option::is_none")]
message: Option<String>,
},
ContainsSpecial {
#[serde(default, skip_serializing_if = "Option::is_none")]
message: Option<String>,
},
MatchesField {
field: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
message: Option<String>,
},
}
impl ValidationRule {
#[must_use]
pub fn required() -> Self {
Self::Required { message: None }
}
#[must_use]
pub fn min_length(value: usize) -> Self {
Self::MinLength {
value,
message: None,
}
}
#[must_use]
pub fn max_length(value: usize) -> Self {
Self::MaxLength {
value,
message: None,
}
}
#[must_use]
pub fn pattern(pattern: impl Into<String>) -> Self {
Self::Pattern {
pattern: pattern.into(),
message: None,
}
}
#[must_use]
pub fn email() -> Self {
Self::Email { message: None }
}
#[must_use]
pub fn url() -> Self {
Self::Url { message: None }
}
#[must_use]
pub fn contains_uppercase() -> Self {
Self::ContainsUppercase { message: None }
}
#[must_use]
pub fn contains_lowercase() -> Self {
Self::ContainsLowercase { message: None }
}
#[must_use]
pub fn contains_digit() -> Self {
Self::ContainsDigit { message: None }
}
#[must_use]
pub fn contains_special() -> Self {
Self::ContainsSpecial { message: None }
}
#[must_use]
pub fn matches_field(field: impl Into<String>) -> Self {
Self::MatchesField {
field: field.into(),
message: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FormData {
pub values: HashMap<String, Value>,
#[serde(default)]
pub submitted: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_modified: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub submitted_by: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub submitted_at: Option<DateTime<Utc>>,
}
impl FormData {
#[must_use]
pub fn new() -> Self {
Self {
values: HashMap::new(),
submitted: false,
last_modified: None,
submitted_by: None,
submitted_at: None,
}
}
pub fn set(&mut self, field_id: impl Into<String>, value: Value) {
self.values.insert(field_id.into(), value);
self.last_modified = Some(Utc::now());
}
#[must_use]
pub fn get(&self, field_id: &str) -> Option<&Value> {
self.values.get(field_id)
}
#[must_use]
pub fn get_string(&self, field_id: &str) -> Option<&str> {
self.values.get(field_id).and_then(Value::as_str)
}
#[must_use]
pub fn get_bool(&self, field_id: &str) -> Option<bool> {
self.values.get(field_id).and_then(Value::as_bool)
}
pub fn submit(&mut self, by: Option<String>) {
self.submitted = true;
self.submitted_by = by;
self.submitted_at = Some(Utc::now());
}
}
impl Default for FormData {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_text_input_field() {
let field = TextInputField::new("Full Name")
.with_id("name")
.with_placeholder("Enter your name")
.required();
assert_eq!(field.label, "Full Name");
assert_eq!(field.id, Some("name".to_string()));
assert!(field.required);
}
#[test]
fn test_text_input_serialization() {
let field = TextInputField::new("Email")
.with_id("email")
.with_input_type("email")
.with_validation(FormValidation::with_rule(ValidationRule::email()));
let json = serde_json::to_string_pretty(&field).unwrap();
assert!(json.contains("\"label\": \"Email\""));
assert!(json.contains("\"inputType\": \"email\""));
assert!(json.contains("\"type\": \"email\""));
}
#[test]
fn test_checkbox_field() {
let field = CheckboxField::new("I agree to the terms")
.with_id("terms")
.required();
assert_eq!(field.label, "I agree to the terms");
assert!(field.required);
assert!(!field.default_checked);
}
#[test]
fn test_radio_group() {
let options = vec![
RadioOption::new("sm", "Small"),
RadioOption::new("md", "Medium"),
RadioOption::new("lg", "Large"),
];
let field = RadioGroupField::new("Size", options)
.with_id("size")
.with_default("md");
assert_eq!(field.options.len(), 3);
assert_eq!(field.default_value, Some("md".to_string()));
}
#[test]
fn test_dropdown_with_groups() {
let options = vec![
DropdownOption::new("us", "United States").with_group("North America"),
DropdownOption::new("ca", "Canada").with_group("North America"),
DropdownOption::new("uk", "United Kingdom").with_group("Europe"),
];
let field = DropdownField::new("Country", options)
.with_placeholder("Select a country")
.required();
assert_eq!(field.options.len(), 3);
assert_eq!(field.options[0].group, Some("North America".to_string()));
}
#[test]
fn test_date_picker() {
let field = DatePickerField::new("Appointment Date")
.with_id("date")
.with_mode(DatePickerMode::Datetime)
.with_min("2024-01-01")
.required();
assert_eq!(field.mode, DatePickerMode::Datetime);
assert_eq!(field.min, Some("2024-01-01".to_string()));
}
#[test]
fn test_signature_field() {
let field = SignatureField::new("Signature")
.with_id("sig")
.with_legal_text("By signing, you agree to...")
.required();
assert!(field.required);
assert!(field.legal_text.is_some());
}
#[test]
fn test_validation_rules() {
let validation = FormValidation::new(vec![
ValidationRule::required(),
ValidationRule::min_length(2),
ValidationRule::max_length(50),
ValidationRule::pattern(r"^[A-Za-z\s]+$"),
]);
assert_eq!(validation.rules.len(), 4);
}
#[test]
fn test_form_data() {
let mut data = FormData::new();
data.set("name", json!("John Doe"));
data.set("age", json!(30));
data.set("active", json!(true));
assert_eq!(data.get_string("name"), Some("John Doe"));
assert_eq!(data.get_bool("active"), Some(true));
assert!(!data.submitted);
data.submit(Some("user@example.com".to_string()));
assert!(data.submitted);
assert_eq!(data.submitted_by, Some("user@example.com".to_string()));
}
#[test]
fn test_form_field_enum() {
let text_input = FormField::TextInput(TextInputField::new("Name").required());
assert!(text_input.is_required());
assert_eq!(text_input.label(), "Name");
}
#[test]
fn test_form_field_serialization() {
let field = FormField::TextInput(TextInputField::new("Name").with_id("name"));
let json = serde_json::to_string(&field).unwrap();
assert!(json.contains("\"fieldType\":\"textInput\""));
}
#[test]
fn test_declarative_validation_rules() {
let uppercase = ValidationRule::contains_uppercase();
let lowercase = ValidationRule::contains_lowercase();
let digit = ValidationRule::contains_digit();
let special = ValidationRule::contains_special();
let matches = ValidationRule::matches_field("password");
let json = serde_json::to_string(&uppercase).unwrap();
assert!(json.contains("\"type\":\"containsUppercase\""));
let json = serde_json::to_string(&lowercase).unwrap();
assert!(json.contains("\"type\":\"containsLowercase\""));
let json = serde_json::to_string(&digit).unwrap();
assert!(json.contains("\"type\":\"containsDigit\""));
let json = serde_json::to_string(&special).unwrap();
assert!(json.contains("\"type\":\"containsSpecial\""));
let json = serde_json::to_string(&matches).unwrap();
assert!(json.contains("\"type\":\"matchesField\""));
assert!(json.contains("\"field\":\"password\""));
}
#[test]
fn test_matches_field_with_message() {
let rule = ValidationRule::MatchesField {
field: "confirm_password".to_string(),
message: Some("Passwords must match".to_string()),
};
let json = serde_json::to_string(&rule).unwrap();
assert!(json.contains("\"field\":\"confirm_password\""));
assert!(json.contains("\"message\":\"Passwords must match\""));
let parsed: ValidationRule = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, rule);
}
#[test]
fn test_conditional_validation_equals() {
let cv = ConditionalValidation::new(
Condition::equals("contact_method", json!("email")),
ConditionalAction::require(),
);
let json = serde_json::to_string_pretty(&cv).unwrap();
assert!(json.contains("\"field\": \"contact_method\""));
assert!(json.contains("\"equals\": \"email\""));
assert!(json.contains("\"required\": true"));
let parsed: ConditionalValidation = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, cv);
}
#[test]
fn test_conditional_validation_not_equals() {
let cv = ConditionalValidation::new(
Condition::not_equals("status", json!("inactive")),
ConditionalAction::with_validation(FormValidation::with_rule(
ValidationRule::required(),
)),
);
let json = serde_json::to_string(&cv).unwrap();
assert!(json.contains("\"notEquals\":\"inactive\""));
let parsed: ConditionalValidation = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, cv);
}
#[test]
fn test_conditional_validation_is_empty() {
let cv = ConditionalValidation::new(
Condition::is_empty("other_field"),
ConditionalAction::require(),
);
let json = serde_json::to_string(&cv).unwrap();
assert!(json.contains("\"isEmpty\":true"));
let parsed: ConditionalValidation = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, cv);
}
#[test]
fn test_conditional_validation_is_not_empty() {
let cv = ConditionalValidation::new(
Condition::is_not_empty("parent_field"),
ConditionalAction::require_with_validation(FormValidation::with_rule(
ValidationRule::min_length(3),
)),
);
let json = serde_json::to_string(&cv).unwrap();
assert!(json.contains("\"isNotEmpty\":true"));
assert!(json.contains("\"required\":true"));
assert!(json.contains("\"minLength\""));
let parsed: ConditionalValidation = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, cv);
}
#[test]
fn test_field_with_conditional_validation() {
let cv = ConditionalValidation::new(
Condition::equals("contact_method", json!("email")),
ConditionalAction::require(),
);
let field = TextInputField::new("Email Address")
.with_id("email")
.with_conditional_validation(cv);
assert!(field.conditional_validation.is_some());
let json = serde_json::to_string_pretty(&field).unwrap();
assert!(json.contains("\"conditionalValidation\""));
assert!(json.contains("\"contact_method\""));
let parsed: TextInputField = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, field);
}
#[test]
fn test_backward_compat_no_conditional_validation() {
let json = r#"{
"label": "Name",
"required": true
}"#;
let field: TextInputField = serde_json::from_str(json).unwrap();
assert_eq!(field.label, "Name");
assert!(field.required);
assert!(field.conditional_validation.is_none());
}
#[test]
fn test_conditional_validation_on_checkbox() {
let cv = ConditionalValidation::new(
Condition::equals("has_address", json!(true)),
ConditionalAction::require(),
);
let field = CheckboxField::new("Confirm address").with_conditional_validation(cv);
let json = serde_json::to_string(&field).unwrap();
let parsed: CheckboxField = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, field);
}
#[test]
fn test_conditional_validation_skipped_when_none() {
let field = TextInputField::new("Name");
let json = serde_json::to_string(&field).unwrap();
assert!(!json.contains("conditionalValidation"));
}
}