super_settings 2.4.3 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +27 -0
  3. data/ARCHITECTURE.md +232 -0
  4. data/CHANGELOG.md +29 -0
  5. data/README.md +56 -6
  6. data/VERSION +1 -1
  7. data/app/helpers/super_settings/settings_helper.rb +5 -11
  8. data/app/locales/ar.json +44 -0
  9. data/app/locales/cs.json +44 -0
  10. data/app/locales/da.json +44 -0
  11. data/app/locales/de.json +44 -0
  12. data/app/locales/el.json +44 -0
  13. data/app/locales/en.json +55 -0
  14. data/app/locales/es.json +44 -0
  15. data/app/locales/fa.json +44 -0
  16. data/app/locales/fr.json +44 -0
  17. data/app/locales/gd.json +44 -0
  18. data/app/locales/he.json +44 -0
  19. data/app/locales/hi.json +44 -0
  20. data/app/locales/it.json +44 -0
  21. data/app/locales/ja.json +44 -0
  22. data/app/locales/ko.json +44 -0
  23. data/app/locales/lt.json +44 -0
  24. data/app/locales/nb.json +44 -0
  25. data/app/locales/nl.json +44 -0
  26. data/app/locales/pl.json +44 -0
  27. data/app/locales/pt-br.json +44 -0
  28. data/app/locales/pt.json +44 -0
  29. data/app/locales/ru.json +44 -0
  30. data/app/locales/sv.json +44 -0
  31. data/app/locales/ta.json +44 -0
  32. data/app/locales/tr.json +44 -0
  33. data/app/locales/uk.json +44 -0
  34. data/app/locales/ur.json +44 -0
  35. data/app/locales/vi.json +44 -0
  36. data/app/locales/zh-cn.json +44 -0
  37. data/app/locales/zh-tw.json +44 -0
  38. data/app/views/layouts/super_settings/settings.html.erb +4 -4
  39. data/config/routes.rb +2 -0
  40. data/lib/super_settings/application/api.js +37 -12
  41. data/lib/super_settings/application/helper.rb +78 -11
  42. data/lib/super_settings/application/index.html.erb +44 -44
  43. data/lib/super_settings/application/layout.html.erb +113 -6
  44. data/lib/super_settings/application/layout_styles.css +311 -21
  45. data/lib/super_settings/application/layout_vars.css.erb +15 -7
  46. data/lib/super_settings/application/scripts.js +72 -13
  47. data/lib/super_settings/application/style_vars.css.erb +108 -53
  48. data/lib/super_settings/application/styles.css +179 -84
  49. data/lib/super_settings/application.rb +21 -3
  50. data/lib/super_settings/configuration.rb +27 -5
  51. data/lib/super_settings/controller_actions.rb +39 -2
  52. data/lib/super_settings/http_client.rb +2 -1
  53. data/lib/super_settings/local_cache.rb +12 -16
  54. data/lib/super_settings/mini_i18n.rb +110 -0
  55. data/lib/super_settings/rack_application.rb +94 -7
  56. data/lib/super_settings/rest_api.rb +7 -3
  57. data/lib/super_settings/setting.rb +1 -1
  58. data/lib/super_settings/storage/active_record_storage/models.rb +7 -9
  59. data/lib/super_settings/storage/http_storage.rb +1 -2
  60. data/lib/super_settings/storage/json_storage.rb +1 -1
  61. data/lib/super_settings/storage/mongodb_storage.rb +2 -1
  62. data/lib/super_settings/storage/s3_storage.rb +1 -1
  63. data/lib/super_settings/storage.rb +1 -1
  64. data/lib/super_settings/version.rb +1 -1
  65. data/lib/super_settings.rb +16 -18
  66. data/super_settings.gemspec +2 -1
  67. metadata +35 -3
@@ -8,12 +8,16 @@ module SuperSettings
8
8
  include Helper
9
9
 
10
10
  # @param layout [String, Symbol] path to an ERB template to use as the layout around the application UI. You can
11
- # pass the symbol +:default+ to use the default layout that ships with the gem.
11
+ # pass the symbol +:default+ to use the default layout that ships with the gem.
12
12
  # @param add_to_head [String] HTML code to add to the <head> element on the page.
13
13
  # @param api_base_url [String] the base URL for the REST API.
14
14
  # @param color_scheme [Symbol] whether to use dark mode for the application UI. If +nil+, the user's system
