qonfig 0.21.0 → 0.25.0

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.travis.yml +23 -20
  4. data/CHANGELOG.md +58 -2
  5. data/LICENSE.txt +1 -1
  6. data/README.md +124 -10
  7. data/Rakefile +0 -1
  8. data/bin/rspec +1 -0
  9. data/gemfiles/with_external_deps.gemfile +2 -0
  10. data/lib/qonfig.rb +4 -0
  11. data/lib/qonfig/commands/definition/expose_json.rb +2 -2
  12. data/lib/qonfig/commands/definition/expose_yaml.rb +2 -2
  13. data/lib/qonfig/commands/definition/load_from_json.rb +2 -2
  14. data/lib/qonfig/commands/definition/load_from_yaml.rb +2 -2
  15. data/lib/qonfig/commands/instantiation/values_file.rb +13 -4
  16. data/lib/qonfig/data_set.rb +13 -9
  17. data/lib/qonfig/dsl.rb +7 -7
  18. data/lib/qonfig/plugins.rb +1 -0
  19. data/lib/qonfig/plugins/pretty_print.rb +8 -1
  20. data/lib/qonfig/plugins/pretty_print/requirements.rb +3 -0
  21. data/lib/qonfig/plugins/pretty_print/ruby_2_7_basic_object_pp_patch.rb +44 -0
  22. data/lib/qonfig/plugins/toml/commands/definition/expose_toml.rb +4 -4
  23. data/lib/qonfig/plugins/toml/commands/definition/load_from_toml.rb +1 -1
  24. data/lib/qonfig/plugins/toml/data_set.rb +2 -2
  25. data/lib/qonfig/plugins/toml/dsl.rb +2 -2
  26. data/lib/qonfig/plugins/vault.rb +24 -0
  27. data/lib/qonfig/plugins/vault/commands/definition/expose_vault.rb +142 -0
  28. data/lib/qonfig/plugins/vault/commands/definition/load_from_vault.rb +53 -0
  29. data/lib/qonfig/plugins/vault/dsl.rb +35 -0
  30. data/lib/qonfig/plugins/vault/errors.rb +9 -0
  31. data/lib/qonfig/plugins/vault/loaders/vault.rb +73 -0
  32. data/lib/qonfig/settings.rb +78 -21
  33. data/lib/qonfig/settings/key_matcher.rb +2 -0
  34. data/lib/qonfig/uploaders/file.rb +2 -2
  35. data/lib/qonfig/uploaders/yaml.rb +1 -1
  36. data/lib/qonfig/version.rb +1 -1
  37. data/qonfig.gemspec +5 -6
  38. metadata +20 -26
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.25.0
5
+ class Qonfig::Plugins::Vault < Qonfig::Plugins::Abstract
6
+ class << self
7
+ # @return [void]
8
+ #
9
+ # @api private
10
+ # @since 0.25.0
11
+ def install!
12
+ raise(
13
+ Qonfig::UnresolvedPluginDependencyError,
14
+ '::Vault does not exist or "vault" gem is not loaded'
15
+ ) unless const_defined?('::Vault')
16
+
17
+ require_relative 'vault/errors'
18
+ require_relative 'vault/loaders/vault'
19
+ require_relative 'vault/commands/definition/load_from_vault'
20
+ require_relative 'vault/commands/definition/expose_vault'
21
+ require_relative 'vault/dsl'
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.25.0
5
+ class Qonfig::Commands::Definition::ExposeVault < Qonfig::Commands::Base
6
+ # @since 0.25.0
7
+ self.inheritable = true
8
+
9
+ # @return [Hash<Symbol,Symbol>]
10
+ #
11
+ # @api private
12
+ # @since 0.25.0
13
+ EXPOSERS = { path: :path, env_key: :env_key }.freeze
14
+
15
+ # @return [Hash]
16
+ #
17
+ # @api private
18
+ # @since 0.25.0
19
+ EMPTY_VAULT_DATA = {}.freeze
20
+
21
+ # @return [String, Pathname]
22
+ #
23
+ # @api private
24
+ # @since 0.25.0
25
+ attr_reader :path
26
+
27
+ # @return [Boolean]
28
+ #
29
+ # @api private
30
+ # @since 0.25.0
31
+ attr_reader :strict
32
+
33
+ # @return [Symbol]
34
+ #
35
+ # @api private
36
+ # @since 0.25.0
37
+ attr_reader :via
38
+
39
+ # @return [Symbol, String]
40
+ #
41
+ # @api private
42
+ # @since 0.25.0
43
+ attr_reader :env
44
+
45
+ # @param path [String Pathname]
46
+ # @option strict [Boolean]
47
+ # @option via [Symbol]
48
+ # @option env [String, Symbol]
49
+ # @return [void]
50
+ #
51
+ # @api private
52
+ # @since 0.25.0
53
+ def initialize(path, strict: true, via:, env:)
54
+ unless env.is_a?(Symbol) || env.is_a?(String) || env.is_a?(Numeric)
55
+ raise Qonfig::ArgumentError, ':env should be a string or a symbol'
56
+ end
57
+
58
+ raise Qonfig::ArgumentError, ':env should be provided' if env.to_s.empty?
59
+ raise Qonfig::ArgumentError, 'used :via is unsupported' unless EXPOSERS.key?(via)
60
+
61
+ @path = path
62
+ @strict = strict
63
+ @via = via
64
+ @env = env
65
+ end
66
+
67
+ # @param data_set [Qonfig::DataSet]
68
+ # @param settings [Qonfig::Settings]
69
+ # @return [void]
70
+ #
71
+ # @api private
72
+ # @since 0.25.0
73
+ def call(_data_set, settings)
74
+ case via
75
+ when EXPOSERS[:path]
76
+ expose_path!(settings)
77
+ when EXPOSERS[:env_key]
78
+ expose_env_key!(settings)
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ # @param settings [Qonfig::Settings]
85
+ # @return [void]
86
+ #
87
+ # @api private
88
+ # @since 0.25.0
89
+ def expose_path!(settings)
90
+ # NOTE: transform path (insert environment name into a secret name)
91
+ # from: kv/data/secret_name
92
+ # to: kv/data/env_name/secret_name
93
+
94
+ splitted_path = path.split('/')
95
+ real_path = splitted_path.insert(-2, env.to_s).join('/')
96
+
97
+ vault_data = load_vault_data(real_path)
98
+ vault_based_settings = build_data_set_class(vault_data).new.settings
99
+
100
+ settings.__append_settings__(vault_based_settings)
101
+ end
102
+
103
+ # @param settings [Qonfig::Settings]
104
+ # @return [void]
105
+ #
106
+ # @raise [Qonfig::ExposeError]
107
+ #
108
+ # @api private
109
+ # @since 0.25.0
110
+ def expose_env_key!(settings)
111
+ vault_data = load_vault_data(path)
112
+ vault_data_slice = vault_data[env.to_sym]
113
+ vault_data_slice = EMPTY_VAULT_DATA.dup if vault_data_slice.nil? && !strict
114
+
115
+ raise(
116
+ Qonfig::ExposeError,
117
+ "#{path} does not contain settings with <#{env}> environment key!"
118
+ ) unless vault_data_slice
119
+
120
+ vault_based_settings = build_data_set_class(vault_data_slice).new.settings
121
+
122
+ settings.__append_settings__(vault_based_settings)
123
+ end
124
+
125
+ # @param path [String]
126
+ # @return [Hash]
127
+ #
128
+ # @api private
129
+ # @since 0.25.0
130
+ def load_vault_data(path)
131
+ Qonfig::Loaders::Vault.load_file(path, fail_on_unexist: strict)
132
+ end
133
+
134
+ # @param vault_data [Hash]
135
+ # @return [Class<Qonfig::DataSet>]
136
+ #
137
+ # @api private
138
+ # @since 0.25.0
139
+ def build_data_set_class(vault_data)
140
+ Qonfig::DataSet::ClassBuilder.build_from_hash(vault_data)
141
+ end
142
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.25.0
5
+ class Qonfig::Commands::Definition::LoadFromVault < Qonfig::Commands::Base
6
+ # @since 0.25.0
7
+ self.inheritable = true
8
+
9
+ # @return [String, Pathname]
10
+ #
11
+ # @api private
12
+ # @since 0.25.0
13
+ attr_reader :path
14
+
15
+ # @return [Boolean]
16
+ #
17
+ # @api private
18
+ # @since 0.25.0
19
+ attr_reader :strict
20
+
21
+ # @param path [String]
22
+ # @option strict [Boolean]
23
+ #
24
+ # @api private
25
+ # @since 0.25.0
26
+ def initialize(path, strict: true)
27
+ @path = path
28
+ @strict = strict
29
+ end
30
+
31
+ # @param data_set [Qonfig::DataSet]
32
+ # @param settings [Qonfig::Settings]
33
+ # @return [void]
34
+ #
35
+ # @api private
36
+ # @since 0.25.0
37
+ def call(_data_set, settings)
38
+ vault_data = Qonfig::Loaders::Vault.load_file(path, fail_on_unexist: strict)
39
+ vault_based_settings = build_data_set_klass(vault_data).new.settings
40
+ settings.__append_settings__(vault_based_settings)
41
+ end
42
+
43
+ private
44
+
45
+ # @param toml_data [Hash]
46
+ # @return [Class<Qonfig::DataSet>]
47
+ #
48
+ # @api private
49
+ # @since 0.25.0
50
+ def build_data_set_klass(toml_data)
51
+ Qonfig::DataSet::ClassBuilder.build_from_hash(toml_data)
52
+ end
53
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.25.0
5
+ module Qonfig::DSL
6
+ # @param path [String, Pathname]
7
+ # @option strict [Boolean]
8
+ # @return [void]
9
+ #
10
+ # @see Qonfig::Commands::Definition::LoadFromVault
11
+ #
12
+ # @api public
13
+ # @since 0.25.0
14
+ def load_from_vault(path, strict: true)
15
+ definition_commands << Qonfig::Commands::Definition::LoadFromVault.new(
16
+ path, strict: strict
17
+ )
18
+ end
19
+
20
+ # @param path [String, Pathname]
21
+ # @option strict [Boolean]
22
+ # @option via [Symbol]
23
+ # @option env [Symbol, String]
24
+ # @return [void]
25
+ #
26
+ # @see Qonfig::Commands::Definition::ExposeVault
27
+ #
28
+ # @api public
29
+ # @since 0.25.0
30
+ def expose_vault(path, strict: true, via:, env:)
31
+ definition_commands << Qonfig::Commands::Definition::ExposeVault.new(
32
+ path, strict: strict, via: via, env: env
33
+ )
34
+ end
35
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api public
4
+ # @since 0.25.0
5
+ module Qonfig
6
+ # @api public
7
+ # @since 0.25.0
8
+ VaultLoaderError = Class.new(Vault::VaultError)
9
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.25.0
5
+ class Qonfig::Loaders::Vault < Qonfig::Loaders::Basic
6
+ # @return [Binding]
7
+ #
8
+ # @api private
9
+ # @since 0.25.0
10
+ VAULT_EXPR_EVAL_SCOPE = BasicObject.new.__binding__.tap do |binding|
11
+ Object.new.method(:freeze).unbind.bind(binding.receiver).call
12
+ end
13
+
14
+ class << self
15
+ # @param path [String, Pathname]
16
+ # @option fail_on_unexist [Boolean]
17
+ # @return [Object]
18
+ #
19
+ # @raise [Qonfig::FileNotFoundError]
20
+ #
21
+ # @api private
22
+ # @since 0.25.0
23
+ def load_file(path, fail_on_unexist: true)
24
+ data = ::Vault.with_retries(Vault::HTTPError) do
25
+ ::Vault.logical.read(path.to_s)&.data&.dig(:data)
26
+ end
27
+ raise Qonfig::FileNotFoundError, "Path #{path} not exist" if data.nil? && fail_on_unexist
28
+ result = data || empty_data
29
+ deep_transform_values(result)
30
+ rescue Vault::VaultError => error
31
+ raise(Qonfig::VaultLoaderError.new(error.message).tap do |exception|
32
+ exception.set_backtrace(error.backtrace)
33
+ end)
34
+ end
35
+
36
+ # @return [Hash]
37
+ #
38
+ # @api private
39
+ # @since 0.25.0
40
+ def empty_data
41
+ {}
42
+ end
43
+
44
+ private
45
+
46
+ # @param vault_data [Hash<Object,Object>]
47
+ # @return [Object]
48
+ #
49
+ # @api private
50
+ # @since 0.25.0
51
+ def deep_transform_values(vault_data)
52
+ return vault_data unless vault_data.is_a?(Hash)
53
+
54
+ vault_data.transform_values do |value|
55
+ next safely_evaluate(value) if value.is_a?(String)
56
+
57
+ deep_transform_values(value)
58
+ end
59
+ end
60
+
61
+ # @param vault_expr [String]
62
+ # @return [Object]
63
+ #
64
+ # @api private
65
+ # @since 0.25.0
66
+ def safely_evaluate(vault_expr)
67
+ parsed_expr = ::ERB.new(vault_expr).result
68
+ VAULT_EXPR_EVAL_SCOPE.eval(parsed_expr)
69
+ rescue StandardError, ScriptError
70
+ parsed_expr
71
+ end
72
+ end
73
+ end
@@ -22,6 +22,12 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
22
22
  # @since 0.11.0
