frise 0.1.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
-
[](https://travis-ci.org/velocidi/frise)
|
3
|
+
[](https://coveralls.io/github/velocidi/frise?branch=master)
|
4
|
+
[](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
|