Skip to content

Skip does_not_contain on an absent value - #400

Open
youdie006 wants to merge 1 commit into
Keats:masterfrom
youdie006:does-not-contain-skip-none
Open

Skip does_not_contain on an absent value#400
youdie006 wants to merge 1 commit into
Keats:masterfrom
youdie006:does-not-contain-skip-none

Conversation

@youdie006

Copy link
Copy Markdown

11cc0e9 ("Use generics and macros", #274) replaced the concrete impl ValidateContains for Option<String> with a blanket impl<T: ValidateContains> ValidateContains for Option<T> that returns true for None -- so contains skips absent values, the way length, email and url already do. git show 11cc0e9 --stat touches nine files and does not include does_not_contain.rs, which is still:

fn validate_does_not_contain(&self, needle: &str) -> bool {
    !self.validate_contains(needle)
}

Negating a skip turns it into a failure. So does_not_contain is the only validator in the crate that rejects None:

None contains         = true
None length(1..10)    = true
None email            = true
None url              = true
None does_not_contain = false

Who actually hits it

The derive usually papers over this. number_options (validator_derive/src/types.rs:153) matches only the literal spellings in OPTIONS_TYPE -- "Option|", "std|option|Option|", "core|option|Option|" -- and wraps the call in if let Some(..). A type alias defeats that inference and the raw trait call is emitted:

type MaybeName = Option<String>;

#[derive(Validate)]
struct S {
    #[validate(does_not_contain(pattern = "he"))]
    val: MaybeName,
}

S { val: None }.validate()
// Err: does_not_contain, params { "value": Null }

The same struct with contains or length returns Ok. And ValidateDoesNotContain is publicly exported, so the trait is wrong for Option regardless of the derive.

validator_derive_tests/tests/complex.rs:190 (test_works_with_none_values) and tests/range.rs:262 both assert that None passes. contains / does_not_contain simply had no None row.

The fix

ValidateContains gains a defaulted contains_is_absent() returning false; Option<T> overrides it; does_not_contain skips on it.

I kept the blanket impl<T: ValidateContains> ValidateDoesNotContain for T. The alternative -- deleting it and mirroring contains.rs's six concrete impls -- would break any downstream type that implements ValidateContains and relies on getting ValidateDoesNotContain for free, so I did not take it.

Behaviour change

does_not_contain on an absent value now passes instead of failing. No existing test row asserts the old behaviour, so nothing had to change.

Verification

cargo fmt --check clean. cargo clippy -p validator --all-targets -- -D warnings -- 0 errors. cargo test -p validator --lib 62/62; does_not_contain 6/6, contains 4/4, complex 5/5, nested 18/18, range 17/17. is_none_or is fine against the workspace rust-version = "1.88".

I mutation-checked each site on its own:

mutation result
revert does_not_contain to the plain negation fails
Option::contains_is_absent -> false fails
Option::contains_is_absent -> true, so Some is skipped too fails
replace is_none_or with an equivalent match passes

The last is deliberate: it is the same predicate, and its passing shows the tests pin "an absent value is skipped" rather than the particular helper I reached for.

One thing I want to mention rather than leave buried. My first draft also forwarded contains_is_absent through the &T and Cow impls. Mutating either of those survived, and the reason is that neither is reachable with an Option: Cow's bound (for<'a> &'a T: ValidateContains) admits only str/String, and (&opt).validate_does_not_contain(..) autoderefs to the Option<T> impl. Rather than ship two lines no test could pin, I removed them.

Pre-existing failures I did not touch

cargo test -p validator_derive_tests --test compile_test (the trybuild ui test) fails identically on unmodified master -- I confirmed by stashing. Workspace-wide clippy also reports errors at master here, which I believe is my local clippy 1.97 against your pinned 1.88 rather than anything real. Neither is related to these lines; I mention them so the failures are not read as mine.


Disclosure: AI-assisted. I found and prepared this with an AI assistant, and I ran and verified everything above myself.

11cc0e9 generalised ValidateContains to a blanket Option<T> impl that
returns true for None, so contains skips absent values the way length,
email and url do. It did not touch does_not_contain.rs, which is still
a plain negation of validate_contains, so negating that skip turns it
into a failure.

The derive usually hides this because number_options matches the
literal spellings of Option in OPTIONS_TYPE and wraps the call, but a
type alias defeats that inference and the exported ValidateDoesNotContain
trait is wrong for Option either way.

Add a defaulted contains_is_absent to ValidateContains, override it on
Option, and let does_not_contain skip on it.
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.

1 participant