Ability to apply rules to external schema

Is there a trick to making rules work when a rule path refers to a key in re-used schema?

AddressSchema = Dry::Schema.Params do
  required(:country).value(:string)
  required(:zipcode).value(:string)
  required(:street).value(:string)
end

ContactSchema = Dry::Schema.Params do
  required(:email).value(:string) 
  required(:mobile).value(:string)
end

class NewUserContract < Dry::Validation::Contract
  params(AddressSchema, ContactSchema) do
    required(:name).value(:string)
    required(:age).value(:integer) 
  end

  rule(:country) { key.failure('must be foo') unless value != 'foo' }
end

If I do something similar to above, I get a Dry::Validation::InvalidKeysError stating that the rule specifies keys that are not found in schema.

This is a bug. Please report.

Will do!

@solnic I have a better example/test which is similar to my schema/rule use case. The first test fails but the second passes fine. As I was trying to debug, I realized that the fix will require a change in dry-schema due to what #key_map returns in ClassInterface.

I really like this gem and would like to help and contribute for a fix but I am not sure 100% sure what would be the best course of action. Could you give me some guidance?

  context 'when schema is nested and reused' do
    let(:contract_class) do
      Class.new(Dry::Validation::Contract) do
        def self.name
          "TestContract"
        end

        UserSchema = Dry::Schema.Params do
          required(:email).filled(:string)
          optional(:login).filled(:string)
          optional(:details).hash do
            optional(:address).hash do
              required(:street).value(:string)
            end
          end
        end

        IdentifierSchema = Dry::Schema.Params do
          required(:external_id).filled(:string)
          optional(:aternate_id).filled(:string)
        end

        UserRequestSchema = Dry::Schema.Params do
          required(:user).hash do
            IdentifierSchema
            UserSchema
          end
        end

        params(UserRequestSchema)
      end
    end

    context 'when the rule being applied to a key is in a reused nested schema' do
      let(:request) { { user: { external_id: '12345abc', email: 'jane@doe.org', login: 'ab'} }}

      before do
        contract_class.rule(user: :login) do
          key.failure("is too short") if values[user: :login].size < 3
        end
      end

      it 'applies the rule when passed schema checks' do
        expect(contract.(request).errors.to_h)
          .to eql(user: { login: ["is too short"] })
      end
    end

    context 'when schema has no rules' do
      let(:request) { { user: { external_id: '12345abc', email: 'jane@doe.org' } } }

      it 'validates as successful' do
        expect(contract.(request).success?).to eq true
      end
    end
  end

Update: I tried making the schema structure for UserRequestSchema to directly use the schema defined in IdentifiersSchema and UserSchema and I can make the tests that I wrote above pass.

        UserRequestSchema = Dry::Schema.Params do
          required(:user).hash do
            required(:external_id).filled(:string)
            optional(:aternate_id).filled(:string)
            required(:email).filled(:string)
            optional(:login).filled(:string)
            optional(:details).hash do
              optional(:address).hash do
                required(:street).value(:string)
              end
            end
          end
        end

Ideally I would like to be able to combine schemas within another schema along with rules so that I can consolidate schemas that are shared across multiple requests. (IdentifiersSchema)

This is not gonna work. Block-based syntax uses whatever the block returns, which in this case is UserSchema so IdentifierSchema is simply ignored.

Try this:

required(:user).hash(IdentifierSchema & UserSchema)
1 Like