ultra_settings 2.9.1 → 2.10.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/README.md +24 -0
  4. data/VERSION +1 -1
  5. data/app/_config_description.html.erb +10 -1
  6. data/app/_data_source.html.erb +2 -2
  7. data/app/application.css +119 -26
  8. data/app/application_vars.css.erb +40 -14
  9. data/app/configuration.html.erb +19 -1
  10. data/app/layout.css +41 -2
  11. data/app/layout.html.erb +46 -0
  12. data/app/layout_vars.css.erb +12 -8
  13. data/app/locales/ar.json +5 -2
  14. data/app/locales/cs.json +5 -2
  15. data/app/locales/da.json +5 -2
  16. data/app/locales/de.json +5 -2
  17. data/app/locales/el.json +5 -2
  18. data/app/locales/en.json +5 -2
  19. data/app/locales/es.json +5 -2
  20. data/app/locales/fa.json +5 -2
  21. data/app/locales/fr.json +5 -2
  22. data/app/locales/gd.json +5 -2
  23. data/app/locales/he.json +5 -2
  24. data/app/locales/hi.json +5 -2
  25. data/app/locales/it.json +5 -2
  26. data/app/locales/ja.json +5 -2
  27. data/app/locales/ko.json +5 -2
  28. data/app/locales/lt.json +5 -2
  29. data/app/locales/nb.json +5 -2
  30. data/app/locales/nl.json +5 -2
  31. data/app/locales/pl.json +5 -2
  32. data/app/locales/pt-br.json +5 -2
  33. data/app/locales/pt.json +5 -2
  34. data/app/locales/ru.json +5 -2
  35. data/app/locales/sv.json +5 -2
  36. data/app/locales/ta.json +5 -2
  37. data/app/locales/tr.json +5 -2
  38. data/app/locales/uk.json +5 -2
  39. data/app/locales/ur.json +5 -2
  40. data/app/locales/vi.json +5 -2
  41. data/app/locales/zh-cn.json +5 -2
  42. data/app/locales/zh-tw.json +5 -2
  43. data/lib/ultra_settings/application_view.rb +17 -7
  44. data/lib/ultra_settings/audit_data_sources.rb +1 -1
  45. data/lib/ultra_settings/coerce.rb +1 -1
  46. data/lib/ultra_settings/configuration.rb +36 -10
  47. data/lib/ultra_settings/configuration_view.rb +166 -9
  48. data/lib/ultra_settings/mini_i18n.rb +14 -10
  49. data/lib/ultra_settings/rack_app.rb +4 -4
  50. data/lib/ultra_settings/view_helper.rb +27 -5
  51. data/lib/ultra_settings/web_view.rb +26 -16
  52. data/lib/ultra_settings/yaml_config.rb +10 -2
  53. data/lib/ultra_settings.rb +66 -2
  54. data/ultra_settings.gemspec +1 -1
  55. metadata +2 -2
@@ -24,13 +24,24 @@ module UltraSettings
24
24
 
25
25
  # Render the HTML for the configuration view.
26
26
  #
27
+ # The runtime settings cache is reloaded before rendering so that values changed
28
+ # from the UI are displayed immediately rather than after the runtime settings
29
+ # engine next refreshes itself. If a page renders more than one view, wrap them all
30
+ # in `UltraSettings.with_runtime_settings_reloaded` so that the settings are only
31
+ # reloaded once for the page instead of once per view.
32
+ #
27
33
  # @param table_class [String] @deprecated CSS class for the table element (maintained for backwards compatibility).
28
34
  # @return [String] The rendered HTML.
29
35
  def render(table_class: "")
30
- configuration = @configuration # used by ERB template via binding
31
- html = ViewHelper.erb_template("configuration.html.erb").result(binding)
32
- html = html.html_safe if html.respond_to?(:html_safe)
33
- html
36
+ UltraSettings.with_runtime_settings_reloaded do
37
+ # Expose configuration as a local for the ERB template without a direct
38
+ # assignment, which would emit an unused variable warning under ruby -w.
39
+ template_binding = binding
40
+ template_binding.local_variable_set(:configuration, @configuration)
41
+ html = ViewHelper.erb_template("configuration.html.erb").result(template_binding)
42
+ html = html.html_safe if html.respond_to?(:html_safe)
43
+ html
44
+ end
34
45
  end
35
46
 
36
47
  # Convert the view to a string by rendering it.
@@ -70,6 +81,25 @@ module UltraSettings
70
81
  end
71
82
  end
72
83
 
