qonfig 0.29.0 → 0.31.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +50 -0
  3. data/.rubocop.yml +1 -1
  4. data/CHANGELOG.md +22 -0
  5. data/Gemfile +0 -3
  6. data/Gemfile.lock +77 -98
  7. data/LICENSE.txt +1 -1
  8. data/README.md +6 -4
  9. data/gemfiles/with_external_deps.gemfile +2 -0
  10. data/gemfiles/with_external_deps.gemfile.lock +84 -99
  11. data/gemfiles/without_external_deps.gemfile.lock +78 -95
  12. data/lib/qonfig/commands/definition/add_nested_option.rb +5 -1
  13. data/lib/qonfig/commands/definition/load_from_env/value_converter.rb +6 -6
  14. data/lib/qonfig/commands/definition/re_define_option.rb +5 -1
  15. data/lib/qonfig/commands/instantiation/values_file.rb +4 -6
  16. data/lib/qonfig/imports/direct_key.rb +4 -4
  17. data/lib/qonfig/imports/mappings.rb +4 -4
  18. data/lib/qonfig/plugins/pretty_print/requirements.rb +1 -1
  19. data/lib/qonfig/plugins/toml/loaders/dynamic.rb +1 -1
  20. data/lib/qonfig/plugins/toml/loaders.rb +1 -1
  21. data/lib/qonfig/plugins.rb +2 -4
  22. data/lib/qonfig/settings/callbacks.rb +9 -0
  23. data/lib/qonfig/settings.rb +104 -18
  24. data/lib/qonfig/uploaders/yaml.rb +1 -1
  25. data/lib/qonfig/validation/building/instance_builder/attribute_consistency.rb +2 -2
  26. data/lib/qonfig/version.rb +1 -1
  27. data/lib/qonfig.rb +1 -0
  28. data/qonfig.gemspec +8 -8
  29. data/spec/features/config_definition_and_representation_spec.rb +7 -7
  30. data/spec/features/freeze_state_spec.rb +10 -10
  31. data/spec/features/nested_access_caching_spec.rb +134 -0
  32. data/spec/features/plugins/pretty_print_spec.rb +15 -15
  33. data/spec/features/plugins/toml/save_to_toml_spec.rb +1 -1
  34. data/spec/features/plugins/vault/expose_vault_spec.rb +1 -1
  35. data/spec/features/run_code_with_temporary_settings_spec.rb +70 -68
  36. data/spec/features/save_to_file/save_to_json_spec.rb +38 -18
  37. metadata +37 -49
  38. data/.github/workflows/test.yml +0 -62
  39. data/.jrubyrc +0 -1
  40. data/sig/.keep +0 -0
@@ -1,139 +1,122 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- qonfig (0.27.0)
4
+ qonfig (0.31.0)
5
+ base64 (>= 0.2)
5
6
 
6
7
  GEM
7
8
  remote: https://rubygems.org/
8
9
  specs:
9
- activesupport (7.0.3)
10
+ activesupport (6.1.7.10)
10
11
  concurrent-ruby (~> 1.0, >= 1.0.2)
11
12
  i18n (>= 1.6, < 2)
12
13
  minitest (>= 5.1)
13
14
  tzinfo (~> 2.0)
14
- armitage-rubocop (1.30.1.1)
15
- rubocop (= 1.30.1)
16
- rubocop-performance (= 1.14.2)
17
- rubocop-rails (= 2.15.0)
15
+ zeitwerk (~> 2.3)
16
+ armitage-rubocop (1.68.0.2)
17
+ rubocop (= 1.68.0)
18
+ rubocop-capybara (= 2.21.0)
19
+ rubocop-factory_bot (= 2.26.1)
20
+ rubocop-performance (= 1.23.0)
21
+ rubocop-rails (= 2.27.0)
18
22
  rubocop-rake (= 0.6.0)
19
- rubocop-rspec (= 2.11.1)
23
+ rubocop-rspec (= 3.2.0)
24
+ rubocop-rspec_rails (= 2.30.0)
20
25
  ast (2.4.2)
