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
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"]}
--------------------