frise 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +1 -0
- data/.rubocop.yml +2 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +13 -0
- data/README.md +131 -0
- data/Rakefile +13 -0
- data/frise.gemspec +31 -0
- data/lib/frise/defaults_loader.rb +69 -0
- data/lib/frise/loader.rb +60 -0
- data/lib/frise/parser.rb +17 -0
- data/lib/frise/validator.rb +179 -0
- data/lib/frise/version.rb +3 -0
- data/lib/frise.rb +5 -0
- metadata +157 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 41db8000684dfb7ee23cc7f26a790a57bb38ad76
|
4
|
+
data.tar.gz: 782cf2ff2d3866a754950fd8edd3735a1bb1f425
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c37c19e1f9edbad815b30d52f867d4344fd3b3da8a376e30236814a47ddd579b889459b1a702190fe5d1c1d2b99afdc3abef685f54a062616ea632872640a5e2
|
7
|
+
data.tar.gz: c9c87e93dbd3b361a5632431ac8f83650749c7d1de090c3397299b403cda7e1a986767356d8795dc3cf037f9461227a8287dd3870df79a573031ee5928d31931
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2017 ShiftForward, S.A. [http://www.shiftforward.eu]
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
4
|
+
use this file except in compliance with the License. You may obtain a copy of
|
5
|
+
the License at
|
6
|
+
|
7
|
+
[http://www.apache.org/licenses/LICENSE-2.0]
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
11
|
+
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
12
|
+
License for the specific language governing permissions and limitations under
|
13
|
+
the License.
|
data/README.md
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
# Frise
|
2
|
+
[![Build Status](https://travis-ci.org/ShiftForward/frise.svg?branch=master)](https://travis-ci.org/ShiftForward/frise)
|
3
|
+
[![Coverage Status](https://coveralls.io/repos/github/ShiftForward/frise/badge.svg?branch=master)](https://coveralls.io/github/ShiftForward/frise?branch=master)
|
4
|
+
|
5
|
+
Frise is a library for loading configuration files as native Ruby structures. Besides reading and
|
6
|
+
parsing the files themselves, it also:
|
7
|
+
|
8
|
+
- Completes it with default values specified in another file or set of files;
|
9
|
+
- Interprets [Liquid](https://shopify.github.io/liquid) templates in configs and defaults;
|
10
|
+
- Validates the loaded config according to a schema file or set of files.
|
11
|
+
|
12
|
+
## Install
|
13
|
+
|
14
|
+
```
|
15
|
+
gem install frise
|
16
|
+
```
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
### Basic configs
|
21
|
+
|
22
|
+
The simplest example would be to load [a simple configuration](example/config.yml) from a file:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
require 'frise'
|
26
|
+
require 'pp'
|
27
|
+
|
28
|
+
loader = Frise::Loader.new
|
29
|
+
loader.load('example/config.yml')
|
30
|
+
# => {"movies"=>
|
31
|
+
# [{"title"=>"The Shawshank Redemption",
|
32
|
+
# "year"=>1994,
|
33
|
+
# "categories"=>["Crime", "Drama"],
|
34
|
+
# "rating"=>9.3},
|
35
|
+
# {"title"=>"The Godfather",
|
36
|
+
# "year"=>1972,
|
37
|
+
# "director"=>"Francis Ford Coppola",
|
38
|
+
# "categories"=>["Crime", "Drama"],
|
39
|
+
# "rating"=>9.2}]}
|
40
|
+
```
|
41
|
+
|
42
|
+
Currently Frise only supports YAML files, but it may support JSON and other formats in the future.
|
43
|
+
|
44
|
+
### Default values
|
45
|
+
|
46
|
+
By defining directories where the files with default values can be found (in this example,
|
47
|
+
[example/_defaults](example/_defaults)), Frise can handle its application internally on load time:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
loader = Frise::Loader.new(defaults_load_paths: ['example/_defaults'])
|
51
|
+
loader.load('example/config.yml')
|
52
|
+
# => {"movies"=>
|
53
|
+
# [{"title"=>"The Shawshank Redemption",
|
54
|
+
# "year"=>1994,
|
55
|
+
# "categories"=>["Crime", "Drama"],
|
56
|
+
# "rating"=>9.3,
|
57
|
+
# "director"=>"N/A"},
|
58
|
+
# {"title"=>"The Godfather",
|
59
|
+
# "year"=>1972,
|
60
|
+
# "director"=>"Francis Ford Coppola",
|
61
|
+
# "categories"=>["Crime", "Drama"],
|
62
|
+
# "rating"=>9.2}],
|
63
|
+
# "ui"=>
|
64
|
+
# {"default_movie"=>"The Shawshank Redemption",
|
65
|
+
# "filter_from"=>1972,
|
66
|
+
# "filter_to"=>1994}}
|
67
|
+
```
|
68
|
+
|
69
|
+
Note that files with default values follow exactly the same structure of the config file itself.
|
70
|
+
Special values such as `$all` allow users to define default values for all elements of an object or
|
71
|
+
array. Liquid templates are also used to define some defaults as a function of other objects of the
|
72
|
+
config.
|
73
|
+
|
74
|
+
### Schemas
|
75
|
+
|
76
|
+
Additionally, configuration files can also be validated against a schema. By specifying
|
77
|
+
`schema_load_paths`, users can provide directories where schema files such as
|
78
|
+
[example/_schemas/config.yml](example/_schemas/config.yml) can be found:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
loader = Frise::Loader.new(
|
82
|
+
schema_load_paths: ['example/_schemas'],
|
83
|
+
defaults_load_paths: ['example/_defaults'])
|
84
|
+
|
85
|
+
loader.load('example/config.yml')
|
86
|
+
# {"movies"=>
|
87
|
+
# [{"title"=>"The Shawshank Redemption",
|
88
|
+
# "year"=>1994,
|
89
|
+
# "categories"=>["Crime", "Drama"],
|
90
|
+
# "rating"=>9.3,
|
91
|
+
# "director"=>"N/A"},
|
92
|
+
# {"title"=>"The Godfather",
|
93
|
+
# "year"=>1972,
|
94
|
+
# "director"=>"Francis Ford Coppola",
|
95
|
+
# "categories"=>["Crime", "Drama"],
|
96
|
+
# "rating"=>9.2}],
|
97
|
+
# "ui"=>
|
98
|
+
# {"default_movie"=>"The Shawshank Redemption",
|
99
|
+
# "filter_from"=>1972,
|
100
|
+
# "filter_to"=>1994}}
|
101
|
+
```
|
102
|
+
|
103
|
+
If this config is loaded without the defaults instead, there are now required values that are
|
104
|
+
missing and Frise by default prints a summary of the errors and terminates the program:
|
105
|
+
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
loader = Frise::Loader.new(schema_load_paths: ['example/_schemas'])
|
109
|
+
|
110
|
+
loader.load('example/config.yml')
|
111
|
+
# 2 config error(s) found:
|
112
|
+
# - At movies.0.director: missing required value
|
113
|
+
# - At ui: missing required value
|
114
|
+
```
|
115
|
+
|
116
|
+
Once more, the structure of the schema mimics the structure of the config itself, making it easy to
|
117
|
+
write schemas and to create a config scaffold from its schema later.
|
118
|
+
|
119
|
+
Users can check a whole range of properties in config values besides their type: optional values,
|
120
|
+
hashes with validated keys, hashes with unknown keys and even custom validations are also supported.
|
121
|
+
The [specification](spec/frise/validator_spec.rb) of the validator provides various examples of
|
122
|
+
schemas and describes the behavior of each value (more documentation will be written soon).
|
123
|
+
|
124
|
+
### Other features
|
125
|
+
|
126
|
+
Users can also define custom code to be run before defaults and schemas are applied and can even do
|
127
|
+
each of the steps separately. Additionally, defaults and schemas can be loaded at a specific path
|
128
|
+
inside an existing Ruby object. The [Loader](lib/frise/loader.rb) class provides high-level methods
|
129
|
+
to access those features, while lower-level functionality can be accessed through
|
130
|
+
[Parser](lib/frise/parser.rb), [DefaultsLoader](lib/frise/defaults_loader.rb) and
|
131
|
+
[Validator](lib/frise/validator.rb).
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
|
3
|
+
def load_if_available(req_path)
|
4
|
+
require req_path
|
5
|
+
yield
|
6
|
+
rescue LoadError
|
7
|
+
false # req not available
|
8
|
+
end
|
9
|
+
|
10
|
+
load_if_available('rspec/core/rake_task') { RSpec::Core::RakeTask.new(:spec) }
|
11
|
+
load_if_available('rubocop/rake_task') { RuboCop::RakeTask.new(:rubocop) }
|
12
|
+
|
13
|
+
task default: %i[rubocop spec]
|
data/frise.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'frise/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'frise'
|
9
|
+
spec.version = Frise::VERSION
|
10
|
+
spec.authors = ['ShiftForward']
|
11
|
+
spec.email = ['info@shiftforward.eu']
|
12
|
+
|
13
|
+
spec.summary = 'Ruby config library with schema validation, default values and templating'
|
14
|
+
spec.homepage = 'https://github.com/ShiftForward/frise'
|
15
|
+
spec.license = 'Apache-2.0'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features|example)/})
|
19
|
+
end
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
spec.required_ruby_version = '>= 2.1.0'
|
22
|
+
|
23
|
+
spec.add_dependency 'liquid', '~> 3.0'
|
24
|
+
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.14'
|
26
|
+
spec.add_development_dependency 'coveralls', '~> 0.8.21'
|
27
|
+
spec.add_development_dependency 'simplecov', '~> 0.14.1'
|
28
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
29
|
+
spec.add_development_dependency 'rspec', '~> 3.4'
|
30
|
+
spec.add_development_dependency 'rubocop', '~> 0.49.1'
|
31
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'frise/parser'
|
2
|
+
|
3
|
+
module Frise
|
4
|
+
# Provides the logic for merging config defaults into pre-loaded configuration objects.
|
5
|
+
#
|
6
|
+
# The merge_defaults and merge_defaults_at entrypoint methods provide ways to read files with
|
7
|
+
# defaults and apply them to configuration objects.
|
8
|
+
module DefaultsLoader
|
9
|
+
class << self
|
10
|
+
def widened_class(obj)
|
11
|
+
class_name = obj.class.to_s
|
12
|
+
return 'Boolean' if %w[TrueClass FalseClass].include? class_name
|
13
|
+
return 'Integer' if %w[Fixnum Bignum].include? class_name
|
14
|
+
class_name
|
15
|
+
end
|
16
|
+
|
17
|
+
def merge_defaults_obj(config, defaults)
|
18
|
+
if defaults.nil?
|
19
|
+
config
|
20
|
+
|
21
|
+
elsif config.nil?
|
22
|
+
if defaults.class != Hash then defaults
|
23
|
+
elsif defaults['$optional'] then nil
|
24
|
+
else merge_defaults_obj({}, defaults)
|
25
|
+
end
|
26
|
+
|
27
|
+
elsif defaults.class == Array && config.class == Array
|
28
|
+
defaults + config
|
29
|
+
|
30
|
+
elsif defaults.class == Hash && defaults['$all'] && config.class == Array
|
31
|
+
config.map { |elem| merge_defaults_obj(elem, defaults['$all']) }
|
32
|
+
|
33
|
+
elsif defaults.class == Hash && config.class == Hash
|
34
|
+
new_config = {}
|
35
|
+
(config.keys + defaults.keys).uniq.each do |key|
|
36
|
+
next if key.start_with?('$')
|
37
|
+
new_config[key] = config[key]
|
38
|
+
new_config[key] = merge_defaults_obj(new_config[key], defaults[key]) if defaults.key?(key)
|
39
|
+
new_config[key] = merge_defaults_obj(new_config[key], defaults['$all']) unless new_config[key].nil?
|
40
|
+
new_config.delete(key) if new_config[key].nil?
|
41
|
+
end
|
42
|
+
new_config
|
43
|
+
|
44
|
+
elsif widened_class(defaults) != widened_class(config)
|
45
|
+
raise "Cannot merge config #{config.inspect} (#{widened_class(config)}) " \
|
46
|
+
"with default #{defaults.inspect} (#{widened_class(defaults)})"
|
47
|
+
|
48
|
+
else
|
49
|
+
config
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def merge_defaults_obj_at(config, at_path, defaults)
|
54
|
+
at_path.reverse.each { |key| defaults = { key => defaults } }
|
55
|
+
merge_defaults_obj(config, defaults)
|
56
|
+
end
|
57
|
+
|
58
|
+
def merge_defaults(config, defaults_file, symbol_table = config)
|
59
|
+
defaults = Parser.parse(defaults_file, symbol_table)
|
60
|
+
merge_defaults_obj(config, defaults)
|
61
|
+
end
|
62
|
+
|
63
|
+
def merge_defaults_at(config, at_path, defaults_file, symbol_table = config)
|
64
|
+
defaults = Parser.parse(defaults_file, symbol_table)
|
65
|
+
merge_defaults_obj_at(config, at_path, defaults)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/frise/loader.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'frise/defaults_loader'
|
2
|
+
require 'frise/parser'
|
3
|
+
require 'frise/validator'
|
4
|
+
|
5
|
+
module Frise
|
6
|
+
# The entrypoint for loading configs from files according to the conventions defined for Frise.
|
7
|
+
#
|
8
|
+
# The load method loads a configuration file, merges it with the applicable defaults and validates
|
9
|
+
# its schema. Other methods in Loader perform only parts of the process.
|
10
|
+
class Loader
|
11
|
+
def initialize(schema_load_paths: [], defaults_load_paths: [], pre_loaders: [], validators: nil)
|
12
|
+
@schema_load_paths = schema_load_paths
|
13
|
+
@defaults_load_paths = defaults_load_paths
|
14
|
+
@pre_loaders = pre_loaders
|
15
|
+
@validators = validators
|
16
|
+
end
|
17
|
+
|
18
|
+
def load(config_file, exit_on_fail = true, symbol_table = {})
|
19
|
+
config = Parser.parse(config_file, symbol_table)
|
20
|
+
config_name = File.basename(config_file)
|
21
|
+
|
22
|
+
@pre_loaders.each do |pre_loader|
|
23
|
+
config = pre_loader.call(config)
|
24
|
+
end
|
25
|
+
|
26
|
+
config = merge_defaults(config, config_name, symbol_table)
|
27
|
+
validate(config, config_name, exit_on_fail)
|
28
|
+
end
|
29
|
+
|
30
|
+
def merge_defaults(config, defaults_name, symbol_table = {})
|
31
|
+
merge_defaults_at(config, [], defaults_name, symbol_table)
|
32
|
+
end
|
33
|
+
|
34
|
+
def merge_defaults_at(config, at_path, defaults_name, symbol_table = {})
|
35
|
+
@defaults_load_paths.map do |defaults_dir|
|
36
|
+
defaults_file = File.join(defaults_dir, defaults_name)
|
37
|
+
config = DefaultsLoader.merge_defaults_at(
|
38
|
+
config, at_path, defaults_file, symbol_table.merge(config)
|
39
|
+
)
|
40
|
+
end
|
41
|
+
config
|
42
|
+
end
|
43
|
+
|
44
|
+
def validate(config, schema_name, exit_on_fail = true)
|
45
|
+
validate_at(config, [], schema_name, exit_on_fail)
|
46
|
+
end
|
47
|
+
|
48
|
+
def validate_at(config, at_path, schema_name, exit_on_fail = true)
|
49
|
+
@schema_load_paths.map do |schema_dir|
|
50
|
+
schema_file = File.join(schema_dir, schema_name)
|
51
|
+
errors = Validator.validate_at(config, at_path, schema_file,
|
52
|
+
validators: @validators,
|
53
|
+
print: exit_on_fail,
|
54
|
+
fatal: exit_on_fail)
|
55
|
+
return nil if errors.any?
|
56
|
+
end
|
57
|
+
config
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/frise/parser.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'liquid'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Frise
|
5
|
+
# Provides a static parse method for reading a config from a YAML file applying the required
|
6
|
+
# transformations.
|
7
|
+
module Parser
|
8
|
+
class << self
|
9
|
+
def parse(file, symbol_table = nil)
|
10
|
+
return {} unless File.file? file
|
11
|
+
content = File.open(file).read
|
12
|
+
content = Liquid::Template.parse(content).render symbol_table if symbol_table
|
13
|
+
YAML.safe_load(content, [], [], true) || {}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'frise/parser'
|
4
|
+
require 'set'
|
5
|
+
|
6
|
+
module Frise
|
7
|
+
# Checks if a pre-loaded config object conforms to a schema file.
|
8
|
+
#
|
9
|
+
# The validate and validate_at static methods read schema files and validates config objects
|
10
|
+
# against the parsed schema. They can optionally be initialized with a set of user-defined
|
11
|
+
# validators that can be used in the schema files for custom validations.
|
12
|
+
class Validator
|
13
|
+
attr_reader :errors
|
14
|
+
|
15
|
+
def initialize(root, validators = nil)
|
16
|
+
@root = root
|
17
|
+
@validators = validators
|
18
|
+
@errors = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def widened_class(obj)
|
22
|
+
class_name = obj.class.to_s
|
23
|
+
return 'Boolean' if %w[TrueClass FalseClass].include? class_name
|
24
|
+
return 'Integer' if %w[Fixnum Bignum].include? class_name
|
25
|
+
class_name
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_validation_error(path, msg)
|
29
|
+
logged_path = path.empty? ? '<root>' : path[1..-1]
|
30
|
+
@errors << "At #{logged_path}: #{msg}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_full_schema(schema)
|
34
|
+
case schema
|
35
|
+
when Hash then schema
|
36
|
+
when Symbol then { type: 'Object', validate: schema }
|
37
|
+
when Array then
|
38
|
+
if schema.size == 1
|
39
|
+
{ type: 'Array', all: schema[0] }
|
40
|
+
else
|
41
|
+
(raise "Invalid schema: #{schema.inspect}")
|
42
|
+
end
|
43
|
+
when String then
|
44
|
+
if schema.end_with?('?')
|
45
|
+
{ type: schema[0..-2], optional: true }
|
46
|
+
else
|
47
|
+
{ type: schema }
|
48
|
+
end
|
49
|
+
else raise "Invalid schema: #{schema.inspect}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def validate_optional(full_schema, obj, path)
|
54
|
+
if obj.nil?
|
55
|
+
add_validation_error(path, 'missing required value') unless full_schema[:optional]
|
56
|
+
return false
|
57
|
+
end
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
def get_expected_types(full_schema)
|
62
|
+
type_key = full_schema.fetch(:type, 'Hash')
|
63
|
+
allowed_types = %w[Hash Array String Integer Float Object]
|
64
|
+
return [Object.const_get(type_key)] if allowed_types.include?(type_key)
|
65
|
+
return [TrueClass, FalseClass] if type_key == 'Boolean'
|
66
|
+
raise "Invalid expected type in schema: #{type_key}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def validate_type(full_schema, obj, path)
|
70
|
+
expected_types = get_expected_types(full_schema)
|
71
|
+
unless expected_types.any? { |typ| obj.is_a?(typ) }
|
72
|
+
type_key = full_schema.fetch(:type, 'Hash')
|
73
|
+
add_validation_error(path, "expected #{type_key}, found #{widened_class(obj)}")
|
74
|
+
return false
|
75
|
+
end
|
76
|
+
true
|
77
|
+
end
|
78
|
+
|
79
|
+
def validate_custom(full_schema, obj, path)
|
80
|
+
if full_schema[:validate]
|
81
|
+
begin
|
82
|
+
@validators.method(full_schema[:validate]).call(@root, obj)
|
83
|
+
rescue StandardError => ex
|
84
|
+
add_validation_error(path, ex.message)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
true
|
88
|
+
end
|
89
|
+
|
90
|
+
def validate_spec_keys(full_schema, obj, path, processed_keys)
|
91
|
+
full_schema.each do |spec_key, spec_value|
|
92
|
+
next if spec_key.is_a?(Symbol)
|
93
|
+
validate_object("#{path}.#{spec_key}", obj[spec_key], spec_value)
|
94
|
+
processed_keys << spec_key
|
95
|
+
end
|
96
|
+
true
|
97
|
+
end
|
98
|
+
|
99
|
+
def validate_remaining_keys(full_schema, obj, path, processed_keys)
|
100
|
+
expected_types = get_expected_types(full_schema)
|
101
|
+
if expected_types.size == 1 && expected_types[0].ancestors.member?(Enumerable)
|
102
|
+
hash = obj.is_a?(Hash) ? obj : Hash[obj.map.with_index { |x, i| [i, x] }]
|
103
|
+
hash.each do |key, value|
|
104
|
+
if full_schema[:all_keys] && !key.is_a?(Symbol)
|
105
|
+
validate_object(path, key, full_schema[:all_keys])
|
106
|
+
end
|
107
|
+
|
108
|
+
next if processed_keys.member? key
|
109
|
+
if full_schema[:all]
|
110
|
+
validate_object("#{path}.#{key}", value, full_schema[:all])
|
111
|
+
elsif !full_schema[:allow_unknown_keys]
|
112
|
+
add_validation_error(path, "unknown key: #{key}")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
true
|
117
|
+
end
|
118
|
+
|
119
|
+
def validate_object(path, obj, schema)
|
120
|
+
full_schema = get_full_schema(schema)
|
121
|
+
|
122
|
+
return unless validate_optional(full_schema, obj, path)
|
123
|
+
return unless validate_type(full_schema, obj, path)
|
124
|
+
return unless validate_custom(full_schema, obj, path)
|
125
|
+
|
126
|
+
processed_keys = Set.new
|
127
|
+
return unless validate_spec_keys(full_schema, obj, path, processed_keys)
|
128
|
+
validate_remaining_keys(full_schema, obj, path, processed_keys)
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.parse_symbols(obj)
|
132
|
+
case obj
|
133
|
+
when Array then obj.map { |e| parse_symbols(e) }
|
134
|
+
when Hash then Hash[obj.map { |k, v| [parse_symbols(k), parse_symbols(v)] }]
|
135
|
+
when String then obj.start_with?('$') ? obj[1..-1].to_sym : obj
|
136
|
+
else obj
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.validate_obj(config, schema, options = {})
|
141
|
+
validator = Validator.new(config, options[:validators])
|
142
|
+
validator.validate_object('', config, schema)
|
143
|
+
|
144
|
+
if validator.errors.any?
|
145
|
+
if options[:print]
|
146
|
+
puts "#{validator.errors.length} config error(s) found:"
|
147
|
+
validator.errors.each do |error|
|
148
|
+
puts " - #{error}"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
exit 1 if options[:fatal]
|
153
|
+
raise ValidationError.new(validator.errors), 'Invalid configuration' if options[:raise_error]
|
154
|
+
end
|
155
|
+
validator.errors
|
156
|
+
end
|
157
|
+
|
158
|
+
def self.validate(config, schema_file, options = {})
|
159
|
+
schema = parse_symbols(Parser.parse(schema_file))
|
160
|
+
validate_obj(config, schema, options)
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.validate_at(config, at_path, schema_file, options = {})
|
164
|
+
schema = parse_symbols(Parser.parse(schema_file))
|
165
|
+
at_path.reverse.each { |key| schema = { key => schema, :allow_unknown_keys => true } }
|
166
|
+
validate_obj(config, schema, options)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# An error resulting of the validation of a config. The list of errors can be inspected using the
|
171
|
+
# errors method.
|
172
|
+
class ValidationError < StandardError
|
173
|
+
attr_reader :errors
|
174
|
+
|
175
|
+
def initialize(errors)
|
176
|
+
@errors = errors
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
data/lib/frise.rb
ADDED
metadata
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: frise
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- ShiftForward
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-08-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: liquid
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.14'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.14'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: coveralls
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.8.21
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.8.21
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: simplecov
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.14.1
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.14.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '10.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '10.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.4'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.4'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.49.1
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.49.1
|
111
|
+
description:
|
112
|
+
email:
|
113
|
+
- info@shiftforward.eu
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- ".rspec"
|
120
|
+
- ".rubocop.yml"
|
121
|
+
- ".travis.yml"
|
122
|
+
- Gemfile
|
123
|
+
- LICENSE.txt
|
124
|
+
- README.md
|
125
|
+
- Rakefile
|
126
|
+
- frise.gemspec
|
127
|
+
- lib/frise.rb
|
128
|
+
- lib/frise/defaults_loader.rb
|
129
|
+
- lib/frise/loader.rb
|
130
|
+
- lib/frise/parser.rb
|
131
|
+
- lib/frise/validator.rb
|
132
|
+
- lib/frise/version.rb
|
133
|
+
homepage: https://github.com/ShiftForward/frise
|
134
|
+
licenses:
|
135
|
+
- Apache-2.0
|
136
|
+
metadata: {}
|
137
|
+
post_install_message:
|
138
|
+
rdoc_options: []
|
139
|
+
require_paths:
|
140
|
+
- lib
|
141
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 2.1.0
|
146
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - ">="
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '0'
|
151
|
+
requirements: []
|
152
|
+
rubyforge_project:
|
153
|
+
rubygems_version: 2.6.11
|
154
|
+
signing_key:
|
155
|
+
specification_version: 4
|
156
|
+
summary: Ruby config library with schema validation, default values and templating
|
157
|
+
test_files: []
|