Add initial rules context interface

This commit is contained in:
Arkadiy Zabazhanov 2020-11-11 18:35:09 +03:00
parent 8408598a89
commit 63f75e5da8
No known key found for this signature in database
GPG Key ID: 4889C29D08ADE53A
4 changed files with 82 additions and 5 deletions

2
.gitignore vendored
View File

@ -10,3 +10,5 @@ Gemfile.lock
doc
.yardoc/
.vscode
.ruby-version
.ruby-gemset

View File

@ -260,6 +260,51 @@ PersonContract.new.call(email: 'bar', name: 'foo').errors.to_h
# {name: ['name rule error'], email: ['email rule error']}
```
### Rules context
Rules context is convenient for sharing data between rules or return data in the validation result. It can be used, for example, to return the data that was fetched from DB.
For example:
```ruby
class UpdateUserContract < Dry::Validation::Contract
option :user_repo
params do
required(:user_id).filled(:string)
end
rule(:user_id) do |context:|
context[:user] ||= user_repo.find(value)
key.failure(:not_found) unless context[:user]
end
end
contract = UserContract.new(address_repo: UserRepo.new)
contract.call(user_id: 42).context.each.to_h
# => {user: #<User id: 42>}
```
Initial context can be passed to the contract and in this case, the contract is not going to fetch user from the repo (we don't even need to pass the repo instance as a dependency because this code will not be executed here):
```ruby
user = UserRepo.new.find(42)
contract = UserContract.new
contract.call({user_id: 42}, context: {user: user}).context.each.to_h
# => {user: #<User id: 42>}
```
Also, defualt context can be provided on contract initialization:
```ruby
user = UserRepo.new.find(42)
contract = UserContract.new(default_context: {user: user})
contract.call(user_id: 42).context.each.to_h
# => {user: #<User id: 42>}
```
Context passed to the `call` method overrides keys from `default_context`.
### Defining a rule for each element of an array
To check each element of an array you can simply use `Rule#each` shortcut. It works just like a normal rule, which means it's only applied when a value passed schema checks and supports setting failure messages in the standard way.

View File

@ -68,6 +68,11 @@ module Dry
# @api public
option :macros, default: -> { config.macros }
# @!attribute [r] default_context
# @return [Hash] Default context for rules
# @api public
option :default_context, default: -> { EMPTY_HASH }
# @!attribute [r] schema
# @return [Dry::Schema::Params, Dry::Schema::JSON, Dry::Schema::Processor]
# @api private
@ -86,12 +91,18 @@ module Dry
# Apply the contract to an input
#
# @param [Hash] input The input to validate
# @param [Hash] context Initial context for rules
#
# @return [Result]
#
# @api public
def call(input)
Result.new(schema.(input), Concurrent::Map.new) do |result|
def call(input, context: EMPTY_HASH)
context_map = Concurrent::Map.new.tap do |map|
default_context.each { |key, value| map[key] = value }
context.each { |key, value| map[key] = value }
end
Result.new(schema.(input), context_map) do |result|
rules.each do |rule|
next if rule.keys.any? { |key| error?(result, key) }

View File

@ -6,8 +6,10 @@ RSpec.describe Dry::Validation::Evaluator, "using context" do
end
context "when key does not exist" do
subject(:contract) do
Dry::Validation.Contract do
subject(:contract) { contract_class.new }
let(:contract_class) do
Class.new(Dry::Validation::Contract) do
schema do
required(:email).filled(:string)
required(:user_id).filled(:integer)
@ -33,7 +35,24 @@ RSpec.describe Dry::Validation::Evaluator, "using context" do
end
it "exposes context in result" do
expect(contract.(user_id: 312, email: "jane@doe.org").context[:user]).to eql("jane")
expect(contract.(user_id: 312, email: "jane@doe.org").context.each.to_h).to eql(user: "jane")
end
it "uses the initial context" do
expect(contract.({user_id: 312}, context: {name: "John"}).context.each.to_h)
.to eql(user: "jane", name: "John")
end
context "when default context is defined" do
subject(:contract) do
contract_class.new(default_context: {user: "Redefined", name: "Redefined", details: "Present"})
end
it "initial context redefines it" do
expect(contract.({user_id: 312}, context: {name: "John"}).context.each.to_h)
.to eql(user: "jane", name: "John", details: "Present")
end
end
end
end