23
23
  BASIC_SETTING_VALUE_TRANSFORMER = (proc { |value| value }).freeze
24
24
 
25
+ # @return [Boolean]
26
+ #
27
+ # @api private
28
+ # @since 0.25.0
29
+ REPRESENT_HASH_IN_DOT_STYLE = false
30
+
25
31
  # @return [String]
26
32
  #
27
33
  # @api private
@@ -133,16 +139,13 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
133
139
  # @param key [Symbol, String]
134
140
  # @return [Object]
135
141
  #
142
+ # @raise [Qonfig::ArgumentError]
143
+ #
136
144
  # @api public
137
145
  # @since 0.1.0
138
- def [](key)
139
- __lock__.thread_safe_access do
140
- begin
141
- __get_value__(key)
142
- rescue Qonfig::UnknownSettingError
143
- __deep_access__(*__parse_dot_notated_key__(key))
144
- end
145
- end
146
+ # @version 0.25.0
147
+ def [](*keys)
148
+ __dig__(*keys)
146
149
  end
147
150
 
148
151
  # @param key [String, Symbol]
@@ -211,27 +214,47 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
211
214
  __lock__.thread_safe_access { __deep_subset__(*keys) }
212
215
  end
213
216
 
217
+ # @option dot_notation [Boolean]
214
218
  # @option transform_key [Proc]