84
+ # The text placed on the clipboard by the copy button. This is the raw value
85
+ # rather than the inspected value shown in the UI so that, for example, copying
86
+ # a string setting does not include the surrounding quotes.
87
+ #
88
+ # @param value [Object] The setting value.
89
+ # @return [String] The text to copy.
90
+ def copy_value(value)
91
+ case value
92
+ when nil
93
+ ""
94
+ when Time
95
+ value.iso8601
96
+ when Array
97
+ value.join("\n")
98
+ else
99
+ value.to_s
100
+ end
101
+ end
102
+
73
103
  def secret_value(value)
74
104
  if value.nil?
75
105
  t("field.nil")
@@ -78,13 +108,63 @@ module UltraSettings
78
108
  end
79
109
  end
80
110
 
111
+ # Shorten a file path for display by making it relative to the working
112
+ # directory or to the YAML configuration directory. The absolute path is
113
+ # used if the file is not inside either directory.
114
+ #
115
+ # @param path [Pathname] The absolute file path.
116
+ # @return [String] The path to display.
81
117
  def relative_path(path)
82
- root_path = Pathname.new(Dir.pwd)
83
- config_path = UltraSettings::Configuration.yaml_config_path
84
- unless config_path.realpath.to_s.start_with?("#{root_path.realpath}#{File::SEPARATOR}")
85
- root_path = config_path
118
+ paths = display_paths(Pathname.new(path).expand_path)
119
+ display_path_roots.each do |root|
120
+ paths.each do |file_path|
121
+ relative = path_inside(file_path, root)
122
+ return relative if relative
123
+ end
86
124
  end
87
- path.relative_path_from(root_path)
125
+ paths.first.to_s
126
+ end
127
+
128
+ # The file path in both its literal and symlink resolved forms so that it can
129
+ # be matched against a directory that is specified in either form.
130
+ #
131
+ # @param path [Pathname] The absolute file path.
132
+ # @return [Array<Pathname>]
133
+ def display_paths(path)
134
+ [path, resolved_path(path.dirname)&.join(path.basename)].compact.uniq
135
+ end
136
+
137
+ # Directories that a displayed path can be made relative to, in order of
138
+ # preference. Directories are listed in both their literal and symlink
139
+ # resolved forms since a file path can be in either form.
140
+ #
141
+ # @return [Array<Pathname>]
142
+ def display_path_roots
143
+ roots = [Pathname.new(Dir.pwd)]
144
+ config_path = UltraSettings::Configuration.yaml_config_path
145
+ roots << Pathname.new(config_path) if config_path
146
+ roots.flat_map { |root| [root.expand_path, resolved_path(root)] }.compact.uniq
147
+ end
148
+
149
+ # @param path [Pathname] The absolute file path.
150
+ # @param root [Pathname] The absolute directory path.
151
+ # @return [String, nil] The path relative to the directory or nil if it is not inside it.
152
+ def path_inside(path, root)
153
+ relative = path.relative_path_from(root).to_s.delete_prefix("./")
154
+ return nil if relative == "." || relative.start_with?("..")
155
+
156
+ relative
157
+ rescue ArgumentError
158
+ # relative_path_from raises if the paths have no common root (e.g. different drives).
159
+ nil
160
+ end
161
+
162
+ # @param path [Pathname] The directory path.
163
+ # @return [Pathname, nil] The path with symlinks resolved or nil if it does not exist.
164
+ def resolved_path(path)
165
+ path.realpath
166
+ rescue SystemCallError
167
+ nil
88
168
  end
89
169
 
90
170
  def source_chip_label(source)
@@ -116,6 +196,34 @@ module UltraSettings
116
196
  end
117
197
  end
118
198
 
199
+ # True if the YAML keys for a configuration are hidden behind a toggle button.
200
+ # They are hidden by default when the YAML file does not exist since the keys
201
+ # are not used by the application.
202
+ #
203
+ # @param configuration [UltraSettings::Configuration] The configuration instance.
204
+ # @return [Boolean]
205
+ def hide_yaml_keys?(configuration)
206
+ config_class = configuration.class
207
+ file = config_class.configuration_file
208
+ file.is_a?(Pathname) && !file.exist? && config_class.fields.any?(&:yaml_key)
209
+ end
210
+
211
+ # Inline script for the button that shows and hides the YAML keys. It is
212
+ # inlined on the element so that the button also works when the configuration
213
+ # view is embedded in a host application page that does not include the
214
+ # bundled JavaScript.
215
+ #
216
+ # @return [String] JavaScript source for an onclick attribute.
217
+ def toggle_yaml_keys_script
218
+ <<~JAVASCRIPT.gsub(/\s+/, " ").tr('"', "'")
219
+ var block = this.closest('.ultra-settings-block');
220
+ if (block) {
221
+ var hidden = block.classList.toggle('ultra-settings-yaml-hidden');
222
+ this.setAttribute('aria-checked', hidden ? 'false' : 'true');
223
+ }
224
+ JAVASCRIPT
225
+ end
226
+
119
227
  def open_panel_script
