qonfig 0.30.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bff64afc18729b2319a870162938579b2442a21ffcf808371c49d0449f8ad56e
4
- data.tar.gz: f65cbd7f5fa80e35f42f03f7f14886dddebf7e4e6572dd18eda38c6c79e88b6f
3
+ metadata.gz: 7a51d2be5daa99fe68c0a7b3db5a802409a8b02e3ca2fe62abca61c0d1f7f0cf
4
+ data.tar.gz: a54a12a2fa42abd3b7025b1427e1939582063a5ccc0f4d3648fe8a336361ce52
5
5
  SHA512:
6
- metadata.gz: dd25b7a25be0f1a5c481d8a2c8672e10fc50fcdb8013924be149d3815fb28709919f414bf2ee1fb8360b75680b3f6dbfab4cf19eb5315615ca1b4ed76f69e32b
7
- data.tar.gz: 790284b0e80c1131fce3d0e4b1d353b147cae0209204c674c2e1042ded3048608d563fedc2ecd32e53240a3a067f02b64037e755e88a803fd879a5f66e9ba98a
6
+ metadata.gz: 915fb311e7af429433f218b04b7162c4691d7208329ae6d104990952a417c59ce6cdc55721f3393b435aa2e07642000c4afdd7902f3f20a018c287661f4462c5
7
+ data.tar.gz: 73f06741831e901ea385c1712d6a56f42e7a780809d5fa8dca848a6025f95fa24ddd84c3a4e80bb2a03934f2c55c65e43827537114801d16d1780b4e7c34572e
@@ -36,6 +36,8 @@ jobs:
36
36
  include:
37
37
  - ruby: head
38
38
  experimental: true
39
+ - ruby: truffleruby
40
+ experimental: true
39
41
  steps:
40
42
  - uses: actions/checkout@v4
41
43
  - uses: ruby/setup-ruby@v1
data/CHANGELOG.md CHANGED
@@ -1,15 +1,27 @@
1
1
  # Changelog
2
2
  All notable changes to this project will be documented in this file.
3
3
 
4
+ ## [Unreleased]
5
+ ### Added
6
+ - Support for `truffleruby`;
7
+
8
+ ## [0.31.0] - 2026-06-24
9
+ ### Changed
10
+ - Performance: nested / dot-notated reads (`config['a.b.c.d.e.f']`, `config.dig(...)`)
11
+ are now memoized inside each config instance instead of re-splitting and re-digging
12
+ the key on every access. The cache is dropped on any mutation and the invalidation
13
+ cascades up through all parent config instances ("matryoshka" cache invalidation),
14
+ so nested configs (which are separate instances) never serve stale values.
15
+
4
16
  ## [0.30.0] - 2024-12-14
5
17
  ### Changed
6
18
  - Updated development dependencies;
7
19
  - Updated code style;
8
- - Dropped EOL rubies support. Now Qonfig supports ruby with version `>= 3.1`;
20
+ - Dropped EOL rubies support. Now Qonfig supports rubies started from `>= 3.1`;
9
21
  - CI: returned separated plugin-related specs (`bin/rspec -w` + `bin/rspec -n` (with- and without- plugins respectively));
10
22
  - CI: dropped coveralls (temporary);
11
23
  - CI: droppet java-based rubies support (temporary);
12
- - Simplified CI;
24
+ - Simplified CI (temporary);
13
25
 
14
26
  ## [0.29.0] - 2024-05-06
15
27
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- qonfig (0.29.1)
4
+ qonfig (0.31.0)
5
5
  base64 (>= 0.2)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -3329,9 +3329,9 @@ Qonfig.plugin(:vault)
3329
3329
 
3330
3330
  ## Roadmap
3331
3331
 
3332
- - **General**:
3333
- - documentation rework;
3334
3332
  - **Major**:
3333
+ - `on_change`-hooks (in order to proviade the possibility to change something like "re-initialize redis connection pool on each redis config changement" and so on);
3334
+ - Runtime Configuration (with in-memory cache);
3335
3335
  - support for Rails-like secrets;
3336
3336
  - support for persistent data storages (we want to store configs in multiple databases and files);
3337
3337
  - rails plugin;
@@ -3347,6 +3347,7 @@ Qonfig.plugin(:vault)
3347
3347
  - File geneartors (.rb-files with a pre-filled code (and (maybe) with a pre-generated yaml/json/etc files));
3348
3348
  - Setting value changement subscriptions and callbacks;
3349
3349
  - CI rework (new configs, new steps, no strange dependencies);
3350
+ - CI: thread-safety specs for `thruffleruby`;
3350
3351
 
3351
3352
  ## Build
3352
3353
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- qonfig (0.29.1)
4
+ qonfig (0.31.0)
5
5
  base64 (>= 0.2)
6
6
 
7
7
  GEM
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- qonfig (0.29.1)
4
+ qonfig (0.31.0)
5
5
  base64 (>= 0.2)
6
6
 
7
7
  GEM
@@ -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
@@ -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)
@@ -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>]
@@ -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,7 +107,7 @@ 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)
@@ -102,11 +116,11 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
102
116
 
103
117
  case
104
118
  when with_redefinition || !__options__.key?(key)
105
- __options__[key] = value
119
+ __store_setting_value__(key, value)
106
120
  when __is_a_setting__(__options__[key]) && __is_a_setting__(value)
107
121
  __options__[key].__append_settings__(value)
108
122
  else
109
- __options__[key] = value
123
+ __store_setting_value__(key, value)
110
124
  end
111
125
 
112
126
  __define_option_reader__(key)
@@ -175,16 +189,21 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
175
189
  #
176
190
  # @api private
177
191
  # @since 0.2.0
192
+ # @version 0.31.0
178
193
  def __dig__(*keys)
179
194
  __lock__.thread_safe_access do
180
- begin
181
- __deep_access__(*keys)
182
- rescue Qonfig::UnknownSettingError
183
- if keys.size == 1
184
- __deep_access__(*__parse_dot_notated_key__(keys.first))
185
- else
186
- raise
187
- 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 }
188
207
  end
189
208
  end
190
209
  end
@@ -575,6 +594,75 @@ class Qonfig::Settings # NOTE: Layout/ClassStructure is disabled only for CORE_M
575
594
  __invoke_mutation_callbacks__
576
595
  end
577
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
+
578
666
  # @param keys [Array<Symbol, String>]
579
667
  # @return [Object]
580
668
  #
@@ -5,5 +5,5 @@ module Qonfig
5
5
  #
6
6
  # @api public
7
7
  # @since 0.1.0
8
- VERSION = '0.30.0'
8
+ VERSION = '0.31.0'
9
9
  end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe 'Nested access caching (with matryoshka cache invalidation)' do
4
+ let(:config) do
5
+ Qonfig::DataSet.build do
6
+ setting :a do
7
+ setting :b do
8
+ setting :c do
9
+ setting :d, 1
10
+ end
11
+ end
12
+ end
13
+
14
+ setting :flat, 'value'
15
+ end
16
+ end
17
+
18
+ specify 'repeated dot-notated / nested reads return the same (memoized) result' do
19
+ expect(config['a.b.c.d']).to eq(1)
20
+ expect(config['a.b.c.d']).to eq(1)
21
+
22
+ nested = config['a.b.c']
23
+ expect(nested).to be_a(Qonfig::Settings)
24
+ # the very same nested settings instance is returned from the cache
25
+ expect(config['a.b.c']).to equal(nested)
26
+ expect(config.dig(:a, :b, :c)).to equal(nested)
27
+ end
28
+
29
+ specify 'symbol and string key variants share the cache (indifferent access)' do
30
+ expect(config[:a][:b][:c][:d]).to eq(1)
31
+ expect(config['a.b.c.d']).to eq(1)
32
+ expect(config.dig('a', :b, 'c', :d)).to eq(1)
33
+ end
34
+
35
+ specify 'assigning a value invalidates the cache' do
36
+ expect(config['a.b.c.d']).to eq(1)
37
+
38
+ config['a.b.c.d'] = 2
39
+ expect(config['a.b.c.d']).to eq(2)
40
+ end
41
+
42
+ specify 'mutating a nested config instance invalidates every parent cache (matryoshka)' do
43
+ # warm up caches on every level
44
+ expect(config['a.b.c.d']).to eq(1)
45
+ expect(config['a.b.c']['d']).to eq(1)
46
+
47
+ # mutate through a directly-grabbed nested instance
48
+ nested = config['a.b.c']
49
+ nested['d'] = 99
50
+
51
+ # the mutation must be visible from the root through the (now stale) cache
52
+ expect(config['a.b.c.d']).to eq(99)
53
+ expect(config['a.b']['c.d']).to eq(99)
54
+ expect(nested['d']).to eq(99)
55
+ end
56
+
57
+ specify 'a flat value mutation invalidates the cache' do
58
+ expect(config['flat']).to eq('value')
59
+ config['flat'] = 'changed'
60
+ expect(config['flat']).to eq('changed')
61
+ end
62
+
63
+ specify '#clear! invalidates the cache' do
64
+ expect(config['a.b.c.d']).to eq(1)
65
+ config.clear!
66
+ expect(config['a.b.c.d']).to be_nil
67
+ end
68
+
69
+ specify 'full data-set validators observe fresh values after mutation (no stale cache)' do
70
+ validated_config = Class.new(Qonfig::DataSet) do
71
+ setting :nested do
72
+ setting :flag, :yes
73
+ end
74
+
75
+ validate { settings.nested.flag.is_a?(Symbol) }
76
+ end
77
+
78
+ instance = validated_config.new
79
+ # warm the cache through the validator-read path
80
+ expect(instance.settings.nested.flag).to eq(:yes)
81
+
82
+ expect { instance.settings.nested.flag = 123 }.to raise_error(Qonfig::ValidationError)
83
+ end
84
+
85
+ specify 'composed nested settings invalidate the host cache on mutation' do
86
+ shared = Class.new(Qonfig::DataSet) do
87
+ setting :s do
88
+ setting :v, 1
89
+ end
90
+ end
91
+
92
+ host = Qonfig::DataSet.build { compose shared }
93
+
94
+ expect(host['s.v']).to eq(1) # warm up the cache through the host
95
+ host['s.v'] = 88
96
+ # the nested instance was merged in "by reference"; mutating it must still
97
+ # cascade invalidation up to the host (otherwise the host serves a stale 1)
98
+ expect(host['s.v']).to eq(88)
99
+ end
100
+
101
+ specify 're-defined nested settings invalidate the cache on mutation' do
102
+ redefined_config = Class.new(Qonfig::DataSet) do
103
+ setting :s do
104
+ setting :v, 1
105
+ end
106
+
107
+ re_setting :s do
108
+ setting :v, 10
109
+ end
110
+ end
111
+
112
+ instance = redefined_config.new
113
+
114
+ expect(instance['s.v']).to eq(10) # warm up the cache
115
+ instance['s.v'] = 77
116
+ expect(instance['s.v']).to eq(77)
117
+ end
118
+
119
+ specify 'cache invalidation always swaps in a fresh hash, even when already empty' do
120
+ # Guards the lock-free invalidation contract: invalidation may propagate up
121
+ # from a nested mutation without holding our access lock, so the swap must be
122
+ # unconditional. A `unless empty?` guard would let a concurrent reader that has
123
+ # resolved a stale value (but not yet written it) persist that value forever.
124
+ settings = config.settings
125
+ empty_cache = settings.instance_variable_get(:@__deep_access_cache__)
126
+ expect(empty_cache).to be_empty
127
+
128
+ settings.send(:__invalidate_deep_access_cache__)
129
+
130
+ swapped_cache = settings.instance_variable_get(:@__deep_access_cache__)
131
+ expect(swapped_cache).to be_empty
132
+ expect(swapped_cache).not_to equal(empty_cache)
133
+ end
134
+ end
@@ -22,83 +22,85 @@ describe 'Run code with temporary settings' do
22
22
  expect(config.settings.api.login).to eq('D@iVeR')
23
23
  end
24
24
 
25
- specify 'thread-safety' do
26
- config = Qonfig::DataSet.build do
27
- setting :api do
28
- setting :token, 'test123'
29
- setting :login, 'D@iVeR'
30
- end
25
+ unless RUBY_ENGINE.include?('truffleruby')
26
+ specify 'thread-safety' do
27
+ config = Qonfig::DataSet.build do
28
+ setting :api do
29
+ setting :token, 'test123'
30
+ setting :login, 'D@iVeR'
31
+ end
31
32
 
32
- setting :credentials do
33
- setting :user, 'admin'
34
- setting :password, '1234asdf'
33
+ setting :credentials do
34
+ setting :user, 'admin'
35
+ setting :password, '1234asdf'
36
+ end
35
37
  end
36
- end
37
38
 
38
- ThreadGroup.new.tap do |thread_group|
39
- 10.times do
40
- thread_group.add(Thread.new do
41
- # NOTE: change settings temporary
42
- config.with(api: { token: '777555' }, credentials: { password: 'test123' }) do
43
- config.settings.credentials.user = 'nimda'
44
- end
45
- end)
39
+ ThreadGroup.new.tap do |thread_group|
40
+ 10.times do
41
+ thread_group.add(Thread.new do
42
+ # NOTE: change settings temporary
43
+ config.with(api: { token: '777555' }, credentials: { password: 'test123' }) do
44
+ config.settings.credentials.user = 'nimda'
45
+ end
46
+ end)
46
47
 
47
- thread_group.add(Thread.new do
48
- # NOTE: settings are not changed :)
49
- expect(config.settings.credentials.user).to eq('admin')
50
- expect(config.settings.credentials.password).to eq('1234asdf')
51
- expect(config.settings.api.token).to eq('test123')
52
- expect(config.settings.api.login).to eq('D@iVeR')
53
- end)
48
+ thread_group.add(Thread.new do
49
+ # NOTE: settings are not changed :)
50
+ expect(config.settings.credentials.user).to eq('admin')
51
+ expect(config.settings.credentials.password).to eq('1234asdf')
52
+ expect(config.settings.api.token).to eq('test123')
53
+ expect(config.settings.api.login).to eq('D@iVeR')
54
+ end)
54
55
 
55
- thread_group.add(Thread.new do
56
- # NOTE: change settings temporary
57
- config.with do
58
- config.settings.api.login = 'provider'
59
- config.settings.api.token = 'super_puper_123'
60
- end
61
- end)
56
+ thread_group.add(Thread.new do
57
+ # NOTE: change settings temporary
58
+ config.with do
59
+ config.settings.api.login = 'provider'
60
+ config.settings.api.token = 'super_puper_123'
61
+ end
62
+ end)
62
63
 
63
- thread_group.add(Thread.new do
64
- # NOTE: settings are not changed :)
65
- expect(config.settings.credentials.user).to eq('admin')
66
- expect(config.settings.credentials.password).to eq('1234asdf')
67
- expect(config.settings.api.token).to eq('test123')
68
- expect(config.settings.api.login).to eq('D@iVeR')
69
- end)
64
+ thread_group.add(Thread.new do
65
+ # NOTE: settings are not changed :)
66
+ expect(config.settings.credentials.user).to eq('admin')
67
+ expect(config.settings.credentials.password).to eq('1234asdf')
68
+ expect(config.settings.api.token).to eq('test123')
69
+ expect(config.settings.api.login).to eq('D@iVeR')
70
+ end)
70
71
 
71
- thread_group.add(Thread.new do
72
- config.with(api: { login: '0exp' }, credentials: { user: 'D@iVeR' }) do
73
- config.settings.api.token = 'kekpek123'
74
- config.settings.credentials.password = 'admin'
75
- end
76
- end)
72
+ thread_group.add(Thread.new do
73
+ config.with(api: { login: '0exp' }, credentials: { user: 'D@iVeR' }) do
74
+ config.settings.api.token = 'kekpek123'
75
+ config.settings.credentials.password = 'admin'
76
+ end
77
+ end)
77
78
 
78
- thread_group.add(Thread.new do
79
- # NOTE: settings are not changed :)
80
- expect(config.settings.credentials.user).to eq('admin')
81
- expect(config.settings.credentials.password).to eq('1234asdf')
82
- expect(config.settings.api.token).to eq('test123')
83
- expect(config.settings.api.login).to eq('D@iVeR')
84
- end)
79
+ thread_group.add(Thread.new do
80
+ # NOTE: settings are not changed :)
81
+ expect(config.settings.credentials.user).to eq('admin')
82
+ expect(config.settings.credentials.password).to eq('1234asdf')
83
+ expect(config.settings.api.token).to eq('test123')
84
+ expect(config.settings.api.login).to eq('D@iVeR')
85
+ end)
85
86
 
86
- thread_group.add(Thread.new do
87
- # NOTE: change settings temporary
88
- config.with(api: { login: 'mobile_legends' }, credentials: { user: 'dota2' }) do
89
- config.settings.api.token = 'league_of_legends'
90
- config.settings.credentials.password = 'overwatch'
91
- end
92
- end)
87
+ thread_group.add(Thread.new do
88
+ # NOTE: change settings temporary
89
+ config.with(api: { login: 'mobile_legends' }, credentials: { user: 'dota2' }) do
90
+ config.settings.api.token = 'league_of_legends'
91
+ config.settings.credentials.password = 'overwatch'
92
+ end
93
+ end)
93
94
 
94
- thread_group.add(Thread.new do
95
- # NOTE: settings are not changed :)
96
- expect(config.settings.credentials.user).to eq('admin')
97
- expect(config.settings.credentials.password).to eq('1234asdf')
98
- expect(config.settings.api.token).to eq('test123')
99
- expect(config.settings.api.login).to eq('D@iVeR')
100
- end)
101
- end
102
- end.list.map(&:join)
95
+ thread_group.add(Thread.new do
96
+ # NOTE: settings are not changed :)
97
+ expect(config.settings.credentials.user).to eq('admin')
98
+ expect(config.settings.credentials.password).to eq('1234asdf')
99
+ expect(config.settings.api.token).to eq('test123')
100
+ expect(config.settings.api.login).to eq('D@iVeR')
101
+ end)
102
+ end
103
+ end.list.map(&:join)
104
+ end
103
105
  end
104
106
  end
@@ -108,22 +108,44 @@ describe 'Save to .json (JSON)' do
108
108
  # NOTE: step 2) read saved file
109
109
  file_data = File.read(config_file_path)
110
110
 
111
- expect(file_data).to eq(<<~JSON.strip)
112
- {
113
- "true_bollean": true,
114
- "false_boolean": false,
115
- "empty_object": {},
116
- "filled_object": {
117
- "a": 1,
118
- "b": null,
119
- "c": true,
120
- "d": "1",
121
- "e": false
122
- },
123
- "null_data": null,
124
- "collection": [ "1", 2, true, false, null, [], {}]
125
- }
126
- JSON
111
+ expected_json =
112
+ if RUBY_ENGINE.include?('truffleruby')
113
+ <<~JSON.strip
114
+ {
115
+ "true_bollean": true,
116
+ "false_boolean": false,
117
+ "empty_object": {},
118
+ "filled_object": {
119
+ "a": 1,
120
+ "b": null,
121
+ "c": true,
122
+ "d": "1",
123
+ "e": false
124
+ },
125
+ "null_data": null,
126
+ "collection": ["1",2,true,false,null,[],{}]
127
+ }
128
+ JSON
129
+ else
130
+ <<~JSON.strip
131
+ {
132
+ "true_bollean": true,
133
+ "false_boolean": false,
134
+ "empty_object": {},
135
+ "filled_object": {
136
+ "a": 1,
137
+ "b": null,
138
+ "c": true,
139
+ "d": "1",
140
+ "e": false
141
+ },
142
+ "null_data": null,
143
+ "collection": [ "1", 2, true, false, null, [], {}]
144
+ }
145
+ JSON
146
+ end
147
+
148
+ expect(file_data).to eq(expected_json)
127
149
  end
128
150
  end
129
151
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qonfig
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.30.0
4
+ version: 0.31.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rustam Ibragimov
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-12-14 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: base64
@@ -154,7 +153,6 @@ extra_rdoc_files: []
154
153
  files:
155
154
  - ".github/workflows/build.yml"
156
155
  - ".gitignore"
157
- - ".jrubyrc"
158
156
  - ".rspec"
159
157
  - ".rubocop.yml"
160
158
  - CHANGELOG.md
@@ -319,6 +317,7 @@ files:
319
317
  - spec/features/load_setting_values_from_file/load_from_yaml_spec.rb
320
318
  - spec/features/load_setting_values_from_file/shared_behavior_spec.rb
321
319
  - spec/features/mixin_spec.rb
320
+ - spec/features/nested_access_caching_spec.rb
322
321
  - spec/features/non_redefineable_core_methods_spec.rb
323
322
  - spec/features/plugins/pretty_print_spec.rb
324
323
  - spec/features/plugins/toml/expose_self/format_option_toml_spec.rb
@@ -399,7 +398,6 @@ homepage: https://github.com/0exp/qonfig
399
398
  licenses:
400
399
  - MIT
401
400
  metadata: {}
402
- post_install_message:
403
401
  rdoc_options: []
404
402
  require_paths:
405
403
  - lib
@@ -414,8 +412,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
414
412
  - !ruby/object:Gem::Version
415
413
  version: '0'
416
414
  requirements: []
417
- rubygems_version: 3.5.23
418
- signing_key:
415
+ rubygems_version: 3.6.9
419
416
  specification_version: 4
420
417
  summary: Config object
421
418
  test_files: []
data/.jrubyrc DELETED
@@ -1 +0,0 @@
1
- debug.fullTrace=true