super_settings 0.0.1.rc2 → 1.0.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: a9a76ec61662ea71965541177f828db81a6db13a2808bbdaca9d823053dd33a0
4
- data.tar.gz: b6980aa31511e88dae2e9034a82cb6d514e8cf5b1d5b8b54439819dd5d5e0a62
3
+ metadata.gz: 3b4e4df83235545b59d31ec20206e265ab19bbe0d4a21de81405bdc7fab892c6
4
+ data.tar.gz: 8de1f0d4f30714f93f11e1c5b7f004232793676877f2f0396d3fe757cbd2629e
5
5
  SHA512:
6
- metadata.gz: dd24a62c276574dd407f32534964ec019d00813570abaf8b799a57f7b82a53828ac9ee572e1efeceb5b2f4a4ca83cfaa74b6da2f5f0bb3317fe3ee785ddc00d8
7
- data.tar.gz: 3b66ffe10504f1de430aea5fe7a5b0c3d529e7374e974c3f61e92b660de4896466cb33531a7d0acf7d97881917e8ea68be186955c88470c4e1a2e17ae8404ecc
6
+ metadata.gz: e7d3fa4f5025a6c541fab3f61d906bf1be43e1378c3c88c27d33e726ff18fd3f5d3d3a73c43b11f43945fcc5677eb23ac34fc8f52d66582d755c4a44247454df
7
+ data.tar.gz: 7b0915ea64422d358d5d93a299c75ee63c620b1d2a476dd15f379c0129ccf67d3760536aaf16798094c1f8c8afa99594b5ace27126d18eaafde004fb30556e44
data/README.md CHANGED
@@ -19,6 +19,10 @@ SuperSettings provides a simple interface for accessing settings backed by a thr
19
19
 
20
20
  There is also an out of the box Web UI and REST API for managing settings. You can specify data types for your settings (string, integer, float, boolean, datetime, or array) to ensure that values will be valid. You can also supply documentation for each setting so that it's clear what each one does and how it is used.
21
21
 