120
228
  <<~JAVASCRIPT.gsub(/\s+/, " ").tr('"', "'")
121
229
  var el = this;
@@ -146,6 +254,38 @@ module UltraSettings
146
254
  JAVASCRIPT
147
255
  end
148
256
 
257
+ # Inline script for the copy button. It is inlined on the element so that the
258
+ # button also works when the configuration view is embedded in a host
259
+ # application page that does not include the bundled JavaScript.
260
+ #
261
+ # @return [String] JavaScript source for an onclick attribute.
262
+ def copy_value_script
263
+ <<~JAVASCRIPT.gsub(/\s+/, " ").tr('"', "'")
264
+ var btn = this;
265
+ var text = btn.dataset.copyValue || '';
266
+ var flash = function() {
267
+ btn.classList.add('copied');
268
+ window.setTimeout(function() { btn.classList.remove('copied'); }, 1500);
269
+ };
270
+ var fallback = function() {
271
+ var input = document.createElement('textarea');
272
+ input.value = text;
273
+ input.setAttribute('readonly', '');
274
+ input.style.position = 'fixed';
275
+ input.style.opacity = '0';
276
+ document.body.appendChild(input);
277
+ input.select();
278
+ try { if (document.execCommand('copy')) { flash(); } } catch (e) {}
279
+ document.body.removeChild(input);
280
+ };
281
+ if (navigator.clipboard && navigator.clipboard.writeText) {
282
+ navigator.clipboard.writeText(text).then(flash, fallback);
283
+ } else {
284
+ fallback();
285
+ }
286
+ JAVASCRIPT
287
+ end
288
+
149
289
  def source_priority
150
290
  [:env, :settings, :yaml, :default]
151
291
  end
@@ -196,6 +336,23 @@ module UltraSettings
196
336
  HTML
197
337
  end
198
338
 
339
+ def copy_icon(size = 13)
340
+ <<~HTML
341
+ <svg class="ultra-settings-copy-icon" width="#{size}" height="#{size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
342
+ <rect x="9" y="9" width="13" height="13" rx="2"/>
343
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
344
+ </svg>
345
+ HTML
346
+ end
347
+
348
+ def check_icon(size = 13)
349
+ <<~HTML
350
+ <svg class="ultra-settings-copy-check" width="#{size}" height="#{size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
351
+ <polyline points="20 6 9 17 4 12"/>
352
+ </svg>
353
+ HTML
354
+ end
355
+
199
356
  def close_icon(size = 16)
200
357
  <<~HTML
201
358
  <svg width="#{size}" height="#{size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
@@ -76,26 +76,30 @@ module UltraSettings
76
76
  end
77
77
 
78
78
  # Load every JSON file from the locales directory, keyed by filename
79
- # stem (e.g. "en").
79
+ # stem (e.g. "en"). The cache is built into a local hash and then
80
+ # published with a single assignment so that concurrent readers never
81
+ # see a partially loaded cache.
80
82
  def load_all_locales
81
- if development_mode?
82
- @mutex.synchronize { @cache = {} }
83
- end
83
+ clear_cache! if development_mode?
84
84
 
85
- return @cache unless @cache.empty?
85
+ cached = @cache
86
+ return cached unless cached.empty?
86
87
 
87
88
  @mutex.synchronize do
88
- return @cache unless @cache.empty?
89
+ cached = @cache
90
+ return cached unless cached.empty?
89
91
 
92
+ cache = {}
90
93
  Dir.glob(File.join(locales_dir, "*.json")).each do |path|
91
94
  code = File.basename(path, ".json").downcase
92
- @cache[code] = JSON.parse(File.read(path))
95
+ cache[code] = JSON.parse(File.read(path, encoding: Encoding::UTF_8))
93
96
  rescue JSON::ParserError
94
97
  # Skip malformed locale files
95
98
  end
96
- end
97
99
 
98
- @cache
100
+ @cache = cache
101
+ cache
102
+ end
99
103
  end
100
104
 
101
105
  def locales_dir
@@ -103,7 +107,7 @@ module UltraSettings
103
107
  end
104
108
 
105
109
  def development_mode?
106
- ENV.fetch("RACK_ENV", "development") == "development"
110
+ UltraSettings.__development_mode__?
107
111
  end
108
112
  end
109
113
  end
@@ -26,9 +26,9 @@ module UltraSettings
26
26
  private
27
27
 
28
28
  def webview
29
- if ENV.fetch("RAILS_ENV", ENV.fetch("RACK_ENV", "development")) == "development"
30
- @webview = nil
31
- end
29
+ # Don't cache the view in development mode so that changes to the app files
30
+ # are picked up without having to restart the server.
31
+ @webview = nil if UltraSettings.__development_mode__?
32
32
  @webview ||= WebView.new(color_scheme: @color_scheme)
33
33
  end
34
34
 
@@ -62,7 +62,7 @@ module UltraSettings
62
62
  parts = entry.strip.split(";")
63
63
  tag = parts[0].to_s.strip.downcase.tr("_", "-")
64
64
  q = 1.0
65
- parts[1..-1].each do |p|
65
+ parts[1..].each do |p|
66
66
  if p.strip.start_with?("q=")
67
67
  q = p.strip.sub("q=", "").to_f
68
68
  end
@@ -4,6 +4,7 @@ module UltraSettings
4
4
  # Base class for rendering views.
5
5
  module ViewHelper
6
6
  @cache = {}
7
+ @mutex = Mutex.new
7
8
 
8
9
  class << self
9
10
  # Get an ERB template for rendering.
@@ -11,8 +12,7 @@ module UltraSettings
11
12
  # @param path [String] The path to the template file.
12
13
  # @return [ERB] The compiled ERB template.
13
14
  def erb_template(path)
14
- @cache.clear if development_mode?
15
- @cache["erb:#{path}"] ||= ERB.new(read_app_file(path))
15
+ fetch("erb:#{path}") { ERB.new(read_app_file(path)) }
16
16
  end
17
17
 
18
18
  # Read a file from the app directory.
@@ -20,8 +20,7 @@ module UltraSettings
20
20
  # @param path [String] The path to the file relative to the app directory.
21
21
  # @return [String] The contents of the file.
22
22
  def read_app_file(path)
23
- @cache.clear if development_mode?
24
- @cache["file:#{path}"] ||= File.read(File.join(app_dir, path))
23
+ fetch("file:#{path}") { File.read(File.join(app_dir, path), encoding: Encoding::UTF_8) }
25
24
  end
26
25
 
27
26
  # Get the app directory path.
@@ -33,8 +32,31 @@ module UltraSettings
33
32
 
34
33
  private
35
34
 
35
+ # Fetch a value from the cache, generating it with the block if needed.
36
+ # The value is generated outside of the lock since the block may itself
37
+ # fetch other cached values. The cache hash is never mutated in place; a
38
+ # copy is published with a single assignment so concurrent readers always
39
+ # see a consistent hash. Two threads may generate the same value
40
+ # concurrently; the first one to publish wins.
41
+ def fetch(key, &block)
42
+ return yield if development_mode?
43
+
44
+ cached = @cache
45
+ return cached[key] if cached.include?(key)
46
+
47
+ value = yield
48
+ @mutex.synchronize do
49
+ if @cache.include?(key)
50
+ @cache[key]
51
+ else
52
+ @cache = @cache.merge(key => value)
53
+ value
54
+ end
55
+ end
56
+ end
57
+
36
58
  def development_mode?
37
- ENV.fetch("RACK_ENV", "development") == "development"
59
+ UltraSettings.__development_mode__?
38
60
  end
39
61
  end
40
62
  end
@@ -9,23 +9,38 @@ module UltraSettings
9
9
 
10
10
  # Initialize a new WebView with the specified color scheme.
11
11
  #
12
- # @param color_scheme [Symbol] The color scheme to use in the UI. This can be `:light`,
13
- # `:dark`, or `:system`. The default is `:light`.
12
+ # @param color_scheme [Symbol, nil] The color scheme to use in the UI. This can be `:light`,
13
+ # `:dark`, or `:system`. When `nil`, a toggle control is rendered and
14
+ # `[data-theme=dark]` is used as the dark mode CSS selector.
14
15
  def initialize(color_scheme: :light)
15
- @color_scheme = (color_scheme || :light).to_sym
16
+ @color_scheme = color_scheme&.to_sym
17
+ @dark_mode_selector = @color_scheme.nil? ? "[data-theme=dark]" : nil
16
18
  @layout_template = ViewHelper.erb_template("layout.html.erb")
17
- @layout_css = scheme_layout_css(@color_scheme)
19
+ @layout_css = scheme_layout_css(@color_scheme, @dark_mode_selector)
20
+ @locale = nil
18
21
  end
19
22
 
20
23
  # Render the complete settings page HTML.
21
24
  #
22
- # @param request [Rack::Request, nil] The current Rack request for access control.
25
+ # This instance may be shared between concurrent requests, so per-request
26
+ # state is set on a copy of the view rather than on the shared instance.
27
+ #
28
+ # @param request [Rack::Request, nil] The current Rack request. It is accepted
29
+ # for backward compatibility but is not used by the view; access control and
30
+ # locale resolution are the caller's responsibility.
23
31
  # @param locale [String] The locale code for translations.
24
32
  # @return [String] The rendered HTML page.
25
33
  def render_settings(request = nil, locale: UltraSettings::MiniI18n::DEFAULT_LOCALE)
26
- @request = request
27
- @locale = locale
28
- refresh_super_settings!
34
+ renderer = dup
35
+ renderer.instance_variable_set(:@locale, locale)
36
+ renderer.render_layout
37
+ end
38
+
39
+ # Render the layout template in the context of this instance.
40
+ #
41
+ # @return [String] The rendered HTML page.
42
+ # @api private
43
+ def render_layout
29
44
  @layout_template.result(binding)
30
45
  end
31
46
 
@@ -34,7 +49,8 @@ module UltraSettings
34
49
  # @return [String] The HTML content for the settings.
35
50
  def content
36
51
  UltraSettings::ApplicationView.new(
37
- color_scheme: @color_scheme,
52
+ color_scheme: @color_scheme || :light,
53
+ dark_mode_selector: @dark_mode_selector,
38
54
  locale: @locale || UltraSettings::MiniI18n::DEFAULT_LOCALE
39
55
  ).render
40
56
  end
@@ -56,16 +72,10 @@ module UltraSettings
56
72
 
57
73
  private
58
74
 
59
- def scheme_layout_css(color_scheme)
75
+ def scheme_layout_css(color_scheme, dark_mode_selector)
60
76
  vars = ViewHelper.erb_template("layout_vars.css.erb").result(binding)
61
77
  css = ViewHelper.read_app_file("layout.css")
62
78
  "#{vars}\n#{css}"
63
79
  end
64
-
65
- def refresh_super_settings!
66
- return unless defined?(SuperSettings) && UltraSettings.__runtime_settings__ == SuperSettings
67
-
68
- SuperSettings.refresh_settings
69
- end
70
80
  end
71
81
  end
@@ -52,17 +52,25 @@ module UltraSettings
52
52
  private
53
53
 
54
54
  def load_yaml(path)
55
- yaml = File.read(path)
55
+ yaml = File.read(path, encoding: Encoding::UTF_8)
56
56
 
57
57
  if yaml.include?("<%")
58
58
  yaml = ERB.new(yaml).result
59
59
  end
60
60
 
61
- hash = YAML.load(yaml) # rubocop:disable Security/YAMLLoad
61
+ hash = parse_yaml(yaml)
62
62
  hash = {} unless hash.is_a?(Hash)
63
63
  hash
64
64
  end
65
65
 
66
+ def parse_yaml(yaml)
67
+ YAML.load(yaml, aliases: true, permitted_classes: [Symbol, Date, Time]) # rubocop:disable Security/YAMLLoad
68
+ rescue ArgumentError
69
+ # Psych 3 does not support the aliases or permitted_classes options; its
70
+ # YAML.load already allows aliases and arbitrary classes.
71
+ YAML.load(yaml) # rubocop:disable Security/YAMLLoad
72
+ end
73
+
66
74
  def environment_config(yaml, environment)
67
75
  shared = flatten_hash(yaml.fetch("shared", {}))
68
76
  env = flatten_hash(yaml.fetch(environment, {}))
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "erb"
4
4
  require "yaml"
5
+ require "date"
5
6
  require "time"
6
7
  require "pathname"
7
8
  require "singleton"
@@ -32,6 +33,10 @@ module UltraSettings
32
33
 
33
34
  VALID_NAME_PATTERN = /\A[a-z_][a-zA-Z0-9_]*\z/
34
35
 