15
- # preference will be used.
16
- def initialize(layout: nil, add_to_head: nil, api_base_url: nil, color_scheme: nil)
15
+ # preference will be used.
16
+ # @param dark_mode_selector [String] a CSS selector that sets dark mode when it matches an element in the page.
17
+ # This is an alternative to using the color_scheme option.
18
+ # @param read_only [Boolean] whether to render the application in read-only mode (edit controls hidden).
19
+ # @param locale [String] the locale code for translations (default: "en").
20
+ def initialize(layout: nil, add_to_head: nil, api_base_url: nil, color_scheme: nil, dark_mode_selector: nil, read_only: false, locale: nil)
17
21
  if layout
18
22
  layout = File.expand_path(File.join("application", "layout.html.erb"), __dir__) if layout == :default
19
23
  @layout = ERB.new(File.read(layout)) if layout
@@ -25,6 +29,9 @@ module SuperSettings
25
29
 
26
30
  @api_base_url = api_base_url
27
31
  @color_scheme = color_scheme&.to_sym
32
+ @dark_mode_selector = dark_mode_selector
33
+ @read_only = !!read_only
34
+ @locale = locale || SuperSettings::MiniI18n::DEFAULT_LOCALE
28
35
  end
29
36
 
30
37
  # Render the web UI application HTML.
@@ -38,6 +45,17 @@ module SuperSettings
38
45
  html
39
46
  end
40
47
 
48
+ # Render the edit form HTML for a single setting.
49
+ #
50
+ # @return [String] the rendered HTML
51
+ def render_edit
52
+ template = ERB.new(File.read(File.expand_path(File.join("application", "edit.html.erb"), __dir__)))
53
+ html = template.result(binding)
54
+ html = render_layout { html } if @layout
55
+ html = html.html_safe if html.respond_to?(:html_safe)
56
+ html
57
+ end
58
+
41
59
  private
42
60
 
43
61
  def render_layout
@@ -12,6 +12,8 @@ module SuperSettings
12
12
 
13
13
  # Configuration for the controller.
14
14
  class Controller
15
+ VALID_COLOR_SCHEMES = [:light, :dark, :system].freeze
16
+
15
17
  # @api private
16
18
  attr_reader :enhancement
17
19
 
@@ -25,12 +27,13 @@ module SuperSettings
25
27
  def initialize
26
28
  @superclass = nil
27
29
  @web_ui_enabled = true
28
- @color_scheme = false
30
+ @color_scheme = nil
29
31
  @changed_by_block = nil
30
32
  @enhancement = nil
31
33
  @application_name = nil
32
34
  @application_logo = nil
33
35
  @application_link = nil
36
+ @dark_mode_selector = nil
34
37
  end
35
38
 
36
39
  def superclass
@@ -41,10 +44,10 @@ module SuperSettings
41
44
  end
42
45
  end
43
46
 
44
- # Optinal name of the application displayed in the view.
47
+ # Optional name of the application displayed in the view.
45
48
  attr_accessor :application_name
46
49
 
47
- # Optional mage URL for the application logo.
50
+ # Optional image URL for the application logo.
48
51
  attr_accessor :application_logo
49
52
 
50
53
  # Optional URL for a link back to the rest of the application.
@@ -69,11 +72,30 @@ module SuperSettings
69
72
  end
70
73
 
71
74
  # Set dark mode for the web UI. Possible values are :light, :dark, or :system.
72
- # The default value is :light.
75
+ # When the value is not set (or is invalid), theme selection is left dynamic.
73
76
  attr_writer :color_scheme
74
77
 
78
+ # Return the configured color scheme.
79
+ #
80
+ # @return [Symbol, nil]
75
81
  def color_scheme
76
- (@color_scheme ||= :light).to_sym
82
+ scheme = @color_scheme&.to_sym
83
+ VALID_COLOR_SCHEMES.include?(scheme) ? scheme : nil
84
+ end
85
+
86
+ # Set a CSS selector that sets dark mode. This is an alternative to using the color_scheme
87
+ # option and can be used if you have a custom implementation for dark mode in your application.
88
+ # Dark mode will be enabled in the settings web UI when the selector matches an element in the page.
89
+ attr_accessor :dark_mode_selector
90
+
91
+ # Return the selector used to enable dark mode from host page state.
92
+ #
93
+ # @return [String, nil]
94
+ def resolved_dark_mode_selector
95
+ return @dark_mode_selector if @dark_mode_selector
96
+ return "[data-theme=dark]" if color_scheme.nil?
97
+
98
+ nil
77
99
  end