22
+ Changes to settings are stored whenever a setting is changed to give you an audit trail if you need it for compliance reasons. In addition, you can provide your own callbacks to execute whenever a setting is changed.
23
+
24
+ There is a companion gem [ultra_settings](https://github.com/bdurand/ultra_settings) that can be used to integrate SuperSettings into a combined configuration system alongside YAML files and environment variables.
25
+
22
26
  ## Usage
23
27
 
24
28
  - [Getting Value](#getting-values)
@@ -51,65 +55,6 @@ SuperSettings.datetime("key") # -> returns a `Time` object
51
55
  SuperSettings.array("key") # -> returns an array of strings
52
56
  ```
53
57
 
54
- #### Hashes
55
- There is also a method to get multiple settings at once structured as a hash.
56
-
57
- ```ruby
58
- SuperSettings.structured("parent") # -> returns an hash
59
- ```
60
-
61
- The key provided to the `SuperSettings.structured` method indicates the key prefix and constructs the hash from settings that have keys beginning with that prefix. Keys are also broken down by a delimiter so you can create nested hashes. The delimiter defaults to `"."`, but you can specify a different one with the `delimiter` keyword argument.
62
-
63
- You can also set a maximum depth to the returned hash with the `max_depth` keyword argument.
64
-
65
- So, if you have the following settings:
66
-
67
- ```
68
- vendors.company_1.path = "/co1"
69
- vendors.company_1.timeout = 5
70
- vendors.company_2.path = "/co2"
71
- page_size = 20
72
- ```
73
-
74
- You would get these results:
75
-
76
- ```ruby
77
- SuperSettings.structured("vendors")
78
- # {
79
- # "company_1" => {
80
- # "path" => "/co1",
81
- # "timeout" => 5
82
- # },
83
- # "company_2" => {
84
- # "path" => "/co2"
85
- # }
86
- # }
87
-
88
- SuperSettings.structured("vendors.company_1")
89
- # {"path" => "/co1", "timeout" => 5}
90
-
91
- SuperSettings.structured("vendors.company_2")
92
- # {"path" => "/co2"}
93
-
94
- # Get all the settings by omitting the key
95
- SuperSettings.structured
96
- # {
97
- # "vendors" => {
98
- # "company_1" => {"path" => "/co1", "timeout" => 5},
99
- # "company_2" => {"path" => "/co2"}
100
- # },
101
- # "page_size" => 20
102
- # }
103
-
104
- # Limit the nesting depth of the returned hash to one level
105
- SuperSettings.structured(max_depth: 1)
106
- # {
107
- # "vendors.company_1.path => "/co1",
108
- # "vendors.company_1.timeout" => 5,
109
- # "vendors.company_2.path" => "/co2",
110
- # "page_size" => 20
111
- # }
112
- ```
113
58
 
114
59
  #### Defaults
115
60
 
@@ -132,13 +77,38 @@ SuperSettings.enabled?("enabled_users.#{id}")
132
77
 
133
78
  # GOOD: use an array if there are a limited number of values
134
79
  SuperSettings.array("enabled_users", []).include?(id)
135
-
136
- # GOOD: use a hash if you need to scale to any number of values
137
- SuperSettings.structured("enabled_users", {})["id"]
138
80
  ```
139
81
 
140
82
  The cache will scale without issue to handle hundreds of settings. However, you should avoid creating thousands of settings. Because all settings are read into memory, having too many settings records can lead to performance or memory issues.
141
83
 
84
+ #### Request Context
85
+
86
+ You can ensure that settings won't change in a block of code by surrounding it with a `SuperSettings.context` block. Inside a `context` block, a setting will always return the same value. This can prevent race conditions where you code may branch based on a setting value.
87
+
88
+ ```ruby
89
+ # This code could be unsafe since the value of the "threshold" setting could
90
+ # change after the if statement is checked.
91
+ if SuperSettings.integer("threshold") > 0
92
+ do_something(SuperSettings.integer("threshold"))
93
+ end
94
+
95
+ # With a context block, the value for the "threshold setting will always
96
+ # return the same value
97
+ SuperSettings.context do
98
+ if SuperSettings.integer("threshold") > 0
99
+ do_something(SuperSettings.integer("threshold"))
100
+ end
101
+ end
102
+ ```
103
+
104
+ It's a good idea to add a `context` block around your main unit of work:
105
+
106
+ - Rack application: add `SuperSettings::Context::RackMiddleware` to your middleware stack
107
+ - Sidekiq: add `SuperSettings::Context::SidekiqMiddleware` to your server middleware
108
+ - ActiveJob: add an `around_perform` callback that calls `SuperSettings.context`
109
+
110
+ In a Rails application all of these will be done automatically.
111
+
142
112
  ### Data Model
143
113
 
144
114
  Each setting has a key, value, value type, and optional description. The key must be unique. The value type can be one of "string", "integer", "float", "boolean", "datetime", or "array". The array value type will always return an array of strings.
@@ -149,6 +119,16 @@ It is not possible to store an empty string in a setting; empty strings will be
149
119
 
150
120
  A history of all settings changes is updated every time the value is changed in the `histories` association. You can also record who made the changes.
151
121
 
122
+ #### Callbacks
123
+
124
+ You can define custom callbacks on the `SuperSettings::Setting` model that will be called whenever a setting is changed. For example, if you needed to log all changes to you settings in your application logs, you could do something like this:
125
+
126
+ ```ruby
127
+ SuperSettings::Setting.after_save do |setting|
128
+ Application.logger.info("Setting #{setting.key} changed: #{setting.changes.inspect})
129
+ end
130
+ ```
131
+
152
132
  #### Storage Engines
153
133
 
154
134
  This gem abstracts out the storage engine and can support multiple storage mechanisms. It has built in support for ActiveRecord, Redis, and HTTP storage.
@@ -220,7 +200,7 @@ The gem ships with a Rails engine that provides easy integration with a Rails ap
220
200
  The default storage engine for a Rails application will be the ActiveRecord storage. You need to install the database migrations first with:
221
201
 
222
202
  ```bash
223
- rails app:super_settings:install:migrations
203
+ rails super_settings:install:migrations
224
204
  ```
225
205
 
226
206
  You also need to mount the engine routes in your application's `config/routes.rb` file. The routes can be mounted under any prefix you'd like.
@@ -270,10 +250,15 @@ SuperSettings.configure do |config|
270
250
  end
271
251
  end
272
252
 
273
- # Define a method that returns the value that will be stored in the settings history in
253
+ # Define a block that returns the value that will be stored in the settings history in
274
254
  # the `changed_by` column.
275
255
  config.controller.define_changed_by do
276
- current_user.name
256
+ current_user.id
257
+ end
258
+
259
+ # Define a block that determines how to display the `changed_by`` value in the setting history.
260
+ config.model.define_changed_by_display do |changed_by_id|
261
+ User.find_by(id: changed_by_id)&.name
277
262
  end
278
263
 
279
264
  # You can define the storage engine for the model. This can be either done either with a Class
@@ -284,6 +269,11 @@ SuperSettings.configure do |config|
284
269
  # You can also specify a cache implementation to use to cache the last updated timestamp
285
270
  # for model changes. By default this will use `Rails.cache`.
286
271
  # config.model.cache = Rails.cache
272
+
273
+ # You can define after_save callbacks for the model.
274
+ # config.model.after_save do |setting|
275
+ # Rail.logger.info("Setting #{setting.key} changed to #{setting.value.inspect}")
276
+ # end
287
277
  end
288
278
  ```
289
279
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1.rc2
1
+ 1.0.0
@@ -19,7 +19,7 @@ module SuperSettings
19
19
  def super_settings_application_header
20
20
  config = Configuration.instance.controller
21
21
  content = "#{super_settings_application_name} Settings"
22
- if config.application_logo.present?
22
+ if Coerce.present?(config.application_logo)
23
23
  content = image_tag(config.application_logo, alt: "").concat(content)
24
24
  end
25
25
  if config.application_link
@@ -35,13 +35,19 @@
35
35
 
36
36
  params = options.params
37
37
  let queryParams = null;
38
- const fetchOptions = {credentials: "same-origin"};
38
+ const headers = new Headers();
39
+ const fetchOptions = {credentials: "same-origin", headers: headers};
39
40
  const accessToken = window.sessionStorage.getItem("super_settings_access_token");
40
- const headers = {"Accept": "application/json"};
41
+
42
+ headers.set("Accept", "application/json");
41
43
  if (accessToken) {
42
- headers["Authorization"] = "Bearer " + accessToken;
44
+ headers.set("Authorization", "Bearer " + accessToken);
43
45
  }
44
- Object.assign(headers, SuperSettingsAPI.headers);
46
+ Object.entries(SuperSettingsAPI.headers).forEach(function(entry) {
47
+ const [key, value] = entry;
48
+ headers.set(key, value);
49
+ });
50
+
45
51
  if (method === "POST") {
46
52
  queryParams = Object.assign({}, SuperSettingsAPI.queryParams);
47
53
  csrfParam = document.querySelector("meta[name=csrf-param]");
@@ -52,11 +58,10 @@
52
58
  }
53
59
  fetchOptions["method"] = "POST";
54
60
  fetchOptions["body"] = JSON.stringify(params);
55
- headers["Content-Type"] = "application/json";
61
+ headers.set("Content-Type", "application/json");
56
62
  } else {
57
63
  queryParams = Object.assign({}, SuperSettingsAPI.queryParams, params);
58
64
  }
59
- fetchOptions["headers"] = new Headers(headers);
60
65
  const url = apiURL(path, queryParams);
61
66
 
62
67
  fetch(url, fetchOptions)
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  module SuperSettings
4
6
  # Utility functions for coercing values to other data types.
5
7
  class Coerce
6
8
  # rubocop:disable Lint/BooleanSymbol
7
- FALSE_VALUES = [
9
+ FALSE_VALUES = Set.new([
8
10
  false, 0,
9
11
  "0", :"0",
10
12
  "f", :f,
@@ -13,7 +15,7 @@ module SuperSettings
13
15
  "FALSE", :FALSE,
14
16
  "off", :off,
15
17
  "OFF", :OFF
16
- ].to_set.freeze
18
+ ]).freeze
17
19
  # rubocop:enable Lint/BooleanSymbol
18
20
 
19
21
  class << self
@@ -126,6 +126,39 @@ module SuperSettings
126
126
  end
127
127
  end
128
128
  end
129
+
130
+ # Add an after_save callback to the setting model. The block will be called with the
131
+ # setting object after it is saved.
132
+ #
133
+ # @yieldparam setting [SuperSettings::Setting]
134
+ def after_save(&block)
135
+ after_save_blocks << block
136
+ end
137
+
138
+ # @return [Array<Proc>] The after_save block
139
+ # @api private
140
+ def after_save_blocks
141
+ @after_save_blocks ||= []
142
+ end
143
+
144
+ # Define how the changed_by attibute on the setting history will be displayed. The block
145
+ # will be called with the changed_by attribute and should return a string to display.
146
+ # The block will not be called if the changed_by attribute is nil.
147
+ #
148
+ # @example
149
+ # define_changed_by_display { |changed_by| User.find_by(id: changed_by)&.name }
150
+ #
151
+ # @yield Block of code to call on the controller at request time
152
+ # @yieldparam changed_by [String] The value of the changed_by attribute
153
+ def define_changed_by_display(&block)
154
+ @changed_by_display = block
155
+ end
156
+
157
+ # @return [Proc, nil] The block to call to display the changed_by attribute in setting history
158
+ # @api private
159
+ def changed_by_display
160
+ @changed_by_display if defined?(@changed_by_display)
161
+ end
129
162
  end
130
163
 
131
164
  # Return the model specific configuration object.
@@ -141,6 +174,7 @@ module SuperSettings
141
174
  def initialize
142
175
  @model = Model.new
143
176
  @controller = Controller.new
177
+ @deferred_configs = []
144
178
  end
145
179
 
146
180
  # Defer the execution of a block that will be yielded to with the config object. This
@@ -149,15 +183,16 @@ module SuperSettings
149
183
  #
150
184
  # @api private
151
185
  def defer(&block)
152
- @block = block
186
+ @deferred_configs << block
153
187
  end
154
188
 
155
189
  # Call the block deferred during initialization.
156
190
  #
157
191
  # @api private
158
192
  def call
159
- @block&.call(self)
160
- @block = nil
193
+ while (block = @deferred_configs.shift)
194
+ block&.call(self)
195
+ end
161
196
  end
162
197
  end
163
198
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperSettings
4
+ module Context
5
+ # Rack middleware you can use to add a context to your requests so that
6
+ # settings are not changed during request execution.
7
+ #
8
+ # This middleware is automatically added to Rails applications.
9
+ class RackMiddleware
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call(env)
15
+ SuperSettings.context do
16
+ @app.call(env)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperSettings
4
+ module Context
5
+ # Sidekiq middleware you can use to add a context to your jobs so that
6
+ # settings are not changed during job execution.
7
+ #
8
+ # @example
9
+ # require "super_settings/context/sidekiq_middleware"
10
+ #
11
+ # Sidekiq.configure_server do |config|
12
+ # config.server_middleware do |chain|
13
+ # chain.add SuperSettings::Context::SidekiqMiddleware
14
+ # end
15
+ # end
16
+ #
17
+ # You can disable the context by setting the `super_settings_context` key
18
+ # to `false` in the job payload.
19
+ #
20
+ # @example
21
+ # class MyWorker
22
+ # include Sidekiq::Worker
23
+ # sidekiq_options super_settings_context: false
24
+ # end
25
+ class SidekiqMiddleware
26
+ if defined?(Sidekiq::ServerMiddleware)
27
+ include Sidekiq::ServerMiddleware
28
+ end
29
+
30
+ def call(job_instance, job_payload, queue)
31
+ if job_payload["super_settings_context"] == false
32
+ yield
33
+ else
34
+ SuperSettings.context do
35
+ yield
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperSettings
4
+ module Context
5
+ end
6
+ end
@@ -7,6 +7,26 @@ module SuperSettings
7
7
  class Engine < Rails::Engine
8
8
  isolate_namespace ::SuperSettings
9
9
 
10
+ initializer("SuperSettings") do
11
+ Rails.configuration.middleware.unshift(SuperSettings::Context::RackMiddleware)
12
+
13
+ if defined?(ActiveJob::Base.around_perform)
14
+ ActiveJob::Base.around_perform do |job, block|
15
+ SuperSettings.context(&block)
16
+ end
17
+ end
18
+
19
+ if defined?(Sidekiq.server?) && Sidekiq.server?
20
+ require_relative "context/sidekiq_middleware"
21
+
22
+ Sidekiq.configure_server do |sidekiq_config|
23
+ sidekiq_config.server_middleware do |chain|
24
+ chain.prepend(SuperSettings::Context::SidekiqMiddleware)
25
+ end
26
+ end
27
+ end
28
+ end
29
+
10
30
  config.after_initialize do
11
31
  # Call the deferred initialization block.
12
32
  configuration = Configuration.instance
@@ -46,6 +66,10 @@ module SuperSettings
46
66
  Setting.cache = (configuration.model.cache || Rails.cache)
47
67
  Setting.storage = configuration.model.storage_class
48
68
 
69
+ configuration.model.after_save_blocks.each do |block|
70
+ Setting.after_save(&block)
71
+ end
72
+
49
73
  if !SuperSettings.loaded?
50
74
  begin
51
75
  SuperSettings.load_settings
@@ -14,21 +14,21 @@ module SuperSettings
14
14
  !!(defined?(@deleted) && @deleted)
15
15
  end
16
16
 
17
- # The method could be overriden to change how the changed_by attribute is displayed.
18
- # For instance, you could store a user id in the changed_by column and add an association
19
- # on this model.
17
+ # The display value for the changed_by attribute. This method can be overridden
18
+ # in the configuration by calling `model.define_changed_by_display` with the block to use
19
+ # to get the display value for the changed_by attribute. The default value is
20
+ # the changed_by attribute itself.
20
21
  #
21
- # @example
22
- # class SuperSettings::HistoryItem
23
- # def changed_by_display
24
- # user = User.find_by(id: changed_by) if changed_by
25
- # user ? user.name : changed_by
26
- # end
27
- # end
28
- #
29
- # @return [String]
22
+ # @return [String, nil]
30
23
  def changed_by_display
31
- changed_by
24
+ return changed_by if changed_by.nil?
25
+
26
+ display_proc = Configuration.instance.model.changed_by_display
27
+ if display_proc && !changed_by.nil?
28
+ display_proc.call(changed_by) || changed_by
29
+ else
30
+ changed_by
31
+ end
32
32
  end
33
33
  end
34
34
  end
@@ -54,7 +54,6 @@ module SuperSettings
54
54
  @lock.synchronize do
55
55
  # For case where one thread could be iterating over the cache while it's updated causing an error
56
56
  @cache = @cache.merge(key => value).freeze
57
- @hashes = {}
58
57
  end
59
58
  end
60
59
  end
@@ -64,53 +63,6 @@ module SuperSettings
64
63
  value
65
64
  end
66
65
 
67
- # Return the setting as structured data. The keys will be split by the specified delimiter
68
- # to create a nested hash.
69
- #
70
- # @example
71
- # Setting with key "a.b.c" and value 1 becomes
72
- #
73
- # {
74
- # "a" => {
75
- # "b" => {
76
- # "c" => 1
77
- # }
78
- # }
79
- # }
80
- #
81
- # See SuperSettings.structured for more details.
82
- def structured(key = nil, delimiter: ".", max_depth: nil)
83
- key = key.to_s
84
- cache_key = [key, delimiter, max_depth]
85
- cached_value = @hashes[cache_key]
86
- return cached_value if cached_value
87
-
88
- flattened = to_h
89
- root_key = ""
90
- if Coerce.present?(key)
91
- root_key = "#{key}#{delimiter}"
92
- reduced_hash = {}
93
- flattened.each do |k, v|
94
- if k.start_with?(root_key)
95
- reduced_hash[k[root_key.length, k.length]] = v
96
- end
97
- end
98
- flattened = reduced_hash
99
- end
100
-
101
- structured_hash = {}
102
- flattened.each do |key, value|
103
- set_nested_hash_value(structured_hash, key, value, 0, delimiter: delimiter, max_depth: max_depth)
104
- end
105
-
106
- deep_freeze_hash(structured_hash)
107
- @lock.synchronize do
108
- @hashes[cache_key] = structured_hash
109
- end
110
-
111
- structured_hash
112
- end
113
-
114
66
  # Check if the cache includes a key. Note that this will return true if you have tried
115
67
  # to fetch a non-existent key since the cache will store that as undefined. This method
116
68
  # is provided for testing purposes.
@@ -217,7 +169,6 @@ module SuperSettings
217
169
  def reset
218
170
  @lock.synchronize do
219
171
  @cache = {}.freeze
220
- @hashes = {}
221
172
  @last_refreshed = nil
222
173
  @next_check_at = Time.now + @refresh_interval
223
174
  @refreshing = false
@@ -240,7 +191,6 @@ module SuperSettings
240
191
  return if Coerce.blank?(setting.key)
241
192
  @lock.synchronize do
242
193
  @cache = @cache.merge(setting.key => setting.value)
243
- @hashes = {}
244
194
  end
245
195
  end
246
196
 
@@ -277,7 +227,7 @@ module SuperSettings
277
227
  end
278
228
  load_settings(Setting.storage.load_asynchronous?)
279
229
  elsif Time.now >= @next_check_at
280
- refresh
230
+ refresh(Setting.storage.load_asynchronous?)
281
231
  end
282
232
  end
283
233
 
@@ -287,7 +237,6 @@ module SuperSettings
287
237
  @last_refreshed = refreshed_at_time
288
238
  @refreshing = false
289
239
  @cache = block.call.freeze
290
- @hashes = {}
291
240
  end
292
241
  end
293
242
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rack"
4
-
5
3
  module SuperSettings
6
4
  # Rack middleware for serving the REST API. See SuperSettings::RestAPI for more details on usage.
7
5
  #
@@ -39,6 +37,13 @@ module SuperSettings
39
37
  # end
40
38
  # end
41
39
  def initialize(app = nil, path_prefix = "/", &block)
40
+ # Requiring rack here so that the gem does not have a hard dependency on it.
41
+ begin
42
+ require "rack"
43
+ rescue LoadError
44
+ raise LoadError, "SuperSettings::RackApplication requires the rack gem"
45
+ end
46
+
42
47
  @app = app
43
48
  @path_prefix = path_prefix.to_s.chomp("/")
44
49
  instance_eval(&block) if block
@@ -54,6 +54,21 @@ module SuperSettings
54
54
  end
55
55
  end
56
56
 
57
+ # Add a block of code that will be called when a setting is saved. The block will be
58
+ # called with a Setting object. The object will have been saved, but the `changes`
59
+ # hash will still be set indicating what was changed. You can define multiple after_save blocks.
60
+ #
61
+ # @yieldparam setting [SuperSetting::Setting]
62
+ def after_save(&block)
63
+ after_save_blocks << block
64
+ end
65
+
66
+ # @return [Array<Proc>] Blocks to be called after a setting is saved.
67
+ # @api private
68
+ def after_save_blocks
69
+ @after_save_blocks ||= []
70
+ end
71
+
57
72
  # Create a new setting with the specified attributes.
58
73
  #
59
74
  # @param attributes [Hash] hash of attribute names and values
@@ -433,6 +448,7 @@ module SuperSettings
433
448
 
434
449
  begin
435
450
  self.class.clear_last_updated_cache
451
+ call_after_save_callbacks
436
452
  ensure
437
453
  clear_changes
438
454
  end
@@ -508,6 +524,15 @@ module SuperSettings
508
524
  as_json.to_json(options)
509
525
  end
510
526
 
527
+ # Get hash of attribute changes. The hash keys are the names of attributes that
528
+ # have changed and the values are an array with [old value, new value]. The keys
529
+ # will be one of key, raw_value, value_type, description, deleted, created_at, or updated_at.
530
+ #
531
+ # @return [Hash<String, Array>]
532
+ def changes
533
+ @changes.dup
534
+ end
535
+
511
536
  private
512
537
 
513
538
  # Coerce a value for the appropriate value type.
@@ -618,5 +643,11 @@ module SuperSettings
618
643
  end
619
644
  attribute_errors << "#{attribute.tr("_", " ")} #{message}"
620
645
  end
646
+
647
+ def call_after_save_callbacks
648
+ self.class.after_save_blocks.each do |block|
649
+ block.call(self)
650
+ end
651
+ end
621
652
  end
622
653
  end
@@ -3,6 +3,8 @@
3
3
  require_relative "super_settings/application"
4
4
  require_relative "super_settings/coerce"
5
5
  require_relative "super_settings/configuration"
6
+ require_relative "super_settings/context"
7
+ require_relative "super_settings/context/rack_middleware"
6
8
  require_relative "super_settings/local_cache"
7
9
  require_relative "super_settings/rest_api"
8
10
  require_relative "super_settings/rack_application"
@@ -28,7 +30,7 @@ module SuperSettings
28
30
  # @param default [String] value to return if the setting value is nil
29
31
  # @return [String]
30
32
  def get(key, default = nil)
31
- val = local_cache[key]
33
+ val = context_setting(key)
32
34
  val.nil? ? default : val.to_s
33
35
  end
34
36
 
@@ -46,7 +48,7 @@ module SuperSettings
46
48
  # @param default [Integer] value to return if the setting value is nil
47
49
  # @return [Integer]
48
50
  def integer(key, default = nil)
49
- val = local_cache[key]
51
+ val = context_setting(key)
50
52
  (val.nil? ? default : val)&.to_i
51
53
  end
52
54
 
@@ -56,7 +58,7 @@ module SuperSettings
56
58
  # @param default [Numeric] value to return if the setting value is nil
57
59
  # @return [Float]
58
60
  def float(key, default = nil)
59
- val = local_cache[key]
61
+ val = context_setting(key)
60
62
  (val.nil? ? default : val)&.to_f
61
63
  end
62
64
 
@@ -66,7 +68,7 @@ module SuperSettings
66
68
  # @param default [Boolean] value to return if the setting value is nil
67
69
  # @return [Boolean]
68
70
  def enabled?(key, default = false)
69
- val = local_cache[key]
71
+ val = context_setting(key)
70
72
  Coerce.boolean(val.nil? ? default : val)
71
73
  end
72
74
 
@@ -85,7 +87,7 @@ module SuperSettings
85
87
  # @param default [Time] value to return if the setting value is nil
86
88
  # @return [Time]
87
89
  def datetime(key, default = nil)
88
- val = local_cache[key]
90
+ val = context_setting(key)
89
91
  Coerce.time(val.nil? ? default : val)
90
92
  end
91
93
 
@@ -95,36 +97,12 @@ module SuperSettings
95
97
  # @param default [Array] value to return if the setting value is nil
96
98
  # @return [Array]
97
99
  def array(key, default = nil)
98
- val = local_cache[key]
100
+ val = context_setting(key)
99
101
  val = default if val.nil?
100
102
  return nil if val.nil?
101
103
  Array(val).collect { |v| v&.to_s }
102
104
  end
103
105
 
104
- # Get setting values cast to a hash. This method can be used to cast the flat setting key/value
105
- # store into a structured data store. It uses a delimiter to define how keys are nested which
106
- # defaults to a dot.
107
- #
108
- # If, for example, you have three keys in you settings +A.B1.C1 = 1+, +A.B1.C2 = 2+, and +A.B2.C3 = 3+, the
109
- # nested structure will be:
110
- #
111
- # +{"A" => {"B1" => {"C1" => 1, "C2" => 2}, "B2" => {"C3" => 3}}}+
112
- #
113
- # This whole hash would be returned if you called +hash+ without any key. If you called it with the
114
- # key "A.B1", it would return
115
- #
116
- # +{"C1" => 1, "C2" => 2}+
117
- #
118
- # @param key [String, Symbol] the prefix patter to fetch keys for; default to returning all settings
119
- # @param default [Hash] value to return if the setting value is nil
120
- # @param delimiter [String] the delimiter to use to define nested keys in the hash; defaults to "."
121
- # @return [Hash]
122
- def structured(key = nil, default = nil, delimiter: ".", max_depth: nil)
123
- value = local_cache.structured(key, delimiter: delimiter, max_depth: max_depth)
124
- return (default || {}) if value.empty?
125
- value
126
- end
127
-
128
106
  # Create settings and update the local cache with the values. If a block is given, then the
129
107
  # value will be reverted at the end of the block. This method can be used in tests when you
130
108
  # need to inject a specific value into your settings.
@@ -148,6 +126,7 @@ module SuperSettings
148
126
  setting.save!
149
127
  local_cache.load_settings unless local_cache.loaded?
150
128
  local_cache.update_setting(setting)
129
+
151
130
  if block_given?
152
131
  yield
153
132
  end
@@ -161,6 +140,19 @@ module SuperSettings
161
140
  end
162
141
  end
163
142
 
143
+ # Define a block where settings will remain unchanged. This is useful to
144
+ # prevent settings from changing while you are in the middle of a block of
145
+ # code that depends on the settings.
146
+ def context(&block)
147
+ reset_context = Thread.current[:super_settings_context].nil?
148
+ begin
149
+ Thread.current[:super_settings_context] ||= {}
150
+ yield
151
+ ensure
152
+ Thread.current[:super_settings_context] = nil if reset_context
153
+ end
154
+ end
155
+
164
156
  # Load the settings from the database into the in memory cache.
165
157
  #
166
158
  # @return [void]
@@ -230,5 +222,22 @@ module SuperSettings
230
222
  def local_cache
231
223
  @local_cache ||= LocalCache.new(refresh_interval: DEFAULT_REFRESH_INTERVAL)
232
224
  end
225
+
226
+ def current_context
227
+ Thread.current[:super_settings_context]
228
+ end
229
+
230
+ def context_setting(key)
231
+ key = key.to_s
232
+ context = current_context
233
+ if context
234
+ unless context.include?(key)
235
+ context[key] = local_cache[key]
236
+ end
237
+ context[key]
238
+ else
239
+ local_cache[key]
240
+ end
241
+ end
233
242
  end
234
243
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: super_settings
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1.rc2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-06-01 00:00:00.000000000 Z
11
+ date: 2023-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -56,6 +56,9 @@ files:
56
56
  - lib/super_settings/attributes.rb
57
57
  - lib/super_settings/coerce.rb
58
58
  - lib/super_settings/configuration.rb
59
+ - lib/super_settings/context.rb
60
+ - lib/super_settings/context/rack_middleware.rb
61
+ - lib/super_settings/context/sidekiq_middleware.rb
59
62
  - lib/super_settings/controller_actions.rb
60
63
  - lib/super_settings/engine.rb
61
64
  - lib/super_settings/history_item.rb
@@ -85,11 +88,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
85
88
  version: '2.5'
86
89
  required_rubygems_version: !ruby/object:Gem::Requirement
87
90
  requirements:
88
- - - ">"
91
+ - - ">="
89
92
  - !ruby/object:Gem::Version
90
- version: 1.3.1
93
+ version: '0'
91
94
  requirements: []
92
- rubygems_version: 3.2.22
95
+ rubygems_version: 3.4.12
93
96
  signing_key:
94
97
  specification_version: 4
95
98
  summary: Fast access runtime settings for a Rails application with an included UI