super_settings 2.4.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a409f83b0e25f1a6822167604061a205a9f2ba9144776ae12c917a28934af8aa
4
- data.tar.gz: 7b58a037a30031cbdadaf2afbfcaf37faa0c6d48dee38a6d0a61b7559d99806a
3
+ metadata.gz: e851110a7e8ebb10348b536a94121aab3c6657ef7c8115f3de6fd7adcd5f1a3b
4
+ data.tar.gz: 3aa309b399ee775cf5427d5e14a0b3f143e3ba0b441da4bcf41708f7806e2c17
5
5
  SHA512:
6
- metadata.gz: 797087d1072b5f2dd88fee2f361ad2e4dc53d44145ee57f2a17f0e31fc0fb88c99ddcc73cac8d5f3e39ad5a00d56123748c544fb724e1ac8ef2945d8a004ad19
7
- data.tar.gz: d586e20dac006fde52f5da754acf7c154a891bf82e4682c7b90b5a432c7261f0a60caaa951783c4586dc960a1d31f3865680b8732142cd0ccb8e26acac80a5ee
6
+ metadata.gz: 79ba25dcd135459a8a5332aac5c4807fb04a183e573ae761ecd76b4b079d1772895fc2917ac2fe2c26e723193020eb43dbbb50d672dfb4f9d5e89ecd4d3a3c29
7
+ data.tar.gz: 6190dca1e68e103c982696ab7075a20e39a32e4ab5cf68d213cb679e552c475feb61d4ed111d151392a1f37010c42e3b69810ebeb7d6de5ee9a46239ca1ffbdf
data/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 2.4.1
8
+
9
+ ### Added
10
+
11
+ - Incoming links to the web UI now support specifying a description for the setting being edited by passing `description=description_text` in the URL hash. For example, `#edit=port&type=integer&description=Port%20number%20for%20the%20server` will open the setting with the key `port` for editing as an integer and pre-fill the description field.
12
+
7
13
  ## 2.4.0
8
14
 
9
15
  ### Added
data/README.md CHANGED
@@ -181,10 +181,12 @@ Then go to http://localhost:3000/settings in your browser.
181
181
 
182
182
  It is not required to use the bundled Web UI. You can implement your own UI using the `SuperSettings::Setting` model.
183
183
 
184
- You can link directly to editing a setting by passing `#edit=key` in the URL hash. This will open the Web UI with the setting with the key `key` selected for editing. You can also `type` in the URL hash to specify the value type for the setting. For example, `#edit=port&type=integer` will open the setting with the key `port` for editing as an integer.
184
+ You can link directly to editing a setting by passing `#edit=key` in the URL hash. This will open the Web UI with the setting with the key `key` selected for editing.
185
+
186
+ You can also add `type` and `description` as parameters in the URL hash to specify the values to pre-fill for the setting if it doesn't already exist. For example, `#edit=port&type=integer&description=Server+port+number` will open the setting with the key `port` for editing as an integer with the description filled in if it doesn't already exist. If the setting does exist, then the type and description will be ignored.
185
187
 
186
188
  ```html
187
- <a href="/settings#edit=port&type=integer">Edit Port Setting</a>
189
+ <a href="/settings#edit=port&type=integer&description=Server+port+number">Edit Port Setting</a>
188
190
  ```
189
191
 
190
192
  #### REST API
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.4.0
1
+ 2.4.1
@@ -76,6 +76,10 @@ module SuperSettings
76
76
 
77
77
  # Render an image tag for one of the SVG images in the images directory. If the :color option
78
78
  # is specified, it will be applied to the SVG image.
79
+ #
80
+ # @param name [String] the name of the icon to render
81
+ # @param options [Hash] options for the icon (style, color, etc.)
82
+ # @return [String] the HTML for the icon
79
83
  def icon_image(name, options = {})
80
84
  svg = ICON_SVG[name.to_s]
81
85
  style = (options[:style] || {})
@@ -91,6 +95,16 @@ module SuperSettings
91
95
  end
92
96
 
93
97
  # Render an icon image as a link tag.