21
- bundler-audit (0.9.1)
22
- bundler (>= 1.2.0, < 3)
23
- thor (~> 1.0)
24
- ci-helper (0.5.0)
25
- colorize (~> 0.8)
26
- dry-inflector (~> 0.2)
27
- umbrellio-sequel-plugins (~> 0.4)
26
+ base64 (0.2.0)
28
27
  coderay (1.1.3)
29
- colorize (0.8.1)
30
- concurrent-ruby (1.1.10)
31
- diff-lcs (1.5.0)
32
- docile (1.4.0)
33
- dry-inflector (0.2.1)
34
- ffi (1.15.5)
35
- i18n (1.10.0)
28
+ concurrent-ruby (1.3.4)
29
+ diff-lcs (1.5.1)
30
+ docile (1.4.1)
31
+ i18n (1.14.6)
36
32
  concurrent-ruby (~> 1.0)
37
- language_server-protocol (3.16.0.3)
38
- listen (3.7.1)
39
- rb-fsevent (~> 0.10, >= 0.10.3)
40
- rb-inotify (~> 0.9, >= 0.9.10)
41
- method_source (1.0.0)
42
- minitest (5.16.0)
43
- parallel (1.22.1)
44
- parser (3.1.2.0)
33
+ json (2.9.0)
34
+ language_server-protocol (3.17.0.3)
35
+ method_source (1.1.0)
36
+ minitest (5.25.4)
37
+ parallel (1.26.3)
38
+ parser (3.3.6.0)
45
39
  ast (~> 2.4.1)
46
- pry (0.14.1)
40
+ racc
41
+ pry (0.15.0)
47
42
  coderay (~> 1.1)
48
43
  method_source (~> 1.0)
49
- rack (2.2.3.1)
44
+ racc (1.8.1)
45
+ rack (3.1.8)
50
46
  rainbow (3.1.1)
51
- rake (13.0.6)
52
- rb-fsevent (0.11.1)
53
- rb-inotify (0.10.1)
54
- ffi (~> 1.0)
55
- rbs (2.5.0)
56
- regexp_parser (2.5.0)
57
- rexml (3.2.5)
58
- rspec (3.11.0)
59
- rspec-core (~> 3.11.0)
60
- rspec-expectations (~> 3.11.0)
61
- rspec-mocks (~> 3.11.0)
62
- rspec-core (3.11.0)
63
- rspec-support (~> 3.11.0)
64
- rspec-expectations (3.11.0)
47
+ rake (13.2.1)
48
+ regexp_parser (2.9.3)
49
+ rspec (3.13.0)
50
+ rspec-core (~> 3.13.0)
51
+ rspec-expectations (~> 3.13.0)
52
+ rspec-mocks (~> 3.13.0)
53
+ rspec-core (3.13.2)
54
+ rspec-support (~> 3.13.0)
55
+ rspec-expectations (3.13.3)
65
56
  diff-lcs (>= 1.2.0, < 2.0)
66
- rspec-support (~> 3.11.0)
67
- rspec-mocks (3.11.1)
57
+ rspec-support (~> 3.13.0)
58
+ rspec-mocks (3.13.2)
68
59
  diff-lcs (>= 1.2.0, < 2.0)
69
- rspec-support (~> 3.11.0)
70
- rspec-support (3.11.0)
71
- rubocop (1.30.1)
60
+ rspec-support (~> 3.13.0)
61
+ rspec-support (3.13.2)
62
+ rubocop (1.68.0)
63
+ json (~> 2.3)
64
+ language_server-protocol (>= 3.17.0)
72
65
  parallel (~> 1.10)
73
- parser (>= 3.1.0.0)
66
+ parser (>= 3.3.0.2)
74
67
  rainbow (>= 2.2.2, < 4.0)
75
- regexp_parser (>= 1.8, < 3.0)
76
- rexml (>= 3.2.5, < 4.0)
77
- rubocop-ast (>= 1.18.0, < 2.0)
68
+ regexp_parser (>= 2.4, < 3.0)
69
+ rubocop-ast (>= 1.32.2, < 2.0)
78
70
  ruby-progressbar (~> 1.7)
79
- unicode-display_width (>= 1.4.0, < 3.0)
80
- rubocop-ast (1.18.0)
81
- parser (>= 3.1.1.0)
82
- rubocop-performance (1.14.2)
83
- rubocop (>= 1.7.0, < 2.0)
84
- rubocop-ast (>= 0.4.0)
85
- rubocop-rails (2.15.0)
71
+ unicode-display_width (>= 2.4.0, < 3.0)
72
+ rubocop-ast (1.37.0)
73
+ parser (>= 3.3.1.0)
74
+ rubocop-capybara (2.21.0)
75
+ rubocop (~> 1.41)
76
+ rubocop-factory_bot (2.26.1)
77
+ rubocop (~> 1.61)
78
+ rubocop-performance (1.23.0)
79
+ rubocop (>= 1.48.1, < 2.0)
80
+ rubocop-ast (>= 1.31.1, < 2.0)
81
+ rubocop-rails (2.27.0)
86
82
  activesupport (>= 4.2.0)
87
83
  rack (>= 1.1)
88
- rubocop (>= 1.7.0, < 2.0)
84
+ rubocop (>= 1.52.0, < 2.0)
85
+ rubocop-ast (>= 1.31.1, < 2.0)
89
86
  rubocop-rake (0.6.0)
90
87
  rubocop (~> 1.0)
91
- rubocop-rspec (2.11.1)
92
- rubocop (~> 1.19)
93
- ruby-progressbar (1.11.0)
94
- sequel (5.57.0)
95
- simplecov (0.21.2)
88
+ rubocop-rspec (3.2.0)
89
+ rubocop (~> 1.61)
90
+ rubocop-rspec_rails (2.30.0)
91
+ rubocop (~> 1.61)
92
+ rubocop-rspec (~> 3, >= 3.0.1)
93
+ ruby-progressbar (1.13.0)
94
+ simplecov (0.22.0)
96
95
  docile (~> 1.1)
97
96
  simplecov-html (~> 0.11)
98
97
  simplecov_json_formatter (~> 0.1)
99
- simplecov-html (0.12.3)
98
+ simplecov-html (0.13.1)
100
99
  simplecov-lcov (0.8.0)
101
100
  simplecov_json_formatter (0.1.4)
102
- steep (1.0.0)
103
- activesupport (>= 5.1)
104
- language_server-protocol (>= 3.15, < 4.0)
105
- listen (~> 3.0)
106
- parallel (>= 1.0.0)
107
- parser (>= 3.0)
108
- rainbow (>= 2.2.2, < 4.0)
109
- rbs (>= 2.3.2)
110
- terminal-table (>= 2, < 4)
111
- symbiont-ruby (0.7.0)
112
- terminal-table (3.0.2)
113
- unicode-display_width (>= 1.1.1, < 3)
114
- thor (1.2.1)
115
- tzinfo (2.0.4)
101
+ tzinfo (2.0.6)
116
102
  concurrent-ruby (~> 1.0)
117
- umbrellio-sequel-plugins (0.6.0.36)
118
- sequel
119
- symbiont-ruby
120
- unicode-display_width (2.1.0)
103
+ unicode-display_width (2.6.0)
104
+ zeitwerk (2.7.1)
121
105
 
122
106
  PLATFORMS
123
- arm64-darwin-21
107
+ arm64-darwin-24
108
+ ruby
124
109
 
125
110
  DEPENDENCIES
126
- armitage-rubocop (~> 1.30)
111
+ activesupport (~> 6, < 8)
112
+ armitage-rubocop (~> 1.59)
127
113
  bundler (>= 1)
128
- bundler-audit (~> 0.9)
129
- ci-helper (~> 0.5)
130
- pry (~> 0.14)
114
+ pry (~> 0.15)
131
115
  qonfig!
132
116
  rake (>= 13)
