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 +4 -4
- data/README.md +55 -65
- data/VERSION +1 -1
- data/app/helpers/super_settings/settings_helper.rb +1 -1
- data/lib/super_settings/application/api.js +11 -6
- data/lib/super_settings/coerce.rb +4 -2
- data/lib/super_settings/configuration.rb +38 -3
- data/lib/super_settings/context/rack_middleware.rb +21 -0
- data/lib/super_settings/context/sidekiq_middleware.rb +41 -0
- data/lib/super_settings/context.rb +6 -0
- data/lib/super_settings/engine.rb +24 -0
- data/lib/super_settings/history_item.rb +13 -13
- data/lib/super_settings/local_cache.rb +1 -52
- data/lib/super_settings/rack_application.rb +7 -2
- data/lib/super_settings/setting.rb +31 -0
- data/lib/super_settings.rb +39 -30
- metadata +8 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b4e4df83235545b59d31ec20206e265ab19bbe0d4a21de81405bdc7fab892c6
|
4
|
+
data.tar.gz: 8de1f0d4f30714f93f11e1c5b7f004232793676877f2f0396d3fe757cbd2629e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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.
|
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
|
+
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
|
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
|
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
|
-
|
41
|
+
|
42
|
+
headers.set("Accept", "application/json");
|
41
43
|
if (accessToken) {
|
42
|
-
headers
|
44
|
+
headers.set("Authorization", "Bearer " + accessToken);
|
43
45
|
}
|
44
|
-
Object.
|
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
|
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
|
-
].
|
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
|
-
@
|
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
|
-
@
|
160
|
-
|
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
|
@@ -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
|
18
|
-
#
|
19
|
-
#
|
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
|
-
# @
|
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
|
data/lib/super_settings.rb
CHANGED
@@ -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 =
|
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 =
|
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 =
|
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 =
|
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 =
|
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 =
|
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
|
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-
|
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:
|
93
|
+
version: '0'
|
91
94
|
requirements: []
|
92
|
-
rubygems_version: 3.
|
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
|