215
219
  # @option transform_value [Proc]
216
220
  # @return [Hash]
217
221
  #
218
222
  # @api private
219
223
  # @since 0.1.0
220
- # rubocop:disable Metrics/LineLength
221
- def __to_hash__(transform_key: BASIC_SETTING_KEY_TRANSFORMER, transform_value: BASIC_SETTING_VALUE_TRANSFORMER)
224
+ # @version 0.25.0
225
+ def __to_hash__(
226
+ dot_notation: REPRESENT_HASH_IN_DOT_STYLE,
227
+ transform_key: BASIC_SETTING_KEY_TRANSFORMER,
228
+ transform_value: BASIC_SETTING_VALUE_TRANSFORMER
229
+ )
222
230
  unless transform_key.is_a?(Proc)
223
- ::Kernel.raise(Qonfig::IncorrectKeyTransformerError, 'Key transformer should be a type of proc')
231
+ ::Kernel.raise(
232
+ Qonfig::IncorrectKeyTransformerError,
233
+ 'Key transformer should be a type of proc'
234
+ )
224
235
  end
225
236
 
226
237
  unless transform_value.is_a?(Proc)
227
- ::Kernel.raise(Qonfig::IncorrectValueTransformerError, 'Value transformer should be a type of proc')
238
+ ::Kernel.raise(
239
+ Qonfig::IncorrectValueTransformerError,
240
+ 'Value transformer should be a type of proc'
241
+ )
228
242
  end