98
+ #
99
+ # @param icon [String] the name of the icon to render
100
+ # @param title [String] the title/tooltip for the button
101
+ # @param color [String] the color for the icon
102
+ # @param js_class [String] CSS class for JavaScript behavior
103
+ # @param url [String] the URL for the link (optional)
104
+ # @param disabled [Boolean] whether the button is disabled
105
+ # @param style [Hash] CSS styles for the icon
106
+ # @param link_style [String] CSS styles for the link
107
+ # @return [String] the HTML for the icon button
94
108
  def icon_button(icon, title:, color:, js_class:, url: nil, disabled: false, style: {}, link_style: nil)
95
109
  url = "#" if Coerce.blank?(url)
96
110
  image = icon_image(icon, alt: title, style: ICON_BUTTON_STYLE.merge(style).merge(color: color))
@@ -43,7 +43,7 @@
43
43
  </div>
44
44
  </form>
45
45
 
46
- <div id="super-settings-modal" class="super-settings-modal js-close-modal" aria-hidden="true" aria-role="dialog">
46
+ <div id="super-settings-modal" class="super-settings-modal js-close-modal" aria-hidden="true" role="dialog">
47
47
  <div class="super-settings-modal-dialog">
48
48
  <button type="button" title="Close Dialog" class="super-settings-modal-close super-settings-btn-no-chrome js-close-modal">&times;</button>
49
49
  <div class="super-settings-modal-content">
@@ -4,7 +4,7 @@
4
4
  <title><%= application_name %> Settings</title>
5
5
  <meta charset="utf-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <meta name="pinterest" content="nopin" />
7
+ <meta name="pinterest" content="nopin">
8
8
  <meta name="format-detection" content="telephone=no email=no date=no address=no">
9
9
  <meta name="robots" content="noindex, nofollow">
10
10
  <meta name="referrer" content="no-referrer-when-downgrade">
@@ -232,7 +232,7 @@
232
232
  }
233
233
 
234
234
  // Create a card with form elements for creating a new setting.
235
- function newSettingCard(key, valueType) {
235
+ function newSettingCard(key, valueType, description) {
236
236
  if (!key) {
237
237
  key = "";
238
238
  }
@@ -240,7 +240,7 @@
240
240
  valueType = "string";
241
241
  }
242
242
  const randomId = "new" + Math.floor((Math.random() * 0xFFFFFFFFFFFFFF)).toString(16);
243
- const setting = {id: randomId, key: key, value: "", value_type: valueType, new_record: true}
243
+ const setting = {id: randomId, key: key, value: "", value_type: valueType, description: description, new_record: true}
244
244
  card = editSettingCard(setting);
245
245
  return card;
246
246
  }
@@ -489,8 +489,8 @@
489
489
  }
490
490
 
491
491
  // Add a new setting.
492
- function addSetting(key, valueType) {
493
- const card = addCardToContainer(newSettingCard(key, valueType));
492
+ function addSetting(key, valueType, description) {
493
+ const card = addCardToContainer(newSettingCard(key, valueType, description));
494
494
  card.querySelector(".super-settings-card-key input").focus();
495
495
  }
496
496
 
@@ -722,7 +722,7 @@
722
722
  hashParams.split('&').forEach(function(param) {
723
723
  const [key, value] = param.split('=', 2);
724
724
  if (key && value) {
725
- params[decodeURIComponent(key)] = decodeURIComponent(value);
725
+ params[decodeURIComponent(key)] = decodeURIComponent(value.replace(/\+/g, ' '));
726
726
  }
727
727
  });
728
728
 
@@ -886,7 +886,7 @@
886
886
  if (setting) {
887
887
  editSetting(setting);
888
888
  } else {
889
- addSetting(hashParams.edit, hashParams.type);
889
+ addSetting(hashParams.edit, hashParams.type, hashParams.description);
890
890
  }
891
891
  }
892
892
  enableSaveButton();
@@ -29,7 +29,7 @@ module SuperSettings
29
29
 
30
30
  # Render the web UI application HTML.
31
31
  #
32
- # @return [void]
32
+ # @return [String] the rendered HTML
33
33
  def render
34
34
  template = ERB.new(File.read(File.expand_path(File.join("application", "index.html.erb"), __dir__)))
35
35
  html = template.result(binding)
