Creating nested/reusable validators in Ruby with dry-validation

1.3k views Asked by At

Let's say I want to set up a validation contract for addresses, but then I also want to set up a validator for users, and for coffee shops; both of which include an address, is it possible to re-use the AddressContract in UserContract and CoffeeShopContract?

For example, the data I want to validate might look like:

# Address
{
    "first_line": "100 Main street",
    "zipcode": "12345",
}

# User
{
    "first_name": "Joe",
    "last_name": "Bloggs",
    "address:" {
        "first_line": "123 Boulevard",
        "zipcode": "12346",
    }
}

# Coffee Shop
{
    "shop": "Central Perk",
    "floor_space": "2000sqm",
    "address:" {
        "first_line": "126 Boulevard",
        "zipcode": "12347",
    }
}
2

There are 2 answers

5
engineersmnky On BEST ANSWER

Yes you can reuse schemas (See: Reusing Schemas)

It would look something like this:

require 'dry/validation'
class AddressContract < Dry::Validation::Contract 
  params do 
    required(:first_line).value(:string) 
    required(:zipcode).value(:string) 
  end
end

class UserContract < Dry::Validation::Contract 
  params do
    required(:first_name).value(:string)
    required(:last_name).value(:string)
    required(:address).schema(AddressContract.schema)
  end
end 


a = {first_line: '123 Street Rd'}
u = {first_name: 'engineers', last_name: 'mnky', address: a }

AddressContract.new.(a)
#=> #<Dry::Validation::Result{:first_line=>"123 Street Rd"} errors={:zipcode=>["is missing"]}>
UserContract.new.(u)
#=> #<Dry::Validation::Result{:first_name=>"engineers", :last_name=>"mnky", :address=>{:first_line=>"123 Street Rd"}} errors={:address=>{:zipcode=>["is missing"]}}>

Alternatively you can create schema mixins as well e.g.

AddressSchema = Dry::Schema.Params do 
  required(:first_line).value(:string) 
  required(:zipcode).value(:string) 
end 

class AddressContract < Dry::Validation::Contract 
    params(AddressSchema) 
end

class UserContract < Dry::Validation::Contract 
  params do
    required(:first_name).value(:string)
    required(:last_name).value(:string)
    required(:address).schema(AddressSchema)
  end
end 
0
svelandiag On

I had that same issue when I was upgrading my dry-validations from 0.13 to 1.x version, it caused me a lot of headaches.

Currently today (2024 when I'm writing this) that's not supported, you can reuse schemas in nested data but you can't reuse rules in nested data.

The only solution I could find is to use the following macro, I wrote this macro in my AppContract:

register_macro(:contract) do |macro:|
    if key? && value.present?
      contract_instance = macro.args[0].new
      contract_result   = contract_instance.call(value)
      unless contract_result.success?
        errors = contract_result.errors
        errors.each do |error|
          key(key.path.to_a + error.path).failure(error.text)
        end
      end
    end
  end

The above solved the problem. Here are some examples of how to use it:

# For nested hashes
params do
 # We validate the schema for the nested hash
 required(:my_param).hash(MyOtherContract.schema)
end

# Now we validate the rules for the nested hash using the macro
rule(:my_param).validate(contract: MyOtherContract)

# -------------------------
# For a nested Array of hashes
params do
 required(:my_param).array(:hash, MyOtherContract.schema)
end

rule(:my_param).each(contract: MyOtherContract)