Skip to content
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

Trait validation #225

Merged
merged 17 commits into from
Apr 14, 2023
Merged

Trait validation #225

merged 17 commits into from
Apr 14, 2023

Conversation

pintariching
Copy link
Contributor

I've implemented trait validation as mentioned in #205.

For now I've just implemented it for the length validation just to see if I'm on the right track. I've also copied the tests just to be sure it works.

Now if you want length validation for a custom type you just have to write an impl block for it like

impl ValidateLength for CustomType {
    fn validate_length(self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
        // write length validation in here 
    }
}

@@ -32,11 +34,33 @@ pub fn validate_length<T: HasLen>(
true
}

pub trait ValidateLenght: HasLen {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like if the user wants to implement ValidateLength on their own type, they'd need to also implement HaLen?

Might it be simpler if HasLen is not involved?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo on the name and it shouldn't use HasLen at all. Then we can just create a macro to generate all the impls we want for each type. With this trait, HasLen doesn't exist anymore

@@ -32,11 +34,33 @@ pub fn validate_length<T: HasLen>(
true
}

pub trait ValidateLenght: HasLen {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo on the name and it shouldn't use HasLen at all. Then we can just create a macro to generate all the impls we want for each type. With this trait, HasLen doesn't exist anymore

@pintariching
Copy link
Contributor Author

Okay I changed the trait and updated the implementations. The length extraction has to be done inside the impl for every type out there. I just added the temporary function validate_length_for_trait so the crate still builds.

@pintariching
Copy link
Contributor Author

I tried making a macro with macro_rules! for quickly implementing for all the types but my macro-fu is not on that level. I'm not sure how to specify lifetimes so the macro makes something like this

impl<'a> HasLen for &'a String {
    fn length(&self) -> u64 {
        self.chars().count() as u64
    }
}

from something like this

impl_validate_length!(&'a String)

@pintariching
Copy link
Contributor Author

I have implemented all the impls that had the HasLen trait by hand. I'm unsure how to make a macro. I also added a derive test can_validate_custom_impl_for_length that shows the functionality that I managed to get to work. Right now you have to implement the length validation by yourself unless validator::length::validate_length_for_trait is made public


impl ValidateLength for String {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.chars().count() as u64, min, max, equal)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What made you decide to go with self.chars().count() instead of self.len()?
And why use u64 everywhere instead of usize?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used self.chars().count() because the HasLen trait used them too and I didn't really think about it. The same is for u64 instead of usize. The validate_length function used Option<u64>. I can refactor the trait to use self.len() and usize if it makes any difference.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strings should use self.chars().count()

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Keats interesting, what is the advantage of that over .len()?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fn main() {
    let txt = "日本語";
    println!("len {:?} vs chars {:?}", txt.len(), txt.chars().count());
}

will print

len 9 vs chars 3

In this case no one reasonable will think of len as number of bytes used (.len()) vs number of characters, which is what people think of when they think of a len for a string.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow very interesting! Makes sense to use chars count then

@pintariching
Copy link
Contributor Author

Now I'm unsure what path to go. Do we let users implement the validation logic by themselves like so

#[derive(Debug, Serialize)]
struct CustomString(String);

impl validator::ValidateLength for &CustomString {
    fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
        let length = self.0.chars().count() as u64;

        if let Some(eq) = equal {
            return length == eq;
        } else {
            if let Some(m) = min {
	    if length < m {
	        return false;
	    }
        }
        if let Some(m) = max {
	    if length > m {
                return false;
	        }
            }
        }
        true
    }
}

This fees like cumebrsome and I would prefer if the user just has to implement a length function like

#[derive(Debug, Serialize)]
struct CustomString(String);

impl validator::ValidateLength for &CustomString {
    fn length(&self) -> u64 {
        self.0.chars().count() as u64
    }
}

And maybe leave the first example for the custom validator?

@Keats
Copy link
Owner

Keats commented Jul 11, 2022

I don't see what this length method gives us though? It's not validating anything

@pintariching
Copy link
Contributor Author

pintariching commented Jul 11, 2022

It's just there so the user can specify how to get the length value from a custom type. This is then used in the validate_length function with value.length().

Now that I think about it, it's the same as it was before with the HasLen trait so I just renamed it 😅.

@pintariching
Copy link
Contributor Author

How about this, the trait ValidateLength implements a default validate_length() function that you can override if you want. Or you can just implement length() for your custom type that the default implementation uses if you don't want to change the logic.

@Keats
Copy link
Owner

Keats commented Jul 27, 2022

I think that looks good

@pintariching pintariching requested a review from Keats August 2, 2022 21:06
true
}

fn to_email_string(&self) -> String;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That implies allocation, it should return a &str instead

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning a &str is impossible if I want to return a formatted string for example

fn to_email_string(&self) -> &str {
    format!("{}@{}", self.user_part, self.domain_part).as_str()
}

How about returning Cow?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cow is fine then

use std::{borrow::Cow, collections::{HashMap, HashSet, BTreeMap, BTreeSet}};

#[cfg(feature = "indexmap")]
use indexmap::{IndexMap, IndexSet};
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on letting crates handle validator support themselves like they do for serde?

pub use validation::ip::{validate_ip, validate_ip_v4, validate_ip_v6};
pub use validation::length::validate_length;
pub use validation::length::{validate_length, ValidateLength};
pub use validation::must_match::validate_must_match;
#[cfg(feature = "unic")]
pub use validation::non_control_character::validate_non_control_character;
#[cfg(feature = "phone")]
pub use validation::phone::validate_phone;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm tempted to remove that one from validator, it's currently too generic to be easily usable and can easily be added manually if needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe leaving an example on how to add it with a custom validator after removing it would be a good idea?

@svenstaro
Copy link

What's the status on this? Is there anything that can be picked up here?

@Keats
Copy link
Owner

Keats commented Mar 27, 2023

Just need some time to take that PR in and re-do the derive impl. I should have some more free time from next week on and could use a little break from Zola.

@svenstaro
Copy link

Just a friendly ping @Keats :) We could also try to assist as we use this lib quite a bit.