229
243
 
230
244
  __lock__.thread_safe_access do
231
- __build_hash_representation__(transform_key: transform_key, transform_value: transform_value)
245
+ if dot_notation
246
+ __build_dot_notated_hash_representation__(
247
+ transform_key: transform_key,
248
+ transform_value: transform_value
249
+ )
250
+ else
251
+ __build_basic_hash_representation__(
252
+ transform_key: transform_key,
253
+ transform_value: transform_value
254
+ )
255
+ end
232
256
  end
233
257
  end
234
- # rubocop:enable Metrics/LineLength
235
258
  alias_method :__to_h__, :__to_hash__
236
259
 
237
260
  # @option all_variants [Boolean]
@@ -496,7 +519,7 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
496
519
  #
497
520
  # @api private
498
521
  # @since 0.21.0
499
- # rubocop:disable Naming/RescuedExceptionsVariableName
522
+ # rubocop:disable Naming/RescuedExceptionsVariableName, Style/SlicingWithRange
500
523
  def __assign_value__(key, value)
501
524
  key = __indifferently_accessable_option_key__(key)
502
525
  __set_value__(key, value)
@@ -507,6 +530,7 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
507
530
  raise(initial_error) if key_set.size == 1
508
531
 
509
532
  begin
533
+ # TODO: rewrite with __deep_access__-like key resolving functionality
510
534
  setting_value = __get_value__(key_set.first)
