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

Rule fired for optional attribute which is missing #540

Closed
mateusz-useo opened this issue Jun 11, 2019 · 17 comments
Closed

Rule fired for optional attribute which is missing #540

mateusz-useo opened this issue Jun 11, 2019 · 17 comments
Milestone

Comments

@mateusz-useo
Copy link

Description

Rule is getting fired for a required attribute which is nested within an optional attribute and when the whole optional hash is missing (which should be allowed) it reports an error causing the validation to fail.

To Reproduce

Create a contract with a rule for an optional hash attribute with a required key + a rule for the required key and run with an input not including that hash. I use custom macro and I don't know if it's related to the bug.

So given:

class SignUpContract < Dry::Validation::Contract
  params do
    required(:user).hash do
      optional(:id) { filled? & str? }
      optional(:first_name) { filled? & str? }
      optional(:last_name) { filled? & str? }
      required(:email) { filled? & str? }
      required(:password) { filled? & str? }
      required(:accepts_terms) { filled? & bool? }
      optional(:freelancer).hash do
        required(:id) { filled? & str? }
      end
      optional(:clients).array(:hash) do
        required(:id) { filled? & str? }
      end
    end
  end

  rule('user.id').validate(:uuid_format)
  rule('user.email').validate(:email_format)
  rule('user.password').validate(min_length: 8)

  rule(user: %i[freelancer clients]) do
    freelancer = values.dig(:user, :freelancer)
    clients = values.dig(:user, :clients)

    unless freelancer ^ (clients && clients.size == 1)
      key(:user).failure(:profile_missing)
    end
  end

  rule('user.freelancer.id').validate(:uuid_format)
  rule('user.clients') do
    have_valid_uuid = lambda { |client|
      client[:id].nil? || client[:id].match?(Macros::UUID_REGEXP)
    }

    unless value.all?(have_valid_uuid)
      key.failure('not a valid UUID format')
    end
  end
end

And params:

{
  "user": {
    "id": "d16dfef3-50f9-4588-b7d7-837801e403cd",
    "first_name": "Blah",
    "last_name": "Blah",
    "email": "blah@blah.blah",
    "password": "blahblahblah",
    "accepts_terms": true,
    "clients": [
      {
        "id": "d287fdff-78ac-45ed-871b-b1b5c9548399"
      }
    ]
  }
}

It produces:

#<Dry::Validation::Result{:user=>{:id=>"d16dfef3-50f9-4588-b7d7-837801e403cd", :first_name=>"Blah", :last_name=>"Blah", :email=>"blah@blah.blah", :password=>"blahblahblah", :accepts_terms=>true, :clients=>[{:id=>"d287fdff-78ac-45ed-871b-b1b5c9548399"}]}} errors={:user=>{:freelancer=>{:id=>["not a valid UUID format"]}}}>

Expected behavior

The validation rule for optional attribute should never be run when it's missing.
In the example rules related to freelancer attributes should not be run and the validation should pass

My environment

  • Affects my production application: NO as I haven't upgraded yet
  • Ruby version: 2.6.3
  • OS: MacOS Mojave Version 10.14.5 (Darwin Mateuszs-MacBook-Pro.local 18.6.0 Darwin Kernel Version 18.6.0: Thu Apr 25 23:16:27 PDT 2019; root:xnu-4903.261.4~2/RELEASE_X86_64 x86_64)
@solnic solnic added this to the 1.0.1 milestone Jun 11, 2019
solnic pushed a commit that referenced this issue Jun 14, 2019
This is helpful when dealing with optional keys. We can't just assume
that we do not want to execute a rule when a value was missing.

Good example: a password must be provided when login is provided, and
both are optional.

Refs #540
@solnic
Copy link
Member

solnic commented Jun 14, 2019

Please see #550 and let me know if this makes sense to you.

solnic pushed a commit that referenced this issue Jun 14, 2019
This is helpful when dealing with optional keys. We can't just assume
that we do not want to execute a rule when a value was missing.

Good example: a password must be provided when login is provided, and
both are optional.

Refs #540
@solnic
Copy link
Member

solnic commented Jun 14, 2019

I'm gonna close this one since, after all, this is not a bug. See #550 where new features have been added to make it easier to work with optional keys and rules that depend on them.

@solnic solnic closed this as completed Jun 14, 2019
@solnic solnic removed the bug label Jun 14, 2019
@mateusz-useo
Copy link
Author

It is awesome, it does add the condition here and then more often than not but I do get the idea behind it so thank you, I really appreciate your work :)

@solnic
Copy link
Member

solnic commented Jun 17, 2019

@mateusz-useo cool! I plan to add a bigger feature soon where you'll be able to use pattern matching to run rules conditionally, so ie this would work:

