use jiff::tz::TimeZone;
use super::{ControlKind, Field, text::Kind};
use crate::{
DeclarationErrorKind, FieldRuleFault, Text,
schema::MAX_REPEATER_ROWS,
text,
values::{FieldError, FormValue, is_email, is_web_url},
};
#[derive(Clone, Copy, Default)]
pub(crate) struct Items {
pub(crate) min: Option<usize>,
pub(crate) max: Option<usize>,
}
impl Items {
fn is_set(self) -> bool {
self.min.is_some() || self.max.is_some()
}
fn refusal(self, label: &Text, count: usize, items: bool) -> Option<Text> {
let (fewest, most) = if items {
("tablo-field-min-items", "tablo-field-max-items")
} else {
("tablo-field-min-options", "tablo-field-max-options")
};
if let Some(min) = self.min
&& count < min
{
return Some(text!(fewest, label = label, count = min));
}
if let Some(max) = self.max
&& count > max
{
return Some(text!(most, label = label, count = max));
}
None
}
fn empty_range(self, most: Option<usize>) -> Option<FieldRuleFault> {
let most = [self.max, most].into_iter().flatten().min()?;
self.min
.is_some_and(|min| min > most)
.then_some(FieldRuleFault::EmptyRange)
}
}
impl Field {
fn blank(&self) -> Option<FormValue> {
match &self.control {
_ if self.checkbox => Some(FormValue::Text("false".to_string())),
ControlKind::Choice(choice) if choice.is_multiple() => {
Some(FormValue::List(Vec::new()))
}
ControlKind::Repeater(_) => Some(FormValue::Rows(Vec::new())),
_ => None,
}
}
pub(crate) fn check(&self, value: &FormValue, zone: &TimeZone) -> Option<FieldError> {
let label = self.label();
let message = match (&self.control, value) {
(_, FormValue::Text(posted)) if self.accepted && posted.trim() != "true" => {
text!("tablo-field-accepted", label = &label)
}
(ControlKind::Choice(choice), FormValue::List(chosen)) => {
choice.bounds.refusal(&label, chosen.len(), false)?
}
(ControlKind::Repeater(repeater), FormValue::Rows(rows)) => {
repeater.bounds.refusal(&label, rows.len(), true)?
}
(control, FormValue::Text(posted)) => {
let value = posted.trim();
if value.is_empty() {
return None;
}
match control {
ControlKind::Text(text) if text.kind == Kind::Email && !is_email(value) => {
text!("tablo-field-email", label = &label)
}
ControlKind::Text(text) if text.kind == Kind::Url && !is_web_url(value) => {
text!("tablo-field-url", label = &label)
}
ControlKind::Text(text) => text.refusal(&label, value, zone)?,
#[cfg(feature = "rich-text")]
ControlKind::RichText(rich) => rich.refusal(&label, value)?,
_ => return None,
}
}
_ => return None,
};
Some(FieldError::invalid(self.name(), message))
}
pub(crate) fn check_unposted(&self, zone: &TimeZone) -> Option<FieldError> {
self.check(&self.blank()?, zone)
}
pub(crate) fn rule_errors(&self, field: &str) -> Vec<DeclarationErrorKind> {
let mut faults = match &self.control {
ControlKind::Text(text) => text.rule_faults(),
#[cfg(feature = "rich-text")]
ControlKind::RichText(rich) => rich.rule_faults(),
ControlKind::File(file) => file.rule_faults(),
ControlKind::Choice(choice) if !choice.is_multiple() => choice
.bounds
.is_set()
.then_some(FieldRuleFault::SingleValue)
.into_iter()
.collect(),
ControlKind::Choice(choice) => {
let offered = (!choice.is_relationship()).then_some(choice.options.len());
match choice.bounds.min {
Some(min) if offered.is_some_and(|offered| min > offered) => {
vec![FieldRuleFault::FewerOptions(choice.options.len())]
}
_ => choice.bounds.empty_range(None).into_iter().collect(),
}
}
ControlKind::Repeater(repeater) => match repeater.bounds.max {
Some(max) if max > MAX_REPEATER_ROWS => vec![FieldRuleFault::AboveRowCap(max)],
_ => repeater
.bounds
.empty_range(Some(MAX_REPEATER_ROWS))
.into_iter()
.collect(),
},
_ => Vec::new(),
};
if self.accepted && !self.checkbox {
faults.push(FieldRuleFault::NotCheckbox);
}
let utc = TimeZone::UTC;
match self.default_value() {
Some(value) => {
let spelled = match value {
FormValue::Text(text) => text.trim().to_string(),
FormValue::List(items) => format!("{items:?}"),
FormValue::Rows(_) => "rows".to_string(),
};
let unparsed = match &self.control {
ControlKind::Text(text) => !spelled.is_empty() && !text.parses(&spelled),
_ => false,
};
if unparsed {
faults.push(FieldRuleFault::UnparsedDefault(spelled));
} else if self.check(value, &utc).is_some() {
faults.push(FieldRuleFault::RefusedDefault(spelled));
}
}
None if self.is_always_disabled() && self.check_unposted(&utc).is_some() => {
faults.push(FieldRuleFault::RefusedBlank);
}
None => {}
}
faults
.into_iter()
.map(|fault| DeclarationErrorKind::FieldRule {
field: field.to_string(),
fault,
})
.collect()
}
}