Skip to content

Commit da398d0

Browse files
authored
Merge pull request #497 from dry-rb/491-move-storage-out-of-result-object
Add shared context to rules for additional storage
2 parents 75c9b3d + 5ebe53d commit da398d0

File tree

8 files changed

+42
-84
lines changed

8 files changed

+42
-84
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* Nested keys are properly handled when generating messages hash (issue #489) (flash-gordon + solnic)
1010
* Result objects support `locale` and `full` options now (solnic)
1111
* Ability to configure `top_namespace` for messages, which will be used for both schema and rule localization (solnic)
12+
* Rule blocks receive a context object that you can use to share data between rules (solnic)
1213

1314
### Changed
1415

lib/dry/validation/contract.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require 'concurrent/map'
4+
35
require 'dry/equalizer'
46
require 'dry/initializer'
57

@@ -86,10 +88,12 @@ class Contract
8688
# @api public
8789
def call(input)
8890
Result.new(schema.(input), locale: locale) do |result|
91+
context = Concurrent::Map.new
92+
8993
rules.each do |rule|
9094
next if rule.keys.any? { |key| result.error?(key) }
9195

92-
rule.(self, result).failures.each do |failure|
96+
rule.(self, result, context).failures.each do |failure|
9397
result.add_error(message_resolver[failure])
9498
end
9599
end

lib/dry/validation/evaluator.rb

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,21 @@ def failure(message, tokens = EMPTY_HASH)
5454
end
5555
end
5656

57-
# @!attribute [r] _context
57+
# @!attribute [r] _contract
5858
# @return [Contract]
5959
# @api private
60-
param :_context
60+
param :_contract
6161

6262
# @!attribute [r] keys
6363
# @return [Array<String, Symbol, Hash>]
6464
# @api private
6565
option :keys
6666

67+
# @!attribute [r] _context
68+
# @return [Concurrent::Map]
69+
# @api public
70+
option :_context
71+
6772
# @!attribute [r] path
6873
# @return [Dry::Schema::Path]
6974
# @api private
@@ -79,7 +84,7 @@ def failure(message, tokens = EMPTY_HASH)
7984
# @api private
8085
def initialize(*args, &block)
8186
super(*args)
82-
instance_eval(&block)
87+
instance_exec(_context, &block)
8388
end
8489

8590
# Get failures object for the default or provided path
@@ -120,18 +125,18 @@ def failures
120125

121126
# @api private
122127
def respond_to_missing?(meth, include_private = false)
123-
super || _context.respond_to?(meth, true)
128+
super || _contract.respond_to?(meth, true)
124129
end
125130

126131
private
127132

128-
# Forward to the underlying context
133+
# Forward to the underlying contract
129134
#
130135
# @api private
131136
def method_missing(meth, *args, &block)
132137
# yes, we do want to delegate to private methods too
133-
if _context.respond_to?(meth, true)
134-
_context.__send__(meth, *args, &block)
138+
if _contract.respond_to?(meth, true)
139+
_contract.__send__(meth, *args, &block)
135140
else
136141
super
137142
end

lib/dry/validation/result.rb

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -94,25 +94,7 @@ def add_error(error)
9494
#
9595
# @api public
9696
def [](key)
97-
if values.key?(key)
98-
values[key]
99-
elsif storage.key?(key)
100-
storage[key]
101-
end
102-
end
103-
104-
# Store value under specified key
105-
#
106-
# @param [Symbol] key
107-
# @param [Object] value
108-
#
109-
# @return [Object]
110-
#
111-
# @api public
112-
def []=(key, value)
113-
raise ArgumentError, "Key +#{key}+ was already set" if key?(key)
114-
115-
storage[key] = value
97+
values[key]
11698
end
11799

118100
# Check if a key was set
@@ -123,7 +105,7 @@ def []=(key, value)
123105
#
124106
# @api public
125107
def key?(key)
126-
values.key?(key) || storage.key?(key)
108+
values.key?(key)
127109
end
128110

129111
# Coerce to a hash
@@ -159,11 +141,6 @@ def initialize_errors(options = self.options)
159141
def schema_errors(options)
160142
values.message_set(options).to_a
161143
end
162-
163-
# @api private
164-
def storage
165-
@storage ||= EMPTY_HASH.dup
166-
end
167144
end
168145
end
169146
end

lib/dry/validation/rule.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ class Rule
2525

2626
# Evaluate the rule within the provided context
2727
#
28-
# @param [Contract] context
29-
# @param [Object] values
28+
# @param [Contract] contract
29+
# @param [Result] result
30+
# @param [Concurrent::Map] context
3031
#
3132
# @api private
32-
def call(context, values)
33-
Evaluator.new(context, values: values, keys: keys, &block)
33+
def call(contract, result, context)
34+
Evaluator.new(contract, values: result, keys: keys, _context: context, &block)
3435
end
3536
end
3637
end
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
RSpec.describe Dry::Validation::Evaluator, 'values writer' do
3+
RSpec.describe Dry::Validation::Evaluator, 'using context' do
44
context 'when key does not exist' do
55
subject(:contract) do
66
Dry::Validation::Contract.build do
@@ -9,16 +9,16 @@
99
required(:user_id).filled(:integer)
1010
end
1111

12-
rule(:user_id) do
12+
rule(:user_id) do |ctx|
1313
if values[:user_id].equal?(312)
14-
values[:user] = 'jane'
14+
ctx[:user] = 'jane'
1515
else
1616
key(:user).failure('must be jane')
1717
end
1818
end
1919

20-
rule(:email) do
21-
key.failure('is invalid') if values[:user] == 'jane' && values[:email] != '[email protected]'
20+
rule(:email) do |ctx|
21+
key.failure('is invalid') if ctx[:user] == 'jane' && values[:email] != '[email protected]'
2222
end
2323
end
2424
end
@@ -28,22 +28,4 @@
2828
expect(contract.(user_id: 312, email: '[email protected]').errors.to_h).to eql(email: ['is invalid'])
2929
end
3030
end
31-
32-
context 'when key already exists' do
33-
subject(:contract) do
34-
Dry::Validation::Contract.build do
35-
schema do
36-
required(:email).filled(:string)
37-
end
38-
39-
rule(:email) do
40-
values[:email] = 'foo'
41-
end
42-
end
43-
end
44-
45-
it 'raises error' do
46-
expect { contract.(email: '[email protected]') }.to raise_error(ArgumentError, /email/)
47-
end
48-
end
4931
end

spec/integration/evaluator_spec.rb

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44

55
RSpec.describe Dry::Validation::Evaluator do
66
subject(:evaluator) do
7-
Dry::Validation::Evaluator.new(context, options, &block)
7+
Dry::Validation::Evaluator.new(contract, options, &block)
88
end
99

10-
let(:context) do
11-
double(:context)
10+
let(:contract) do
11+
double(:contract)
1212
end
1313

1414
let(:options) do
15-
{ keys: [:email], values: values }
15+
{ keys: [:email], values: values, _context: {} }
1616
end
1717

1818
let(:values) do
@@ -26,23 +26,23 @@
2626
}
2727
end
2828

