diff --git a/lib/dry/schema/key_validator.rb b/lib/dry/schema/key_validator.rb index e9ad86c6..70acf708 100644 --- a/lib/dry/schema/key_validator.rb +++ b/lib/dry/schema/key_validator.rb @@ -21,7 +21,7 @@ def call(result) input = result.to_h input_paths = key_paths(input) - key_paths = key_map.to_dot_notation + key_paths = key_map.to_dot_notation.sort input_paths.each do |path| error_path = validate_path(key_paths, path) @@ -40,20 +40,31 @@ def call(result) def validate_path(key_paths, path) if path[INDEX_REGEX] key = path.gsub(INDEX_REGEX, BRACKETS) - - if key_paths.none? { paths_match?(key, _1) } + if none_key_paths_match?(key_paths, key) arr = path.gsub(INDEX_REGEX) { ".#{_1[1]}" } arr.split(DOT).map { DIGIT_REGEX.match?(_1) ? Integer(_1, 10) : _1.to_sym } end - elsif key_paths.none? { paths_match?(path, _1) } + elsif none_key_paths_match?(key_paths, path) path end end # @api private - def paths_match?(input_path, key_path) - residue = key_path.sub(input_path, "") - residue.empty? || residue.start_with?(DOT, BRACKETS) + def none_key_paths_match?(key_paths, path) + !any_key_paths_match?(key_paths, path) + end + + # @api private + def any_key_paths_match?(key_paths, path) + find_path(key_paths, path, false) || + find_path(key_paths, path + DOT, true) || + find_path(key_paths, path + BRACKETS, true) + end + + # @api private + def find_path(key_paths, path, prefix_match) + key = key_paths.bsearch { |key_path| key_path >= path } + prefix_match ? key&.start_with?(path) : key == path end # @api private diff --git a/spec/integration/schema/key_searching_algorithm_spec.rb b/spec/integration/schema/key_searching_algorithm_spec.rb new file mode 100644 index 00000000..595cf232 --- /dev/null +++ b/spec/integration/schema/key_searching_algorithm_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +RSpec.describe Dry::Schema, "key searching algorithm" do + it "works properly with keys that are prefixes of other keys" do + schema = Dry::Schema.define do + config.validate_keys = true + + required(:a).filled(:string) + required(:fooA).filled(:string) + required(:foo).array(:hash) do + required(:bar).filled(:string) + end + end + + expect(schema.(a: "string", fooA: "string", foo: "string").errors.to_h) + .to eql({foo: ["must be an array"]}) + end +end