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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 41db8000684dfb7ee23cc7f26a790a57bb38ad76
4
- data.tar.gz: 782cf2ff2d3866a754950fd8edd3735a1bb1f425
2
+ SHA256:
3
+ metadata.gz: 86558df34daa0ba92e7e7a1863ceb5124ea52c39dc82a85924c4db1bd89cf127
4
+ data.tar.gz: fd286367be54e292a6df52daa6f5255810d6e3980bce538d0c2391d5fdab9ca9
5
5
  SHA512:
6
- metadata.gz: c37c19e1f9edbad815b30d52f867d4344fd3b3da8a376e30236814a47ddd579b889459b1a702190fe5d1c1d2b99afdc3abef685f54a062616ea632872640a5e2
7
- data.tar.gz: c9c87e93dbd3b361a5632431ac8f83650749c7d1de090c3397299b403cda7e1a986767356d8795dc3cf037f9461227a8287dd3870df79a573031ee5928d31931
6
+ metadata.gz: c48f86d8190c30bf650055dfa7749ecdf9d2545f4a6c34593c29e402c0da06dd5420eb8244c3f112f8501564a5bf26798acae2fbeb006c10bb1c4eb5dc4e7c1a
7
+ data.tar.gz: 679a4edb6364662160f4e1d6872c8949e3fd6338433da50de5a109addec0a31dec69b3013ca4038fbb40283383b15f160ecf4723c546dea53ab6a8d75db6f77c
@@ -1,2 +1,10 @@
1
+ AllCops:
2
+ TargetRubyVersion: '2.3'
3
+ Layout/EmptyLineAfterGuardClause:
4
+ Enabled: false
1
5
  Metrics:
2
6
  Enabled: false
7
+ Naming/BlockParameterName:
8
+ Enabled: false
9
+ Naming/MethodParameterName:
10
+ Enabled: false
@@ -1,6 +1,10 @@
1
1
  language: ruby
2
+ cache: bundler
2
3
  rvm:
3
- - 2.1
4
- - 2.2
5
4
  - 2.3
6
5
  - 2.4
6
+ - 2.5
7
+ - 2.6
8
+ - 2.7
9
+ before_install:
10
+ - gem install bundler
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in frise.gemspec
@@ -1,4 +1,4 @@
1
- Copyright 2017 ShiftForward, S.A. [http://www.shiftforward.eu]
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/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)
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
- - Completes it with default values specified in another file or set of files;
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 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:
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 = Frise::Loader.new(defaults_load_paths: ['example/_defaults'])
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
- `schema_load_paths`, users can provide directories where schema files such as
78
- [example/_schemas/config.yml](example/_schemas/config.yml) can be found:
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 = Frise::Loader.new(
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 = Frise::Loader.new(schema_load_paths: ['example/_schemas'])
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 to create a config scaffold from its schema later.
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
 
3
5
  def load_if_available(req_path)
@@ -1,31 +1,31 @@
1
- # coding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path('../lib', __FILE__)
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 = ['ShiftForward']
11
- spec.email = ['info@shiftforward.eu']
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/ShiftForward/frise'
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.1.0'
21
+ spec.required_ruby_version = '>= 2.3.0'
22
22
 
23
- spec.add_dependency 'liquid', '~> 3.0'
23
+ spec.add_dependency 'liquid', '~> 4.0'
24
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'
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
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'frise/defaults_loader'
4
+ require 'frise/loader/lazy'
2
5
  require 'frise/loader'
3
6
  require 'frise/parser'
4
7
  require 'frise/validator'
@@ -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
- 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
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
- 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
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
- 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
41
+ elsif config == @delete_sym
42
+ config
57
43
 
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
44
+ elsif defaults_class == 'Array' && config_class == 'Array'
45
+ defaults + config
62
46
 
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)
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
@@ -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 it with the applicable defaults and validates
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(schema_load_paths: [], defaults_load_paths: [], pre_loaders: [], validators: nil)
12
- @schema_load_paths = schema_load_paths
13
- @defaults_load_paths = defaults_load_paths
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, exit_on_fail = true, symbol_table = {})
19
- config = Parser.parse(config_file, symbol_table)
20
- config_name = File.basename(config_file)
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 = merge_defaults(config, config_name, symbol_table)
27
- validate(config, config_name, exit_on_fail)
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
- def merge_defaults(config, defaults_name, symbol_table = {})
31
- merge_defaults_at(config, [], defaults_name, symbol_table)
32
- end
50
+ private
33
51
 
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
- )
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
- config
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 validate(config, schema_name, exit_on_fail = true)
45
- validate_at(config, [], schema_name, exit_on_fail)
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 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)
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
@@ -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 {} 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) || {}
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
@@ -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[1..-1]
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 schema
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 then
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 then
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 => ex
84
- add_validation_error(path, ex.message)
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
- validator = Validator.new(config, options[:validators])
142
- validator.validate_object('', config, schema)
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 options[:print]
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 options[:fatal]
153
- raise ValidationError.new(validator.errors), 'Invalid configuration' if options[:raise_error]
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
- schema = parse_symbols(Parser.parse(schema_file))
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
- 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)
195
+ validate_obj_at(config, at_path, Parser.parse(schema_file) || { allow_unknown_keys: true }, **options)
167
196
  end
168
197
  end
169
198
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Frise
2
- VERSION = '0.1.0'.freeze
4
+ VERSION = '0.4.1'
3
5
  end
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.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
- - ShiftForward
7
+ - Velocidi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-08-10 00:00:00.000000000 Z
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: '3.0'
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: '3.0'
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: '1.14'
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: '1.14'
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.21
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.21
54
+ version: '0.8'
55
55
  - !ruby/object:Gem::Dependency
56
- name: simplecov
56
+ name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 0.14.1
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.14.1
68
+ version: '13.0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: rake
70
+ name: rspec
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '10.0'
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: '10.0'
82
+ version: '3.9'
83
83
  - !ruby/object:Gem::Dependency
84
- name: rspec
84
+ name: rubocop
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - "~>"
87
+ - - '='
88
88
  - !ruby/object:Gem::Version
89
- version: '3.4'
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: '3.4'
96
+ version: 0.77.0
97
97
  - !ruby/object:Gem::Dependency
98
- name: rubocop
98
+ name: simplecov
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 0.49.1
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.49.1
110
+ version: '0.16'
111
111
  description:
112
112
  email:
113
- - info@shiftforward.eu
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/ShiftForward/frise
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.1.0
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
- rubyforge_project:
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