rule(:numbers).if(:key?).each do
  key.failure("must be even") if value.odd?
end

@flash-gordon
Copy link
Member

for the record, this thing in PM is usually called "guard", just to save some time on coming with proper naming which is, you know, hard...

@mateusz-useo
Copy link
Author

mateusz-useo commented Jun 26, 2019

Maybe rule? instead of rule(...).if(:key?) as a shorthand?

@solnic
Copy link
Member

solnic commented Jun 26, 2019

@mateusz-useo oh that's a nice idea, thanks. I'll keep it in mind!

@shepmaster
Copy link

Please consider how this can work with .validate as well.

@solnic
Copy link
Member

solnic commented Jul 4, 2019

@shepmaster good point

@musaffa
Copy link

musaffa commented Dec 12, 2020

I've a contract like this:

params do
  optional(:per_page).filled(:integer)
  .....
end

rule(:per_page).validate(gt?: 0, lteq?: 20)
.....

If there's no per_page key, this code creates error with the predicate macros as defined in its rule. As an alternative, we can check for key? in a rule block. But doing the works of predicate macros and their associated errors manually inside a rule block will create a lot of boilerplate codes.

I've been trying to upgrade dry-validation from 0.13 to >= 1.0.0. Optional keys are very common and this appears to be a major blocker for me to upgrade.

We would love to see a solution implemented soon. Thank you.

@solnic @flash-gordon

@solnic
Copy link
Member

solnic commented Dec 13, 2020

@musaffa I'm not yet sure how to solve this. Adding key? check automatically may not be desired in various cases, see #550's description for more info.

@musaffa
Copy link

musaffa commented Dec 13, 2020

Exposing validate inside the rule block might solve the issue. For example:

params do
  optional(:per_page).filled(:integer)
end

rule(:per_page) do
  validate(gt?: 0, lteq?: 20) if key?
end

For maybe with optional:

params do
  optional(:per_page).maybe(:integer)
end

rule(:per_page) do
  validate(gt?: 0, lteq?: 20) if key? && value
end

@solnic
Copy link
Member

solnic commented Dec 14, 2020

@musaffa I reckon it'd be simpler (and better UX too) to extend rule DSL with pattern-matching-like API, starting with support for key? like I mentioned above, so:

params do
  optional(:per_page).maybe(:integer)
end

rule(:per_page).guard(:key?).validate(gt?: 0, lteq?: 20)

that would just work and you could also use it with block-based rules. WDYT?

@musaffa
Copy link

musaffa commented Dec 14, 2020

A guard can be a good shortcut. It's simpler and concise.

rule also supports multiple keys. How will guard(:key?) function in those cases?

guard(:key?) will only cover for optional key. For maybe, we will also need to guard the value before passing it to the validate function.

I still think that the ability to call validate inside the rule block can be a valuable addition. Not just for optional key or value but for the other use cases. For example, I would love to use registered macro inside a rule block.

@solnic
Copy link
Member

solnic commented Dec 14, 2020

guard(:key?) will only cover for optional key. For maybe, we will also need to guard the value before passing it to the validate function.

Yeah we can have probably if and unless instead of guard, then you could have:

# for optional
rule(:per_page).if(:key?).validate(gt?: 0, lteq?: 20)

# for maybe
rule(:per_page).unless(:nil?).validate(gt?: 0, lteq?: 20)

We could even go nuts a bit and provide a shortcut (but I've got mixed feelings about this):

rule(:per_page).if(:present?).validate(gt?: 0, lteq?: 20)

I still think that the ability to call validate inside the rule block can be a valuable addition. Not just for optional key or value but for the other use cases. For example, I would love to use registered macro inside a rule block.

Yes this is a really good point, thank you. I'll experiment with this and get back to you.

@musaffa
Copy link

musaffa commented Dec 14, 2020

rule(:per_page).unless(:nil?).validate(gt?: 0, lteq?: 20)

:nil? looks ambiguous. It may mean key or value. :value? may be more suitable:

# for optional and maybe
rule(:per_page).if(:key?).if(:value?).validate(gt?: 0, lteq?: 20)

# for optional and maybe (combined if)
rule(:per_page).if(:key?, :value?).validate(gt?: 0, lteq?: 20)

:present? -> I've got mixed feelings about this

I know where it's coming from. The spectre of ActiveModel validation haunts us all.

@solnic
Copy link
Member

solnic commented Dec 16, 2020

@musaffa I reported two feature requests:

It's tentatively scheduled to be part of 1.7.0 release. I marked both as help wanted for the time being though. I'll give this a shot during xmas break unless somebody beats me to 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

No branches or pull requests

5 participants