36
+ # Thread local key used to make the runtime settings reload in the web views reentrant.
37
+ RELOAD_GUARD_KEY = :ultra_settings_runtime_settings_reloaded
38
+ private_constant :RELOAD_GUARD_KEY
39
+
35
40
  @configurations = {}
36
41
  @mutex = Mutex.new
37
42
  @runtime_settings = nil
@@ -177,6 +182,57 @@ module UltraSettings
177
182
  @runtime_settings
178
183
  end
179
184
 
185
+ # Reload the runtime settings cache and then yield to the block. The web views call
186
+ # this when rendering so that a setting changed from the UI is displayed on the next
187
+ # page load rather than whenever the runtime settings engine gets around to
188
+ # refreshing itself.
189
+ #
190
+ # The runtime settings object is reloaded if it responds to `load_settings`. The
191
+ # `super_settings` gem does, as does anything that delegates to it.
192
+ #
193
+ # Calls are reentrant, so a block that renders several views only reloads once.
194
+ # Wrap a page that embeds more than one `ConfigurationView` in this method to avoid
195
+ # reloading the runtime settings once per view.
196
+ #
197
+ # Errors are not fatal; the page can still show values from the other sources.
198
+ #
199
+ # @example Rendering several configurations with a single reload.
200
+ # UltraSettings.with_runtime_settings_reloaded do
201
+ # configurations.each { |config| output << UltraSettings::ConfigurationView.new(config).render }
202
+ # end
203
+ #
204
+ # @return [Object] The result of the block.
205
+ def with_runtime_settings_reloaded
206
+ return yield if Thread.current[RELOAD_GUARD_KEY]
207
+
208
+ Thread.current[RELOAD_GUARD_KEY] = true
209
+ begin
210
+ settings = __runtime_settings__
211
+ if settings.respond_to?(:load_settings)
212
+ begin
213
+ settings.load_settings
214
+ rescue => e
215
+ warn("UltraSettings: unable to reload runtime settings: #{e.class}: #{e.message}")
216
+ end
217
+ end
218
+
219
+ yield
220
+ ensure
221
+ Thread.current[RELOAD_GUARD_KEY] = nil
222
+ end
223
+ end
224
+
225
+ # Returns true if the application is running in development mode. This is used by the
226
+ # web UI to decide if templates, stylesheets, and translations can be cached in memory
227
+ # or if they need to be re-read from disk on every request.
228
+ #
229
+ # @return [Boolean]
230
+ # @api private
231
+ def __development_mode__?
232
+ env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || ENV["APP_ENV"] || "development"
233
+ env == "development"
234
+ end
235
+
180
236
  # Set the URL for changing runtime settings. If this is set, then a link to the
181
237
  # URL will be displayed in the web view for fields that support runtime settings.
182
238
  # The URL may contain a `${name}` placeholder that will be replaced with the name
@@ -266,7 +322,7 @@ module UltraSettings
266
322
  settings = settings.to_a
267
323
  config_name, values = settings.first
268
324
  config_name = config_name.to_s
269
- other_settings = settings[1..-1]
325
+ other_settings = settings[1..]
270
326
 
271
327
  unless @configurations.include?(config_name)
272
328
  raise ArgumentError.new("Unknown configuration: #{config_name.inspect}")
@@ -299,7 +355,15 @@ module UltraSettings
299
355
  __load_config__(name, class_name)
300
356
  end
301
357
 
302
- config_classes = ObjectSpace.each_object(Class).select { |klass| klass < Configuration }
358
+ config_classes = ObjectSpace.each_object(Class).select do |klass|
359
+ next false unless klass < Configuration
360
+ next false if klass.name.nil?
361
+ begin
362
+ constantize(klass.name).equal?(klass)
363
+ rescue NameError
364
+ false
365
+ end
366
+ end
303
367
  config_classes.collect(&:instance)
304
368
  end
305
369
 
@@ -38,7 +38,7 @@ Gem::Specification.new do |spec|
38
38
 
39
39
  spec.require_paths = ["lib"]
40
40
 
41
- spec.required_ruby_version = ">= 2.5"
41
+ spec.required_ruby_version = ">= 2.6"
42
42
 
43
43
  spec.add_development_dependency "bundler"
44
44
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ultra_settings
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.9.1
4
+ version: 2.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
@@ -110,7 +110,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
110
110
  requirements:
111
111
  - - ">="
112
112
  - !ruby/object:Gem::Version
113
- version: '2.5'
113
+ version: '2.6'
114
114
  required_rubygems_version: !ruby/object:Gem::Requirement
115
115
  requirements:
116
116
  - - ">="