use std::fmt::{Debug, Display, Formatter};
use askama::filters::HtmlSafe;
pub use cot_macros::SelectAsFormField;
pub use cot_macros::SelectChoice;
use indexmap::IndexSet;
use crate::form::fields::impl_form_field;
use crate::form::{
FormField, FormFieldOptions, FormFieldValidationError, FormFieldValue, FormFieldValueError,
};
use crate::html::HtmlTag;
macro_rules! impl_as_form_field_mult_collection {
(($($generics:tt)+) => $collection:ty, $element:ty $(where $($where_clause:tt)+)?) => {
impl<$($generics)+> crate::form::AsFormField for $collection
$(where $($where_clause)+)?
{
type Type = crate::form::fields::SelectMultipleField<$element>;
fn clean_value(
field: &Self::Type,
) -> Result<Self, crate::form::FormFieldValidationError> {
let values = crate::form::fields::check_required_multiple(field)?;
values.iter().map(|id| <$element>::from_str(id)).collect()
}
fn to_field_value(&self) -> String {
String::new()
}
}
};
(() => $collection:ty, $element:ty $(where $($where_clause:tt)+)?) => {
impl crate::form::AsFormField for $collection
$(where $($where_clause)+)?
{
type Type = crate::form::fields::SelectMultipleField<$element>;
fn clean_value(
field: &Self::Type,
) -> Result<Self, crate::form::FormFieldValidationError> {
let values = crate::form::fields::check_required_multiple(field)?;
values.iter().map(|id| <$element>::from_str(id)).collect()
}
fn to_field_value(&self) -> String {
String::new()
}
}
};
}
pub(crate) use impl_as_form_field_mult_collection;
impl_form_field!(SelectField, SelectFieldOptions, "a dropdown list", T: SelectChoice + Send);
#[derive(Debug, Clone)]
pub struct SelectFieldOptions<T> {
pub choices: Option<Vec<T>>,
pub none_option: Option<String>,
}
impl<T> Default for SelectFieldOptions<T> {
fn default() -> Self {
Self {
choices: None,
none_option: None,
}
}
}
impl<T: SelectChoice + Send> Display for SelectField<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
const DEFAULT_NONE_OPTION: &str = "—";
let value = if let Some(value) = self.value.clone() {
IndexSet::from([value])
} else {
IndexSet::new()
};
let none_option = if let Some(none_option) = &self.custom_options.none_option {
Some(none_option.as_str())
} else if self.options.required {
None
} else {
Some(DEFAULT_NONE_OPTION)
};
render_select(
f,
self,
false,
none_option,
None,
self.custom_options.choices.as_ref(),
&value,
)
}
}
impl<T: SelectChoice + Send> HtmlSafe for SelectField<T> {}
#[derive(Debug)]
pub struct SelectMultipleField<T> {
options: FormFieldOptions,
custom_options: SelectMultipleFieldOptions<T>,
value: IndexSet<String>,
}
impl<T> SelectMultipleField<T> {
pub fn values(&self) -> impl Iterator<Item = &str> {
self.value.iter().map(AsRef::as_ref)
}
}
impl<T: SelectChoice + Send> FormField for SelectMultipleField<T> {
type CustomOptions = SelectMultipleFieldOptions<T>;
fn with_options(options: FormFieldOptions, custom_options: Self::CustomOptions) -> Self {
Self {
options,
custom_options,
value: IndexSet::new(),
}
}
fn options(&self) -> &FormFieldOptions {
&self.options
}
fn value(&self) -> Option<&str> {
None
}
async fn set_value(&mut self, field: FormFieldValue<'_>) -> Result<(), FormFieldValueError> {
self.value.insert(field.into_text().await?);
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct SelectMultipleFieldOptions<T> {
pub choices: Option<Vec<T>>,
pub size: Option<u32>,
}
impl<T> Default for SelectMultipleFieldOptions<T> {
fn default() -> Self {
Self {
choices: None,
size: None,
}
}
}
impl<T: SelectChoice + Send> Display for SelectMultipleField<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
render_select(
f,
self,
true,
None,
self.custom_options.size,
self.custom_options.choices.as_ref(),
&self.value,
)
}
}
impl<T: SelectChoice + Send> HtmlSafe for SelectMultipleField<T> {}
fn render_select<T: FormField, S: SelectChoice>(
f: &mut Formatter<'_>,
field: &T,
multiple: bool,
empty_option: Option<&str>,
size: Option<u32>,
choices: Option<&Vec<S>>,
selected: &IndexSet<String>,
) -> std::fmt::Result {
let mut tag: HtmlTag = HtmlTag::new("select");
tag.attr("name", field.id());
tag.attr("id", field.id());
if multiple {
tag.bool_attr("multiple");
}
if field.options().required {
tag.bool_attr("required");
}
if let Some(size) = size {
tag.attr("size", size.to_string());
}
if let Some(empty_option) = empty_option {
tag.push_tag(
HtmlTag::new("option")
.attr("value", "")
.push_str(empty_option),
);
}
let choices = if let Some(choices) = choices {
choices
} else {
&S::default_choices()
};
for choice in choices {
let mut child = HtmlTag::new("option");
child
.attr("value", choice.id())
.push_str(choice.to_string());
if selected.contains(&choice.id()) {
child.bool_attr("selected");
}
tag.push_tag(child);
}
write!(f, "{}", tag.render())
}
pub(crate) fn check_required_multiple<T>(
field: &SelectMultipleField<T>,
) -> Result<&IndexSet<String>, FormFieldValidationError> {
if field.value.is_empty() {
Err(FormFieldValidationError::Required)
} else {
Ok(&field.value)
}
}
impl_as_form_field_mult_collection!((T: SelectChoice + Send) => ::std::vec::Vec<T>, T);
impl_as_form_field_mult_collection!(
(T: SelectChoice + Send) => ::std::collections::VecDeque<T>,
T
);
impl_as_form_field_mult_collection!(
(T: SelectChoice + Send) => ::std::collections::LinkedList<T>,
T
);
impl_as_form_field_mult_collection!(
(
T: SelectChoice + Eq + ::std::hash::Hash + Send,
S: ::std::hash::BuildHasher + Default
) => ::std::collections::HashSet<T, S>,
T
);
impl_as_form_field_mult_collection!(
(T: SelectChoice + Eq + ::std::hash::Hash + Send) => ::indexmap::IndexSet<T>,
T
);
pub trait SelectChoice {
#[must_use]
fn default_choices() -> Vec<Self>
where
Self: Sized,
{
vec![]
}
fn from_str(s: &str) -> Result<Self, FormFieldValidationError>
where
Self: Sized;
fn id(&self) -> String;
fn to_string(&self) -> String;
}
#[cfg(test)]
mod tests {
use std::collections::{HashSet, LinkedList, VecDeque};
use indexmap::IndexSet;
use super::*;
use crate::form::AsFormField;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum TestChoice {
Option1,
Option2,
Option3,
}
impl SelectChoice for TestChoice {
fn default_choices() -> Vec<Self> {
vec![Self::Option1, Self::Option2, Self::Option3]
}
fn from_str(s: &str) -> Result<Self, FormFieldValidationError> {
match s {
"opt1" => Ok(Self::Option1),
"opt2" => Ok(Self::Option2),
"opt3" => Ok(Self::Option3),
_ => Err(FormFieldValidationError::invalid_value(s.to_owned())),
}
}
fn id(&self) -> String {
match self {
Self::Option1 => "opt1".to_string(),
Self::Option2 => "opt2".to_string(),
Self::Option3 => "opt3".to_string(),
}
}
fn to_string(&self) -> String {
match self {
Self::Option1 => "Option 1".to_string(),
Self::Option2 => "Option 2".to_string(),
Self::Option3 => "Option 3".to_string(),
}
}
}
#[test]
fn select_field_render_default() {
let field = SelectField::<TestChoice>::with_options(
FormFieldOptions {
id: "test_select".to_owned(),
name: "test_select".to_owned(),
required: false,
},
SelectFieldOptions::default(),
);
let html = field.to_string();
assert!(html.contains("<select"));
assert!(html.contains("name=\"test_select\""));
assert!(html.contains("id=\"test_select\""));
assert!(!html.contains("required"));
assert!(html.contains("—")); assert!(html.contains("Option 1"));
assert!(html.contains("Option 2"));
assert!(html.contains("Option 3"));
assert!(html.contains("value=\"opt1\""));
assert!(html.contains("value=\"opt2\""));
assert!(html.contains("value=\"opt3\""));
}
#[test]
fn select_field_render_required() {
let field = SelectField::<TestChoice>::with_options(
FormFieldOptions {
id: "test_select".to_owned(),
name: "test_select".to_owned(),
required: true,
},
SelectFieldOptions::default(),
);
let html = field.to_string();
assert!(html.contains("required"));
assert!(!html.contains("—")); }
#[test]
fn select_field_render_custom_none_option() {
let field = SelectField::<TestChoice>::with_options(
FormFieldOptions {
id: "test_select".to_owned(),
name: "test_select".to_owned(),
required: false,
},
SelectFieldOptions {
choices: None,
none_option: Some("Please select...".to_string()),
},
);
let html = field.to_string();
assert!(html.contains("Please select..."));
assert!(!html.contains("—"));
}
#[test]
fn select_field_render_custom_choices() {
let field = SelectField::<TestChoice>::with_options(
FormFieldOptions {
id: "test_select".to_owned(),
name: "test_select".to_owned(),
required: false,
},
SelectFieldOptions {
choices: Some(vec![TestChoice::Option1, TestChoice::Option3]),
none_option: None,
},
);
let html = field.to_string();
assert!(html.contains("Option 1"));
assert!(!html.contains("Option 2")); assert!(html.contains("Option 3"));
}
#[cot::test]
async fn select_field_with_value() {
let mut field = SelectField::<TestChoice>::with_options(
FormFieldOptions {
id: "test_select".to_owned(),
name: "test_select".to_owned(),
required: false,
},
SelectFieldOptions::default(),
);
field
.set_value(FormFieldValue::new_text("opt2"))
.await
.unwrap();
let html = field.to_string();
assert!(html.contains("<option value=\"opt2\" selected>Option 2</option>"));
}
#[test]
fn select_multiple_field_render_default() {
let field = SelectMultipleField::<TestChoice>::with_options(
FormFieldOptions {
id: "test_multi".to_owned(),
name: "test_multi".to_owned(),
required: false,
},
SelectMultipleFieldOptions::default(),
);
let html = field.to_string();
assert!(html.contains("<select"));
assert!(html.contains("multiple"));
assert!(html.contains("name=\"test_multi\""));
assert!(html.contains("id=\"test_multi\""));
assert!(!html.contains("required"));
assert!(html.contains("Option 1"));
assert!(html.contains("Option 2"));
assert!(html.contains("Option 3"));
}
#[test]
fn select_multiple_field_render_with_size() {
let field = SelectMultipleField::<TestChoice>::with_options(
FormFieldOptions {
id: "test_multi".to_owned(),
name: "test_multi".to_owned(),
required: false,
},
SelectMultipleFieldOptions {
choices: None,
size: Some(5),
},
);
let html = field.to_string();
assert!(html.contains("size=\"5\""));
}
#[test]
fn select_multiple_field_render_required() {
let field = SelectMultipleField::<TestChoice>::with_options(
FormFieldOptions {
id: "test_multi".to_owned(),
name: "test_multi".to_owned(),
required: true,
},
SelectMultipleFieldOptions::default(),
);
let html = field.to_string();
assert!(html.contains("required"));
}
#[cot::test]
async fn select_multiple_field_with_values() {
let mut field = SelectMultipleField::<TestChoice>::with_options(
FormFieldOptions {
id: "test_multi".to_owned(),
name: "test_multi".to_owned(),
required: false,
},
SelectMultipleFieldOptions::default(),
);
field
.set_value(FormFieldValue::new_text("opt1"))
.await
.unwrap();
field
.set_value(FormFieldValue::new_text("opt3"))
.await
.unwrap();
let html = field.to_string();
assert!(html.contains("<option value=\"opt1\" selected>Option 1</option>"));
assert!(html.contains("<option value=\"opt3\" selected>Option 3</option>"));
assert!(!html.contains("<option value=\"opt2\" selected>"));
let values: Vec<&str> = field.values().collect();
assert_eq!(values.len(), 2);
assert!(values.contains(&"opt1"));
assert!(values.contains(&"opt3"));
}
#[test]
fn select_choice_default_choices() {
let choices = TestChoice::default_choices();
assert_eq!(choices.len(), 3);
assert_eq!(choices[0], TestChoice::Option1);
assert_eq!(choices[1], TestChoice::Option2);
assert_eq!(choices[2], TestChoice::Option3);
}
#[test]
fn select_choice_from_str_invalid() {
let result = TestChoice::from_str("invalid");
assert!(result.is_err());
if let Err(FormFieldValidationError::InvalidValue(value)) = result {
assert_eq!(value, "invalid");
} else {
panic!("Expected InvalidValue error");
}
}
#[test]
fn check_required_multiple_empty() {
let field = SelectMultipleField::<TestChoice>::with_options(
FormFieldOptions {
id: "test".to_owned(),
name: "test".to_owned(),
required: true,
},
SelectMultipleFieldOptions::default(),
);
let result = check_required_multiple(&field);
assert_eq!(result, Err(FormFieldValidationError::Required));
}
#[cot::test]
async fn check_required_multiple_with_values() {
let mut field = SelectMultipleField::<TestChoice>::with_options(
FormFieldOptions {
id: "test".to_owned(),
name: "test".to_owned(),
required: true,
},
SelectMultipleFieldOptions::default(),
);
field
.set_value(FormFieldValue::new_text("opt1"))
.await
.unwrap();
let result = check_required_multiple(&field);
assert!(result.is_ok());
let values = result.unwrap();
assert_eq!(values.len(), 1);
assert!(values.contains("opt1"));
}
#[cot::test]
async fn select_multiple_field_values_iterator() {
let mut field = SelectMultipleField::<TestChoice>::with_options(
FormFieldOptions {
id: "test".to_owned(),
name: "test".to_owned(),
required: false,
},
SelectMultipleFieldOptions::default(),
);
let values: Vec<&str> = field.values().collect();
assert!(values.is_empty());
field
.set_value(FormFieldValue::new_text("opt2"))
.await
.unwrap();
field
.set_value(FormFieldValue::new_text("opt1"))
.await
.unwrap();
field
.set_value(FormFieldValue::new_text("opt2"))
.await
.unwrap();
let values: Vec<&str> = field.values().collect();
assert_eq!(values.len(), 2); assert!(values.contains(&"opt1"));
assert!(values.contains(&"opt2"));
}
#[cot::test]
async fn vec_as_form_field_clean_value() {
let mut field = SelectMultipleField::<TestChoice>::with_options(
FormFieldOptions {
id: "choices".to_owned(),
name: "choices".to_owned(),
required: true,
},
SelectMultipleFieldOptions::default(),
);
field
.set_value(FormFieldValue::new_text("opt1"))
.await
.unwrap();
field
.set_value(FormFieldValue::new_text("opt3"))
.await
.unwrap();
let values = Vec::<TestChoice>::clean_value(&field).unwrap();
assert_eq!(values, vec![TestChoice::Option1, TestChoice::Option3]);
}
#[cot::test]
async fn vec_as_form_field_required_empty() {
let field = SelectMultipleField::<TestChoice>::with_options(
FormFieldOptions {
id: "choices".to_owned(),
name: "choices".to_owned(),
required: true,
},
SelectMultipleFieldOptions::default(),
);
let result = Vec::<TestChoice>::clean_value(&field);
assert_eq!(result, Err(FormFieldValidationError::Required));
}
#[cot::test]
async fn vec_as_form_field_invalid_value() {
let mut field = SelectMultipleField::<TestChoice>::with_options(
FormFieldOptions {
id: "choices".to_owned(),
name: "choices".to_owned(),
required: false,
},
SelectMultipleFieldOptions::default(),
);
field
.set_value(FormFieldValue::new_text("opt1"))
.await
.unwrap();
field
.set_value(FormFieldValue::new_text("bad"))
.await
.unwrap();
let result = Vec::<TestChoice>::clean_value(&field);
assert!(matches!(
result,
Err(FormFieldValidationError::InvalidValue(value)) if value == "bad"
));
}
#[test]
fn vec_as_form_field_to_field_value() {
let items = vec![TestChoice::Option1, TestChoice::Option2];
assert_eq!(items.to_field_value(), "");
}
#[cot::test]
async fn vec_deque_as_form_field_clean_value() {
let mut field = SelectMultipleField::<TestChoice>::with_options(
FormFieldOptions {
id: "choices".to_owned(),
name: "choices".to_owned(),
required: false,
},
SelectMultipleFieldOptions::default(),
);
field
.set_value(FormFieldValue::new_text("opt2"))
.await
.unwrap();
field
.set_value(FormFieldValue::new_text("opt1"))
.await
.unwrap();
let mut values = VecDeque::<TestChoice>::clean_value(&field).unwrap();
assert_eq!(values.pop_front(), Some(TestChoice::Option2));
assert_eq!(values.pop_back(), Some(TestChoice::Option1));
}
#[cot::test]
async fn linked_list_as_form_field_clean_value() {
let mut field = SelectMultipleField::<TestChoice>::with_options(
FormFieldOptions {
id: "choices".to_owned(),
name: "choices".to_owned(),
required: false,
},
SelectMultipleFieldOptions::default(),
);
field
.set_value(FormFieldValue::new_text("opt3"))
.await
.unwrap();
let mut values = LinkedList::<TestChoice>::clean_value(&field).unwrap();
assert_eq!(values.pop_front(), Some(TestChoice::Option3));
assert!(values.is_empty());
}
#[cot::test]
async fn hash_set_as_form_field_clean_value() {
let mut field = SelectMultipleField::<TestChoice>::with_options(
FormFieldOptions {
id: "choices".to_owned(),
name: "choices".to_owned(),
required: false,
},
SelectMultipleFieldOptions::default(),
);
field
.set_value(FormFieldValue::new_text("opt1"))
.await
.unwrap();
field
.set_value(FormFieldValue::new_text("opt1"))
.await
.unwrap();
field
.set_value(FormFieldValue::new_text("opt2"))
.await
.unwrap();
let values = HashSet::<TestChoice>::clean_value(&field).unwrap();
assert_eq!(values.len(), 2);
assert!(values.contains(&TestChoice::Option1));
assert!(values.contains(&TestChoice::Option2));
}
#[cot::test]
async fn index_set_as_form_field_preserves_order() {
let mut field = SelectMultipleField::<TestChoice>::with_options(
FormFieldOptions {
id: "choices".to_owned(),
name: "choices".to_owned(),
required: false,
},
SelectMultipleFieldOptions::default(),
);
field
.set_value(FormFieldValue::new_text("opt2"))
.await
.unwrap();
field
.set_value(FormFieldValue::new_text("opt3"))
.await
.unwrap();
field
.set_value(FormFieldValue::new_text("opt2"))
.await
.unwrap();
let values = IndexSet::<TestChoice>::clean_value(&field).unwrap();
let mut iter = values.iter();
assert_eq!(iter.next(), Some(&TestChoice::Option2));
assert_eq!(iter.next(), Some(&TestChoice::Option3));
assert_eq!(iter.next(), None);
}
#[derive(SelectChoice, SelectAsFormField, Debug, Clone, PartialEq, Eq, Hash)]
enum DerivedStatus {
#[select_choice(id = "draft", name = "Draft")]
Draft,
#[select_choice(id = "published", name = "Published")]
Published,
#[select_choice(id = "archived", name = "Archived")]
Archived,
}
#[test]
fn select_as_form_field_render() {
let field = SelectField::<DerivedStatus>::with_options(
FormFieldOptions {
id: "status".to_owned(),
name: "status".to_owned(),
required: false,
},
SelectFieldOptions::default(),
);
let html = field.to_string();
assert!(html.contains("<select"));
assert!(html.contains("name=\"status\""));
assert!(html.contains("id=\"status\""));
assert!(html.contains("value=\"draft\""));
assert!(html.contains("value=\"published\""));
assert!(html.contains("value=\"archived\""));
assert!(html.contains("Draft"));
assert!(html.contains("Published"));
assert!(html.contains("Archived"));
}
#[cot::test]
async fn select_as_form_field_clean_value_valid() {
let mut field = SelectField::<DerivedStatus>::with_options(
FormFieldOptions {
id: "status".to_owned(),
name: "status".to_owned(),
required: true,
},
SelectFieldOptions::default(),
);
field
.set_value(FormFieldValue::new_text("published"))
.await
.unwrap();
let value = DerivedStatus::clean_value(&field).unwrap();
assert_eq!(value, DerivedStatus::Published);
}
#[cot::test]
async fn select_as_form_field_clean_value_required_empty() {
let mut field = SelectField::<DerivedStatus>::with_options(
FormFieldOptions {
id: "status".to_owned(),
name: "status".to_owned(),
required: true,
},
SelectFieldOptions::default(),
);
field.set_value(FormFieldValue::new_text("")).await.unwrap();
let result = DerivedStatus::clean_value(&field);
assert_eq!(result, Err(FormFieldValidationError::Required));
}
#[cot::test]
async fn select_as_form_field_clean_value_invalid() {
let mut field = SelectField::<DerivedStatus>::with_options(
FormFieldOptions {
id: "status".to_owned(),
name: "status".to_owned(),
required: false,
},
SelectFieldOptions::default(),
);
field
.set_value(FormFieldValue::new_text("not-a-valid-id"))
.await
.unwrap();
let result = DerivedStatus::clean_value(&field);
assert!(matches!(
result,
Err(FormFieldValidationError::InvalidValue(value)) if value == "not-a-valid-id"
));
}
#[test]
fn select_as_form_field_to_field_value() {
assert_eq!(DerivedStatus::Draft.to_field_value(), "Draft");
assert_eq!(DerivedStatus::Published.to_field_value(), "Published");
}
}