dry-schema for nested attributes

I want to use dry-rails and dry-schema (“safe_params”) in my Ruby on Rails app.

I have the following params for my controller action and I want to define a schema for this:

input = {
  "id"=>"asdf",
  "collection"=>{
    "name"=>"My Collection",
    "description"=>"My Description",
    "users_attributes"=>{
      "0"=>{"user_id"=>"1", "owner"=>"1", "editor"=>"1", "_destroy"=>"0"},
      "2"=>{"user_id"=>"223", "owner"=>"0", "editor"=>"0", "_destroy"=>"0"},
      "3"=>{"user_id"=>"312332", "owner"=>"1", "editor"=>"1", "_destroy"=>"0"},
      "4"=>{"user_id"=>"44", "owner"=>"0", "editor"=>"0", "_destroy"=>"1"},
    }
  }
}

the issue is “users_attributes”, as it contains an arbitrary list of keys, which again have a well-formed value.

How to do this the smart way?

This is the beast I came up with:

Schema = Dry::Schema.Params do
  before(:value_coercer) do |result|
    result.to_h.tap do |hash|
      if hash[:collection][:users_attributes].nil?
        hash[:collection][:users_attributes] = []
      else
        hash[:collection][:users_attributes] = hash[:collection][:users_attributes].values.map(&:symbolize_keys)
      end
      # puts "hash[:collection][:users_attributes]: #{hash[:collection][:users_attributes]}"
    end
  end

  after(:rule_applier) do |result|
    puts "result: #{result.inspect}"

    result.to_h.tap do |hash|

      # hash[:collection][:users_attributes] is an array of hashes
      # Convert it to a hash of hashes where the key is the index of the array
      hash[:collection][:users_attributes] = hash[:collection][:users_attributes].map.with_index do |user_attributes, index|
        [index, user_attributes]
      end.to_h

      # puts "hash[:collection][:users_attributes]: #{hash[:collection][:users_attributes]}"
    end
  end

  required(:id).filled(:string) # uuid
  required(:collection).schema do
    optional(:name).maybe(:string)
    optional(:description).maybe(:string)
    optional(:users_attributes).array(:hash) do
      optional(:user_id).maybe(:integer)
      optional(:owner).maybe(:bool)
      optional(:editor).maybe(:bool)
      optional(:_destroy).maybe(:bool)
    end
  end
end