29-
it 'delegates to the context' do
30-
expect(context).to receive(:works?).and_return(true)
29+
it 'delegates to the contract' do
30+
expect(contract).to receive(:works?).and_return(true)
3131
expect(evaluator.failures[0][:path].to_a).to eql([:email])
3232
expect(evaluator.failures[0][:message]).to eql('it works')
3333
end
3434

35-
describe 'with custom methods defined on the context' do
36-
let(:context) do
37-
double(context: :my_context)
35+
describe 'with custom methods defined on the contract' do
36+
let(:contract) do
37+
double(contract: :my_contract)
3838
end
3939

4040
let(:block) do
41-
proc { key.failure("message with #{context}") }
41+
proc { key.failure("message with #{contract}") }
4242
end
4343

44-
it 'forwards to the context' do
45-
expect(evaluator.failures[0][:message]).to eql('message with my_context')
44+
it 'forwards to the contract' do
45+
expect(evaluator.failures[0][:message]).to eql('message with my_contract')
4646
end
4747
end
4848
end

spec/integration/result_spec.rb

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,6 @@
1515
end
1616
end
1717

18-
describe '#[]' do
19-
let(:params) do
20-
double(:params, message_set: [], to_h: {}, key?: false)
21-
end
22-
23-
it 'returns nil for missing values' do
24-
Dry::Validation::Result.new(params) do |r|
25-
expect(r[:missing]).to be nil
26-
end
27-
end
28-
end
29-
3018
describe '#errors' do
3119
subject(:errors) { result.errors }
3220

0 commit comments

Comments
 (0)