Open
Description
Describe the bug
When dealing with predicates that are nested within a hash, either they all succeed or all fail.
To Reproduce
# frozen_string_literal: true
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'dry-validation'
gem 'rspec'
end
require 'rspec/autorun'
class RecurringTransferContract < Dry::Validation::Contract
DATE_ISO8601_REGEX = /\A\d{4}-\d{2}-\d{2}\z/
params do
required(:recurring_transfer).hash do
required(:first_execution_date).filter(format?: DATE_ISO8601_REGEX).value(:date)
optional(:last_execution_date).filter(format?: DATE_ISO8601_REGEX).value(:date)
end
end
rule(recurring_transfer: :first_execution_date) do
key.failure({code: "i_am_a_string", text: "It is a string instead of a date"}) if value.is_a?(String)
end
RSpec.describe RecurringTransferContract do
it "succeeds when both dates are valid" do
contract = described_class.new.call(recurring_transfer: {first_execution_date: "2025-01-01", last_execution_date: "2025-11-03"})
expect(contract).to be_success
end
it "raises an error when last_execution_date is not passing the filter predicate" do
contract = described_class.new.call(recurring_transfer: {first_execution_date: "2025-01-01", last_execution_date: ""})
expect(contract).to be_failure
expect(contract.errors.to_h[:recurring_transfer]).to eq(
{
first_execution_date: [{code: "i_am_a_string", text: "It is a string instead of a date"}],
last_execution_date: ["is in invalid format"],
}
)
end
end
end
Running the test above fails because of first_execution_date: [{text: "It is a string instead of a date", code: "i_am_a_string"}]
Expected behavior
My expectation would be that first_execution_date
is always casted to a Date
since it is passing the filter predicate.
However, if you you move the predicates to the top level, it works as expected
# frozen_string_literal: true
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'dry-validation'
gem 'rspec'
end
require 'rspec/autorun'
class RecurringTransferContract < Dry::Validation::Contract
DATE_ISO8601_REGEX = /\A\d{4}-\d{2}-\d{2}\z/
params do
required(:first_execution_date).filter(format?: DATE_ISO8601_REGEX).value(:date)
optional(:last_execution_date).filter(format?: DATE_ISO8601_REGEX).value(:date)
end
rule(:first_execution_date) do
key.failure({code: "i_am_a_string", text: "It is a string instead of a date"}) if value.is_a?(String)
end
RSpec.describe RecurringTransferContract do
it "succeeds when both dates are valid" do
contract = described_class.new.call({first_execution_date: "2025-01-01", last_execution_date: "2025-11-03"})
expect(contract).to be_success
end
it "raises an error when last_execution_date is not passing the filter predicate" do
contract = described_class.new.call({first_execution_date: "2025-01-01", last_execution_date: ""})
expect(contract).to be_failure
expect(contract.errors.to_h).to eq(
{
last_execution_date: ["is in invalid format"]
}
)
end
end
end
My environment
- Affects my production application: YES
- Ruby version:
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]
- OS:
MacOS