dry-validation icon indicating copy to clipboard operation
dry-validation copied to clipboard

Calling a macro on a deeply nested rule

Open DawidJanczak opened this issue 4 years ago • 2 comments

I'd like to be able to specify a macro in the following manner: rule(foo: :bar).each where both foo and bar are arrays.

Examples

As an example please have a look at this spec that's currently failing: https://github.com/dry-rb/dry-validation/compare/master...DawidJanczak:macro-on-deeply-nested-rule

Resources

Discussed with @solnic here: https://discourse.dry-rb.org/t/calling-a-macro-on-a-depply-nested-rule/842

DawidJanczak avatar Jul 31 '19 09:07 DawidJanczak

I just hit the same kind of issue, where my params look like:

{
  "first_name": "Francois",
  "last_name": "Beausoleil",
  "email_locators": [
    {
      "name": "professional",
      "email": "[email protected]"
    }
  ]
}

I have a macro named email_format, which I'd like to apply to the email fields on the nested locators, but this fails:

class ApplicationContract < Dry::Validation::Contract
  config.messages.backend = :i18n

  register_macro(:email_format) do
    if value.present? && !/\A[-\w+.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.match?(value)
      key.failure(:email_format)
    end
  end
end

class ManuallyRegisterPersonContract < ApplicationContract
  params do
    required(:first_name).maybe(Types::StrippedString, min_size?: 1)
    required(:last_name).maybe(Types::StrippedString, min_size?: 1)
    required(:date_of_birth).maybe(:date)
    required(:email_locators).maybe(:array).each do
      hash do
        required(:name).filled(Types::StrippedString, min_size?: 1)
        required(:email).filled(Types::StrippedString, min_size?: 1)
      end
    end

    optional(:metadata).hash(MetadataContract)
  end

  rule(email_locators: [:email]).validate(:email_format)
end

RSpec.describe ManuallyRegisterPersonContract do
  describe "a contract with one email locator that has an invalid email address format" do
    fit "fails validation" do
      params = {
        first_name: nil,
        last_name: nil,
        date_of_birth: nil,
        email_locators: [
          {
            name: "personal",
            email: "baden@"
          }
        ]
      }

      result = subject.call(params)
      expect(result).to be_failure
    end
  end
end
  1) ManuallyRegisterPersonContract a contract with one email locator that has an invalid email address format fails validation
     Failure/Error: if value.present? && !/\A[-\w+.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.match?(value)

     NoMethodError:
       undefined method `fetch_values' for #<Dry::Validation::Values:0x00007fe255003518>
     # /Users/francois/.rvm/gems/ruby-2.7.2/bundler/gems/dry-validation-9b03b9ead558/lib/dry/validation/values.rb:98:in `method_missing'
     # /Users/francois/.rvm/gems/ruby-2.7.2/bundler/gems/dry-validation-9b03b9ead558/lib/dry/validation/values.rb:57:in `[]'
     # /Users/francois/.rvm/gems/ruby-2.7.2/bundler/gems/dry-validation-9b03b9ead558/lib/dry/validation/evaluator.rb:151:in `value'
     # ./app/models/application_contract.rb:7:in `block in <class:ApplicationContract>'
     # /Users/francois/.rvm/gems/ruby-2.7.2/bundler/gems/dry-validation-9b03b9ead558/lib/dry/validation/evaluator.rb:82:in `instance_exec'
     # /Users/francois/.rvm/gems/ruby-2.7.2/bundler/gems/dry-validation-9b03b9ead558/lib/dry/validation/evaluator.rb:82:in `block in initialize'
     # /Users/francois/.rvm/gems/ruby-2.7.2/bundler/gems/dry-validation-9b03b9ead558/lib/dry/validation/evaluator.rb:80:in `each'
     # /Users/francois/.rvm/gems/ruby-2.7.2/bundler/gems/dry-validation-9b03b9ead558/lib/dry/validation/evaluator.rb:80:in `initialize'
     # /Users/francois/.rvm/gems/ruby-2.7.2/bundler/gems/dry-validation-9b03b9ead558/lib/dry/validation/rule.rb:38:in `new'
     # /Users/francois/.rvm/gems/ruby-2.7.2/bundler/gems/dry-validation-9b03b9ead558/lib/dry/validation/rule.rb:38:in `call'
     # /Users/francois/.rvm/gems/ruby-2.7.2/bundler/gems/dry-validation-9b03b9ead558/lib/dry/validation/contract.rb:98:in `block (2 levels) in call'
     # /Users/francois/.rvm/gems/ruby-2.7.2/bundler/gems/dry-validation-9b03b9ead558/lib/dry/validation/contract.rb:95:in `each'
     # /Users/francois/.rvm/gems/ruby-2.7.2/bundler/gems/dry-validation-9b03b9ead558/lib/dry/validation/contract.rb:95:in `block in call'
     # /Users/francois/.rvm/gems/ruby-2.7.2/bundler/gems/dry-validation-9b03b9ead558/lib/dry/validation/result.rb:25:in `new'
     # /Users/francois/.rvm/gems/ruby-2.7.2/bundler/gems/dry-validation-9b03b9ead558/lib/dry/validation/contract.rb:94:in `call'
     # ./spec/people/manually_register_person_contract_spec.rb:59:in `block (3 levels) in <main>'
     # ./spec/rails_helper.rb:23:in `block (3 levels) in <top (required)>'
     # /Users/francois/.rvm/gems/ruby-2.7.2/gems/database_cleaner-sequel-1.8.0/lib/database_cleaner/sequel/transaction.rb:36:in `block in cleaning'
     # /Users/francois/.rvm/gems/ruby-2.7.2/gems/sequel-5.37.0/lib/sequel/database/transactions.rb:251:in `_transaction'
     # /Users/francois/.rvm/gems/ruby-2.7.2/gems/sequel-5.37.0/lib/sequel/database/transactions.rb:233:in `block in transaction'
     # /Users/francois/.rvm/gems/ruby-2.7.2/gems/sequel-5.37.0/lib/sequel/connection_pool/threaded.rb:92:in `hold'
     # /Users/francois/.rvm/gems/ruby-2.7.2/gems/sequel-5.37.0/lib/sequel/database/connecting.rb:270:in `synchronize'
     # /Users/francois/.rvm/gems/ruby-2.7.2/gems/sequel-5.37.0/lib/sequel/database/transactions.rb:195:in `transaction'
     # /Users/francois/.rvm/gems/ruby-2.7.2/gems/database_cleaner-sequel-1.8.0/lib/database_cleaner/sequel/transaction.rb:36:in `cleaning'
     # /Users/francois/.rvm/gems/ruby-2.7.2/gems/database_cleaner-1.8.5/lib/database_cleaner/configuration.rb:87:in `block (2 levels) in cleaning'
     # /Users/francois/.rvm/gems/ruby-2.7.2/gems/database_cleaner-1.8.5/lib/database_cleaner/configuration.rb:88:in `cleaning'
     # ./spec/rails_helper.rb:22:in `block (2 levels) in <top (required)>'

I fixed my immediate problem by using the solution in Calling a macro on a depply nested rule. It would be really nice if dry-validation handled it for us.

francois avatar Oct 15 '20 22:10 francois

So, for that, we want to come up with an explicit key path syntax that points to arrays. It's partially implemented in some places in dry-schema but we really need to properly encapsulate it in Dry::Schema::Path so that parsing and processing such paths is simple. In general, handling key paths must be improved and cleaned up (both in validation and schema), thus I'm scheduling this for 2.0.0.

solnic avatar Nov 11 '20 11:11 solnic