511
535
  required_key = key_set[1..-1].join(DOT_NOTATION_SEPARATOR)
512
536
  setting_value[required_key] = value # NOTE: pseudo-recoursive assignment
@@ -514,7 +538,7 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
514
538
  raise(initial_error)
515
539
  end
516
540
  end
517
- # rubocop:enable Naming/RescuedExceptionsVariableName
541
+ # rubocop:enable Naming/RescuedExceptionsVariableName, Style/SlicingWithRange
518
542
 
519
543
  # @param key [String, Symbol]
520
544
  # @param value [Object]
@@ -557,11 +581,24 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
557
581
  #
558
582
  # @api private
559
583
  # @since 0.2.0
584
+ # rubocop:disable Metrics/AbcSize, Style/SlicingWithRange
560
585
  def __deep_access__(*keys)
561
586
  ::Kernel.raise(Qonfig::ArgumentError, 'Key list can not be empty') if keys.empty?
562
587
 
563
- result = __get_value__(keys.first)
564
- rest_keys = Array(keys[1..-1])
588
+ result = nil
589
+ rest_keys = nil
590
+ key_parts_boundary = keys.size - 1
591
+
592
+ 0.upto(key_parts_boundary) do |key_parts_slice_boundary|
593
+ begin
594
+ setting_key = keys[0..key_parts_slice_boundary].join(DOT_NOTATION_SEPARATOR)
595
+ result = __get_value__(setting_key)
596
+ rest_keys = Array(keys[(key_parts_slice_boundary + 1)..-1])
597
+ break
598
+ rescue Qonfig::UnknownSettingError => error
599
+ key_parts_boundary == key_parts_slice_boundary ? raise(error) : next
600
+ end
601
+ end
565
602
 
566
603
  case
567
604
  when rest_keys.empty?
@@ -575,6 +612,7 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
575
612
  result.__dig__(*rest_keys)
576
613
  end
577
614
  end
615
+ # rubocop:enable Metrics/AbcSize, Style/SlicingWithRange
578
616
 
579
617
  # @param keys [Array<Symbol, String>]
580
618
  # @return [Hash]
@@ -667,14 +705,15 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
667
705
  # @return [Hash]
668
706
  #
669
707
  # @api private
670
- # @since 0.2.0
671
- def __build_hash_representation__(options_part = __options__, transform_key:, transform_value:)
708
+ # @since 0.25.0
709
+ # rubocop:disable Layout/LineLength
710
+ def __build_basic_hash_representation__(options_part = __options__, transform_key:, transform_value:)
672
711
  options_part.each_with_object({}) do |(key, value), hash|
673
712
  final_key = transform_key.call(key)
674
713
 
675
714
  case
676
715
  when value.is_a?(Hash)
677
- hash[final_key] = __build_hash_representation__(
716
+ hash[final_key] = __build_basic_hash_representation__(
678
717
  value,
679
718
  transform_key: transform_key,
680
719
  transform_value: transform_value
@@ -690,6 +729,24 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
690
729
  end
691
730
  end
692
731
  end
732
+ # rubocop:enable Layout/LineLength
733
+
734
+ # @option transform_key [Proc]
735
+ # @option transform_value [Proc]
736
+ # @return [Hash]
737
+ #
738
+ # @api private
739
+ # @since 0.25.0
740
+ def __build_dot_notated_hash_representation__(transform_key:, transform_value:)
741
+ {}.tap do |hash|
742
+ __deep_each_key_value_pair__ do |setting_key, setting_value|
743
+ final_key = transform_key.call(setting_key)
744
+ final_value = transform_value.call(setting_value)
745
+
746
+ hash[final_key] = final_value
747
+ end
748
+ end
749
+ end
693
750
 
694
751
  # @param key [Symbol, String]
695
752
  # @return [void]