@@ -5,6 +5,7 @@ require "set"
5
5
  module SuperSettings
6
6
  # Utility functions for coercing values to other data types.
7
7
  class Coerce
8
+ # Set of values that should be considered false when converting to boolean.
8
9
  # rubocop:disable Lint/BooleanSymbol
9
10
  FALSE_VALUES = Set.new([
10
11
  "0", :"0",
@@ -2,28 +2,51 @@
2
2
 
3
3
  module SuperSettings
4
4
  module Context
5
+ # Current context for maintaining consistent setting values and random numbers
6
+ # within a specific execution context.
5
7
  class Current
6
8
  def initialize
7
9
  @context = {}
8
10
  @seed = nil
9
11
  end
10
12
 
13
+ # Check if the context includes a specific key.
14
+ #
15
+ # @param key [String] the key to check
16
+ # @return [Boolean] true if the key exists in the context
11
17
  def include?(key)
12
18
  @context.include?(key)
13
19
  end
14
20
 
21
+ # Get a value from the context.
22
+ #
23
+ # @param key [String] the key to retrieve
24
+ # @return [Object] the value associated with the key
15
25
  def [](key)
16
26
  @context[key]
17
27
  end
18
28
 
29
+ # Set a value in the context.
30
+ #
31
+ # @param key [String] the key to set
32
+ # @param value [Object] the value to store
33
+ # @return [Object] the stored value
19
34
  def []=(key, value)
20
35
  @context[key] = value
21
36
  end
22
37
 
38
+ # Delete a value from the context.
39
+ #
40
+ # @param key [String] the key to delete
41
+ # @return [Object] the deleted value
23
42
  def delete(key)
24
43
  @context.delete(key)
25
44
  end
26
45
 
46
+ # Generate a consistent random number for this context.
47
+ #
48
+ # @param max [Integer, Float, Range] the maximum value or range
49
+ # @return [Integer, Float] the random number
27
50
  def rand(max = nil)
28
51
  @seed ||= Random.new_seed
29
52
  Random.new(@seed).rand(max || 1.0)
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SuperSettings
4
+ # Context classes for maintaining consistent state during execution blocks.
5
+ # These contexts ensure that settings and random values remain constant
6
+ # within a specific execution scope.
4
7
  module Context
5
8
  autoload :Current, File.join(__dir__, "context/current")
6
9
  autoload :RackMiddleware, File.join(__dir__, "context/rack_middleware")
@@ -60,6 +60,7 @@ module SuperSettings
60
60
  end
61
61
 
62
62
  return nil if value == NOT_DEFINED
63
+
63
64
  value
64
65
  end
65
66
 
@@ -105,11 +106,15 @@ module SuperSettings
105
106
  end
106
107
 
107
108
  # Load all the settings from the database into the cache.
109
+ #
110
+ # @param asynchronous [Boolean] whether to load settings in a background thread
111
+ # @return [void]
108
112
  def load_settings(asynchronous = false)
109
113
  return if @refreshing
110
114
 
111
115
  @lock.synchronize do
112
116
  return if @refreshing
117
+
113
118
  @refreshing = true
114
119
  @next_check_at = Time.now + @refresh_interval
115
120
  end
@@ -135,6 +140,9 @@ module SuperSettings
135
140
  end
136
141
 
137
142
  # Load only settings that have changed since the last load.
143
+ #
144
+ # @param asynchronous [Boolean] whether to refresh settings in a background thread
145
+ # @return [void]
138
146
  def refresh(asynchronous = false)
139
147
  last_refresh_time = @last_refreshed
140
148
  return if last_refresh_time.nil?
@@ -142,8 +150,10 @@ module SuperSettings
142
150
 
143
151
  @lock.synchronize do
144
152
  return if @refreshing
153
+
145
154
  @next_check_at = Time.now + @refresh_interval
146
155
  return if @cache.empty?
156
+
147
157
  @refreshing = true
148
158
  end
149
159
 
@@ -166,6 +176,8 @@ module SuperSettings
166
176
  end
167
177
 
168
178
  # Reset the cache to an unloaded state.
179
+ #
180
+ # @return [void]
169
181
  def reset
170
182
  @lock.synchronize do
171
183
  @cache = {}.freeze
@@ -189,6 +201,7 @@ module SuperSettings
189
201
  # @api private
190
202
  def update_setting(setting)
191
203
  return if Coerce.blank?(setting.key)
204
+
192
205
  @lock.synchronize do
193
206
  @cache = @cache.merge(setting.key => setting.value)
194
207
  end
@@ -199,6 +212,7 @@ module SuperSettings
199
212
  def wait_for_load
200
213
  loop do
201
214
  return unless @refreshing
215
+
202
216
  sleep(0.001)
203
217
  end
204
218
  end
@@ -11,7 +11,7 @@ module SuperSettings
11
11
  #
12
12
  # You are also responsible for implementing any CSRF protection if your authentication method
13
13
  # uses stateful requests (i.e. cookies or Basic auth where browser automatically include the
14
- # credentials on every reqeust). There are other gems available that can be integrated into
14
+ # credentials on every request). There are other gems available that can be integrated into
15
15
  # your middleware stack to provide this feature. If you need to inject meta elements into
16
16
  # the page, you can do so with the +add_to_head+ method.
17
17
  class RackApplication
@@ -225,8 +225,10 @@ module SuperSettings
225
225
  def check_authorization(request, write_required: false)
226
226
  user = current_user(request)
227
227
  return json_response(401, error: "Authentiation required") unless authenticated?(user)
228
+
228
229
  allowed = (write_required ? allow_write?(user) : allow_read?(user))
229
230
  return json_response(403, error: "Access denied") unless allowed
231
+
230
232
  yield(user)
231
233
  end
232
234
 
@@ -16,12 +16,14 @@ module SuperSettings
16
16
  # key: string,
17
17
  # value: object,
18
18
  # value_type: string,
19
- # description string,
19
+ # description: string,
20
20
  # created_at: iso8601 string,
21
21
  # updated_at: iso8601 string
22
22
  # },
23
23
  # ...
24
24
  # ]
25
+ #
26
+ # @return [Hash] hash with settings array
25
27
  def index
26
28
  settings = Setting.active.reject(&:deleted?).sort_by(&:key)
27
29
  {settings: settings.collect(&:as_json)}
@@ -29,6 +31,7 @@ module SuperSettings
29
31
 
30
32
  # Get a setting by id.
31
33
  #
34
+ # @param key [String] setting key
32
35
  # @example
33
36
  # GET /setting
34
37
  #
@@ -41,10 +44,12 @@ module SuperSettings
41
44
  # key: string,
42
45
  # value: object,
43
46
  # value_type: string,
44
- # description string,
47
+ # description: string,
45
48
  # created_at: iso8601 string,
46
49
  # updated_at: iso8601 string
47
50
  # }
51
+ #
52
+ # @return [Hash, nil] setting hash or nil if not found
48
53
  def show(key)
49
54
  setting = Setting.find_by_key(key)
50
55
  setting.as_json if setting && !setting.deleted?
@@ -52,6 +57,8 @@ module SuperSettings
52
57
 
53
58
  # The update operation uses a transaction to atomically update all settings.
54
59
  #
60
+ # @param settings_params [Array] array of setting parameter hashes
61
+ # @param changed_by [String] identifier for who made the changes
55
62
  # @example
56
63
  # POST /settings
57
64
  #
@@ -81,6 +88,8 @@ module SuperSettings
81
88
  # or
82
89
  #
83
90
  # {success: false, errors: {key => [string], ...}}
91
+ #
92
+ # @return [Hash] result hash with success status and any errors
84
93
  def update(settings_params, changed_by = nil)
85
94
  all_valid, settings = Setting.bulk_update(Array(settings_params), changed_by)
86
95
  if all_valid
@@ -98,6 +107,9 @@ module SuperSettings
98
107
 
99
108
  # Return the history of the setting.
100
109
  #
110
+ # @param key [String] setting key
111
+ # @param limit [Integer] number of history items to return
112
+ # @param offset [Integer] index to start fetching items from (most recent items are first)
101
113
  # @example
102
114
  # GET /setting/history
103
115
  #
@@ -121,6 +133,8 @@ module SuperSettings
121
133
  # previous_page_params: hash,
122
134
  # next_page_params: hash
123
135
  # }
136
+ #
137
+ # @return [Hash, nil] history hash or nil if setting not found
124
138
  def history(key, limit: nil, offset: 0)
125
139
  setting = Setting.find_by_key(key)
126
140
  return nil unless setting
@@ -162,12 +176,15 @@ module SuperSettings
162
176
  # {
163
177
  # last_updated_at: iso8601 string
164
178
  # }
179
+ #
180
+ # @return [Hash] hash with the last updated timestamp
165
181
  def last_updated_at
166
182
  {last_updated_at: Setting.last_updated_at.utc.iso8601(6)}
167
183
  end
168
184
 
169
185
  # Return settings that have been updated since a specified timestamp.
170
186
  #
187
+ # @param time [Time, String] timestamp to check for updates since
171
188
  # @example
172
189
  # GET /updated_since
173
190
  #
@@ -181,12 +198,14 @@ module SuperSettings
181
198
  # key: string,
182
199
  # value: object,
183
200
  # value_type: string,
184
- # description string,
201
+ # description: string,
185
202
  # created_at: iso8601 string,
186
203
  # updated_at: iso8601 string
187
204
  # },
188
205
  # ...
189
206
  # ]
207
+ #
208
+ # @return [Hash] hash with settings array
190
209
  def updated_since(time)
191
210
  time = Coerce.time(time)
192
211
  settings = Setting.updated_since(time).reject(&:deleted?)
@@ -9,6 +9,7 @@ module SuperSettings
9
9
  # ships with storage engines for ActiveRecord, Redis, and HTTP (microservice). See the SuperSettings::Storage
10
10
  # class for more details.
11
11
  class Setting
12
+ # Cache key used for storing the last updated timestamp.
12
13
  LAST_UPDATED_CACHE_KEY = "SuperSettings.last_updated_at"
13
14
 
14
15
  STRING = "string"
@@ -266,6 +267,7 @@ module SuperSettings
266
267
  setting = changed[key] || Setting.find_by_key(key)
267
268
  unless setting
268
269
  next if Coerce.present?(setting_params["delete"])
270
+
269
271
  setting = Setting.new(key: setting_params["key"])
270
272
  end
271
273
 
@@ -545,6 +547,7 @@ module SuperSettings
545
547
 
546
548
  # Serialize to a hash that is used for rendering JSON responses.
547
549
  #
550
+ # @param options [Hash] options for JSON serialization (unused but maintained for compatibility)
548
551
  # @return [Hash]
549
552
  def as_json(options = nil)
550
553
  attributes = {
@@ -561,6 +564,7 @@ module SuperSettings
561
564
 
562
565
  # Serialize to a JSON string.
563
566
  #
567
+ # @param options [Hash] options to pass to JSON generation
564
568
  # @return [String]
565
569
  def to_json(options = nil)
566
570
  as_json.to_json(options)
@@ -35,15 +35,13 @@ module SuperSettings
35
35
  # Add headers to this hash to add them to all requests to the SuperSettings REST API.
36
36
  #
37
37
  # @example
38
- #
39
- # SuperSettings::HttpStorage.headers["Authorization"] = "Bearer 12345"
38
+ # SuperSettings::HttpStorage.headers["Authorization"] = "Bearer 12345"
40
39
  attr_reader :headers
41
40
 
42
41
  # Add query parameters to this hash to add them to all requests to the SuperSettings REST API.
43
42
  #
44
43
  # @example
45
- #
46
- # SuperSettings::HttpStorage.query_params["access_token"] = "12345"
44
+ # SuperSettings::HttpStorage.query_params["access_token"] = "12345"
47
45
  attr_reader :query_params
48
46
 
49
47
  def all
@@ -35,6 +35,7 @@ module SuperSettings
35
35
  def find_all_by_key(key:, offset: 0, limit: nil)
36
36
  end_index = (limit.nil? ? -1 : offset + limit - 1)
37
37
  return [] unless end_index >= -1
38
+
38
39
  payloads = RedisStorage.with_redis { |redis| redis.lrange("#{HISTORY_KEY_PREFIX}.#{key}", offset, end_index) }
39
40
  payloads.collect do |json|
40
41
  record = new(JSON.parse(json))
@@ -112,6 +113,7 @@ module SuperSettings
112
113
  def find_by_key(key)
113
114
  json = with_redis { |redis| redis.hget(SETTINGS_KEY, key) }
114
115
  return nil unless json
116
+
115
117
  record = load_from_json(json)
116
118
  record unless record.deleted?
117
119
  end
@@ -123,6 +125,7 @@ module SuperSettings
123
125
  def last_updated_at
124
126
  result = with_redis { |redis| redis.zrevrange(UPDATED_KEY, 0, 1, withscores: true).first }
125
127
  return nil unless result
128
+
126
129
  time_at_microseconds(result[1])
127
130
  end
128
131
 
@@ -50,6 +50,7 @@ module SuperSettings
50
50
  def find_by_key(key)
51
51
  attributes = settings[key]
52
52
  return nil unless attributes
53
+
53
54
  setting = new(attributes)
54
55
  setting.send(:set_persisted!)
55
56
  setting unless setting.deleted?
@@ -4,24 +4,42 @@ module SuperSettings
4
4
  # Helper class for truncating timestamps to a specific precision. This is used by storage engines
5
5
  # to ensure that timestamps are stored and compared with the same precision.
6
6
  class TimePrecision
7
+ # The time value with applied precision.
7
8
  attr_reader :time
8
9
 
10
+ # Create a new TimePrecision object.
11
+ #
12
+ # @param time [Time, Numeric] the time to apply precision to
13
+ # @param precision [Symbol] the precision level (:microsecond or :millisecond)
14
+ # @raise [ArgumentError] if precision is not valid
9
15
  def initialize(time, precision = :microsecond)
10
16
  raise ArgumentError.new("Invalid precision: #{precision}") unless valid_precision?(precision)
11
17
 
12
18
  @time = time_with_precision(time.to_f, precision) if time
13
19
  end
14
20
 
21
+ # Convert the time to a float.
22
+ #
23
+ # @return [Float] the time as a floating point number
15
24
  def to_f
16
25
  @time.to_f
17
26
  end
18
27
 
19
28
  private
20
29
 
30
+ # Check if the precision value is valid.
31
+ #
32
+ # @param precision [Symbol] the precision to validate
33
+ # @return [Boolean] true if precision is valid
21
34
  def valid_precision?(precision)
22
35
  [:microsecond, :millisecond].include?(precision)
23
36
  end
24
37
 
38
+ # Apply the specified precision to a timestamp.
39
+ #
40
+ # @param timestamp [Float] the timestamp to apply precision to
41
+ # @param precision [Symbol] the precision level
42
+ # @return [Time] the time with applied precision
25
43
  def time_with_precision(timestamp, precision)
26
44
  usec = (timestamp % 1) * 1_000_000.0
27
45
  if precision == :millisecond
@@ -23,6 +23,7 @@ module SuperSettings
23
23
  autoload :HttpClient, "super_settings/http_client"
24
24
  autoload :VERSION, "super_settings/version"
25
25
 
26
+ # Default number of seconds between cache refresh checks.
26
27
  DEFAULT_REFRESH_INTERVAL = 5.0
27
28
 
28
29
  @local_cache = LocalCache.new(refresh_interval: DEFAULT_REFRESH_INTERVAL)
@@ -41,7 +42,7 @@ module SuperSettings
41
42
  Coerce.string(val)
42
43
  end
43
44
 
44
- # Alias for {#get} to allow using the [] operator to get a setting value.
45
+ # Alias for {.get} to allow using the [] operator to get a setting value.
45
46
  #
46
47
  # @param key [String, Symbol]
47
48
  # @return [String]
@@ -107,6 +108,7 @@ module SuperSettings
107
108
  val = context_setting(key)
108
109
  val = default if val.nil?
109
110
  return nil if val.nil?
111
+
110
112
  Array(val).collect { |v| v&.to_s }
111
113
  end
112
114
 
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: 2.4.0
4
+ version: 2.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-22 00:00:00.000000000 Z
11
+ date: 2025-09-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler