- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 33
 
          feat: Added derive macro SelectAsFormField
          #397
        
          New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
          Codecov Report❌ Patch coverage is  
 
 Flags with carried forward coverage won't be shown. Click here to find out more. 
 🚀 New features to boost your workflow:
  | 
    
| 
           @m4tx can you take a look whenever you get the time.  | 
    
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your patience waiting for my review! As usual, the change looks good, but I have some minor comments about stuff that needs to be fixed before merging this.
Also, please make sure to add additional tests if needed so that the patch has at least 85% code coverage.
About the impl_as_form_field_mult macro—since it's private and is now unused, I don't see a value in keeping it, and so it should be removed.
        
          
                cot/src/form/fields/select.rs
              
                Outdated
          
        
      | impl<T: SelectChoice + Send> crate::form::AsFormField for ::std::vec::Vec<T> { | ||
| type Type = SelectMultipleField<T>; | ||
| 
               | 
          ||
| fn clean_value(field: &Self::Type) -> Result<Self, FormFieldValidationError> { | ||
| let values = check_required_multiple(field)?; | ||
| values.iter().map(|id| T::from_str(id)).collect() | ||
| } | ||
| 
               | 
          ||
| fn to_field_value(&self) -> String { | ||
| String::new() | ||
| } | ||
| } | ||
| 
               | 
          ||
| impl<T: SelectChoice + Send> crate::form::AsFormField for ::std::collections::VecDeque<T> { | ||
| type Type = SelectMultipleField<T>; | ||
| 
               | 
          ||
| fn clean_value(field: &Self::Type) -> Result<Self, FormFieldValidationError> { | ||
| let values = check_required_multiple(field)?; | ||
| let mut out = ::std::collections::VecDeque::new(); | ||
| for id in values { | ||
| out.push_back(T::from_str(id)?); | ||
| } | ||
| Ok(out) | ||
| } | ||
| 
               | 
          ||
| fn to_field_value(&self) -> String { | ||
| String::new() | ||
| } | ||
| } | ||
| 
               | 
          ||
| impl<T: SelectChoice + Send> crate::form::AsFormField for ::std::collections::LinkedList<T> { | ||
| type Type = SelectMultipleField<T>; | ||
| 
               | 
          ||
| fn clean_value(field: &Self::Type) -> Result<Self, FormFieldValidationError> { | ||
| let values = check_required_multiple(field)?; | ||
| let mut out = ::std::collections::LinkedList::new(); | ||
| for id in values { | ||
| out.push_back(T::from_str(id)?); | ||
| } | ||
| Ok(out) | ||
| } | ||
| 
               | 
          ||
| fn to_field_value(&self) -> String { | ||
| String::new() | ||
| } | ||
| } | ||
| 
               | 
          ||
| impl<T: SelectChoice + Eq + ::std::hash::Hash + Send, S: ::std::hash::BuildHasher + Default> | ||
| crate::form::AsFormField for ::std::collections::HashSet<T, S> | ||
| { | ||
| type Type = SelectMultipleField<T>; | ||
| 
               | 
          ||
| fn clean_value(field: &Self::Type) -> Result<Self, FormFieldValidationError> { | ||
| let values = check_required_multiple(field)?; | ||
| let mut out = ::std::collections::HashSet::default(); | ||
| for id in values { | ||
| out.insert(T::from_str(id)?); | ||
| } | ||
| Ok(out) | ||
| } | ||
| 
               | 
          ||
| fn to_field_value(&self) -> String { | ||
| String::new() | ||
| } | ||
| } | ||
| 
               | 
          ||
| impl<T: SelectChoice + Eq + ::std::hash::Hash + Send> crate::form::AsFormField | ||
| for ::indexmap::IndexSet<T> | ||
| { | ||
| type Type = SelectMultipleField<T>; | ||
| 
               | 
          ||
| fn clean_value(field: &Self::Type) -> Result<Self, FormFieldValidationError> { | ||
| let values = check_required_multiple(field)?; | ||
| let mut out = ::indexmap::IndexSet::new(); | ||
| for id in values { | ||
| out.insert(T::from_str(id)?); | ||
| } | ||
| Ok(out) | ||
| } | ||
| 
               | 
          ||
| fn to_field_value(&self) -> String { | ||
| String::new() | ||
| } | ||
| } | ||
| 
               | 
          
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why can't we use the impl_as_form_field_mult_collection macro for these implementations? They are all essentially the same, and as such, prone to mistakes when you want to modify something in one impl and forget to change all the others. We just need to another variant of the macro so that it supports generic bounds.
As an added bonus, Iterator::collect() might be slightly faster than <Collection>::push_back because of preallocation, although admittedly, this likely doesn't matter for the typical element counts in case of HTML form values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have made the changes. Please take a look if this is what you have in mind.
Co-authored-by: Mateusz Maćkowski <mateusz@mackowski.org>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey! This looks good - please fix the only remaining issue and we'll merge this.
| #[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); | ||
| } | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests do not actually test the SelectAsFormField macro, do they? TestChoice has a custom SelectChoice implementation. I'd suggest adding another enum that actually uses SelectAsFormField and use it in the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you check if the new test are accurate?
As per the requirements of the #356 I implemented the
SelectAsFormFieldderive macro. There were two directions for this either creating a derive macro or create a wrapper struct. Since I love derive macro and comfortable implementing it, I went with that.After this change the
impl_as_form_field_multmacro is now unused, I would like to know what is the preference for that?