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 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