78
100
 
79
101
  # Enhance the controller. You can define methods or call controller class methods like
@@ -19,8 +19,13 @@ module SuperSettings
19
19
 
20
20
  # Render the HTML application for managing settings.
21
21
  def root
22
- html = SuperSettings::Application.new.render
23
- render html: html.html_safe, layout: true
22
+ application = SuperSettings::Application.new(
23
+ read_only: super_settings_read_only?,
24
+ locale: I18n.locale,
25
+ color_scheme: SuperSettings.configuration.controller.color_scheme,
26
+ dark_mode_selector: SuperSettings.configuration.controller.resolved_dark_mode_selector
27
+ )
28
+ render html: application.render.html_safe, layout: true
24
29
  end
25
30
 
26
31
  # API endpoint for getting active settings. See SuperSettings::RestAPI for details.
@@ -40,6 +45,10 @@ module SuperSettings
40
45
 
41
46
  # API endpoint for updating settings. See SuperSettings::RestAPI for details.
42
47
  def update
48
+ if super_settings_read_only?
49
+ render json: {error: "Access denied"}, status: 403
50
+ return
51
+ end
43
52
  changed_by = SuperSettings.configuration.controller.changed_by(self)
44
53
  result = SuperSettings::RestAPI.update(params[:settings], changed_by)
45
54
  if result[:success]
@@ -69,12 +78,40 @@ module SuperSettings
69
78
  render json: SuperSettings::RestAPI.updated_since(params[:time])
70
79
  end
71
80
 
81
+ # API endpoint for checking if the user is authorized to edit settings.
82
+ def authorized
83
+ permission = super_settings_read_only? ? "read-only" : "read-write"
84
+ headers["super-settings-permission"] = permission
85
+ headers["cache-control"] = "no-cache"
86
+ render json: {authorized: true, permission: permission}
87
+ end
88
+
89
+ # Serve up the api.js file that defines a JavaScript client for the REST API.
90
+ def api_js
91
+ js = File.read(File.expand_path(File.join("application", "api.js"), __dir__))
92
+ render js: js.html_safe, content_type: "application/javascript; charset=utf-8"
93
+ end
94
+
72
95
  protected
73
96
 
97
+ # Mark the current request as read-only. When read-only, the web UI will hide edit
98
+ # controls and write API endpoints will return 403. Call this in a +before_action+ to
99
+ # restrict a user to read-only access.
100
+ def super_settings_read_only!
101
+ request.env["super_settings.read_only"] = true
102
+ end
103
+
104
+ # Return true if the current request has been marked as read-only.
105
+ def super_settings_read_only?
106
+ !!request.env["super_settings.read_only"]
107
+ end
108
+
74
109
  # Return true if CSRF protection needs to be enabled for the request.
75
110
  # By default it is only enabled on stateful requests that include Basic authorization
76
111
  # or cookies in the request so that stateless REST API calls are allowed.
77
112
  def protect_from_forgery?
113
+ return false if action_name == "api_js"
114
+
78
115
  request.cookies.present? || request.authorization.to_s.split(" ", 2).first&.match?(/\ABasic/i)
79
116
  end
80
117
  end
@@ -135,6 +135,7 @@ module SuperSettings
135
135
  end
136
136
 
137
137
  def set_headers(request)
138
+ request.basic_auth(@user, @password) if @user
138
139
  @headers.each do |name, value|
139
140
  name = name.to_s
140
141
  values = Array(value)
@@ -148,7 +149,7 @@ module SuperSettings
148
149
  def request_uri(path, params = nil)
149
150
  uri = URI.join(@base_uri, path.delete_prefix("/"))
150
151
  if (params && !params.empty?) || (@base_uri.query && !@base_uri.query.empty?)
151
- uri.query = [uri.query, query_string(params)].join("&")
152
+ uri.query = [uri.query, query_string(params)].compact.join("&")
152
153
  end
153
154
  uri
154
155
  end
@@ -120,16 +120,14 @@ module SuperSettings
120
120
  end
121
121
 
122
122
  load_block = lambda do
123
- begin
124
- values = {}
125
- start_time = Time.now
126
- Setting.active.each do |setting|
127
- values[setting.key] = setting.value.freeze
128
- end
129
- set_cache_values(start_time) { values }
130
- ensure
131
- @refreshing = false
123
+ values = {}
124
+ start_time = Time.now
125
+ Setting.active.each do |setting|
126
+ values[setting.key] = setting.value.freeze
132
127
  end
128
+ set_cache_values(start_time) { values }
129
+ ensure
130
+ @refreshing = false
133
131
  end
134
132
 
135
133
  if asynchronous
@@ -158,14 +156,12 @@ module SuperSettings
158
156
  end
159
157
 
160
158
  refresh_block = lambda do
161
- begin
162
- last_db_update = Setting.last_updated_at
163
- if last_db_update.nil? || last_db_update >= last_refresh_time - 1
164
- merge_load(last_refresh_time)
165
- end
166
- ensure
167
- @refreshing = false
159
+ last_db_update = Setting.last_updated_at
160
+ if last_db_update.nil? || last_db_update >= last_refresh_time - 1
161
+ merge_load(last_refresh_time)
168
162
  end
163
+ ensure
164
+ @refreshing = false
169
165
  end
170
166
 
171
167
  if asynchronous
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module SuperSettings
6
+ # Internationalization support for the web UI. Translations are stored as
7
+ # JSON files in +app/locales/+ with one file per locale (e.g. +en.json+).
8
+ #
9
+ # Ruby templates use `#t` to look up a dotted key. JavaScript receives the
10
+ # full translation hash inlined via a +<script>+ tag so that the same JSON
11
+ # file drives both server-side and client-side strings.
12
+ module MiniI18n
13
+ DEFAULT_LOCALE = "en"
14
+
15
+ @cache = {}
16
+ @mutex = Mutex.new
17
+
18
+ class << self
19
+ # Return the list of available locale codes derived from the JSON files
20
+ # present in +app/locales/+.
21
+ #
22
+ # @return [Array<String>] locale codes, e.g. +["en"]+
23
+ def available_locales
24
+ load_all_locales.keys.sort
25
+ end
26
+
27
+ # Look up a translation by dotted key for the given locale.
28
+ # Falls back to the default locale when the key is missing, and
29
+ # ultimately returns the key itself if no translation is found.
30
+ #
31
+ # @param key [String] dotted translation key, e.g. +"page.title"+
32
+ # @param locale [String] the locale code (default: +DEFAULT_LOCALE+)
33
+ # @return [String] the translated string
34
+ def t(key, locale: DEFAULT_LOCALE)
35
+ translations = translations_for(locale)
36
+ translations[key] || translations_for(DEFAULT_LOCALE)[key] || key
37
+ end
38
+
39
+ # Return the full translation hash for a locale. Used to inline
40
+ # translations into the HTML page for JavaScript consumption.
41
+ #
42
+ # @param locale [String] the locale code
43
+ # @return [Hash<String, String>] all key/value translations
44
+ def translations_for(locale)
45
+ load_all_locales[normalize_locale(locale)] || load_all_locales[DEFAULT_LOCALE] || {}
46
+ end
47
+
48
+ # Return the text direction for the given locale. Reads the +"dir"+ key
49
+ # from the locale's translations hash, falling back to +"ltr"+ when not set.
50
+ #
51
+ # @param locale [String] the locale code
52
+ # @return [String] +"ltr"+ or +"rtl"+
53
+ def text_direction(locale = DEFAULT_LOCALE)
54
+ dir = translations_for(locale)["dir"]
55
+ (dir == "rtl") ? "rtl" : "ltr"
56
+ end
57
+
58
+ # Clear the translation cache. Called automatically in development mode.
59
+ #
60
+ # @return [void]
61
+ def clear_cache!
62
+ @mutex.synchronize { @cache = {} }
63
+ end
64
+
65
+ private
66
+
67
+ # Normalize a locale string to just the language subtag if the full
68
+ # tag is not available (e.g. "en-US" → "en").
69
+ def normalize_locale(locale)
70
+ locale = locale.to_s.strip.tr("_", "-").downcase
71
+ return locale if @cache.key?(locale)
72
+
73
+ # Try the language subtag (e.g. "en-us" → "en")
74
+ lang = locale.split("-").first
75
+ lang if @cache.key?(lang)
76
+ end
77
+
78
+ # Load every JSON file from the locales directory, keyed by filename
79
+ # stem (e.g. "en").
80
+ def load_all_locales
81
+ if development_mode?
82
+ @mutex.synchronize { @cache = {} }
83
+ end
84
+
85
+ return @cache unless @cache.empty?
86
+
87
+ @mutex.synchronize do
88
+ return @cache unless @cache.empty?
89
+
90
+ Dir.glob(File.join(locales_dir, "*.json")).each do |path|
91
+ code = File.basename(path, ".json").downcase
92
+ @cache[code] = JSON.parse(File.read(path))
93
+ rescue JSON::ParserError
94
+ # Skip malformed locale files
95
+ end
96
+ end
97
+
98
+ @cache
99
+ end
100
+
101
+ def locales_dir
102
+ File.expand_path(File.join("..", "..", "app", "locales"), __dir__)
103
+ end
104
+
105
+ def development_mode?
106
+ ENV.fetch("RACK_ENV", ENV.fetch("RAILS_ENV", "development")) == "development"
107
+ end
108
+ end
109
+ end
110
+ end
@@ -104,9 +104,9 @@ module SuperSettings
104
104
  # Subclasses can override this method to return the path to an ERB file to use as the layout
