Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* Nested keys are properly handled when generating messages hash (issue #489) (flash-gordon + solnic)
* Result objects support `locale` and `full` options now (solnic)
* Ability to configure `top_namespace` for messages, which will be used for both schema and rule localization (solnic)
* Rule blocks receive a context object that you can use to share data between rules (solnic)

### Changed

Expand Down
6 changes: 5 additions & 1 deletion lib/dry/validation/contract.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'concurrent/map'

require 'dry/equalizer'
require 'dry/initializer'

Expand Down Expand Up @@ -86,10 +88,12 @@ class Contract
# @api public
def call(input)
Result.new(schema.(input), locale: locale) do |result|
context = Concurrent::Map.new

rules.each do |rule|
next if rule.keys.any? { |key| result.error?(key) }

rule.(self, result).failures.each do |failure|
rule.(self, result, context).failures.each do |failure|
result.add_error(message_resolver[failure])
end
end
Expand Down
19 changes: 12 additions & 7 deletions lib/dry/validation/evaluator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,21 @@ def failure(message, tokens = EMPTY_HASH)
end
end

# @!attribute [r] _context
# @!attribute [r] _contract
# @return [Contract]
# @api private
param :_context
param :_contract

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

# @!attribute [r] _context
# @return [Concurrent::Map]
# @api public
option :_context

# @!attribute [r] path
# @return [Dry::Schema::Path]
# @api private
Expand All @@ -79,7 +84,7 @@ def failure(message, tokens = EMPTY_HASH)
# @api private
def initialize(*args, &block)
super(*args)
instance_eval(&block)
instance_exec(_context, &block)
end

# Get failures object for the default or provided path
Expand Down Expand Up @@ -120,18 +125,18 @@ def failures

# @api private
def respond_to_missing?(meth, include_private = false)
super || _context.respond_to?(meth, true)
super || _contract.respond_to?(meth, true)
end

private

# Forward to the underlying context
# Forward to the underlying contract
#
# @api private
def method_missing(meth, *args, &block)
# yes, we do want to delegate to private methods too
if _context.respond_to?(meth, true)
_context.__send__(meth, *args, &block)
if _contract.respond_to?(meth, true)
_contract.__send__(meth, *args, &block)
else
super
end
Expand Down
27 changes: 2 additions & 25 deletions lib/dry/validation/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,25 +94,7 @@ def add_error(error)
#
# @api public
def [](key)
if values.key?(key)
values[key]
elsif storage.key?(key)
storage[key]
end
end

# Store value under specified key
#
# @param [Symbol] key
# @param [Object] value
#
# @return [Object]
#
# @api public
def []=(key, value)
raise ArgumentError, "Key +#{key}+ was already set" if key?(key)

storage[key] = value
values[key]
end

# Check if a key was set
Expand All @@ -123,7 +105,7 @@ def []=(key, value)
#
# @api public
def key?(key)
values.key?(key) || storage.key?(key)
values.key?(key)
end

# Coerce to a hash
Expand Down Expand Up @@ -159,11 +141,6 @@ def initialize_errors(options = self.options)
def schema_errors(options)
values.message_set(options).to_a
end

# @api private
def storage
@storage ||= EMPTY_HASH.dup
end
end
end
end
9 changes: 5 additions & 4 deletions lib/dry/validation/rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ class Rule

# Evaluate the rule within the provided context
#
# @param [Contract] context
# @param [Object] values
# @param [Contract] contract
# @param [Result] result
# @param [Concurrent::Map] context
#
# @api private
def call(context, values)
Evaluator.new(context, values: values, keys: keys, &block)
def call(contract, result, context)
Evaluator.new(contract, values: result, keys: keys, _context: context, &block)
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

RSpec.describe Dry::Validation::Evaluator, 'values writer' do
RSpec.describe Dry::Validation::Evaluator, 'using context' do
context 'when key does not exist' do
subject(:contract) do
Dry::Validation::Contract.build do
Expand All @@ -9,16 +9,16 @@
required(:user_id).filled(:integer)
end

rule(:user_id) do
rule(:user_id) do |ctx|
if values[:user_id].equal?(312)
values[:user] = 'jane'
ctx[:user] = 'jane'
else
key(:user).failure('must be jane')
end
end

rule(:email) do
key.failure('is invalid') if values[:user] == 'jane' && values[:email] != 'jane@doe.org'
rule(:email) do |ctx|
key.failure('is invalid') if ctx[:user] == 'jane' && values[:email] != 'jane@doe.org'
end
end
end
Expand All @@ -28,22 +28,4 @@
expect(contract.(user_id: 312, email: 'john@doe.org').errors.to_h).to eql(email: ['is invalid'])
end
end

context 'when key already exists' do
subject(:contract) do
Dry::Validation::Contract.build do
schema do
required(:email).filled(:string)
end

rule(:email) do
values[:email] = 'foo'
end
end
end

it 'raises error' do
expect { contract.(email: 'jane@doe.org') }.to raise_error(ArgumentError, /email/)
end
end
end
24 changes: 12 additions & 12 deletions spec/integration/evaluator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

RSpec.describe Dry::Validation::Evaluator do
subject(:evaluator) do
Dry::Validation::Evaluator.new(context, options, &block)
Dry::Validation::Evaluator.new(contract, options, &block)
end

let(:context) do
double(:context)
let(:contract) do
double(:contract)
end

let(:options) do
{ keys: [:email], values: values }
{ keys: [:email], values: values, _context: {} }
end

let(:values) do
Expand All @@ -26,23 +26,23 @@
}
end

it 'delegates to the context' do
expect(context).to receive(:works?).and_return(true)
it 'delegates to the contract' do
expect(contract).to receive(:works?).and_return(true)
expect(evaluator.failures[0][:path].to_a).to eql([:email])
expect(evaluator.failures[0][:message]).to eql('it works')
end

describe 'with custom methods defined on the context' do
let(:context) do
double(context: :my_context)
describe 'with custom methods defined on the contract' do
let(:contract) do
double(contract: :my_contract)
end

let(:block) do
proc { key.failure("message with #{context}") }
proc { key.failure("message with #{contract}") }
end

it 'forwards to the context' do
expect(evaluator.failures[0][:message]).to eql('message with my_context')
it 'forwards to the contract' do
expect(evaluator.failures[0][:message]).to eql('message with my_contract')
end
end
end
Expand Down
12 changes: 0 additions & 12 deletions spec/integration/result_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,6 @@
end
end

describe '#[]' do
let(:params) do
double(:params, message_set: [], to_h: {}, key?: false)
end

it 'returns nil for missing values' do
Dry::Validation::Result.new(params) do |r|
expect(r[:missing]).to be nil
end
end
end

describe '#errors' do
subject(:errors) { result.errors }

Expand Down