diff --git a/lib/dry/validation/contract/class_interface.rb b/lib/dry/validation/contract/class_interface.rb index b4992e3c..6c035487 100644 --- a/lib/dry/validation/contract/class_interface.rb +++ b/lib/dry/validation/contract/class_interface.rb @@ -158,11 +158,13 @@ def ensure_valid_keys(*keys) valid_paths = key_map.to_dot_notation key_paths = key_paths(keys) - invalid_keys = key_paths.map { |(key, path)| - unless valid_paths.any? { |vp| vp.include?(path) || vp.include?("#{path}[]") } + invalid_keys = key_paths.filter_map { |(key, path)| + if valid_paths.none? { |vp| + vp == path || vp.start_with?("#{path}.", "#{path}[]") + } key end - }.compact.uniq + }.uniq return if invalid_keys.empty? diff --git a/spec/integration/contract/class_interface/rule_spec.rb b/spec/integration/contract/class_interface/rule_spec.rb index 4910c204..81d8a3c3 100644 --- a/spec/integration/contract/class_interface/rule_spec.rb +++ b/spec/integration/contract/class_interface/rule_spec.rb @@ -167,6 +167,74 @@ def self.name end end + context "when keys are prefixes of valid keys" do + it "raises error with a list of symbol keys" do + expect { contract_class.rule(:details, :addres) } + .to raise_error( + Dry::Validation::InvalidKeysError, + "TestContract.rule specifies keys that are not defined by the schema: [:addres]" + ) + end + + it "raises error with a hash path" do + expect { contract_class.rule(details: :addres) } + .to raise_error( + Dry::Validation::InvalidKeysError, + "TestContract.rule specifies keys that are not defined by the schema: [{:details=>:addres}]" + ) + end + + it "raises error with a dot notation" do + expect { contract_class.rule("details.addres") } + .to raise_error( + Dry::Validation::InvalidKeysError, + 'TestContract.rule specifies keys that are not defined by the schema: ["details.addres"]' + ) + end + + it "raises error with a hash path with multiple nested keys" do + expect { contract_class.rule(details: %i[addres]) } + .to raise_error( + Dry::Validation::InvalidKeysError, + "TestContract.rule specifies keys that are not defined by the schema: [{:details=>[:addres]}]" + ) + end + end + + context "when keys are suffixes of valid keys" do + it "raises error with a list of symbol keys" do + expect { contract_class.rule(:etails, :address) } + .to raise_error( + Dry::Validation::InvalidKeysError, + "TestContract.rule specifies keys that are not defined by the schema: [:etails, :address]" + ) + end + + it "raises error with a hash path" do + expect { contract_class.rule(etails: :address) } + .to raise_error( + Dry::Validation::InvalidKeysError, + "TestContract.rule specifies keys that are not defined by the schema: [{:etails=>:address}]" + ) + end + + it "raises error with a dot notation" do + expect { contract_class.rule("etails.address") } + .to raise_error( + Dry::Validation::InvalidKeysError, + 'TestContract.rule specifies keys that are not defined by the schema: ["etails.address"]' + ) + end + + it "raises error with a hash path with multiple nested keys" do + expect { contract_class.rule(etails: %i[address]) } + .to raise_error( + Dry::Validation::InvalidKeysError, + "TestContract.rule specifies keys that are not defined by the schema: [{:etails=>[:address]}]" + ) + end + end + describe "abstract contract" do let(:abstract_contract) do Class.new(Dry::Validation::Contract) do