Skip to content

Commit

Permalink
Merge pull request #155 from ok32/validation
Browse files Browse the repository at this point in the history
Add optional config validation (using dry-validation schema)
  • Loading branch information
pkuczynski authored Dec 20, 2016
2 parents 32848b6 + 6ad2bd3 commit 88fcd48
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 0 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,29 @@ located at `config/initializers/config.rb`.

Check [Deep Merge](https://github.com/danielsdeleo/deep_merge) for more details.

### Validation

You can optionally define a schema to validate presence (and type) of specific config values:

```ruby
Config.setup do |config|
# ...
config.schema do
required(:youtube).schema do
required(:api_key).filled
end
end
end
```

The above example demonstrates how to ensure that the configuration has the `youtube` structure
with the `api_key` field filled.
If you define a schema it will automatically be used to validate your config. If validation fails it will
raise a `Config::ValidationError` containing a nice message with information about all the mismatches
between the schema and your config.

Check [dry-validation](https://github.com/dry-rb/dry-validation) for more details.

### Environment variables

See section below for more details.
Expand Down
1 change: 1 addition & 0 deletions config.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Gem::Specification.new do |s|

s.add_dependency 'activesupport', '>= 3.0'
s.add_dependency 'deep_merge', '~> 1.1.1'
s.add_dependency 'dry-validation', '~> 0.10.3'

s.add_development_dependency 'bundler', '~> 1.13', '>= 1.13.1'
s.add_development_dependency 'rake', '~> 11.2', '>= 11.2.2'
Expand Down
15 changes: 15 additions & 0 deletions lib/config.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
require 'active_support/core_ext/module/attribute_accessors'

require 'config/compatibility'
require 'config/validation'
require 'config/options'
require 'config/version'
require 'config/integrations/rails/engine' if defined?(::Rails)
require 'config/sources/yaml_source'
require 'config/sources/hash_source'
require 'deep_merge'
require 'dry-validation'

module Config
class ValidationError < StandardError; end

# Ensures the setup only gets run once
@@_ran_once = false

Expand All @@ -25,11 +29,22 @@ module Config
@@knockout_prefix = nil
@@overwrite_arrays = true

mattr_writer :schema
@@schema = nil

def self.setup
yield self if @@_ran_once == false
@@_ran_once = true
end

def self.schema(&block)
if block_given?
@@schema = Dry::Validation.Schema(&block)
else
@@schema
end
end

# Create a populated Options instance from a settings file. If a second file is given, then the sections of that
# file will overwrite existing sections of the first file.
def self.load_files(*files)
Expand Down
2 changes: 2 additions & 0 deletions lib/config/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module Config
class Options < OpenStruct
include Enumerable
include Validation

def keys
marshal_dump.keys
Expand Down Expand Up @@ -83,6 +84,7 @@ def reload!
marshal_load(__convert(conf).marshal_dump)

reload_env! if Config.use_env
validate! if Config.schema

self
end
Expand Down
28 changes: 28 additions & 0 deletions lib/config/validation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module Config
module Validation
def validate!
v_res = Config.schema.(self.to_hash)

unless v_res.success?
raise ValidationError.new("Config validation failed:\n\n#{format_errors(v_res)}")
end
end

def format_errors(v_res)
flatten_hash(v_res.messages).map do |field, msgs|
"#{' ' * 2}#{field}: #{msgs.join('; ')}"
end.join('\n')
end

def flatten_hash(h, acc={}, pref=[])
h.inject(acc) do |a, (k,v)|
if v.is_a?(Hash)
flatten_hash(v, acc, pref + [k])
else
acc[(pref + [k]).join('.')] = v
acc
end
end
end
end
end
34 changes: 34 additions & 0 deletions spec/config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -367,4 +367,38 @@

end
end

context 'validation' do
around(:each) do |example|
Config.reset
example.run
Config.reset
end

it 'should raise if schema is present and validation fails' do
Config.setup do |config|
config.schema do
required(:youtube).schema do
required(:nonexist_field).filled
end
end
end

expect { Config.load_files("#{fixture_path}/validation/config.yml") }.
to raise_error(Config::ValidationError, /youtube.nonexist_field: is missing/)
end

it 'should work if validation passes' do
Config.setup do |config|
config.schema do
required(:youtube).schema do
required(:api_key).filled
end
end
end

expect { Config.load_files("#{fixture_path}/validation/config.yml") }.
to_not raise_error
end
end
end
2 changes: 2 additions & 0 deletions spec/fixtures/validation/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
youtube:
api_key: ADSEF453
2 changes: 2 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ def self.reset
self.use_env = false
self.knockout_prefix = nil
self.overwrite_arrays = true
self.schema = nil
class_variable_set(:@@_ran_once, false)
end
end
end
Expand Down

0 comments on commit 88fcd48

Please sign in to comment.