dry-rb / dry-schema

Coercion and validation for data structures
https://dry-rb.org/gems/dry-schema
MIT License
415 stars 108 forks source link

array(:hash) not validating a filter #448

Open ritxi opened 1 year ago

ritxi commented 1 year ago

Describe the bug

I have the following schema that works in isolation, but not inside an array(:hash) with a required field with a filter that won't work

To Reproduce

# Working in isolation
schema = Dry::Schema.Params do
    required(:date).filter { 
      format?(%r[\d{2}/\d{2}/\d{4}])
    }.value(:date)
end

schema.call(id: 'foo', date: '2022/05/02')

#<Dry::Schema::Result{:date=>"2022/05/02"} errors={:date=>["is in invalid format"]} path=[]>

# Not working when inside `array(:hash)`

schema = Dry::Schema.Params do
  required(:group).array(:hash) do
    required(:id).value(:integer)
    required(:date).filter { 
      format?(%r[\d{2}/\d{2}/\d{4}])
    }.value(:date)
  end
end
schema.call(group: [{id: 1, date: '2022/05/02'}])

#<Dry::Schema::Result{:group=>[{:id=>1, :date=>Mon, 02 May 2022}]} errors={} path=[]>

Expected behavior

I expect filter validation to work

#<Dry::Schema::Result{:group=>[{:id=>1, :date=>"2022/05/02"}]} errors={:group=>{0=>{:date=>["is in invalid format"]}}} path=[]>

My environment

ritxi commented 1 year ago

I've tested with the latest version

irb(main):008:1* schema = Dry::Schema.Params do
irb(main):009:2*   required(:group).array(:hash) do
irb(main):010:2*     required(:id).value(:integer)
irb(main):011:3*     required(:date).filter {
irb(main):012:3*       format?(%r[\d{2}/\d{2}/\d{4}])
irb(main):013:2*       }.value(:date)
irb(main):014:1*   end
irb(main):015:0> end
=> #<Dry::Schema::Params keys=[["group", ["id", "date"]]] rules={:group=>"key?(:group) AND key[group](array? AND each(hash? AND hash? AND set(key?(:id) AND key[id](int?), key?(:date) AND key[dat...
irb(main):016:0> schema.call(group: [{id: 1, date: '2022/05/02'}])
=> #<Dry::Schema::Result{:group=>[{:id=>1, :date=>#<Date: 2022-05-02 ((2459702j,0s,0n),+0s,2299161j)>}]} errors={} path=[]>
irb(main):017:0> require "dry/schema/version"
=> true
irb(main):018:0> Dry::Schema::VERSION
=> "1.13.0"
x2es commented 9 months ago

Describe the bug

Confirming exact the same issue:

I have the following schema that works in isolation, but not inside an array(:hash) with a field with a filter that won't work

Test suite provided below

To Reproduce

https://gist.github.com/x2es/ed6d401bd02698f48e9bf927120e0eb5

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem "dry-schema"
  gem "awesome_print"
  gem "minitest"
end

require 'minitest/autorun'

DocumentSchema = Dry::Schema.JSON do
  optional(:date).filter(format?: /^\d{4}-\d{2}-\d{2}$/).filled(:date)
end

DocumentsSchema = Dry::Schema.JSON do
  required(:documents).array(DocumentSchema)
end

module Sample
  VALID_DOCUMENT = { date: "2023-01-01" }
  INVALID_FORMAT = { date: "20239-01-01" }
  INVALID_DATE =   { date: "2023-38-01" }
  FULL_LIST = {
    documents: [
      VALID_DOCUMENT, # 0
      INVALID_FORMAT, # 1
      INVALID_DATE    # 2
    ]
  }
  FORMAT_LIST = {
    documents: [
      VALID_DOCUMENT, # 0
      INVALID_FORMAT, # 1
    ]
  }
  DATE_LIST = {
    documents: [
      VALID_DOCUMENT, # 0
      INVALID_DATE,   # 1
    ]
  }
end

class DateTest < Minitest::Test
  def test_format
    expected = {:date=>["is in invalid format"]}
    assert_equal expected, DocumentSchema.call(Sample::INVALID_FORMAT).errors.to_h
  end

  def test_date
    expected = {:date=>["must be a date"]}
    assert_equal expected, DocumentSchema.call(Sample::INVALID_DATE).errors.to_h
  end

  # UNEXPECTED BEHAVIOUR
  # and inconsistent with `DocumentSchema.call(Sample::INVALID_FORMAT)`
  #   2) Failure:
  # DateTest#test_format_list [dry_schema_array_hash_filter_issue.rb:68]:
  # --- expected
  # +++ actual
  # @@ -1 +1 @@
  # -{:documents=>{1=>{:date=>["is in invalid format"]}}}
  # +{}
  def test_format_list
    expected = {:documents=>{1=>{:date=>["is in invalid format"]}}}
    assert_equal expected, DocumentsSchema.call(Sample::FORMAT_LIST).errors.to_h
  end

  def test_date_list
    expected = {:documents=>{1=>{:date=>["must be a date"]}}}
    assert_equal expected, DocumentsSchema.call(Sample::DATE_LIST).errors.to_h
  end

  #   1) Failure:
  # DateTest#test_full_list [dry_schema_array_hash_filter_issue.rb:83]:
  # --- expected
  # +++ actual
  # @@ -1 +1 @@
  # -{:documents=>{1=>{:date=>["is in invalid format"]}, 2=>{:date=>["must be a date"]}}}
  # +{:documents=>{1=>{:date=>["must be a date"]}}}
  def test_full_list
    expected = {
      :documents=>{
        1=>{:date=>["is in invalid format"]},
        2=>{:date=>["must be a date"]}
      }
    }
    assert_equal(expected, DocumentsSchema.call(Sample::DATE_LIST).errors.to_h)
  end