133
- rspec (~> 3.11)
134
- simplecov (~> 0.21)
117
+ rspec (~> 3.2)
118
+ simplecov (~> 0.22)
135
119
  simplecov-lcov (~> 0.8)
136
- steep (~> 1.0)
137
120
 
138
121
  BUNDLED WITH
139
- 2.3.15
122
+ 2.5.23
@@ -41,10 +41,14 @@ class Qonfig::Commands::Definition::AddNestedOption < Qonfig::Commands::Base
41
41
  #
42
42
  # @api private
43
43
  # @since 0.1.0
44
+ # @version 0.31.0
44
45
  def call(data_set, settings)
45
46
  nested_settings = nested_data_set_klass.new.settings
46
47
 
47
- nested_settings.__mutation_callbacks__.add(settings.__mutation_callbacks__)
48
+ # NOTE:
49
+ # The nested instance is wired into the host's mutation-callback chain by
50
+ # Qonfig::Settings#__define_setting__ itself (see __store_setting_value__),
51
+ # so every nested-settings entry point shares one wiring path.
48
52
  settings.__define_setting__(key, nested_settings)
49
53
  end
50
54
  end
@@ -8,37 +8,37 @@ module Qonfig::Commands::Definition::LoadFromENV::ValueConverter
8
8
  #
9
9
  # @api private
10
10
  # @since 0.2.0
11
- INTEGER_PATTERN = /\A\d+\z/.freeze
11
+ INTEGER_PATTERN = /\A\d+\z/
12
12
 
13
13
  # @return [Regexp]
14
14
  #
15
15
  # @api private
16
16
  # @since 0.2.0
17
- FLOAT_PATTERN = /\A\d+\.\d+\z/.freeze
17
+ FLOAT_PATTERN = /\A\d+\.\d+\z/
18
18
 
19
19
  # @return [Regexp]
20
20
  #
21
21
  # @api private
22
22
  # @since 0.2.0
23
- TRUE_PATTERN = /\A(t|true)\z/i.freeze
23
+ TRUE_PATTERN = /\A(t|true)\z/i
24
24
 
25
25
  # @return [Regexp]
26
26
  #
27
27
  # @api private
28
28
  # @since 0.2.0
29
- FALSE_PATTERN = /\A(f|false)\z/i.freeze
29
+ FALSE_PATTERN = /\A(f|false)\z/i
30
30
 
31
31
  # @return [Regexp]
32
32
  #
33
33
  # @api private
34
34
  # @since 0.2.0
