Add initial rules context interface
This commit is contained in:
parent
8408598a89
commit
63f75e5da8
|
@ -10,3 +10,5 @@ Gemfile.lock
|
|||
doc
|
||||
.yardoc/
|
||||
.vscode
|
||||
.ruby-version
|
||||
.ruby-gemset
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue