process_settings 0.8.2 → 0.9.0

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
2
  SHA256:
3
- metadata.gz: 4c35c37914363571825848c95ec6d1f3d15811b11b0bea5671f28744a4108129
4
- data.tar.gz: 6c252be7fb7d924f50fd9a6cdbbb35bcfa292da595cb79c3e644a8f9f3da08d1
3
+ metadata.gz: 7ace753f727c3aea91291d906951ff86da6750d6aa4b0db1022ad6155dce42dc
4
+ data.tar.gz: 1081a5ba75c69f17f6ad308125a748751d381b7289b27e63a0e9130fd96db0bd
5
5
  SHA512:
6
- metadata.gz: f07d59b5f034ce85b5e20b117fe731d3b3860b6422c0ba2c8fcdaf4c39d88c5af70ed6636952cd9385c25ad9e320cfd1784671689406dae71ac051fdf1daa709
7
- data.tar.gz: 3a74f7e775d7c4688a20b0c3b93918992ef5acd3163aa581f411b87fa326ef7cc0eaea0f090915464f3b20f45740112c681543b3a7c8c5c58eba3dca8b0ba6f2
6
+ metadata.gz: dbfbe4d945aa9d67b02862be7f5905afd36b0c0c1c5fd1f2054d6b263f6ccdaf878bbc6521361c4454183c9d00f882947572933aa6c52850a8c10e3b32839510
7
+ data.tar.gz: 6e2341fca852024f9d0a518159c6367264376fb760d3efde1904546cc75a40042272ad1c0c9e075b7e68f23891c5812911253f98a66f514f72992aad2324dd7b
data/README.md CHANGED
@@ -5,6 +5,10 @@ Settings are managed in a git repo, in separate YAML files for each concern (for
5
5
 
6
6
  The context can be either static to the process (for example, `service_name` or `datacenter`) or dynamic (for example, the current web request `domain`).
7
7
 
8
+ ## Dependencies
9
+ * Ruby >= 2.6
10
+ * ActiveSupport >= 4.2, < 7
11
+
8
12
  ## Installation
9
13
  To install this gem directly on your machine from rubygems, run the following:
10
14
  ```ruby
@@ -160,18 +164,56 @@ This will be applied in any process that has (`service_name == "frontend"` OR `s
160
164
  The settings YAML files are always combined in alphabetical order by file path. Later settings take precedence over the earlier ones.
161
165
 
162
166
  ### Testing
163
- For testing, it is often necessary to set a specific hash for the process_settings values to use in that test case.
164
- The `ProcessSettings::Testing::MonitorStub` class is provided for this purpose. It can be initialized with a hash and assigned to `ProcessSettings::Monitor.instance`. After the test runs, make sure to call `clear_instance`.
165
- Note that it has no `targeting` or `settings` keys; it is stubbing the resulting settings hash _after_ targeting has been applied.
166
- Here is an example using `rspec` conventions:
167
- ```
168
- before do
169
- settings = { "honeypot" => { "answer_odds" => 100 } }
170
- ProcessSettings::Monitor.instance = ProcessSettings::Testing::MonitorStub.new(settings)
167
+ For testing, it is often necessary to set a specific override hash for the process_settings values to use in
168
+ that use case. The `ProcessSettings::Testing::Helpers` module is provided for this purpose. It can be used to
169
+ override a specific hash of process settings, while leving the rest in tact, and resetting back to the defaults
170
+ after the test case is over. Here are some examples using various testing frameworks:
171
+
172
+ #### RSpec
173
+ ##### `spec_helper.rb`
174
+ ```ruby
175
+ require 'process_settings/testing/helpers'
176
+
177
+ RSpec.configure do |config|
178
+ # ...
179
+
180
+ include ProcessSettings::Testing::Helpers
181
+
182
+ after do
183
+ reset_process_settings
184
+ end
185
+
186
+ # ...
187
+ end
188
+ ```
189
+
190
+ ##### `process_settings_spec.rb`
191
+ ```ruby
192
+ require 'spec_helper'
193
+
194
+ RSpec.describe SomeClass do
195
+ before do
196
+ stub_process_settings(honeypot: { answer_odds: 100 })
197
+ end
198
+
199
+ # ...
171
200
  end
201
+ ```
202
+
203
+ #### Test::Unit / Minitest
204
+ ```ruby
205
+ require 'process_settings/testing/helpers'
206
+
207
+ context SomeClass do
208
+ setup do
209
+ stub_process_settings(honeypot: { answer_odds: 100 })
210
+ end
211
+
212
+ teardown do
213
+ reset_process_settings
214
+ end
172
215
 
173
- after do
174
- ProcessSettings::Monitor.clear_instance
216
+ # ...
175
217
  end
176
218
  ```
177
219
 
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+
5
+ require 'process_settings/targeted_settings'
6
+ require 'process_settings/hash_path'
7
+
8
+ module ProcessSettings
9
+ class SettingsPathNotFound < StandardError; end
10
+
11
+ OnChangeDeprecation = ActiveSupport::Deprecation.new('1.0', 'ProcessSettings::Monitor')
12
+
13
+ class AbstractMonitor
14
+ attr_reader :min_polling_seconds, :logger
15
+ attr_reader :static_context, :statically_targeted_settings
16
+
17
+ def initialize(logger:)
18
+ @logger = logger
19
+ @on_change_callbacks = []
20
+ @when_updated_blocks = Set.new
21
+ @static_context = {}
22
+ end
23
+
24
+ # This is the main entry point for looking up settings on the Monitor instance.
25
+ #
26
+ # @example
27
+ #
28
+ # ['path', 'to', 'setting']
29
+ #
30
+ # will return 42 in this example settings YAML:
31
+ # +code+
32
+ # path:
33
+ # to:
34
+ # setting:
35
+ # 42
36
+ # +code+
37
+ #
38
+ # @param [Array(String)] path The path of one or more strings.
39
+ #
40
+ # @param [Hash] dynamic_context Optional dynamic context hash. It will be merged with the static context.
41
+ #
42
+ # @param [boolean] required If true (default) will raise `SettingsPathNotFound` if not found; otherwise returns `nil` if not found.
43
+ #
44
+ # @return setting value
45
+ def [](*path, dynamic_context: {}, required: true)
46
+ targeted_value(*path, dynamic_context: dynamic_context, required: required)
47
+ end
48
+
49
+ # Idempotently adds the given block to the when_updated collection
50
+ # calls the block first unless initial_update: false is passed
51
+ # returns a handle (the block itself) which can later be passed into cancel_when_updated
52
+ def when_updated(initial_update: true, &block)
53
+ if @when_updated_blocks.add?(block)
54
+ if initial_update
55
+ begin
56
+ block.call(self)
57
+ rescue => ex
58
+ logger.error("ProcessSettings::Monitor#when_updated rescued exception during initialization:\n#{ex.class}: #{ex.message}")
59
+ end
60
+ end
61
+ end
62
+
63
+ block
64
+ end
65
+
66
+ # removes the given when_updated block identified by the handle returned from when_updated
67
+ def cancel_when_updated(handle)
68
+ @when_updated_blocks.delete_if { |callback| callback.eql?(handle) }
69
+ end
70
+
71
+ # Registers the given callback block to be called when settings change.
72
+ # These are run using the shared thread that monitors for changes so be courteous and don't monopolize it!
73
+ # @deprecated
74
+ def on_change(&callback)
75
+ @on_change_callbacks << callback
76
+ end
77
+ deprecate on_change: :when_updated, deprecator: OnChangeDeprecation
78
+
79
+ # Assigns a new static context. Recomputes statically_targeted_settings.
80
+ # Keys must be strings or integers. No symbols.
81
+ def static_context=(context)
82
+ self.class.ensure_no_symbols(context)
83
+
84
+ @static_context = context
85
+
86
+ load_statically_targeted_settings(force_retarget: true)
87
+ end
88
+
89
+ # Returns the process settings value at the given `path` using the given `dynamic_context`.
90
+ # (It is assumed that the static context was already set through static_context=.)
91
+ # If nothing set at the given `path`:
92
+ # if required, raises SettingsPathNotFound
93
+ # else returns nil
94
+ def targeted_value(*path, dynamic_context:, required: true)
95
+ # Merging the static context in is necessary to make sure that the static context isn't shifting
96
+ # this can be rather costly to do every time if the dynamic context is not changing
97
+ # TODO: Warn in the case where dynamic context was attempting to change a static value
98
+ # TODO: Cache the last used dynamic context as a potential optimization to avoid unnecessary deep merges
99
+ # TECH-4402 was created to address these todos
100
+ full_context = dynamic_context.deep_merge(static_context)
101
+ result = statically_targeted_settings.reduce(:not_found) do |latest_result, target_and_settings|
102
+ # find last value from matching targets
103
+ if target_and_settings.target.target_key_matches?(full_context)
104
+ if (value = target_and_settings.settings.json_doc.mine(*path, not_found_value: :not_found)) != :not_found
105
+ latest_result = value
106
+ end
107
+ end
108
+ latest_result
109
+ end
110
+
111
+ if result == :not_found
112
+ if required
113
+ raise SettingsPathNotFound, "no settings found for path #{path.inspect}"
114
+ else
115
+ nil
116
+ end
117
+ else
118
+ result
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ class << self
125
+ def ensure_no_symbols(value)
126
+ case value
127
+ when Symbol
128
+ raise ArgumentError, "symbol value #{value.inspect} found--should be String"
129
+ when Hash
130
+ value.each do |k, v|
131
+ k.is_a?(Symbol) and raise ArgumentError, "symbol key #{k.inspect} found--should be String"
132
+ ensure_no_symbols(v)
133
+ end
134
+ when Array
135
+ value.each { |v| ensure_no_symbols(v) }
136
+ end
137
+ end
138
+ end
139
+
140
+ # Calls all registered on_change callbacks. Rescues any exceptions they may raise.
141
+ # Note: this method can be re-entrant to the class; the on_change callbacks may call right back into these methods.
142
+ # Therefore it's critical to finish all transitions and release any resources before calling this method.
143
+ def notify_on_change
144
+ @on_change_callbacks.each do |callback|
145
+ begin
146
+ callback.call(self)
147
+ rescue => ex
148
+ logger.error("ProcessSettings::Monitor#notify_on_change rescued exception:\n#{ex.class}: #{ex.message}")
149
+ end
150
+ end
151
+ end
152
+
153
+ def call_when_updated_blocks
154
+ @when_updated_blocks.each do |block|
155
+ begin
156
+ block.call(self)
157
+ rescue => ex
158
+ logger.error("ProcessSettings::Monitor#call_when_updated_blocks rescued exception:\n#{ex.class}: #{ex.message}")
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'listen'
5
+ require 'psych'
6
+
7
+ require 'process_settings/abstract_monitor'
8
+ require 'process_settings/targeted_settings'
9
+ require 'process_settings/hash_path'
10
+
11
+ module ProcessSettings
12
+ class FileMonitor < AbstractMonitor
13
+ attr_reader :file_path, :untargeted_settings
14
+
15
+ def initialize(file_path, logger:)
16
+ super(logger: logger)
17
+
18
+ @file_path = File.expand_path(file_path)
19
+ @last_statically_targetted_settings = nil
20
+ @untargeted_settings = nil
21
+ @last_untargetted_settings = nil
22
+
23
+ start
24
+ end
25
+
26
+ # starts listening for changes
27
+ # Note: This method creates a new thread that will be monitoring for changes
28
+ # do to the nature of how the Listen gem works, there is no record of
29
+ # existing threads, calling this mutliple times will result in spinning off
30
+ # multiple listen threads and will have unknow effects
31
+ def start
32
+ path = File.dirname(file_path)
33
+
34
+ # to eliminate any race condition:
35
+ # 1. set up file watcher
36
+ # 2. start it (this should trigger if any changes have been made since (1))
37
+ # 3. load the file
38
+
39
+ @listener = file_change_notifier.to(path) do |modified, added, _removed|
40
+ if modified.include?(file_path) || added.include?(file_path)
41
+ logger.info("ProcessSettings::Monitor file #{file_path} changed. Reloading.")
42
+ load_untargeted_settings
43
+
44
+ load_statically_targeted_settings
45
+ end
46
+ end
47
+
48
+ unless ENV['DISABLE_LISTEN_CHANGE_MONITORING']
49
+ @listener.start
50
+ end
51
+
52
+ load_untargeted_settings
53
+ load_statically_targeted_settings
54
+ end
55
+
56
+ # stops listening for changes
57
+ def stop
58
+ @listener&.stop
59
+ end
60
+
61
+ private
62
+
63
+ # Loads the most recent settings from disk
64
+ def load_untargeted_settings
65
+ new_untargeted_settings = load_file(file_path)
66
+ old_version = @untargeted_settings&.version
67
+ new_version = new_untargeted_settings.version
68
+ @untargeted_settings = new_untargeted_settings
69
+ logger.info("ProcessSettings::Monitor#load_untargeted_settings loaded version #{new_version}#{" to replace version #{old_version}" if old_version}")
70
+ end
71
+
72
+ # Loads the latest untargeted settings from disk. Returns the current process settings as a TargetAndProcessSettings given
73
+ # by applying the static context to the current untargeted settings from disk.
74
+ # If these have changed, borrows this thread to call notify_on_change and call_when_updated_blocks.
75
+ def load_statically_targeted_settings(force_retarget: false)
76
+ if force_retarget || @last_untargetted_settings != @untargeted_settings
77
+ @last_untargetted_settings = @untargeted_settings
78
+ @statically_targeted_settings = @untargeted_settings.with_static_context(@static_context)
79
+ if @last_statically_targetted_settings != @statically_targeted_settings
80
+ @last_statically_targetted_settings = @statically_targeted_settings
81
+
82
+ notify_on_change
83
+ call_when_updated_blocks
84
+ end
85
+ end
86
+ end
87
+
88
+ def load_file(file_path)
89
+ TargetedSettings.from_file(file_path)
90
+ end
91
+
92
+ def file_change_notifier
93
+ Listen
94
+ end
95
+ end
96
+ end
@@ -1,264 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'targeted_settings'
4
- require_relative 'hash_path'
5
- require 'psych'
6
- require 'listen'
7
3
  require 'active_support'
4
+ require 'active_support/deprecation'
8
5
 
9
- module ProcessSettings
10
- class SettingsPathNotFound < StandardError; end
11
-
12
- OnChangeDeprecation = ActiveSupport::Deprecation.new('1.0', 'ProcessSettings::Monitor')
13
-
14
- class Monitor
15
- attr_reader :file_path, :min_polling_seconds, :logger
16
- attr_reader :static_context, :untargeted_settings, :statically_targeted_settings
17
-
18
- DEFAULT_MIN_POLLING_SECONDS = 5
19
-
20
- def initialize(file_path, logger:)
21
- @file_path = File.expand_path(file_path)
22
- @logger = logger
23
- @on_change_callbacks = []
24
- @when_updated_blocks = Set.new
25
- @static_context = {}
26
- @last_statically_targetted_settings = nil
27
- @untargeted_settings = nil
28
- @last_untargetted_settings = nil
29
- @last_untargetted_settings = nil
30
-
31
- start
32
- end
33
-
34
- # []
35
- #
36
- # This is the main entry point for looking up settings on the Monitor instance.
37
- #
38
- # @example
39
- #
40
- # ['path', 'to', 'setting']
41
- #
42
- # will return 42 in this example settings YAML:
43
- # +code+
44
- # path:
45
- # to:
46
- # setting:
47
- # 42
48
- # +code+
49
- #
50
- # @param [Array(String)] path The path of one or more strings.
51
- #
52
- # @param [Hash] dynamic_context Optional dynamic context hash. It will be merged with the static context.
53
- #
54
- # @param [boolean] required If true (default) will raise `SettingsPathNotFound` if not found; otherwise returns `nil` if not found.
55
- #
56
- # @return setting value
57
- def [](*path, dynamic_context: {}, required: true)
58
- targeted_value(*path, dynamic_context: dynamic_context, required: required)
59
- end
60
-
61
- # starts listening for changes
62
- # Note: This method creates a new thread that will be monitoring for changes
63
- # do to the nature of how the Listen gem works, there is no record of
64
- # existing threads, calling this mutliple times will result in spinning off
65
- # multiple listen threads and will have unknow effects
66
- def start
67
- path = File.dirname(@file_path)
68
-
69
- # to eliminate any race condition:
70
- # 1. set up file watcher
71
- # 2. start it (this should trigger if any changes have been made since (1))
72
- # 3. load the file
73
-
74
- @listener = file_change_notifier.to(path) do |modified, added, _removed|
75
- if modified.include?(@file_path) || added.include?(@file_path)
76
- @logger.info("ProcessSettings::Monitor file #{@file_path} changed. Reloading.")
77
- load_untargeted_settings
78
-
79
- load_statically_targeted_settings
80
- end
81
- end
82
-
83
- unless ENV['DISABLE_LISTEN_CHANGE_MONITORING']
84
- @listener.start
85
- end
86
-
87
- load_untargeted_settings
88
- load_statically_targeted_settings
89
- end
90
-
91
- # stops listening for changes
92
- def stop
93
- @listener&.stop
94
- end
95
-
96
- # Idempotently adds the given block to the when_updated collection
97
- # calls the block first unless initial_update: false is passed
98
- # returns a handle (the block itself) which can later be passed into cancel_when_updated
99
- def when_updated(initial_update: true, &block)
100
- if @when_updated_blocks.add?(block)
101
- if initial_update
102
- begin
103
- block.call(self)
104
- rescue => ex
105
- logger.error("ProcessSettings::Monitor#when_updated rescued exception during initialization:\n#{ex.class}: #{ex.message}")
106
- end
107
- end
108
- end
109
-
110
- block
111
- end
112
-
113
- # removes the given when_updated block identified by the handle returned from when_updated
114
- def cancel_when_updated(handle)
115
- @when_updated_blocks.delete_if { |callback| callback.eql?(handle) }
116
- end
117
-
118
- # Registers the given callback block to be called when settings change.
119
- # These are run using the shared thread that monitors for changes so be courteous and don't monopolize it!
120
- # @deprecated
121
- def on_change(&callback)
122
- @on_change_callbacks << callback
123
- end
124
- deprecate on_change: :when_updated, deprecator: OnChangeDeprecation
6
+ require_relative 'file_monitor'
125
7
 
126
- # Assigns a new static context. Recomputes statically_targeted_settings.
127
- # Keys must be strings or integers. No symbols.
128
- def static_context=(context)
129
- self.class.ensure_no_symbols(context)
130
-
131
- @static_context = context
132
-
133
- load_statically_targeted_settings(force_retarget: true)
134
- end
8
+ module ProcessSettings
9
+ # DEPRECATED
10
+ class Monitor < FileMonitor
11
+ class << self
12
+ attr_reader :logger, :file_path
13
+ attr_writer :instance
135
14
 
136
- # Returns the process settings value at the given `path` using the given `dynamic_context`.
137
- # (It is assumed that the static context was already set through static_context=.)
138
- # If nothing set at the given `path`:
139
- # if required, raises SettingsPathNotFound
140
- # else returns nil
141
- def targeted_value(*path, dynamic_context:, required: true)
142
- # Merging the static context in is necessary to make sure that the static context isn't shifting
143
- # this can be rather costly to do every time if the dynamic context is not changing
144
- # TODO: Warn in the case where dynamic context was attempting to change a static value
145
- # TODO: Cache the last used dynamic context as a potential optimization to avoid unnecessary deep merges
146
- # TECH-4402 was created to address these todos
147
- full_context = dynamic_context.deep_merge(static_context)
148
- result = statically_targeted_settings.reduce(:not_found) do |latest_result, target_and_settings|
149
- # find last value from matching targets
150
- if target_and_settings.target.target_key_matches?(full_context)
151
- if (value = target_and_settings.settings.json_doc.mine(*path, not_found_value: :not_found)) != :not_found
152
- latest_result = value
153
- end
154
- end
155
- latest_result
156
- end
15
+ def file_path=(new_file_path)
16
+ ActiveSupport::Deprecation.warn("ProcessSettings::Monitor.file_path= is deprecated and will be removed in v1.0.")
17
+ clear_instance
157
18
 
158
- if result == :not_found
159
- if required
160
- raise SettingsPathNotFound, "no settings found for path #{path.inspect}"
161
- else
162
- nil
163
- end
164
- else
165
- result
19
+ @file_path = new_file_path
166
20
  end
167
- end
168
-
169
- private
170
-
171
- # Loads the most recent settings from disk
172
- def load_untargeted_settings
173
- new_untargeted_settings = load_file(file_path)
174
- old_version = @untargeted_settings&.version
175
- new_version = new_untargeted_settings.version
176
- @untargeted_settings = new_untargeted_settings
177
- logger.info("ProcessSettings::Monitor#load_untargeted_settings loaded version #{new_version}#{" to replace version #{old_version}" if old_version}")
178
- end
179
21
 
180
- # Loads the latest untargeted settings from disk. Returns the current process settings as a TargetAndProcessSettings given
181
- # by applying the static context to the current untargeted settings from disk.
182
- # If these have changed, borrows this thread to call notify_on_change and call_when_updated_blocks.
183
- def load_statically_targeted_settings(force_retarget: false)
184
- if force_retarget || @last_untargetted_settings != @untargeted_settings
185
- @last_untargetted_settings = @untargeted_settings
186
- @statically_targeted_settings = @untargeted_settings.with_static_context(@static_context)
187
- if @last_statically_targetted_settings != @statically_targeted_settings
188
- @last_statically_targetted_settings = @statically_targeted_settings
189
-
190
- notify_on_change
191
- call_when_updated_blocks
192
- end
22
+ def new_from_settings
23
+ file_path or raise ArgumentError, "#{self}::file_path must be set before calling instance method"
24
+ logger or raise ArgumentError, "#{self}::logger must be set before calling instance method"
25
+ new(file_path, logger: logger)
193
26
  end
194
- end
195
-
196
- class << self
197
- attr_accessor :file_path
198
- attr_reader :logger
199
- attr_writer :instance
200
27
 
201
28
  def clear_instance
202
29
  @instance = nil
30
+ @default_instance = nil
203
31
  end
204
32
 
205
33
  def instance
206
- @instance ||= begin
207
- file_path or raise ArgumentError, "#{self}::file_path must be set before calling instance method"
208
- logger or raise ArgumentError, "#{self}::logger must be set before calling instance method"
209
- new(file_path, logger: logger)
210
- end
34
+ ActiveSupport::Deprecation.warn("ProcessSettings::Monitor.instance is deprecated and will be removed in v1.0. Use ProcessSettings.instance instead.")
35
+ @instance ||= default_instance
36
+ end
37
+
38
+ def default_instance
39
+ @default_instance ||= new_from_settings
211
40
  end
212
41
 
213
42
  def logger=(new_logger)
43
+ ActiveSupport::Deprecation.warn("ProcessSettings::Monitor.logger is deprecated and will be removed in v1.0.")
214
44
  @logger = new_logger
215
45
  Listen.logger ||= new_logger
216
46
  end
217
47
 
218
- def ensure_no_symbols(value)
219
- case value
220
- when Symbol
221
- raise ArgumentError, "symbol value #{value.inspect} found--should be String"
222
- when Hash
223
- value.each do |k, v|
224
- k.is_a?(Symbol) and raise ArgumentError, "symbol key #{k.inspect} found--should be String"
225
- ensure_no_symbols(v)
226
- end
227
- when Array
228
- value.each { |v| ensure_no_symbols(v) }
229
- end
230
- end
231
- end
232
-
233
- # Calls all registered on_change callbacks. Rescues any exceptions they may raise.
234
- # Note: this method can be re-entrant to the class; the on_change callbacks may call right back into these methods.
235
- # Therefore it's critical to finish all transitions and release any resources before calling this method.
236
- def notify_on_change
237
- @on_change_callbacks.each do |callback|
238
- begin
239
- callback.call(self)
240
- rescue => ex
241
- logger.error("ProcessSettings::Monitor#notify_on_change rescued exception:\n#{ex.class}: #{ex.message}")
242
- end
243
- end
244
- end
245
-
246
- def call_when_updated_blocks
247
- @when_updated_blocks.each do |block|
248
- begin
249
- block.call(self)
250
- rescue => ex
251
- logger.error("ProcessSettings::Monitor#call_when_updated_blocks rescued exception:\n#{ex.class}: #{ex.message}")
252
- end
253
- end
48
+ deprecate :logger, :logger=, :file_path, :file_path=, deprecator: ActiveSupport::Deprecation.new('1.0', 'ProcessSettings')
254
49
  end
255
50
 
256
- def load_file(file_path)
257
- TargetedSettings.from_file(file_path)
258
- end
259
-
260
- def file_change_notifier
261
- Listen
262
- end
51
+ deprecate :initialize, deprecator: ActiveSupport::Deprecation.new('1.0', 'ProcessSettings')
263
52
  end
264
53
  end
@@ -11,6 +11,8 @@ module ProcessSettings
11
11
  def initialize(json_doc)
12
12
  json_doc.is_a?(Hash) or raise ArgumentError, "ProcessSettings must be a Hash; got #{json_doc.inspect}"
13
13
 
14
+ AbstractMonitor.ensure_no_symbols(json_doc)
15
+
14
16
  @json_doc = HashWithHashPath[json_doc]
15
17
  end
16
18
 
@@ -4,6 +4,8 @@ module ProcessSettings
4
4
  class Target
5
5
  include Comparable
6
6
 
7
+ TRUE_JSON_DOC = {}.freeze
8
+
7
9
  attr_reader :json_doc
8
10
 
9
11
  def initialize(json_doc)
@@ -11,7 +13,7 @@ module ProcessSettings
11
13
  end
12
14
 
13
15
  def target_key_matches?(context_hash)
14
- @json_doc == {} || self.class.target_key_matches?(@json_doc, context_hash)
16
+ @json_doc == TRUE_JSON_DOC || self.class.target_key_matches?(@json_doc, context_hash)
15
17
  end
16
18
 
17
19
  def with_static_context(static_context_hash)
@@ -93,5 +95,11 @@ module ProcessSettings
93
95
  end
94
96
  end
95
97
  end
98
+
99
+ class << self
100
+ def true_target
101
+ @true_target || new(TRUE_JSON_DOC)
102
+ end
103
+ end
96
104
  end
97
105
  end
@@ -11,10 +11,10 @@ module ProcessSettings
11
11
  def initialize(filename, target, settings)
12
12
  @filename = filename
13
13
 
14
- target.is_a?(Target) or raise ArgumentError, "target must be a ProcessTarget; got #{target.inspect}"
14
+ target.is_a?(Target) or raise ArgumentError, "target must be a Target; got #{target.inspect}"
15
15
  @target = target
16
16
 
17
- settings.is_a?(Settings) or raise ArgumentError, "settings must be a ProcessSettings; got #{settings.inspect}"
17
+ settings.is_a?(Settings) or raise ArgumentError, "settings must be a Settings; got #{settings.inspect}"
18
18
  @settings = settings
19
19
  end
20
20
 
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'process_settings/monitor'
4
+ require 'process_settings/settings'
5
+ require 'process_settings/target_and_settings'
6
+
7
+ require 'process_settings/testing/monitor'
8
+
9
+ module ProcessSettings
10
+ module Testing
11
+ module Helpers
12
+
13
+ # Adds the given settings_hash as an override at the end of the process_settings array, with default targeting (true).
14
+ # Therefore this will override these settings while leaving others alone.
15
+ #
16
+ # @param [Hash] settings_hash
17
+ #
18
+ # @return none
19
+ def stub_process_settings(settings_hash)
20
+ new_target_and_settings = ProcessSettings::TargetAndSettings.new(
21
+ '<test_override>',
22
+ Target::true_target,
23
+ ProcessSettings::Settings.new(settings_hash.deep_stringify_keys)
24
+ )
25
+
26
+ new_process_settings = [
27
+ *initial_instance.statically_targeted_settings,
28
+ new_target_and_settings
29
+ ]
30
+
31
+ ProcessSettings.instance = ProcessSettings::Testing::Monitor.new(
32
+ new_process_settings,
33
+ logger: initial_instance.logger
34
+ )
35
+ end
36
+
37
+ private
38
+
39
+ def initial_instance
40
+ @initial_instance ||= ProcessSettings.instance
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext'
4
+ require 'process_settings/abstract_monitor'
5
+
6
+ module ProcessSettings
7
+ module Testing
8
+
9
+ # A special instance of the monitor specifically used for testing that
10
+ # allows the providing of a settings array from memory to initialize
11
+ # the ProcessSetting Monitor for testing
12
+ #
13
+ # @param Array settings_array
14
+ # @param Logger logger
15
+ class Monitor < ::ProcessSettings::AbstractMonitor
16
+ def initialize(settings_array, logger:)
17
+ super(logger: logger)
18
+ @statically_targeted_settings = settings_array
19
+ end
20
+
21
+ private
22
+
23
+ def load_statically_targetted_settings(force_retarget: false)
24
+ @statically_targeted_settings
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/core_ext'
4
+
3
5
  require_relative '../monitor'
4
6
  require_relative '../hash_with_hash_path'
5
7
 
@@ -8,6 +10,7 @@ module ProcessSettings
8
10
  # This class implements the Monitor#targeted_value interface but is stubbed to use a simple hash in tests
9
11
  class MonitorStub
10
12
  def initialize(settings_hash)
13
+ ActiveSupport::Deprecation.warn("ProcessSettings::Testing::MonitorStub is deprecated and will be removed in future versions. Use ProcessSettings::Testing::Monitor instead.", caller)
11
14
  @settings_hash = HashWithHashPath[settings_hash]
12
15
  end
13
16
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ProcessSettings
4
- VERSION = '0.8.2'
4
+ VERSION = '0.9.0'
5
5
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support'
4
+ require 'active_support/deprecation'
5
+
3
6
  module ProcessSettings
4
7
  end
5
8
 
@@ -7,8 +10,28 @@ require 'process_settings/monitor'
7
10
 
8
11
  module ProcessSettings
9
12
  class << self
10
- # []
13
+ # Setter method for assigning the monitor instance for ProcessSettings to use
14
+ #
15
+ # @example
16
+ #
17
+ # ProcessSettings.instance = ProcessSettings::FileMonitor.new(...)
11
18
  #
19
+ # @param [ProcessSettings::AbstractMonitor] monitor The monitor to assign for use by ProcessSettings
20
+ def instance=(monitor)
21
+ if monitor && !monitor.is_a?(ProcessSettings::AbstractMonitor)
22
+ raise ArgumentError, "Invalid monitor of type #{monitor.class.name} provided. Must be of type ProcessSettings::AbstractMonitor"
23
+ end
24
+
25
+ @instance = monitor
26
+ end
27
+
28
+ # Getter method for retrieving the current monitor instance being used by ProcessSettings
29
+ #
30
+ # @return [ProcessSettings::AbstractMonitor]
31
+ def instance
32
+ @instance ||= lazy_create_instance
33
+ end
34
+
12
35
  # This is the main entry point for looking up settings in the process.
13
36
  #
14
37
  # @example
@@ -31,7 +54,14 @@ module ProcessSettings
31
54
  #
32
55
  # @return setting value
33
56
  def [](*path, dynamic_context: {}, required: true)
34
- Monitor.instance[*path, dynamic_context: dynamic_context, required: required]
57
+ instance[*path, dynamic_context: dynamic_context, required: required]
58
+ end
59
+
60
+ private
61
+
62
+ def lazy_create_instance
63
+ ActiveSupport::Deprecation.warn("lazy creation of Monitor instance is deprecated and will be removed from ProcessSettings 1.0")
64
+ Monitor.instance
35
65
  end
36
66
  end
37
67
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: process_settings
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Invoca
@@ -16,14 +16,20 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '4.2'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '7'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
27
  - - ">="
25
28
  - !ruby/object:Gem::Version
26
- version: '0'
29
+ version: '4.2'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '7'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: json
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -70,6 +76,8 @@ files:
70
76
  - bin/process_settings_for_services
71
77
  - bin/process_settings_version
72
78
  - lib/process_settings.rb
79
+ - lib/process_settings/abstract_monitor.rb
80
+ - lib/process_settings/file_monitor.rb
73
81
  - lib/process_settings/hash_path.rb
74
82
  - lib/process_settings/hash_with_hash_path.rb
75
83
  - lib/process_settings/monitor.rb
@@ -78,6 +86,8 @@ files:
78
86
  - lib/process_settings/target.rb
79
87
  - lib/process_settings/target_and_settings.rb
80
88
  - lib/process_settings/targeted_settings.rb
89
+ - lib/process_settings/testing/helpers.rb
90
+ - lib/process_settings/testing/monitor.rb
81
91
  - lib/process_settings/testing/monitor_stub.rb
82
92
  - lib/process_settings/util.rb
83
93
  - lib/process_settings/version.rb