35
- ARRAY_PATTERN = /\A[^'"].*\s*,\s*.*[^'"]\z/.freeze
35
+ ARRAY_PATTERN = /\A[^'"].*\s*,\s*.*[^'"]\z/
36
36
 
37
37
  # @return [Regexp]
38
38
  #
39
39
  # @api private
40
40
  # @since 0.2.0
41
- QUOTED_STRING_PATTERN = /\A['"].*['"]\z/.freeze
41
+ QUOTED_STRING_PATTERN = /\A['"].*['"]\z/
42
42
 
43
43
  class << self
44
44
  # @param env_data [Hash]
@@ -48,10 +48,14 @@ class Qonfig::Commands::Definition::ReDefineOption < Qonfig::Commands::Base
48
48
  #
49
49
  # @api private
50
50
  # @since 0.20.0
51
+ # @version 0.31.0
51
52
  def call(data_set, settings)
52
53
  if nested_data_set_klass
53
54
  nested_settings = nested_data_set_klass.new.settings
54
- nested_settings.__mutation_callbacks__.add(settings.__mutation_callbacks__)
55
+ # NOTE:
56
+ # The nested instance is wired into the host's mutation-callback chain by
57
+ # Qonfig::Settings#__define_setting__ itself (see __store_setting_value__),
58
+ # so every nested-settings entry point shares one wiring path.
55
59
  settings.__define_setting__(key, nested_settings, with_redefinition: true)
56
60
  else
57
61
  settings.__define_setting__(key, value, with_redefinition: true)
@@ -96,7 +96,7 @@ class Qonfig::Commands::Instantiation::ValuesFile < Qonfig::Commands::Base
96
96
  def call(data_set, settings)
97
97
  settings_values = load_settings_values
98
98
  return unless settings_values
99
- settings_values = (settings_values[expose.to_sym] || settings_values[expose.to_s]) if expose
99
+ settings_values = settings_values[expose.to_sym] || settings_values[expose.to_s] if expose
100
100
  data_set.configure(settings_values) if settings_values
101
101
  end
102
102
 
@@ -156,11 +156,9 @@ class Qonfig::Commands::Instantiation::ValuesFile < Qonfig::Commands::Base
156
156
  # @version 0.22.0
157
157
  # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
158
158
  def prevent_incompatible_attributes!(file_path, format, strict, expose)
159
- unless (
160
- file_path.is_a?(String) ||
161
- file_path.is_a?(Pathname) ||
162
- file_path == SELF_LOCATED_FILE_DEFINITION
163
- )
159
+ unless file_path.is_a?(String) ||
160
+ file_path.is_a?(Pathname) ||
161
+ file_path == SELF_LOCATED_FILE_DEFINITION
164
162
  raise Qonfig::ArgumentError, 'Incorrect file path'
165
163
  end
166
164
 
@@ -43,9 +43,9 @@ class Qonfig::Imports::DirectKey < Qonfig::Imports::Abstract
43
43
  raise(
44
44
  Qonfig::UnknownSettingError,
45
45
  "Setting with <#{key_matcher.scope_pattern}> key does not exist!"
46
- ) unless (imported_config.keys(all_variants: true).any? do |setting_key|
46
+ ) unless imported_config.keys(all_variants: true).any? do |setting_key|
47
47
  key_matcher.match?(setting_key)
48
- end || key_matcher.generic?)
48
+ end || key_matcher.generic?
49
49
 
50
50
  imported_config.keys(all_variants: true).each do |setting_key|
51
51
  next unless key_matcher.match?(setting_key)
@@ -69,13 +69,13 @@ class Qonfig::Imports::DirectKey < Qonfig::Imports::Abstract
69
69
  end
70
70
  end
71
71
 
72
- define_method("#{access_method_name}?") do
72
+ define_method(:"#{access_method_name}?") do
73
73
  # NOTE: based on Qonfig::Settings#__define_option_predicate__ realization
74
74
  !!imported_config[setting_key]
75
75
  end
76
76
 
77
77
  if accessor
78
- define_method("#{access_method_name}=") do |value|
78
+ define_method(:"#{access_method_name}=") do |value|
79
79
  imported_config[setting_key] = value
80
80
  end
81
81
  end
@@ -49,9 +49,9 @@ class Qonfig::Imports::Mappings < Qonfig::Imports::Abstract
49
49
  raise(
50
50
  Qonfig::UnknownSettingError,
51
51
  "Setting with <#{key_matcher.scope_pattern}> key does not exist!"
52
- ) unless (imported_config.keys(all_variants: true).any? do |setting_key|
52
+ ) unless imported_config.keys(all_variants: true).any? do |setting_key|
53
53
  key_matcher.match?(setting_key)
54
- end || key_matcher.generic?)
54
+ end || key_matcher.generic?
55
55
 
56
56
  imported_config.keys(all_variants: true).each do |setting_key|
57
57
  next unless key_matcher.match?(setting_key)
@@ -74,13 +74,13 @@ class Qonfig::Imports::Mappings < Qonfig::Imports::Abstract
74
74
  end
75
75
  end
76
76
 
77
- define_method("#{mapped_method_name}?") do
77
+ define_method(:"#{mapped_method_name}?") do
78
78
  # NOTE: based on Qonfig::Settings#__define_option_predicate__ realization
79
79
  !!imported_config[setting_key]
80
80
  end
81
81
 
82
82
  if accessor
83
- define_method("#{mapped_method_name}=") do |value|
83
+ define_method(:"#{mapped_method_name}=") do |value|
84
84
  imported_config[setting_key] = value
85
85
  end
86
86
  end
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'pp'
3
+ require 'pp' # rubocop:disable Lint/RedundantRequireStatement
@@ -13,7 +13,7 @@ class Qonfig::Loaders::Dynamic < Qonfig::Loaders::Basic
13
13
  def load(data)
14
14
  try_to_load_toml_data(data)
15
15
  rescue Qonfig::TOMLLoaderParseError
16
- super(data)
16
+ super
17
17
  end
18
18
 
19
19
  private
@@ -16,7 +16,7 @@ module Qonfig::Loaders
16
16
  # @since 0.15.0
17
17
  def resolve(format)
18
18
  return Qonfig::Loaders::TOML if format.to_s == 'toml'
19
- super(format)
19
+ super
20
20
  end
21
21
  end)
22
22
  end
@@ -31,10 +31,8 @@ module Qonfig::Plugins
31
31
  # @since 0.19.0
32
32
  def loaded_plugins
33
33
  thread_safe do
34
- plugin_registry.select do |_plugin_name, plugin_module|
35
- plugin_module.loaded?
36
- end.map do |plugin_name, _plugin_module|
37
- plugin_name
34
+ plugin_registry.filter_map do |plugin_name, plugin_module|
35
+ plugin_name if plugin_module.loaded?
38
36
  end
39
37
  end
40
38
  end
@@ -35,6 +35,15 @@ class Qonfig::Settings::Callbacks
35
35
  thread_safe { callbacks << callback }
36
36
  end
37
37
 
38
+ # @param callback [Proc, Qonfig::Settings::Callbacks, #call]
39
+ # @return [void]
40
+ #
41
+ # @api private
42
+ # @since 0.31.0
43
+ def prepend(callback)
44
+ thread_safe { callbacks.unshift(callback) }
45
+ end
46
+
38
47
  private
39
48
 
40
49
  # @return [Array<Proc>]
@@ -14,13 +14,13 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
14
14
  #
15
15
  # @api private
16
16
  # @since 0.11.0
17
- BASIC_SETTING_KEY_TRANSFORMER = (proc { |value| value }).freeze
17
+ BASIC_SETTING_KEY_TRANSFORMER = proc { |value| value }.freeze
18
18
 
19
19
  # @return [Proc]
20
20
  #
21
21
  # @api private
22
22
  # @since 0.11.0
23
- BASIC_SETTING_VALUE_TRANSFORMER = (proc { |value| value }).freeze
23
+ BASIC_SETTING_VALUE_TRANSFORMER = proc { |value| value }.freeze
24
24
 
25
25
  # @return [Boolean]
26
26
  #
@@ -48,10 +48,24 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
48
48
 
49
49
  # @api private
50
50
  # @since 0.1.0
51
+ # @version 0.31.0
51
52
  def initialize(__mutation_callbacks__)
52
53
  @__options__ = {}
53
54
  @__lock__ = Lock.new
54
55
  @__mutation_callbacks__ = __mutation_callbacks__
56
+ @__deep_access_cache__ = {}
57
+
58
+ # NOTE:
59
+ # Register a cache invalidator inside our own mutation callbacks.
60
+ # Nested settings add their parent's callbacks object to their own callbacks
61
+ # (see #__store_setting_value__), so a mutation of any nested config propagates
62
+ # upwards through the chain and drops the cached digging results on EACH config
63
+ # instance along the way ("matryoshka" cache invalidation).
64
+ #
65
+ # It is prepended so that it runs BEFORE the validation callback — full
66
+ # data-set validators read setting values back through #__dig__, so the
67
+ # cache must already be dropped by the time they run.
68
+ @__mutation_callbacks__.prepend(proc { __invalidate_deep_access_cache__ })
55
69
  end
56
70
 
57
71
  # @param block [Proc]
@@ -93,23 +107,21 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
93
107
  #
94
108
  # @api private
95
109
  # @since 0.1.0
96
- # @version 0.20.0
110
+ # @version 0.31.0
97
111
  def __define_setting__(key, value, with_redefinition: false) # rubocop:disable Metrics/AbcSize
98
112
  __lock__.thread_safe_definition do
99
113
  key = __indifferently_accessable_option_key__(key)
100
114
 
101
115
  __prevent_core_method_intersection__(key)
102
116
 
103
- # rubocop:disable Lint/DuplicateBranch
104
117
  case
105
118
  when with_redefinition || !__options__.key?(key)
106
- __options__[key] = value
119
+ __store_setting_value__(key, value)
107
120
  when __is_a_setting__(__options__[key]) && __is_a_setting__(value)
108
121
  __options__[key].__append_settings__(value)
109
122
  else
110
- __options__[key] = value
123
+ __store_setting_value__(key, value)
111
124
  end
112
- # rubocop:enable Lint/DuplicateBranch
113
125
 
114
126
  __define_option_reader__(key)
115
127
  __define_option_writer__(key)
@@ -177,16 +189,21 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
177
189
  #
178
190
  # @api private
179
191
  # @since 0.2.0
192
+ # @version 0.31.0
180
193
  def __dig__(*keys)
181
194
  __lock__.thread_safe_access do
182
- begin
183
- __deep_access__(*keys)
184
- rescue Qonfig::UnknownSettingError
185
- if keys.size == 1
186
- __deep_access__(*__parse_dot_notated_key__(keys.first))
187
- else
188
- raise
189
- end
195
+ # NOTE:
196
+ # Resolving a dot-notated/nested key (config['a.b.c.d']) is expensive:
197
+ # it splits and re-joins string fragments and digs through every nested
198
+ # config instance on each access. We memoize the resolved value per key
199
+ # set; the cache is dropped on any mutation (see __invalidate...).
200
+ cache = @__deep_access_cache__
201
+ cache_key = __deep_access_cache_key__(keys)
202
+
203
+ if cache.key?(cache_key)
204
+ cache[cache_key]
205
+ else
206
+ __resolve_deep_access__(*keys).tap { |value| cache[cache_key] = value }
190
207
  end
191
208
  end
192
209
  end
@@ -577,6 +594,75 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
577
594
  __invoke_mutation_callbacks__
578
595
  end
579
596
 
597
+ # @param key [String]
598
+ # @param value [Object]
599
+ # @return [void]
600
+ #
601
+ # @api private
602
+ # @since 0.31.0
603
+ def __store_setting_value__(key, value)
604
+ __options__[key] = value
605
+
606
+ # NOTE:
607
+ # A nested settings instance can arrive here "foreign" — built under another
608
+ # data set and merged into us by reference (compose / load_from_* / expose_* /
609
+ # option re-definition). Its mutation callbacks would then still point only at
610
+ # the discarded foreign parent, so a mutation of the nested config would never
611
+ # invalidate OUR deep-access cache (=> stale reads). Wire the nested instance
612
+ # into our callback chain on assignment so invalidation always cascades upwards
613
+ # ("matryoshka"), regardless of how the nested settings instance got here.
614
+ value.__mutation_callbacks__.add(__mutation_callbacks__) if __is_a_setting__(value)
615
+ end
616
+
617
+ # @param keys [Array<String, Symbol>]
618
+ # @return [Object]
619
+ #
620
+ # @raise [Qonfig::UnknownSettingError]
621
+ #
622
+ # @api private
623
+ # @since 0.31.0
624
+ def __resolve_deep_access__(*keys)
625
+ __deep_access__(*keys)
626
+ rescue Qonfig::UnknownSettingError
627
+ if keys.size == 1
628
+ __deep_access__(*__parse_dot_notated_key__(keys.first))
629
+ else
630
+ raise
631
+ end
632
+ end
633
+
634
+ # @param keys [Array<String, Symbol>]
635
+ # @return [String, Array<String>]
636
+ #
637
+ # @api private
638
+ # @since 0.31.0
639
+ def __deep_access_cache_key__(keys)
640
+ # NOTE:
641
+ # Stringify keys so that symbol/string variants share a single cache slot
642
+ # (indifferent access), while preserving key boundaries — ['a.b', 'c'] and
643
+ # ['a', 'b', 'c'] resolve differently and must not collide. The single-key
644
+ # case (the dominant config['a.b.c'] form) avoids an extra array allocation.
645
+ (keys.size == 1) ? keys.first.to_s : keys.map(&:to_s)
646
+ end
647
+
648
+ # @return [void]
649
+ #
650
+ # @api private
651
+ # @since 0.31.0
652
+ def __invalidate_deep_access_cache__
653
+ # NOTE:
654
+ # Reassign (instead of #clear) so concurrent readers never observe a
655
+ # half-cleared hash — invalidation can propagate upwards from a nested
656
+ # mutation WITHOUT holding our access lock.
657
+ #
658
+ # The reassignment must be UNCONDITIONAL. A `unless empty?` guard would skip
659
+ # the swap during the window where a concurrent reader has already resolved a
660
+ # now-stale value but has not yet written it into the (still-empty) cache —
661
+ # the stale write would then survive and be served forever. Rebinding the ivar
662
+ # to a fresh hash makes such a late write land in an orphaned hash instead.
663
+ @__deep_access_cache__ = {}
664
+ end
665
+
580
666
  # @param keys [Array<Symbol, String>]
581
667
  # @return [Object]
582
668
  #
@@ -600,7 +686,7 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
600
686
  rest_keys = Array(keys[(key_parts_slice_boundary + 1)..-1])
601
687
  break
602
688
  rescue Qonfig::UnknownSettingError => error
603
- key_parts_boundary == key_parts_slice_boundary ? raise(error) : next
689
+ (key_parts_boundary == key_parts_slice_boundary) ? raise(error) : next
604
690
  end
605
691
  end
606
692
 
@@ -771,7 +857,7 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
771
857
  # @api private
772
858
  # @since 0.13.0
773
859
  def __define_option_writer__(key)
774
- define_singleton_method("#{key}=") do |value|
860
+ define_singleton_method(:"#{key}=") do |value|
775
861
  self.[]=(key, value)
776
862
  end
777
863
  end
@@ -782,7 +868,7 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
782
868
  # @api private
783
869
  # @since 0.13.0
784
870
  def __define_option_predicate__(key)
785
- define_singleton_method("#{key}?") do
871
+ define_singleton_method(:"#{key}?") do
786
872
  !!self.[](key)
787
873
  end
788
874
  end
@@ -35,7 +35,7 @@ class Qonfig::Uploaders::YAML < Qonfig::Uploaders::File
35
35
  #
36
36
  # @api private
37
37
  # @since 0.11.0
38
- KEY_SYMBOLIZER = -> (value) { value.to_sym }.freeze
38
+ KEY_SYMBOLIZER = lambda(&:to_sym).freeze
39
39
 
40
40
  class << self
41
41
  # @param settings [Qonfig::Settings]
@@ -114,8 +114,8 @@ class Qonfig::Validation::Building::InstanceBuilder::AttributesConsistency
114
114
  )
115
115
  end
116
116
 
117
- if ((runtime_validation_method && validation_logic) ||
118
- (predefined_validator && (runtime_validation_method || validation_logic)))
117
+ if (runtime_validation_method && validation_logic) ||
118
+ (predefined_validator && (runtime_validation_method || validation_logic))
119
119
  raise(
120
120
  Qonfig::ValidatorArgumentError,
121
121
  'Incosistent validation (you should use: dataset method OR proc OR predefined validator)'
@@ -5,5 +5,5 @@ module Qonfig
5
5
  #
6
6
  # @api public
7
7
  # @since 0.1.0
8
- VERSION = '0.29.0'
8
+ VERSION = '0.31.0'
9
9
  end
data/lib/qonfig.rb CHANGED
@@ -5,6 +5,7 @@ require 'yaml'
5
5
  require 'json'
6
6
  require 'erb'
7
7
  require 'pathname'
8
+ require 'base64'
8
9
 
9
10
  # @api public
10
11
  # @since 0.1.0