@Keats
Copy link
Owner

Keats commented Apr 14, 2023

Yes that would be great. I started to have a look at the derive crate and it seems nicer to use than 7 years ago. It also made me realise we were not handling the rename_all of serde afaik so that rename feature kinda sucked but no one complained? Maybe it's not super useful in the end.

The library itself should be that PR but I don't know whether we should do it in 2 phases (validation as trait -> derive rewrite later) or try to do it in one go.
I would probably go with the 2 phases since it would be too much work to do it in a single phase.

I've created a next branch so we can work on it without causing issues on master

@Keats Keats changed the base branch from master to next April 14, 2023 10:48
@Keats Keats merged commit 4830561 into Keats:next Apr 14, 2023
Keats pushed a commit that referenced this pull request Mar 4, 2024
* implemented validation trait for length

* converted identation to spaces

* changed the trait to not require HasLen

* added macro for generating impls

* implemented ValidateLength for some types

* using trait validation instead of the function

* added cfg for indexmap import

* changed trait to require length

* Revert "changed trait to require length"

This reverts commit a77bdc9.

* moved validation logic inside ValidateLength trait

* added trait validation for required

* added email trait validation

* fixed trait validation for email

* added range trait validation

* fixed range trait

* added url trait validation

---------

Co-authored-by: Tilen Pintarič <tilen.pintaric@aviko.si>
Keats pushed a commit that referenced this pull request Mar 4, 2024
* implemented validation trait for length

* converted identation to spaces

* changed the trait to not require HasLen

* added macro for generating impls

* implemented ValidateLength for some types

* using trait validation instead of the function

* added cfg for indexmap import

* changed trait to require length

* Revert "changed trait to require length"

This reverts commit a77bdc9.

* moved validation logic inside ValidateLength trait

* added trait validation for required

* added email trait validation

* fixed trait validation for email

* added range trait validation

* fixed range trait

* added url trait validation

---------

Co-authored-by: Tilen Pintarič <tilen.pintaric@aviko.si>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants