Search code examples
rubydry-rbdry-validation

Injecting dynamic values for a specific schema to validate against


I have the following yaml file that I would like to validate using dry-rb.

---
environment: # required
  test: # should be dynamic
    service_credentials: "test_credentials" # required
  stage:
    service_credentials: "stage_credentials"
  prod:
    service_credentials: "prod_credentials"

My goal is to create a schema that can validate the above configuration using dynamic fields. In particular, those dynamic values, like test, stage, etc., can be injected or not. In the first case, the validation should fail if the dynamic field is not present.

The first attempt was to create a contract like the following but I think is not the best solution.

class EnvironmentContract < Dry::Validation::Contract
  params do 
    required(:environment).hash do
      optional(:test).hash do
        required(:service_credentials).filled(:str?)
      end
      optional(:preprod).hash do
          required(:service_credentials).filled(:str?)
      end
      optional(:prod).hash do
        required(:service_credentials).filled(:str?)
      end
    end
  end
end

Are you able to suggest me a way to achieve that?

Additionally, changing params to scheme in EnvironmentContract makes the validation to fail. Reading the documentation, I've noticed that the only difference between the two is that the latter does not perform coercion but it's obscure to me why it's failing.

Thanks for your support, Lorenzo


Solution

  • If I understand correctly the environment names do not need to be present nor do they need to have a specific key value but if they are present they need to refer to a Hash containing the key :service_credentials.

    To do this you are better off implementing a custom rule, maybe something like the following:

    class EnvironmentContract < Dry::Validation::Contract
      params do 
        required(:environment).hash
      end
    
      rule(:environment) do 
        if key? 
          if value.empty? || !value.is_a?(Hash)
            key.failure('must specify at least one environment')
          else 
            value.each do |k, v|
              # could use key([:environment,k]) to create nested error messages
              key(k).failure('must specify service_credentials') unless v.is_a?(Hash) && v.key?(:service_credentials)
            end
          end
        end 
      end
    end
    

    Example:

    examples = [{},{environment: {}},{environment: {test: {}}},{environment: {supply: {service_credentials: 123}}},{environment: {stage: 123}},{environment: {test: {service_credentials: 'a'}, stage: {service_credentials:'b'},production: {service_credentials: 'c'}}},{environment: {test: {service_credentials: 'a'}, stage: {service_credentials:'b'},production: {}}}]
    
    examples.each do |example| 
      result = EnvironmentContract.new.call(example)
      puts "-" * 20
      puts "Validating #{example.inspect}"
      puts 
      puts "Sucess: #{result.success?}"
      puts 
      puts "Error messages: #{result.errors(full:true).to_h}"
      puts "-" * 20
    end 
    

    Output:

    --------------------
    Validating {}
    
    Sucess: false
    
    Error messages: {:environment=>["environment is missing"]}
    --------------------
    --------------------
    Validating {:environment=>{}}
    
    Sucess: false
    
    Error messages: {:environment=>["environment must specify at least one environment"]}
    --------------------
    --------------------
    Validating {:environment=>{:test=>{}}}
    
    Sucess: false
    
    Error messages: {:test=>["test must specify service_credentials"]}
    --------------------
    --------------------
    Validating {:environment=>{:supply=>{:service_credentials=>123}}}
    
    Sucess: true
    
    Error messages: {}
    --------------------
    --------------------
    Validating {:environment=>{:stage=>123}}
    
    Sucess: false
    
    Error messages: {:stage=>["stage must specify service_credentials"]}
    --------------------
    --------------------
    Validating {:environment=>{:test=>{:service_credentials=>"a"}, :stage=>{:service_credentials=>"b"}, :production=>{:service_credentials=>"c"}}}
    
    Sucess: true
    
    Error messages: {}
    --------------------
    --------------------
    Validating {:environment=>{:test=>{:service_credentials=>"a"}, :stage=>{:service_credentials=>"b"}, :production=>{}}}
    
    Sucess: false
    
    Error messages: {:production=>["production must specify service_credentials"]}
    --------------------