end

# {
#       :full_list => #<Dry::Schema::Result{:documents=>[{:date=>#<Date: 2023-01-01 ((2459946j,0s,0n),+0s,2299161j)>}, # {:date=>#<Date: 20239-01-01 ((9113203j,0s,0n),+0s,2299161j)>}, {:date=>"2023-38-01"}]} errors={:documents=>{2=># {:date=>["must be a date"]}}} path=[]>,
#     :format_list => #<Dry::Schema::Result{:documents=>[{:date=>#<Date: 2023-01-01 ((2459946j,0s,0n),+0s,2299161j)>}, {:date=>#<Date: 20239-01-01 ((9113203j,0s,0n),+0s,2299161j)>}]} errors={} path=# []>,
#       :date_list => #<Dry::Schema::Result{:documents=>[{:date=>#<Date: 2023-01-01 ((2459946j,0s,0n),+0s,2299161j)>}, {:date=>"2023-38-01"}]} errors={:documents=>{1=>{:date=>["must be a date"]}}} path=[]>,
#          :format => #<Dry::Schema::Result{:date=>"20239-01-01"} errors={:date=>["is in invalid format"]} path=[]>,
#            :date => #<Dry::Schema::Result{:date=>"2023-38-01"} errors={:date=>["must be a date"]} path=[]>
# }
ap ({
  full_list:   DocumentsSchema.call(Sample::FULL_LIST),
  format_list: DocumentsSchema.call(Sample::FORMAT_LIST),
  date_list:   DocumentsSchema.call(Sample::DATE_LIST),

  format:      DocumentSchema.call(Sample::INVALID_FORMAT),
  date:        DocumentSchema.call(Sample::INVALID_DATE)
})

Expected behavior

DocumentSchema.call(Sample::INVALID_FORMAT) expected to be consistent with DocumentsSchema.call(Sample::FORMAT_LIST):

Actual output

Pay attention to :format, :format_list and :full_list outputs

{
      :full_list => #<Dry::Schema::Result{:documents=>[{:date=>#<Date: 2023-01-01 ((2459946j,0s,0n),+0s,2299161j)>}, {:date=>#<Date: 20239-01-01 ((9113203j,0s,0n),+0s,2299161j)>}, {:date=>"2023-38-01"}]} errors={:documents=>{2=>{:date=>["must be a date"]}}} path=[]>,
    :format_list => #<Dry::Schema::Result{:documents=>[{:date=>#<Date: 2023-01-01 ((2459946j,0s,0n),+0s,2299161j)>}, {:date=>#<Date: 20239-01-01 ((9113203j,0s,0n),+0s,2299161j)>}]} errors={} path=[]>,
      :date_list => #<Dry::Schema::Result{:documents=>[{:date=>#<Date: 2023-01-01 ((2459946j,0s,0n),+0s,2299161j)>}, {:date=>"2023-38-01"}]} errors={:documents=>{1=>{:date=>["must be a date"]}}} path=[]>,
         :format => #<Dry::Schema::Result{:date=>"20239-01-01"} errors={:date=>["is in invalid format"]} path=[]>,
           :date => #<Dry::Schema::Result{:date=>"2023-38-01"} errors={:date=>["must be a date"]} path=[]>
}
Run options: --seed 58415

# Running:

F..F.

Finished in 0.007960s, 628.1263 runs/s, 628.1263 assertions/s.

  1) Failure:
DateTest#test_full_list [dry_schema_array_hash_filter_issue.rb:83]:
--- expected
+++ actual
@@ -1 +1 @@
-{:documents=>{1=>{:date=>["is in invalid format"]}, 2=>{:date=>["must be a date"]}}}
+{:documents=>{1=>{:date=>["must be a date"]}}}

  2) Failure:
DateTest#test_format_list [dry_schema_array_hash_filter_issue.rb:68]:
--- expected
+++ actual
@@ -1 +1 @@
-{:documents=>{1=>{:date=>["is in invalid format"]}}}
+{}

5 runs, 5 assertions, 2 failures, 0 errors, 0 skips

My environment