105
105
  # for the HTML application. The layout can use any of the methods defined in SuperSettings::Application::Helper.
106
106
  #
107
- # @return [String]
107
+ # @return [String, Symbol]
108
108
  def layout
109
- "layout.html.erb"
109
+ :default
110
110
  end
111
111
 
112
112
  # Subclasses can override this method to add custom HTML to the <head> element in the HTML application.
@@ -133,12 +133,16 @@ module SuperSettings
133
133
  if request.get?
134
134
  if (path == "/" || path == "") && web_ui_enabled?
135
135
  return handle_root_request(request)
136
+ elsif path == "/authorized"
137
+ return handle_authorization_request(request)
138
+ elsif path == "/api.js"
139
+ return handle_api_js_request(request)
136
140
  elsif path == "/settings"
137
141
  return handle_index_request(request)
138
- elsif path == "/setting"
139
- return handle_show_request(request)
140
142
  elsif path == "/setting/history"
141
143
  return handle_history_request(request)
144
+ elsif path == "/setting"
145
+ return handle_show_request(request)
142
146
  elsif path == "/last_updated_at"
143
147
  return handle_last_updated_at_request(request)
144
148
  elsif path == "/updated_since"
@@ -157,9 +161,40 @@ module SuperSettings
157
161
  end
158
162
  end
159
163
 
164
+ def handle_authorization_request(request)
165
+ check_authorization(request) do |user|
166
+ read_only = !allow_write?(user) || !!request.env["super_settings.read_only"]
167
+ permission = read_only ? "read-only" : "read-write"
168
+ payload = {authorized: true, permission: permission}
169
+ [200, {"content-type" => "application/json; charset=utf-8", "cache-control" => "no-cache", "super-settings-permission" => permission}, [JSON.generate(payload)]]
170
+ end
171
+ end
172
+
173
+ def handle_api_js_request(request)
174
+ check_authorization(request) do |user|
175
+ js = File.read(File.expand_path(File.join("application", "api.js"), __dir__))
176
+ [200, {"content-type" => "application/javascript; charset=utf-8", "cache-control" => "no-cache"}, [js]]
177
+ end
178
+ end
179
+
160
180
  def handle_root_request(request)
161
- response = check_authorization(request, write_required: true) do |user|
162
- [200, {"content-type" => "text/html; charset=utf-8", "cache-control" => "no-cache"}, [Application.new(layout: :default, add_to_head: add_to_head(request), color_scheme: SuperSettings.configuration.controller.color_scheme).render]]
181
+ response = check_authorization(request) do |user|
182
+ read_only = !allow_write?(user) || !!request.env["super_settings.read_only"]
183
+ locale = resolve_locale(request)
184
+ headers = {"content-type" => "text/html; charset=utf-8", "cache-control" => "no-cache"}
185
+ lang = request.GET["lang"] if request.respond_to?(:GET)
186
+ if lang && SuperSettings::MiniI18n.available_locales.include?(lang)
187
+ headers["set-cookie"] = "super_settings_locale=#{lang}; path=/; SameSite=Lax"
188
+ end
189
+ application = Application.new(
190
+ layout: layout,
191
+ add_to_head: add_to_head(request),
192
+ color_scheme: SuperSettings.configuration.controller.color_scheme,
193
+ dark_mode_selector: SuperSettings.configuration.controller.resolved_dark_mode_selector,
194
+ read_only: read_only,
195
+ locale: locale
196
+ )
197
+ [200, headers, [application.render]]
163
198
  end
