frise 0.1.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +8 -0
- data/.travis.yml +6 -2
- data/CHANGELOG.md +43 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +1 -1
- data/README.md +13 -19
- data/Rakefile +2 -0
- data/frise.gemspec +13 -13
- data/lib/frise.rb +3 -0
- data/lib/frise/defaults_loader.rb +69 -53
- data/lib/frise/loader.rb +169 -29
- data/lib/frise/loader/lazy.rb +26 -0
- data/lib/frise/parser.rb +17 -4
- data/lib/frise/validator.rb +50 -21
- data/lib/frise/version.rb +3 -1
- metadata +29 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 86558df34daa0ba92e7e7a1863ceb5124ea52c39dc82a85924c4db1bd89cf127
|
4
|
+
data.tar.gz: fd286367be54e292a6df52daa6f5255810d6e3980bce538d0c2391d5fdab9ca9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c48f86d8190c30bf650055dfa7749ecdf9d2545f4a6c34593c29e402c0da06dd5420eb8244c3f112f8501564a5bf26798acae2fbeb006c10bb1c4eb5dc4e7c1a
|
7
|
+
data.tar.gz: 679a4edb6364662160f4e1d6872c8949e3fd6338433da50de5a109addec0a31dec69b3013ca4038fbb40283383b15f160ecf4723c546dea53ab6a8d75db6f77c
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
### 0.4.1 (July 7, 2020)
|
2
|
+
|
3
|
+
- New features
|
4
|
+
- `$delete` directive is now available in config files, allowing users to delete parts of the
|
5
|
+
config sub-tree ([#20](https://github.com/velocidi/frise/pull/20)).
|
6
|
+
|
7
|
+
### 0.4.0 (November 29, 2019)
|
8
|
+
|
9
|
+
- Breaking changes
|
10
|
+
- Recursive inclusions now respect the hierarchy of configuration files, avoiding inclusions lower
|
11
|
+
in the hiearchy to be resolved before ones higher in the hierarchy
|
12
|
+
([#14](https://github.com/velocidi/frise/pull/14)).
|
13
|
+
- Bug fixes
|
14
|
+
- Fix error messages from validations happening deeper in the config hierarchy, that were wrongly
|
15
|
+
missing the first character in their path ([#16](https://github.com/velocidi/frise/pull/16)).
|
16
|
+
|
17
|
+
### 0.3.0 (April 16, 2018)
|
18
|
+
|
19
|
+
- Breaking changes
|
20
|
+
- Defaults and schemas are now defined using a directive in the YAML configs instead of
|
21
|
+
implicitly by looking at load paths. See ([#7](https://github.com/velocidi/frise/pull/7)) for
|
22
|
+
more information on migration;
|
23
|
+
- New features
|
24
|
+
- `$include` and `$schema` directives are now available in config files, allowing users to
|
25
|
+
validate and include defaults at any part of the config
|
26
|
+
([#7](https://github.com/velocidi/frise/pull/7));
|
27
|
+
- A new `$content_include` directive allows users to include the content of a file as a YAML
|
28
|
+
string ([#8](https://github.com/velocidi/frise/pull/8));
|
29
|
+
- The `_file_dir` Liquid variable is now available in all included files, containing always the
|
30
|
+
absolute path to the file being loaded ([#7](https://github.com/velocidi/frise/pull/7)).
|
31
|
+
|
32
|
+
### 0.2.0 (August 17, 2017)
|
33
|
+
|
34
|
+
- New features
|
35
|
+
- New schema types `$enum` and `$one_of` for specifying enumerations and values with multiple
|
36
|
+
possible schemas ([#5](https://github.com/velocidi/frise/pull/5)).
|
37
|
+
- Bug fixes
|
38
|
+
- Deal correctly with non-existing schema files in the load path
|
39
|
+
([#4](https://github.com/velocidi/frise/pull/4)).
|
40
|
+
|
41
|
+
### 0.1.0 (August 10, 2017)
|
42
|
+
|
43
|
+
Initial version.
|
data/Gemfile
CHANGED
data/LICENSE.txt
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright 2017
|
1
|
+
Copyright 2017-2019 Velocidi [http://www.velocidi.com/]
|
2
2
|
|
3
3
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
4
4
|
use this file except in compliance with the License. You may obtain a copy of
|
data/README.md
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
# Frise
|
2
|
-
[![Build Status](https://travis-ci.org/
|
3
|
-
[![Coverage Status](https://coveralls.io/repos/github/
|
2
|
+
[![Build Status](https://travis-ci.org/velocidi/frise.svg?branch=master)](https://travis-ci.org/velocidi/frise)
|
3
|
+
[![Coverage Status](https://coveralls.io/repos/github/velocidi/frise/badge.svg?branch=master)](https://coveralls.io/github/velocidi/frise?branch=master)
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/frise.svg)](https://badge.fury.io/rb/frise)
|
4
5
|
|
5
6
|
Frise is a library for loading configuration files as native Ruby structures. Besides reading and
|
6
7
|
parsing the files themselves, it also:
|
7
8
|
|
8
|
-
-
|
9
|
+
- Allows defining other files to be merged anywhere in the config, which can be used to provide default values specified
|
10
|
+
in another file or set of files;
|
9
11
|
- Interprets [Liquid](https://shopify.github.io/liquid) templates in configs and defaults;
|
10
12
|
- Validates the loaded config according to a schema file or set of files.
|
11
13
|
|
@@ -23,7 +25,6 @@ The simplest example would be to load [a simple configuration](example/config.ym
|
|
23
25
|
|
24
26
|
```ruby
|
25
27
|
require 'frise'
|
26
|
-
require 'pp'
|
27
28
|
|
28
29
|
loader = Frise::Loader.new
|
29
30
|
loader.load('example/config.yml')
|
@@ -43,12 +44,11 @@ Currently Frise only supports YAML files, but it may support JSON and other form
|
|
43
44
|
|
44
45
|
### Default values
|
45
46
|
|
46
|
-
By
|
47
|
-
[example/_defaults](example/_defaults)), Frise can handle its application internally on load time:
|
47
|
+
By using the `$include` directive pointing to the files where default values can be found (in this example,
|
48
|
+
[example/_defaults/config.yml](example/_defaults/config.yml)), Frise can handle its application internally on load time:
|
48
49
|
|
49
50
|
```ruby
|
50
|
-
loader
|
51
|
-
loader.load('example/config.yml')
|
51
|
+
loader.load('example/config_with_defaults.yml')
|
52
52
|
# => {"movies"=>
|
53
53
|
# [{"title"=>"The Shawshank Redemption",
|
54
54
|
# "year"=>1994,
|
@@ -74,15 +74,11 @@ config.
|
|
74
74
|
### Schemas
|
75
75
|
|
76
76
|
Additionally, configuration files can also be validated against a schema. By specifying
|
77
|
-
`
|
78
|
-
[example/_schemas/config.yml](example/_schemas/config.yml)
|
77
|
+
`$schema` in the config, users can provide schema files such as
|
78
|
+
[example/_schemas/config.yml](example/_schemas/config.yml):
|
79
79
|
|
80
80
|
```ruby
|
81
|
-
loader
|
82
|
-
schema_load_paths: ['example/_schemas'],
|
83
|
-
defaults_load_paths: ['example/_defaults'])
|
84
|
-
|
85
|
-
loader.load('example/config.yml')
|
81
|
+
loader.load('example/config_with_defaults_and_schema.yml')
|
86
82
|
# {"movies"=>
|
87
83
|
# [{"title"=>"The Shawshank Redemption",
|
88
84
|
# "year"=>1994,
|
@@ -105,16 +101,14 @@ missing and Frise by default prints a summary of the errors and terminates the p
|
|
105
101
|
|
106
102
|
|
107
103
|
```ruby
|
108
|
-
loader
|
109
|
-
|
110
|
-
loader.load('example/config.yml')
|
104
|
+
loader.load('example/config_with_schema.yml')
|
111
105
|
# 2 config error(s) found:
|
112
106
|
# - At movies.0.director: missing required value
|
113
107
|
# - At ui: missing required value
|
114
108
|
```
|
115
109
|
|
116
110
|
Once more, the structure of the schema mimics the structure of the config itself, making it easy to
|
117
|
-
write schemas and
|
111
|
+
write schemas first and create a config scaffold from its schema later.
|
118
112
|
|
119
113
|
Users can check a whole range of properties in config values besides their type: optional values,
|
120
114
|
hashes with validated keys, hashes with unknown keys and even custom validations are also supported.
|
data/Rakefile
CHANGED
data/frise.gemspec
CHANGED
@@ -1,31 +1,31 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
lib = File.expand_path('
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
5
|
require 'frise/version'
|
6
6
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
8
|
spec.name = 'frise'
|
9
9
|
spec.version = Frise::VERSION
|
10
|
-
spec.authors = ['
|
11
|
-
spec.email = ['
|
10
|
+
spec.authors = ['Velocidi']
|
11
|
+
spec.email = ['hello@velocidi.com']
|
12
12
|
|
13
13
|
spec.summary = 'Ruby config library with schema validation, default values and templating'
|
14
|
-
spec.homepage = 'https://github.com/
|
14
|
+
spec.homepage = 'https://github.com/velocidi/frise'
|
15
15
|
spec.license = 'Apache-2.0'
|
16
16
|
|
17
17
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
18
|
f.match(%r{^(test|spec|features|example)/})
|
19
19
|
end
|
20
20
|
spec.require_paths = ['lib']
|
21
|
-
spec.required_ruby_version = '>= 2.
|
21
|
+
spec.required_ruby_version = '>= 2.3.0'
|
22
22
|
|
23
|
-
spec.add_dependency 'liquid', '~>
|
23
|
+
spec.add_dependency 'liquid', '~> 4.0'
|
24
24
|
|
25
|
-
spec.add_development_dependency 'bundler', '~>
|
26
|
-
spec.add_development_dependency 'coveralls', '~> 0.8
|
27
|
-
spec.add_development_dependency '
|
28
|
-
spec.add_development_dependency '
|
29
|
-
spec.add_development_dependency '
|
30
|
-
spec.add_development_dependency '
|
25
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
26
|
+
spec.add_development_dependency 'coveralls', '~> 0.8'
|
27
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
28
|
+
spec.add_development_dependency 'rspec', '~> 3.9'
|
29
|
+
spec.add_development_dependency 'rubocop', '0.77.0'
|
30
|
+
spec.add_development_dependency 'simplecov', '~> 0.16'
|
31
31
|
end
|
data/lib/frise.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'frise/parser'
|
2
4
|
|
3
5
|
module Frise
|
@@ -5,65 +7,79 @@ module Frise
|
|
5
7
|
#
|
6
8
|
# The merge_defaults and merge_defaults_at entrypoint methods provide ways to read files with
|
7
9
|
# defaults and apply them to configuration objects.
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
class DefaultsLoader
|
11
|
+
SYMBOLS = %w[$all $optional].freeze
|
12
|
+
|
13
|
+
def initialize(include_sym: '$include', content_include_sym: '$content_include', schema_sym: '$schema', delete_sym: '$delete')
|
14
|
+
@include_sym = include_sym
|
15
|
+
@content_include_sym = content_include_sym
|
16
|
+
@schema_sym = schema_sym
|
17
|
+
@delete_sym = delete_sym
|
18
|
+
end
|
19
|
+
|
20
|
+
def widened_class(obj)
|
21
|
+
class_name = obj.class.to_s
|
22
|
+
return 'String' if class_name == 'Hash' && !obj[@content_include_sym].nil?
|
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
|
16
27
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
28
|
+
def merge_defaults_obj(config, defaults)
|
29
|
+
config_class = widened_class(config)
|
30
|
+
defaults_class = widened_class(defaults)
|
31
|
+
|
32
|
+
if defaults.nil?
|
33
|
+
config
|
34
|
+
|
35
|
+
elsif config.nil?
|
36
|
+
if defaults_class != 'Hash' then defaults
|
37
|
+
elsif defaults['$optional'] then nil
|
38
|
+
else merge_defaults_obj({}, defaults)
|
50
39
|
end
|
51
|
-
end
|
52
40
|
|
53
|
-
|
54
|
-
|
55
|
-
merge_defaults_obj(config, defaults)
|
56
|
-
end
|
41
|
+
elsif config == @delete_sym
|
42
|
+
config
|
57
43
|
|
58
|
-
|
59
|
-
defaults
|
60
|
-
merge_defaults_obj(config, defaults)
|
61
|
-
end
|
44
|
+
elsif defaults_class == 'Array' && config_class == 'Array'
|
45
|
+
defaults + config
|
62
46
|
|
63
|
-
|
64
|
-
|
65
|
-
|
47
|
+
elsif defaults_class == 'Hash' && defaults['$all'] && config_class == 'Array'
|
48
|
+
config.map { |elem| merge_defaults_obj(elem, defaults['$all']) }
|
49
|
+
|
50
|
+
elsif defaults_class == 'Hash' && config_class == 'Hash'
|
51
|
+
new_config = {}
|
52
|
+
(config.keys + defaults.keys).uniq.each do |key|
|
53
|
+
next if SYMBOLS.include?(key)
|
54
|
+
new_config[key] = config[key]
|
55
|
+
new_config[key] = merge_defaults_obj(new_config[key], defaults[key]) if defaults.key?(key)
|
56
|
+
new_config[key] = merge_defaults_obj(new_config[key], defaults['$all']) unless new_config[key].nil?
|
57
|
+
new_config.delete(key) if new_config[key].nil?
|
58
|
+
end
|
59
|
+
new_config
|
60
|
+
|
61
|
+
elsif defaults_class != config_class
|
62
|
+
raise "Cannot merge config #{config.inspect} (#{widened_class(config)}) " \
|
63
|
+
"with default #{defaults.inspect} (#{widened_class(defaults)})"
|
64
|
+
|
65
|
+
else
|
66
|
+
config
|
66
67
|
end
|
67
68
|
end
|
69
|
+
|
70
|
+
def merge_defaults_obj_at(config, at_path, defaults)
|
71
|
+
at_path.reverse.each { |key| defaults = { key => defaults } }
|
72
|
+
merge_defaults_obj(config, defaults)
|
73
|
+
end
|
74
|
+
|
75
|
+
def merge_defaults(config, defaults_file, symbol_table = config)
|
76
|
+
defaults = Parser.parse(defaults_file, symbol_table) || {}
|
77
|
+
merge_defaults_obj(config, defaults)
|
78
|
+
end
|
79
|
+
|
80
|
+
def merge_defaults_at(config, at_path, defaults_file, symbol_table = config)
|
81
|
+
defaults = Parser.parse(defaults_file, symbol_table) || {}
|
82
|
+
merge_defaults_obj_at(config, at_path, defaults)
|
83
|
+
end
|
68
84
|
end
|
69
85
|
end
|
data/lib/frise/loader.rb
CHANGED
@@ -1,60 +1,200 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'frise/defaults_loader'
|
4
|
+
require 'frise/loader/lazy'
|
2
5
|
require 'frise/parser'
|
3
6
|
require 'frise/validator'
|
4
7
|
|
5
8
|
module Frise
|
6
9
|
# The entrypoint for loading configs from files according to the conventions defined for Frise.
|
7
10
|
#
|
8
|
-
# The load method loads a configuration file, merges
|
9
|
-
# its schema. Other methods in Loader perform only parts of the process.
|
11
|
+
# The load method loads a configuration file, merges the applicable includes and validates its schema.
|
10
12
|
class Loader
|
11
|
-
def initialize(
|
12
|
-
|
13
|
-
|
13
|
+
def initialize(include_sym: '$include',
|
14
|
+
content_include_sym: '$content_include',
|
15
|
+
schema_sym: '$schema',
|
16
|
+
delete_sym: '$delete',
|
17
|
+
pre_loaders: [],
|
18
|
+
validators: nil,
|
19
|
+
exit_on_fail: true)
|
20
|
+
|
21
|
+
@include_sym = include_sym
|
22
|
+
@content_include_sym = content_include_sym
|
23
|
+
@schema_sym = schema_sym
|
24
|
+
@delete_sym = delete_sym
|
14
25
|
@pre_loaders = pre_loaders
|
15
26
|
@validators = validators
|
27
|
+
@exit_on_fail = exit_on_fail
|
28
|
+
|
29
|
+
@defaults_loader = DefaultsLoader.new(
|
30
|
+
include_sym: include_sym,
|
31
|
+
content_include_sym: content_include_sym,
|
32
|
+
schema_sym: schema_sym,
|
33
|
+
delete_sym: delete_sym
|
34
|
+
)
|
16
35
|
end
|
17
36
|
|
18
|
-
def load(config_file,
|
19
|
-
config = Parser.parse(config_file,
|
20
|
-
|
37
|
+
def load(config_file, global_vars = {})
|
38
|
+
config = Parser.parse(config_file, global_vars)
|
39
|
+
return nil unless config
|
21
40
|
|
22
41
|
@pre_loaders.each do |pre_loader|
|
23
42
|
config = pre_loader.call(config)
|
24
43
|
end
|
25
44
|
|
26
|
-
config =
|
27
|
-
|
45
|
+
config = process_includes(config, [], config, global_vars) unless @include_sym.nil?
|
46
|
+
config = process_schemas(config, [], global_vars) unless @schema_sym.nil?
|
47
|
+
config
|
28
48
|
end
|
29
49
|
|
30
|
-
|
31
|
-
merge_defaults_at(config, [], defaults_name, symbol_table)
|
32
|
-
end
|
50
|
+
private
|
33
51
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
52
|
+
def process_includes(config, at_path, root_config, global_vars, include_confs_stack = [])
|
53
|
+
return config unless config.class == Hash
|
54
|
+
|
55
|
+
# process $content_include directives
|
56
|
+
config, content_include_confs = extract_content_include(config, at_path)
|
57
|
+
unless content_include_confs.empty?
|
58
|
+
raise "At #{build_path(at_path)}: a #{@content_include_sym} must not have any sibling key" unless config.empty?
|
59
|
+
|
60
|
+
content = ''
|
61
|
+
content_include_confs.each do |include_conf|
|
62
|
+
symbol_table = build_symbol_table(root_config, at_path, nil, global_vars, include_conf)
|
63
|
+
content += Parser.parse_as_text(include_conf['file'], symbol_table) || ''
|
64
|
+
end
|
65
|
+
return content
|
40
66
|
end
|
41
|
-
|
67
|
+
|
68
|
+
# process $include directives
|
69
|
+
config, next_include_confs = extract_include(config, at_path)
|
70
|
+
include_confs = next_include_confs + include_confs_stack
|
71
|
+
res = if include_confs.empty?
|
72
|
+
config.map { |k, v| [k, process_includes(v, at_path + [k], root_config, global_vars)] }.to_h
|
73
|
+
else
|
74
|
+
Lazy.new do
|
75
|
+
include_conf = include_confs.first
|
76
|
+
rest_include_confs = include_confs[1..-1]
|
77
|
+
symbol_table = build_symbol_table(root_config, at_path, config, global_vars, include_conf)
|
78
|
+
included_config = Parser.parse(include_conf['file'], symbol_table)
|
79
|
+
config = @defaults_loader.merge_defaults_obj(config, included_config)
|
80
|
+
process_includes(config, at_path, merge_at(root_config, at_path, config), global_vars, rest_include_confs)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
@delete_sym.nil? ? res : omit_deleted(res)
|
42
84
|
end
|
43
85
|
|
44
|
-
def
|
45
|
-
|
86
|
+
def process_schema_includes(schema, at_path, global_vars)
|
87
|
+
return schema unless schema.class == Hash
|
88
|
+
|
89
|
+
schema, included_schemas = extract_include(schema, at_path)
|
90
|
+
if included_schemas.empty?
|
91
|
+
schema.map { |k, v| [k, process_schema_includes(v, at_path + [k], global_vars)] }.to_h
|
92
|
+
else
|
93
|
+
included_schemas.each do |defaults_conf|
|
94
|
+
schema = Parser.parse(defaults_conf['file'], global_vars).merge(schema)
|
95
|
+
end
|
96
|
+
process_schema_includes(schema, at_path, global_vars)
|
97
|
+
end
|
46
98
|
end
|
47
99
|
|
48
|
-
def
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
100
|
+
def process_schemas(config, at_path, global_vars)
|
101
|
+
return config unless config.class == Hash
|
102
|
+
|
103
|
+
config = config.map do |k, v|
|
104
|
+
new_v = process_schemas(v, at_path + [k], global_vars)
|
105
|
+
return nil if !v.nil? && new_v.nil?
|
106
|
+
[k, new_v]
|
107
|
+
end.to_h
|
108
|
+
|
109
|
+
config, schema_files = extract_schema(config, at_path)
|
110
|
+
schema_files.each do |schema_file|
|
111
|
+
schema = Parser.parse(schema_file, global_vars)
|
112
|
+
schema = process_schema_includes(schema, at_path, global_vars)
|
113
|
+
|
114
|
+
errors = Validator.validate_obj(config,
|
115
|
+
schema,
|
116
|
+
path_prefix: at_path,
|
117
|
+
validators: @validators,
|
118
|
+
print: @exit_on_fail,
|
119
|
+
fatal: @exit_on_fail)
|
55
120
|
return nil if errors.any?
|
56
121
|
end
|
57
122
|
config
|
58
123
|
end
|
124
|
+
|
125
|
+
def extract_schema(config, at_path)
|
126
|
+
extract_special(config, @schema_sym, at_path) do |value|
|
127
|
+
case value
|
128
|
+
when String then value
|
129
|
+
else raise "At #{build_path(at_path)}: illegal value for a #{@schema_sym} element: #{value.inspect}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def extract_include(config, at_path)
|
135
|
+
extract_include_base(config, @include_sym, at_path)
|
136
|
+
end
|
137
|
+
|
138
|
+
def extract_content_include(config, at_path)
|
139
|
+
extract_include_base(config, @content_include_sym, at_path)
|
140
|
+
end
|
141
|
+
|
142
|
+
def extract_include_base(config, sym, at_path)
|
143
|
+
extract_special(config, sym, at_path) do |value|
|
144
|
+
case value
|
145
|
+
when Hash then value
|
146
|
+
when String then { 'file' => value }
|
147
|
+
else raise "At #{build_path(at_path)}: illegal value for a #{sym} element: #{value.inspect}"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def extract_special(config, key, at_path)
|
153
|
+
case config[key]
|
154
|
+
when nil then [config, []]
|
155
|
+
when Array then [config.reject { |k| k == key }, config[key].map { |e| yield e }]
|
156
|
+
else raise "At #{build_path(at_path)}: illegal value for #{key}: #{config[key].inspect}"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# merges the `to_merge` value on `config` at path `at_path`
|
161
|
+
def merge_at(config, at_path, to_merge)
|
162
|
+
return config.merge(to_merge) if at_path.empty?
|
163
|
+
head, *tail = at_path
|
164
|
+
config.merge(head => merge_at(config[head], tail, to_merge))
|
165
|
+
end
|
166
|
+
|
167
|
+
# returns the config without the keys whose values are @delete_sym
|
168
|
+
def omit_deleted(config)
|
169
|
+
config.each_with_object({}) do |(k, v), new_hash|
|
170
|
+
if v.is_a?(Hash)
|
171
|
+
new_hash[k] = omit_deleted(v)
|
172
|
+
else
|
173
|
+
new_hash[k] = v unless v == @delete_sym
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# builds the symbol table for the Liquid renderization of a file, based on:
|
179
|
+
# - `root_config`: the root of the whole config
|
180
|
+
# - `at_path`: the current path
|
181
|
+
# - `config`: the config subtree being built
|
182
|
+
# - `global_vars`: the global variables
|
183
|
+
# - `include_conf`: the $include or $content_include configuration
|
184
|
+
def build_symbol_table(root_config, at_path, config, global_vars, include_conf)
|
185
|
+
extra_vars = (include_conf['vars'] || {}).map { |k, v| [k, root_config.dig(*v.split('.'))] }.to_h
|
186
|
+
extra_consts = include_conf['constants'] || {}
|
187
|
+
|
188
|
+
omit_deleted(config ? merge_at(root_config, at_path, config) : root_config)
|
189
|
+
.merge(global_vars)
|
190
|
+
.merge(extra_vars)
|
191
|
+
.merge(extra_consts)
|
192
|
+
.merge('_this' => config)
|
193
|
+
end
|
194
|
+
|
195
|
+
# builds a user-friendly string indicating a path
|
196
|
+
def build_path(at_path)
|
197
|
+
at_path.empty? ? '<root>' : at_path.join('.')
|
198
|
+
end
|
59
199
|
end
|
60
200
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Frise
|
4
|
+
class Loader
|
5
|
+
# A basic proxy object.
|
6
|
+
class Lazy < BasicObject
|
7
|
+
def initialize(&callable)
|
8
|
+
@callable = callable
|
9
|
+
end
|
10
|
+
|
11
|
+
def __target_object__
|
12
|
+
@__target_object__ ||= @callable.call
|
13
|
+
end
|
14
|
+
|
15
|
+
# rubocop:disable Style/MethodMissingSuper
|
16
|
+
def method_missing(method_name, *args, &block)
|
17
|
+
__target_object__.send(method_name, *args, &block)
|
18
|
+
end
|
19
|
+
# rubocop:enable Style/MethodMissingSuper
|
20
|
+
|
21
|
+
def respond_to_missing?(method_name, include_private = false)
|
22
|
+
__target_object__.respond_to?(method_name, include_private)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/frise/parser.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'liquid'
|
2
4
|
require 'yaml'
|
3
5
|
|
@@ -7,10 +9,21 @@ module Frise
|
|
7
9
|
module Parser
|
8
10
|
class << self
|
9
11
|
def parse(file, symbol_table = nil)
|
10
|
-
return
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
return nil unless File.file? file
|
13
|
+
YAML.safe_load(parse_as_text(file, symbol_table), [], [], true) || {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse_as_text(file, symbol_table = nil)
|
17
|
+
return nil unless File.file? file
|
18
|
+
content = File.read(file)
|
19
|
+
content = Liquid::Template.parse(content).render with_internal_vars(file, symbol_table) if symbol_table
|
20
|
+
content
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def with_internal_vars(file, symbol_table)
|
26
|
+
symbol_table.merge('_file_dir' => File.expand_path(File.dirname(file)))
|
14
27
|
end
|
15
28
|
end
|
16
29
|
end
|
data/lib/frise/validator.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require 'frise/parser'
|
4
5
|
require 'set'
|
@@ -26,21 +27,23 @@ module Frise
|
|
26
27
|
end
|
27
28
|
|
28
29
|
def add_validation_error(path, msg)
|
29
|
-
logged_path = path.empty? ? '<root>' : path
|
30
|
+
logged_path = path.empty? ? '<root>' : path
|
30
31
|
@errors << "At #{logged_path}: #{msg}"
|
31
32
|
end
|
32
33
|
|
33
34
|
def get_full_schema(schema)
|
34
35
|
case schema
|
35
|
-
when Hash then
|
36
|
+
when Hash then
|
37
|
+
default_type = schema[:enum] || schema[:one_of] ? 'Object' : 'Hash'
|
38
|
+
{ type: default_type }.merge(schema)
|
36
39
|
when Symbol then { type: 'Object', validate: schema }
|
37
|
-
when Array
|
40
|
+
when Array
|
38
41
|
if schema.size == 1
|
39
42
|
{ type: 'Array', all: schema[0] }
|
40
43
|
else
|
41
44
|
(raise "Invalid schema: #{schema.inspect}")
|
42
45
|
end
|
43
|
-
when String
|
46
|
+
when String
|
44
47
|
if schema.end_with?('?')
|
45
48
|
{ type: schema[0..-2], optional: true }
|
46
49
|
else
|
@@ -80,17 +83,39 @@ module Frise
|
|
80
83
|
if full_schema[:validate]
|
81
84
|
begin
|
82
85
|
@validators.method(full_schema[:validate]).call(@root, obj)
|
83
|
-
rescue StandardError =>
|
84
|
-
add_validation_error(path,
|
86
|
+
rescue StandardError => e
|
87
|
+
add_validation_error(path, e.message)
|
85
88
|
end
|
86
89
|
end
|
87
90
|
true
|
88
91
|
end
|
89
92
|
|
93
|
+
def validate_enum(full_schema, obj, path)
|
94
|
+
if full_schema[:enum] && !full_schema[:enum].include?(obj)
|
95
|
+
add_validation_error(path, "invalid value #{obj.inspect}. " \
|
96
|
+
"Accepted values are #{full_schema[:enum].map(&:inspect).join(', ')}")
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
def validate_one_of(full_schema, obj, path)
|
103
|
+
if full_schema[:one_of]
|
104
|
+
full_schema[:one_of].each do |schema_opt|
|
105
|
+
opt_validator = Validator.new(@root, @validators)
|
106
|
+
opt_validator.validate_object(path, obj, schema_opt)
|
107
|
+
return true if opt_validator.errors.empty?
|
108
|
+
end
|
109
|
+
add_validation_error(path, "#{obj.inspect} does not match any of the possible schemas")
|
110
|
+
return false
|
111
|
+
end
|
112
|
+
true
|
113
|
+
end
|
114
|
+
|
90
115
|
def validate_spec_keys(full_schema, obj, path, processed_keys)
|
91
116
|
full_schema.each do |spec_key, spec_value|
|
92
117
|
next if spec_key.is_a?(Symbol)
|
93
|
-
validate_object("#{path}.#{spec_key}", obj[spec_key], spec_value)
|
118
|
+
validate_object(path.empty? ? spec_key : "#{path}.#{spec_key}", obj[spec_key], spec_value)
|
94
119
|
processed_keys << spec_key
|
95
120
|
end
|
96
121
|
true
|
@@ -101,13 +126,11 @@ module Frise
|
|
101
126
|
if expected_types.size == 1 && expected_types[0].ancestors.member?(Enumerable)
|
102
127
|
hash = obj.is_a?(Hash) ? obj : Hash[obj.map.with_index { |x, i| [i, x] }]
|
103
128
|
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
|
129
|
+
validate_object(path, key, full_schema[:all_keys]) if full_schema[:all_keys] && !key.is_a?(Symbol)
|
107
130
|
|
108
131
|
next if processed_keys.member? key
|
109
132
|
if full_schema[:all]
|
110
|
-
validate_object("#{path}.#{key}", value, full_schema[:all])
|
133
|
+
validate_object(path.empty? ? key : "#{path}.#{key}", value, full_schema[:all])
|
111
134
|
elsif !full_schema[:allow_unknown_keys]
|
112
135
|
add_validation_error(path, "unknown key: #{key}")
|
113
136
|
end
|
@@ -122,6 +145,8 @@ module Frise
|
|
122
145
|
return unless validate_optional(full_schema, obj, path)
|
123
146
|
return unless validate_type(full_schema, obj, path)
|
124
147
|
return unless validate_custom(full_schema, obj, path)
|
148
|
+
return unless validate_enum(full_schema, obj, path)
|
149
|
+
return unless validate_one_of(full_schema, obj, path)
|
125
150
|
|
126
151
|
processed_keys = Set.new
|
127
152
|
return unless validate_spec_keys(full_schema, obj, path, processed_keys)
|
@@ -138,32 +163,36 @@ module Frise
|
|
138
163
|
end
|
139
164
|
|
140
165
|
def self.validate_obj(config, schema, options = {})
|
141
|
-
|
142
|
-
|
166
|
+
validate_obj_at(config, [], schema, **options)
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.validate_obj_at(config, at_path, schema, path_prefix: nil, validators: nil, print: nil, fatal: nil, raise_error: nil)
|
170
|
+
schema = parse_symbols(schema)
|
171
|
+
at_path.reverse.each { |key| schema = { key => schema, :allow_unknown_keys => true } }
|
172
|
+
|
173
|
+
validator = Validator.new(config, validators)
|
174
|
+
validator.validate_object((path_prefix || []).join('.'), config, schema)
|
143
175
|
|
144
176
|
if validator.errors.any?
|
145
|
-
if
|
177
|
+
if print
|
146
178
|
puts "#{validator.errors.length} config error(s) found:"
|
147
179
|
validator.errors.each do |error|
|
148
180
|
puts " - #{error}"
|
149
181
|
end
|
150
182
|
end
|
151
183
|
|
152
|
-
exit 1 if
|
153
|
-
raise ValidationError.new(validator.errors), 'Invalid configuration' if
|
184
|
+
exit 1 if fatal
|
185
|
+
raise ValidationError.new(validator.errors), 'Invalid configuration' if raise_error
|
154
186
|
end
|
155
187
|
validator.errors
|
156
188
|
end
|
157
189
|
|
158
190
|
def self.validate(config, schema_file, options = {})
|
159
|
-
|
160
|
-
validate_obj(config, schema, options)
|
191
|
+
validate_obj_at(config, [], Parser.parse(schema_file) || { allow_unknown_keys: true }, **options)
|
161
192
|
end
|
162
193
|
|
163
194
|
def self.validate_at(config, at_path, schema_file, options = {})
|
164
|
-
|
165
|
-
at_path.reverse.each { |key| schema = { key => schema, :allow_unknown_keys => true } }
|
166
|
-
validate_obj(config, schema, options)
|
195
|
+
validate_obj_at(config, at_path, Parser.parse(schema_file) || { allow_unknown_keys: true }, **options)
|
167
196
|
end
|
168
197
|
end
|
169
198
|
|
data/lib/frise/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: frise
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- Velocidi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-07-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: liquid
|
@@ -16,101 +16,101 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '4.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '4.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '2.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '2.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: coveralls
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 0.8
|
47
|
+
version: '0.8'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 0.8
|
54
|
+
version: '0.8'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: rake
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 0
|
61
|
+
version: '13.0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: 0
|
68
|
+
version: '13.0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: rspec
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
75
|
+
version: '3.9'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
82
|
+
version: '3.9'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: rubocop
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- -
|
87
|
+
- - '='
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version:
|
89
|
+
version: 0.77.0
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- -
|
94
|
+
- - '='
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version:
|
96
|
+
version: 0.77.0
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
98
|
+
name: simplecov
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: 0.
|
103
|
+
version: '0.16'
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: 0.
|
110
|
+
version: '0.16'
|
111
111
|
description:
|
112
112
|
email:
|
113
|
-
-
|
113
|
+
- hello@velocidi.com
|
114
114
|
executables: []
|
115
115
|
extensions: []
|
116
116
|
extra_rdoc_files: []
|
@@ -119,6 +119,7 @@ files:
|
|
119
119
|
- ".rspec"
|
120
120
|
- ".rubocop.yml"
|
121
121
|
- ".travis.yml"
|
122
|
+
- CHANGELOG.md
|
122
123
|
- Gemfile
|
123
124
|
- LICENSE.txt
|
124
125
|
- README.md
|
@@ -127,10 +128,11 @@ files:
|
|
127
128
|
- lib/frise.rb
|
128
129
|
- lib/frise/defaults_loader.rb
|
129
130
|
- lib/frise/loader.rb
|
131
|
+
- lib/frise/loader/lazy.rb
|
130
132
|
- lib/frise/parser.rb
|
131
133
|
- lib/frise/validator.rb
|
132
134
|
- lib/frise/version.rb
|
133
|
-
homepage: https://github.com/
|
135
|
+
homepage: https://github.com/velocidi/frise
|
134
136
|
licenses:
|
135
137
|
- Apache-2.0
|
136
138
|
metadata: {}
|
@@ -142,15 +144,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
142
144
|
requirements:
|
143
145
|
- - ">="
|
144
146
|
- !ruby/object:Gem::Version
|
145
|
-
version: 2.
|
147
|
+
version: 2.3.0
|
146
148
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
149
|
requirements:
|
148
150
|
- - ">="
|
149
151
|
- !ruby/object:Gem::Version
|
150
152
|
version: '0'
|
151
153
|
requirements: []
|
152
|
-
|
153
|
-
rubygems_version: 2.6.11
|
154
|
+
rubygems_version: 3.0.6
|
154
155
|
signing_key:
|
155
156
|
specification_version: 4
|
156
157
|
summary: Ruby config library with schema validation, default values and templating
|