Passing options along to nested array of hashes


#1

Hi Guys,

Thank you for sharing this great tool!

I have been struggling to validate a hash with an array of hashes which should be checked based on an option.

For instance, if the group is a, then, the content attribute should be A, and if b, then B.

This should be valid:

{ group: "a", list: [{content: "A"}, {content: "A"}] }

And this should not be valid (note the content of the last element of array is B):

{ group: "a", list: [{content: "A"}, {content: "B"}] }

This is how I am think it should be implemented:

require 'rails_helper'

describe "Passing options along to nested array of hashes" do
  let(:base_schema) do
    Dry::Validation.Schema do
      configure do
        def self.messages
          super.merge( en: { errors: { group?: "if group is %{group} then content cannot be %{content}" } } )
        end
      end
    end
  end

  let(:list_schema) do
    Dry::Validation.Schema(base_schema) do
      configure do
        option :group

        def group?(group, content)
          (group == "a" && content == "A") || (group == "b" && content == "B")
        end
      end
      required(:content).filled(group?: group)
    end
  end

  describe "ListSchema (THIS WORKS FINE)" do
    context "Valid input" do
      let(:subject) { list_schema.with(group: "a").call( content: "A" ) }
      it { is_expected.to be_success }
    end
    context "Invalid input" do
      let(:subject) { list_schema.with(group: "a").call( content: "B" ) }
      it { expect(subject.messages).to eq( content: ["if group is a then content cannot be B"] ) }
    end
  end

  describe "MainSchema (THIS DOES NOT WORK)" do
    let(:main_schema) do
      ListSchema ||= list_schema

      Dry::Validation.Schema(base_schema) do
        required(:group).value(included_in?: ["a", "b"])
        required(:list).value(:array?)
        rule list_for_group: [:list, :group] do |list, group|
          group.included_in?(["a", "b"]) > list.each do
            schema(ListSchema.with(group: group))
          end
        end
      end
    end

    context "Valid input" do
      let(:subject) { main_schema.call( group: "a", list: [{ content: "A" }, { content: "A" }] ) }
      it { expect(subject).to be_success }
    end

    context "Invalid input" do
      let(:subject) { main_schema.call( group: "a", list: [{ content: "A" }, { content: "B" }] ) }
      it { expect(subject.messages).to eq( list: { 1 => { content: ["if group is a then content cannot be B"] } } ) }
    end
  end
end

I could not build a MainSchema which makes the last to scenarios to pass. I guess the rule is wrong.

Is there any way to achieve this?

Cheers,
Leandro


#2

I am almost getting there by doing the following:

Dry::Validation.Schema(base_schema) do
  required(:group).value(included_in?: ["a", "b"]).when(included_in?: ["a", "b"]) do
    required(:list).each { schema(ListSchema.with(group: value(:group))) }
  end

  required(:list).value(:array?)
end

but the option is still not passing:

  2) Passing options along to nested array of hashes MainSchema (THIS DOES NOT WORK) Invalid input should eq {:list=>{1=>{:content=>["if group is a then content cannot be B"]}}}
     # No reason given
     Failure/Error: pending { expect(subject.messages).to eq( list: { 1 => { content: ["if group is a then content cannot be B"] } } ) }
     
       expected: {:list=>{1=>{:content=>["if group is a then content cannot be B"]}}}
            got: {:list=>{0=>{:content=>["if group is  then content cannot be A"]}, 1=>{:content=>["if group is  then content cannot be B"]}}}
     
       (compared using ==)

#3

Try something like this:

    Dry::Validation.Schema(base_schema) do
      configure do
        option :group
      end

      required(:content).filled

      validate(:content) do |content|
        (group == "a" && content == "A") || (group == "b" && content == "B")
      end
    end

#4

Thank you, @solnic!

My problem is different though. The code I sent is already able to validate the ‘content’ field on the schema. What I was not able to do is to validate an array of hashes using a wrapper schema.

BTW, I tested your code, and although it validates well, it does not allow to use the option group as an I18n interpolation variable.