164
199
 
165
200
  if [401, 403].include?(response.first)
@@ -224,11 +259,15 @@ module SuperSettings
224
259
 
225
260
  def check_authorization(request, write_required: false)
226
261
  user = current_user(request)
227
- return json_response(401, error: "Authentiation required") unless authenticated?(user)
262
+ return json_response(401, error: "Authentication required") unless authenticated?(user)
228
263
 
229
264
  allowed = (write_required ? allow_write?(user) : allow_read?(user))
230
265
  return json_response(403, error: "Access denied") unless allowed
231
266
 
267
+ if write_required && request.env["super_settings.read_only"]
268
+ return json_response(403, error: "Access denied")
269
+ end
270
+
232
271
  yield(user)
233
272
  end
234
273
 
@@ -236,6 +275,54 @@ module SuperSettings
236
275
  [status, RESPONSE_HEADERS.dup, [payload.to_json]]
237
276
  end
238
277
 
278
+ # Determine the locale for a request. Precedence:
279
+ # 1. ?lang= query parameter
280
+ # 2. super_settings_locale cookie (set by the language picker)
281
+ # 3. Accept-Language header
282
+ # 4. Default locale
283
+ def resolve_locale(request)
284
+ available = SuperSettings::MiniI18n.available_locales
285
+
286
+ # 1. Explicit query parameter
287
+ lang = request.GET["lang"] if request.respond_to?(:GET)
288
+ return lang if lang && available.include?(lang)
289
+
290
+ # 2. Cookie
291
+ cookie = request.cookies["super_settings_locale"] if request.respond_to?(:cookies)
292
+ return cookie if cookie && available.include?(cookie)
293
+
294
+ # 3. Accept-Language header
295
+ accept = request.env["HTTP_ACCEPT_LANGUAGE"] if request.respond_to?(:env)
296
+ locale_from_accept_language(accept.to_s, available) || SuperSettings::MiniI18n::DEFAULT_LOCALE
297
+ end
298
+
299
+ # Parse the Accept-Language header and return the best matching locale.
300
+ def locale_from_accept_language(header, available)
301
+ return nil if header.nil? || header.empty?
302
+
303
+ # Parse tags with optional quality values, e.g. "en-US,en;q=0.9,fr;q=0.8"
304
+ tags = header.split(",").map { |entry|
305
+ parts = entry.strip.split(";")
306
+ tag = parts[0].to_s.strip.downcase.tr("_", "-")
307
+ q = 1.0
308
+ parts[1..].each do |p|
309
+ if p.strip.start_with?("q=")
310
+ q = p.strip.sub("q=", "").to_f
311
+ end
312
+ end
313
+ [tag, q]
314
+ }.sort_by { |_, q| -q }
315
+
316
+ tags.each do |tag, _|
317
+ return tag if available.include?(tag)
318
+ # Try language subtag
319
+ lang = tag.split("-").first
320
+ return lang if available.include?(lang)
321
+ end
322
+
323
+ nil
324
+ end
325
+
239
326
  def post_params(request)
240
327
  if request.content_type.to_s.match?(/\Aapplication\/json/i) && request.body
241
328
  request.params.merge(JSON.parse(request.body.read))
@@ -83,7 +83,7 @@ module SuperSettings
83
83
  #
84
84
  # The response will be either
85
85
  #
86
- # {success: true}
86
+ # {success: true, values: {key => value, ...}}
87
87
  #
88
88
  # or
89
89
  #
@@ -93,7 +93,11 @@ module SuperSettings
93
93
  def update(settings_params, changed_by = nil)
94
94
  all_valid, settings = Setting.bulk_update(Array(settings_params), changed_by)
95
95
  if all_valid
96
- {success: true}
96
+ values = {}
97
+ settings.each do |setting|
98
+ values[setting.key] = setting.value
99
+ end
100
+ {success: true, values: values}
97
101
  else
98
102
  errors = {}
99
103
  settings.each do |setting|
@@ -179,7 +183,7 @@ module SuperSettings
179
183
  #
180
184
  # @return [Hash] hash with the last updated timestamp
181
185
  def last_updated_at
