process_settings 0.8.2 → 0.9.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 +4 -4
- data/README.md +52 -10
- data/lib/process_settings/abstract_monitor.rb +163 -0
- data/lib/process_settings/file_monitor.rb +96 -0
- data/lib/process_settings/monitor.rb +26 -237
- data/lib/process_settings/settings.rb +2 -0
- data/lib/process_settings/target.rb +9 -1
- data/lib/process_settings/target_and_settings.rb +2 -2
- data/lib/process_settings/testing/helpers.rb +44 -0
- data/lib/process_settings/testing/monitor.rb +28 -0
- data/lib/process_settings/testing/monitor_stub.rb +3 -0
- data/lib/process_settings/version.rb +1 -1
- data/lib/process_settings.rb +32 -2
- metadata +13 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7ace753f727c3aea91291d906951ff86da6750d6aa4b0db1022ad6155dce42dc
|
4
|
+
data.tar.gz: 1081a5ba75c69f17f6ad308125a748751d381b7289b27e63a0e9130fd96db0bd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
164
|
-
The `ProcessSettings::Testing::
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
137
|
-
|
138
|
-
|
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
|
-
|
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
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
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
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
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
|
-
|
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
|
-
|
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 ==
|
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
|
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
|
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
|
|
data/lib/process_settings.rb
CHANGED
@@ -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
|
-
|
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.
|
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: '
|
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: '
|
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
|