182
- {last_updated_at: Setting.last_updated_at.utc.iso8601(6)}
186
+ {last_updated_at: Setting.last_updated_at&.utc&.iso8601(6)}
183
187
  end
184
188
 
185
189
  # Return settings that have been updated since a specified timestamp.
@@ -266,7 +266,7 @@ module SuperSettings
266
266
  key = setting_params["key"]
267
267
  setting = changed[key] || Setting.find_by_key(key)
268
268
  unless setting
269
- next if Coerce.present?(setting_params["delete"])
269
+ next if Coerce.present?(setting_params["deleted"])
270
270
 
271
271
  setting = Setting.new(key: setting_params["key"])
272
272
  end
@@ -17,15 +17,13 @@ module SuperSettings
17
17
  # ActiveRecord storage is only available if the connection pool is connected and the table exists.
18
18
 
19
19
  def available?
20
- begin
21
- # table_exists? will attempt to retrieve a connection from the pool and load the schema_cache
22
- # which is memoized per connection. If there is no database or connection, it will raise an error.
23
- table_exists?
24
- rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
25
- # Ignore errors so the application doesn't break if the database is not available.
26
- # Otherwise things like build processes can fail.
27
- false
28
- end
20
+ # table_exists? will attempt to retrieve a connection from the pool and load the schema_cache
21
+ # which is memoized per connection. If there is no database or connection, it will raise an error.
22
+ table_exists?
23
+ rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
24
+ # Ignore errors so the application doesn't break if the database is not available.
25
+ # Otherwise things like build processes can fail.
26
+ false
29
27
  end
30
28
  end
31
29
  end
@@ -6,7 +6,7 @@ module SuperSettings
6
6
  # This storage engine is read only. It is intended to allow microservices to read settings from a
7
7
  # central application that exposes the SuperSettings::RestAPI.
8
8
  #
9
- # You must the the base_url class attribute to the base URL of a SuperSettings REST API endpoint.
9
+ # You must set the base_url class attribute to the base URL of a SuperSettings REST API endpoint.
10
10
  # You can also set the timeout, headers, and query_params used in reqeusts to the API.
11
11
  class HttpStorage < StorageAttributes
12
12
  include Storage
@@ -135,7 +135,6 @@ module SuperSettings
135
135
  end
136
136
 
137
137
  def reload
138
- self.class.find_by_key(key)
139
138
  self.attributes = self.class.find_by_key(key).attributes
140
139
  self
141
140
  end
@@ -112,7 +112,7 @@ module SuperSettings
112
112
  end
113
113
  end
114
114
 
115
- # Heper method to load settings from a JSON string.
115
+ # Helper method to load settings from a JSON string.
116
116
  #
117
117
  # @param json [String] JSON string to parse.
118
118
  # @return [Array<SuperSettings::Storage::JSONStorage>] Array of settings.
@@ -119,6 +119,7 @@ module SuperSettings
119
119
 
120
120
  def save_all(changes)
121
121
  upserts = changes.collect { |setting| upsert(setting) }
122
+ return true if upserts.empty?
122
123
  settings_collection.bulk_write(upserts)
123
124
  true
124
125
  end
@@ -191,7 +192,7 @@ module SuperSettings
191
192
  pipeline << {
192
193
  "$addFields": {
193
194
  history: {
194
- "$slice": ["$history", offset, (limit || {"$size": "$history"})]
195
+ "$slice": ["$history", offset, limit || {"$size": "$history"}]
195
196
  }
196
197
  }
197
198
  }
@@ -61,7 +61,7 @@ module SuperSettings
61
61
  end
62
62
 
63
63
  def path=(value)
64
- @path = "#{value}.chomp('/')/"
64
+ @path = "#{value.chomp("/")}/"
65
65
  end
66
66
 
67
67
  def hash
@@ -17,7 +17,7 @@ module SuperSettings
17
17
 
18
18
  def self.included(base)
19
19
  base.extend(ClassMethods)
20
- base.include(Attributes) unless base.instance_methods.include?(:attributes=)
20
+ base.include(Attributes) unless base.method_defined?(:attributes=)
21
21
 
22
22
  base.instance_variable_set(:@load_asynchronous, nil)
23
23
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SuperSettings
4
- VERSION = File.read(File.expand_path("../../VERSION", __dir__)).chomp
4
+ VERSION = File.read(File.expand_path("../../VERSION", __dir__)).strip.freeze
5
5
  end