super_settings 1.0.1 → 2.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -2
- data/README.md +121 -16
- data/VERSION +1 -1
- data/app/helpers/super_settings/settings_helper.rb +13 -3
- data/app/views/layouts/super_settings/settings.html.erb +1 -1
- data/config/routes.rb +1 -1
- data/db/migrate/20210414004553_create_super_settings.rb +1 -7
- data/lib/super_settings/application/api.js +4 -1
- data/lib/super_settings/application/helper.rb +56 -17
- data/lib/super_settings/application/images/arrow-down-short.svg +3 -0
- data/lib/super_settings/application/images/arrow-up-short.svg +3 -0
- data/lib/super_settings/application/images/info-circle.svg +4 -0
- data/lib/super_settings/application/images/pencil-square.svg +4 -0
- data/lib/super_settings/application/images/plus.svg +3 -1
- data/lib/super_settings/application/images/trash3.svg +3 -0
- data/lib/super_settings/application/images/x-circle.svg +4 -0
- data/lib/super_settings/application/index.html.erb +54 -37
- data/lib/super_settings/application/layout.html.erb +5 -2
- data/lib/super_settings/application/layout_styles.css +7 -151
- data/lib/super_settings/application/layout_vars.css.erb +21 -0
- data/lib/super_settings/application/scripts.js +100 -21
- data/lib/super_settings/application/style_vars.css.erb +62 -0
- data/lib/super_settings/application/styles.css +183 -14
- data/lib/super_settings/application.rb +18 -11
- data/lib/super_settings/attributes.rb +1 -8
- data/lib/super_settings/configuration.rb +9 -0
- data/lib/super_settings/context/current.rb +33 -0
- data/lib/super_settings/context.rb +3 -0
- data/lib/super_settings/controller_actions.rb +2 -2
- data/lib/super_settings/engine.rb +1 -3
- data/lib/super_settings/history_item.rb +1 -1
- data/lib/super_settings/http_client.rb +165 -0
- data/lib/super_settings/local_cache.rb +0 -15
- data/lib/super_settings/rack_application.rb +3 -3
- data/lib/super_settings/rest_api.rb +5 -4
- data/lib/super_settings/setting.rb +14 -3
- data/lib/super_settings/storage/active_record_storage/models.rb +28 -0
- data/lib/super_settings/storage/active_record_storage.rb +10 -20
- data/lib/super_settings/storage/history_attributes.rb +31 -0
- data/lib/super_settings/storage/http_storage.rb +60 -184
- data/lib/super_settings/storage/json_storage.rb +201 -0
- data/lib/super_settings/storage/mongodb_storage.rb +238 -0
- data/lib/super_settings/storage/redis_storage.rb +50 -111
- data/lib/super_settings/storage/s3_storage.rb +165 -0
- data/lib/super_settings/storage/storage_attributes.rb +64 -0
- data/lib/super_settings/storage/test_storage.rb +3 -5
- data/lib/super_settings/storage/transaction.rb +67 -0
- data/lib/super_settings/storage.rb +17 -8
- data/lib/super_settings/time_precision.rb +36 -0
- data/lib/super_settings.rb +48 -13
- data/super_settings.gemspec +11 -2
- metadata +30 -12
- data/lib/super_settings/application/images/edit.svg +0 -1
- data/lib/super_settings/application/images/info.svg +0 -1
- data/lib/super_settings/application/images/slash.svg +0 -1
- data/lib/super_settings/application/images/trash.svg +0 -1
- /data/{MIT-LICENSE → MIT-LICENSE.txt} +0 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
.super-settings {
|
2
|
+
--primary-color-h: 216;
|
3
|
+
--primary-color-s: 98%;
|
4
|
+
--primary-color-l: 52%;
|
5
|
+
--primary-color-hsl: var(--primary-color-h), var(--primary-color-s), var(--primary-color-l);
|
6
|
+
--primary-color: hsl(var(--primary-color-hsl));
|
7
|
+
--primary-contrast-color: #fff;
|
8
|
+
--primary-hover-color: hsl(var(--primary-color-h), var(--primary-color-s), calc(var(--primary-color-l) - 10%));
|
9
|
+
--primary-border-color: hsl(var(--primary-color-h), var(--primary-color-s), calc(var(--primary-color-l) + 10%));
|
10
|
+
}
|
11
|
+
|
12
|
+
<% unless color_scheme == :dark %>
|
13
|
+
.super-settings {
|
14
|
+
--edit-bg-color: #f2fdf2;
|
15
|
+
--deleted-row-color: darkred;
|
16
|
+
--deleted-row-bg-color: #ffd1d8;
|
17
|
+
--history-key-color: royalblue;
|
18
|
+
--table-header-bg-color: #fff;
|
19
|
+
--table-border-color: #dee2e6;
|
20
|
+
--alt-row-color: rgba(0, 0, 0, .05);
|
21
|
+
--form-control-color: #495057;
|
22
|
+
--form-control-bg-color: #fff;
|
23
|
+
--form-control-border-color: #ced4da;
|
24
|
+
--form-control-placeholder-color: #bbb;
|
25
|
+
--modal-bg-color: #fff;
|
26
|
+
--modal-transparency: 0.4;
|
27
|
+
--modal-control-color: #000;
|
28
|
+
--success-color: green;
|
29
|
+
--danger-color: firebrick;
|
30
|
+
--muted-color: #666;
|
31
|
+
--unselected-color: #666;
|
32
|
+
}
|
33
|
+
<% end %>
|
34
|
+
|
35
|
+
<% if color_scheme == :system %>
|
36
|
+
@media (prefers-color-scheme: dark) {
|
37
|
+
<% end %>
|
38
|
+
<% if color_scheme == :system || color_scheme == :dark %>
|
39
|
+
.super-settings {
|
40
|
+
--edit-bg-color: #8dc875;
|
41
|
+
--deleted-row-color: #e0b1b8;
|
42
|
+
--deleted-row-bg-color: #7a3636;
|
43
|
+
--history-key-color: #a7d6f4;
|
44
|
+
--table-header-bg-color: #333;
|
45
|
+
--table-border-color: #555;
|
46
|
+
--alt-row-color: rgba(0, 0, 0, .30);
|
47
|
+
--form-control-color: #eee;
|
48
|
+
--form-control-bg-color: #666;
|
49
|
+
--form-control-border-color: #555;
|
50
|
+
--form-control-placeholder-color: #aaa;
|
51
|
+
--modal-bg-color: #333;
|
52
|
+
--modal-transparency: 0.75;
|
53
|
+
--modal-control-color: #fff;
|
54
|
+
--success-color: #00ff00;
|
55
|
+
--danger-color: #ff0000;
|
56
|
+
--muted-color: #999;
|
57
|
+
--unselected-color: #ccc;
|
58
|
+
}
|
59
|
+
<% end %>
|
60
|
+
<% if color_scheme == :system %>
|
61
|
+
}
|
62
|
+
<% end %>
|
@@ -1,3 +1,10 @@
|
|
1
|
+
.super-settings-container {
|
2
|
+
padding-left: 15px;
|
3
|
+
padding-right: 15px;
|
4
|
+
margin-left: auto;
|
5
|
+
margin-right: auto;
|
6
|
+
}
|
7
|
+
|
1
8
|
#settings-table td p {
|
2
9
|
margin-top: 0;
|
3
10
|
margin-bottom: 0.5rem;
|
@@ -11,13 +18,9 @@
|
|
11
18
|
width: 100%;
|
12
19
|
}
|
13
20
|
|
14
|
-
.super-settings-edit-row {
|
15
|
-
background-color: #f2fdf2 !important;
|
16
|
-
}
|
17
|
-
|
18
21
|
#settings-table tr[data-deleted] td {
|
19
|
-
background-color:
|
20
|
-
color:
|
22
|
+
background-color: var(--deleted-row-bg-color) !important;
|
23
|
+
color: var(--deleted-row-color);
|
21
24
|
text-decoration: line-through;
|
22
25
|
}
|
23
26
|
|
@@ -25,16 +28,49 @@
|
|
25
28
|
display: none;
|
26
29
|
}
|
27
30
|
|
31
|
+
.super-settings-icon {
|
32
|
+
display: inline-block;
|
33
|
+
}
|
34
|
+
|
35
|
+
.super-settings-icon svg {
|
36
|
+
width: 100%;
|
37
|
+
height: 100%;
|
38
|
+
vertical-align: inherit;
|
39
|
+
}
|
40
|
+
|
41
|
+
.super-settings-btn-no-chrome {
|
42
|
+
display: inline-block;
|
43
|
+
vertical-align: middle;
|
44
|
+
background-color: transparent;
|
45
|
+
border: 0;
|
46
|
+
padding: 0;
|
47
|
+
cursor: pointer;
|
48
|
+
}
|
49
|
+
|
50
|
+
|
51
|
+
|
52
|
+
.super-settings-sort-control {
|
53
|
+
color: var(--unselected-color);
|
54
|
+
}
|
55
|
+
|
56
|
+
.super-settings-sort-control svg {
|
57
|
+
vertical-align: middle;
|
58
|
+
}
|
59
|
+
|
60
|
+
.super-settings-edit-row {
|
61
|
+
background-color: var(--edit-bg-color) !important;
|
62
|
+
}
|
63
|
+
|
28
64
|
.super-settings-key {
|
29
65
|
overflow-wrap: break-word;
|
30
66
|
max-width: 30rem;
|
31
|
-
min-width:
|
67
|
+
min-width: 8rem;
|
32
68
|
}
|
33
69
|
|
34
70
|
.super-settings-value {
|
35
71
|
overflow-wrap: break-word;
|
36
72
|
max-width: 30rem;
|
37
|
-
min-width:
|
73
|
+
min-width: 8rem;
|
38
74
|
}
|
39
75
|
|
40
76
|
.super-settings-value-type {
|
@@ -59,7 +95,34 @@
|
|
59
95
|
|
60
96
|
.super-settings-history-key {
|
61
97
|
font-weight: normal;
|
62
|
-
color:
|
98
|
+
color: var(--history-key-color);
|
99
|
+
}
|
100
|
+
|
101
|
+
.super-settings-table {
|
102
|
+
width: 100%;
|
103
|
+
max-width: 100%;
|
104
|
+
margin-bottom: 1rem;
|
105
|
+
border-collapse: collapse;
|
106
|
+
}
|
107
|
+
|
108
|
+
.super-settings-table thead th {
|
109
|
+
vertical-align: bottom;
|
110
|
+
border-bottom: 2px solid var(--table-border-color);
|
111
|
+
white-space: nowrap;
|
112
|
+
}
|
113
|
+
|
114
|
+
.super-settings-table td, .super-settings-table th {
|
115
|
+
padding: 0.75rem;
|
116
|
+
vertical-align: top;
|
117
|
+
border-top: 1px solid var(--table-border-color);
|
118
|
+
}
|
119
|
+
|
120
|
+
.super-settings-table-striped tbody tr:nth-of-type(odd) {
|
121
|
+
background-color: var(--alt-row-color);
|
122
|
+
}
|
123
|
+
|
124
|
+
.super-settings-align-center {
|
125
|
+
text-align: center;
|
63
126
|
}
|
64
127
|
|
65
128
|
.super-settings-modal {
|
@@ -72,12 +135,12 @@
|
|
72
135
|
width: 100%;
|
73
136
|
height: 100%;
|
74
137
|
overflow: auto;
|
75
|
-
background-color: rgba(0,0,0,
|
138
|
+
background-color: rgba(0, 0, 0, var(--modal-transparency));
|
76
139
|
}
|
77
140
|
|
78
141
|
.super-settings-modal-dialog {
|
79
142
|
margin: auto;
|
80
|
-
background-color:
|
143
|
+
background-color: var(--modal-bg-color);
|
81
144
|
position: relative;
|
82
145
|
outline: 0;
|
83
146
|
padding: 2em;
|
@@ -99,7 +162,7 @@
|
|
99
162
|
right: 0;
|
100
163
|
border: 0;
|
101
164
|
padding: 1ex;
|
102
|
-
|
165
|
+
color: var(--modal-control-color);
|
103
166
|
font-size: 1.5rem;
|
104
167
|
font-weight: 500;
|
105
168
|
}
|
@@ -122,6 +185,112 @@
|
|
122
185
|
.super-settings-sticky-top {
|
123
186
|
position: sticky;
|
124
187
|
top: 0;
|
125
|
-
padding: 1rem
|
126
|
-
background-color:
|
188
|
+
padding: 1rem;
|
189
|
+
background-color: var(--table-header-bg-color);
|
190
|
+
}
|
191
|
+
|
192
|
+
.super-settings-form-control {
|
193
|
+
font-family: inherit;
|
194
|
+
margin: 0;
|
195
|
+
overflow: visible;
|
196
|
+
display: block;
|
197
|
+
width: 100%;
|
198
|
+
padding: .375rem .75rem;
|
199
|
+
font-size: 1rem;
|
200
|
+
line-height: 1.5;
|
201
|
+
color: var(--form-control-color);
|
202
|
+
background-color: var(--form-control-bg-color);
|
203
|
+
background-clip: padding-box;
|
204
|
+
border: 1px solid var(--form-control-border-color);
|
205
|
+
border-radius: .25rem;
|
206
|
+
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
207
|
+
}
|
208
|
+
|
209
|
+
.super-settings-form-control::placeholder {
|
210
|
+
color: var(--form-control-placeholder-color);
|
211
|
+
}
|
212
|
+
|
213
|
+
select.super-settings-form-control:not([size]):not([multiple]) {
|
214
|
+
height: calc(2.25rem + 2px);
|
215
|
+
}
|
216
|
+
|
217
|
+
.super-settings-form-check {
|
218
|
+
margin-top: .5rem;
|
219
|
+
display: inline-block;
|
220
|
+
}
|
221
|
+
|
222
|
+
.super-settings-form-check input[type=checkbox] {
|
223
|
+
vertical-align: middle;
|
224
|
+
}
|
225
|
+
|
226
|
+
.super-settings-form-inline {
|
227
|
+
display: inline-block;
|
228
|
+
}
|
229
|
+
|
230
|
+
.super-settings-form-inline .super-settings-form-control {
|
231
|
+
display: inline-block;
|
232
|
+
width: auto;
|
233
|
+
vertical-align: middle;
|
234
|
+
}
|
235
|
+
|
236
|
+
.super-settings-form-control textarea {
|
237
|
+
font-family: inherit;
|
238
|
+
margin: 0;
|
239
|
+
overflow: auto;
|
240
|
+
resize: vertical;
|
241
|
+
}
|
242
|
+
|
243
|
+
.super-settings-text-success {
|
244
|
+
color: var(--success-color) !important;
|
245
|
+
}
|
246
|
+
|
247
|
+
.super-settings-text-danger {
|
248
|
+
color: var(--danger-color) !important;
|
249
|
+
}
|
250
|
+
|
251
|
+
.super-settings-text-muted {
|
252
|
+
color: var(--muted-color) !important;
|
253
|
+
}
|
254
|
+
|
255
|
+
.super-settings-btn {
|
256
|
+
display: inline-block;
|
257
|
+
vertical-align: middle;
|
258
|
+
padding: 0.375rem 0.75rem;
|
259
|
+
border-radius: 0.375rem;
|
260
|
+
border: 1px solid #dcdcdc;
|
261
|
+
background-color:#f9f9f9;
|
262
|
+
text-decoration: none;
|
263
|
+
text-align: center;
|
264
|
+
font-family: Arial, sans-serif;
|
265
|
+
font-size: 1rem;
|
266
|
+
font-weight: 400;
|
267
|
+
line-height: 1.5;
|
268
|
+
user-select: none;
|
269
|
+
cursor: pointer;
|
270
|
+
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
271
|
+
}
|
272
|
+
|
273
|
+
.super-settings-btn:hover:not(:disabled) {
|
274
|
+
background-color:#e9e9e9;
|
275
|
+
}
|
276
|
+
.super-settings-btn:active {
|
277
|
+
position:relative;
|
278
|
+
top:1px;
|
279
|
+
}
|
280
|
+
.super-settings-btn:disabled {
|
281
|
+
opacity: 0.8;
|
282
|
+
cursor: not-allowed;
|
283
|
+
}
|
284
|
+
|
285
|
+
.super-settings-btn-primary {
|
286
|
+
background-color: var(--primary-color);
|
287
|
+
border-color: var(--primary-border-color);
|
288
|
+
color: var(--primary-contrast-color);
|
289
|
+
}
|
290
|
+
.super-settings-btn-primary:hover:not(:disabled) {
|
291
|
+
background-color: var(--primary-hover-color);
|
292
|
+
}
|
293
|
+
|
294
|
+
.super-settings-btn-primary:disabled {
|
295
|
+
opacity: 0.5;
|
127
296
|
}
|
@@ -10,31 +10,38 @@ module SuperSettings
|
|
10
10
|
# @param layout [String, Symbol] path to an ERB template to use as the layout around the application UI. You can
|
11
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
|
+
# @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)
|
14
17
|
if layout
|
15
18
|
layout = File.expand_path(File.join("application", "layout.html.erb"), __dir__) if layout == :default
|
16
|
-
@layout = ERB.new(File.read(layout))
|
19
|
+
@layout = ERB.new(File.read(layout)) if layout
|
17
20
|
@add_to_head = add_to_head
|
21
|
+
else
|
22
|
+
@layout = nil
|
23
|
+
@add_to_head = nil
|
18
24
|
end
|
25
|
+
|
26
|
+
@api_base_url = api_base_url
|
27
|
+
@color_scheme = color_scheme&.to_sym
|
19
28
|
end
|
20
29
|
|
21
|
-
# Render the
|
30
|
+
# Render the web UI application HTML.
|
22
31
|
#
|
23
32
|
# @return [void]
|
24
|
-
def render
|
25
|
-
template = ERB.new(File.read(File.expand_path(File.join("application",
|
33
|
+
def render
|
34
|
+
template = ERB.new(File.read(File.expand_path(File.join("application", "index.html.erb"), __dir__)))
|
26
35
|
html = template.result(binding)
|
27
|
-
if @layout
|
28
|
-
|
29
|
-
|
30
|
-
html
|
31
|
-
end
|
36
|
+
html = render_layout { html } if @layout
|
37
|
+
html = html.html_safe if html.respond_to?(:html_safe)
|
38
|
+
html
|
32
39
|
end
|
33
40
|
|
34
41
|
private
|
35
42
|
|
36
43
|
def render_layout
|
37
|
-
@layout
|
44
|
+
@layout&.result(binding)
|
38
45
|
end
|
39
46
|
end
|
40
47
|
end
|
@@ -4,20 +4,13 @@ module SuperSettings
|
|
4
4
|
# Interface to expose mass setting attributes on an object. Setting attributes with a
|
5
5
|
# hash will simply call the attribute writers for each key in the hash.
|
6
6
|
module Attributes
|
7
|
-
class UnknownAttributeError < StandardError
|
8
|
-
end
|
9
|
-
|
10
7
|
def initialize(attributes = nil)
|
11
8
|
self.attributes = attributes if attributes
|
12
9
|
end
|
13
10
|
|
14
11
|
def attributes=(values)
|
15
12
|
values.each do |name, value|
|
16
|
-
if respond_to?("#{name}=", true)
|
17
|
-
send("#{name}=", value)
|
18
|
-
else
|
19
|
-
raise UnknownAttributeError.new("unknown attribute #{name.to_s.inspect} for #{self.class}")
|
20
|
-
end
|
13
|
+
send(:"#{name}=", value) if respond_to?(:"#{name}=", true)
|
21
14
|
end
|
22
15
|
end
|
23
16
|
end
|
@@ -25,6 +25,7 @@ module SuperSettings
|
|
25
25
|
def initialize
|
26
26
|
@superclass = nil
|
27
27
|
@web_ui_enabled = true
|
28
|
+
@color_scheme = false
|
28
29
|
@changed_by_block = nil
|
29
30
|
end
|
30
31
|
|
@@ -63,6 +64,14 @@ module SuperSettings
|
|
63
64
|
!!@web_ui_enabled
|
64
65
|
end
|
65
66
|
|
67
|
+
# Set dark mode for the web UI. Possible values are :light, :dark, or :system.
|
68
|
+
# The default value is :light.
|
69
|
+
attr_writer :color_scheme
|
70
|
+
|
71
|
+
def color_scheme
|
72
|
+
(@color_scheme ||= :light).to_sym
|
73
|
+
end
|
74
|
+
|
66
75
|
# Enhance the controller. You can define methods or call controller class methods like
|
67
76
|
# +before_action+, etc. in the block. These will be applied to the engine controller.
|
68
77
|
# This is essentially the same a monkeypatching the controller class.
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SuperSettings
|
4
|
+
module Context
|
5
|
+
class Current
|
6
|
+
def initialize
|
7
|
+
@context = {}
|
8
|
+
@seed = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def include?(key)
|
12
|
+
@context.include?(key)
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](key)
|
16
|
+
@context[key]
|
17
|
+
end
|
18
|
+
|
19
|
+
def []=(key, value)
|
20
|
+
@context[key] = value
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete(key)
|
24
|
+
@context.delete(key)
|
25
|
+
end
|
26
|
+
|
27
|
+
def rand(max = nil)
|
28
|
+
@seed ||= Random.new_seed
|
29
|
+
Random.new(@seed).rand(max || 1.0)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -19,7 +19,7 @@ module SuperSettings
|
|
19
19
|
|
20
20
|
# Render the HTML application for managing settings.
|
21
21
|
def root
|
22
|
-
html = SuperSettings::Application.new.render
|
22
|
+
html = SuperSettings::Application.new.render
|
23
23
|
render html: html.html_safe, layout: true
|
24
24
|
end
|
25
25
|
|
@@ -40,7 +40,7 @@ module SuperSettings
|
|
40
40
|
|
41
41
|
# API endpoint for updating settings. See SuperSettings::RestAPI for details.
|
42
42
|
def update
|
43
|
-
changed_by =
|
43
|
+
changed_by = SuperSettings.configuration.controller.changed_by(self)
|
44
44
|
result = SuperSettings::RestAPI.update(params[:settings], changed_by)
|
45
45
|
if result[:success]
|
46
46
|
render json: result
|
@@ -17,8 +17,6 @@ module SuperSettings
|
|
17
17
|
end
|
18
18
|
|
19
19
|
if defined?(Sidekiq.server?) && Sidekiq.server?
|
20
|
-
require_relative "context/sidekiq_middleware"
|
21
|
-
|
22
20
|
Sidekiq.configure_server do |sidekiq_config|
|
23
21
|
sidekiq_config.server_middleware do |chain|
|
24
22
|
chain.prepend(SuperSettings::Context::SidekiqMiddleware)
|
@@ -29,7 +27,7 @@ module SuperSettings
|
|
29
27
|
|
30
28
|
config.after_initialize do
|
31
29
|
# Call the deferred initialization block.
|
32
|
-
configuration =
|
30
|
+
configuration = SuperSettings.configuration
|
33
31
|
configuration.call
|
34
32
|
|
35
33
|
SuperSettings.refresh_interval = configuration.refresh_interval unless configuration.refresh_interval.nil?
|
@@ -27,7 +27,7 @@ module SuperSettings
|
|
27
27
|
def changed_by_display
|
28
28
|
return changed_by if changed_by.nil?
|
29
29
|
|
30
|
-
display_proc =
|
30
|
+
display_proc = SuperSettings.configuration.model.changed_by_display
|
31
31
|
if display_proc && !changed_by.nil?
|
32
32
|
display_proc.call(changed_by) || changed_by
|
33
33
|
else
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
module SuperSettings
|
7
|
+
# This is a simple HTTP client that is used to communicate with the REST API. It
|
8
|
+
# will keep the connection alive and reuse it on subsequent requests.
|
9
|
+
class HttpClient
|
10
|
+
DEFAULT_HEADERS = {"Accept" => "application/json"}.freeze
|
11
|
+
DEFAULT_TIMEOUT = 5.0
|
12
|
+
KEEP_ALIVE_TIMEOUT = 60
|
13
|
+
|
14
|
+
class Error < StandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
class NotFoundError < Error
|
18
|
+
end
|
19
|
+
|
20
|
+
class InvalidRecordError < Error
|
21
|
+
attr_reader :errors
|
22
|
+
|
23
|
+
def initialize(message, errors:)
|
24
|
+
super(message)
|
25
|
+
@errors = errors
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(base_url, headers: nil, params: nil, timeout: nil, user: nil, password: nil)
|
30
|
+
base_url = "#{base_url}/" unless base_url.end_with?("/")
|
31
|
+
@base_uri = URI(base_url)
|
32
|
+
@base_uri.query = query_string(params) if params
|
33
|
+
@headers = headers ? DEFAULT_HEADERS.merge(headers) : DEFAULT_HEADERS
|
34
|
+
@timeout = timeout || DEFAULT_TIMEOUT
|
35
|
+
@user = user
|
36
|
+
@password = password
|
37
|
+
@mutex = Mutex.new
|
38
|
+
@connections = []
|
39
|
+
end
|
40
|
+
|
41
|
+
def get(path, params = nil)
|
42
|
+
request = Net::HTTP::Get.new(request_uri(path, params))
|
43
|
+
send_request(request)
|
44
|
+
end
|
45
|
+
|
46
|
+
def post(path, params = nil)
|
47
|
+
request = Net::HTTP::Post.new(request_uri(path))
|
48
|
+
request.body = JSON.dump(params) if params
|
49
|
+
send_request(request)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def send_request(request)
|
55
|
+
set_headers(request)
|
56
|
+
response_payload = nil
|
57
|
+
attempts = 0
|
58
|
+
|
59
|
+
with_connection do |http|
|
60
|
+
http.start unless http.started?
|
61
|
+
response = http.request(request)
|
62
|
+
|
63
|
+
begin
|
64
|
+
response.value # raises exception unless response is a success
|
65
|
+
response_payload = JSON.parse(response.body)
|
66
|
+
rescue Net::ProtocolError
|
67
|
+
if [404, 410].include?(response.code.to_i)
|
68
|
+
raise NotFoundError.new("#{response.code} #{response.message}")
|
69
|
+
elsif response.code.to_i == 422
|
70
|
+
raise InvalidRecordError.new("#{response.code} #{response.message}", errors: JSON.parse(response.body)["errors"])
|
71
|
+
else
|
72
|
+
raise Error.new("#{response.code} #{response.message}")
|
73
|
+
end
|
74
|
+
rescue JSON::JSONError => e
|
75
|
+
raise Error.new(e.message)
|
76
|
+
end
|
77
|
+
rescue IOError, Errno::ECONNRESET => connection_error
|
78
|
+
attempts += 1
|
79
|
+
retry if attempts <= 1
|
80
|
+
raise connection_error
|
81
|
+
end
|
82
|
+
|
83
|
+
response_payload
|
84
|
+
end
|
85
|
+
|
86
|
+
def with_connection(&block)
|
87
|
+
http = pop_connection
|
88
|
+
begin
|
89
|
+
response = yield(http)
|
90
|
+
return_connection(http)
|
91
|
+
response
|
92
|
+
rescue => e
|
93
|
+
begin
|
94
|
+
http.finish if http.started?
|
95
|
+
rescue IOError
|
96
|
+
end
|
97
|
+
raise e
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def pop_connection
|
102
|
+
http = nil
|
103
|
+
@mutex.synchronize do
|
104
|
+
http = @connections.pop
|
105
|
+
end
|
106
|
+
http = nil unless http&.started?
|
107
|
+
http ||= new_connection
|
108
|
+
http
|
109
|
+
end
|
110
|
+
|
111
|
+
def return_connection(http)
|
112
|
+
@mutex.synchronize do
|
113
|
+
if @connections.empty?
|
114
|
+
@connections.push(http)
|
115
|
+
http = nil
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
if http
|
120
|
+
begin
|
121
|
+
http.finish if http.started?
|
122
|
+
rescue IOError
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def new_connection
|
128
|
+
http = Net::HTTP.new(@base_uri.host, @base_uri.port || @base_uri.inferred_port)
|
129
|
+
http.use_ssl = @base_uri.scheme == "https"
|
130
|
+
http.open_timeout = @timeout
|
131
|
+
http.read_timeout = @timeout
|
132
|
+
http.write_timeout = @timeout
|
133
|
+
http.keep_alive_timeout = KEEP_ALIVE_TIMEOUT
|
134
|
+
http
|
135
|
+
end
|
136
|
+
|
137
|
+
def set_headers(request)
|
138
|
+
@headers.each do |name, value|
|
139
|
+
name = name.to_s
|
140
|
+
values = Array(value)
|
141
|
+
request[name] = values[0].to_s
|
142
|
+
values[1, values.length].each do |val|
|
143
|
+
request.add_field(name, val.to_s)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def request_uri(path, params = nil)
|
149
|
+
uri = URI.join(@base_uri, path.delete_prefix("/"))
|
150
|
+
if (params && !params.empty?) || (@base_uri.query && !@base_uri.query.empty?)
|
151
|
+
uri.query = [uri.query, query_string(params)].join("&")
|
152
|
+
end
|
153
|
+
uri
|
154
|
+
end
|
155
|
+
|
156
|
+
def query_string(params)
|
157
|
+
q = []
|
158
|
+
q << @base_uri.query unless @base_uri.query.to_s.empty?
|
159
|
+
params&.each do |name, value|
|
160
|
+
q << "#{URI.encode_www_form_component(name.to_s)}=#{URI.encode_www_form_component(value.to_s)}"
|
161
|
+
end
|
162
|
+
q.join("&")
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -240,21 +240,6 @@ module SuperSettings
|
|
240
240
|
end
|
241
241
|
end
|
242
242
|
|
243
|
-
# Recusive method for creating a nested hash from delimited keys.
|
244
|
-
def set_nested_hash_value(hash, key, value, current_depth, delimiter:, max_depth:)
|
245
|
-
key, sub_key = ((max_depth && current_depth < max_depth) ? [key, nil] : key.split(delimiter, 2))
|
246
|
-
if sub_key
|
247
|
-
sub_hash = hash[key]
|
248
|
-
unless sub_hash.is_a?(Hash)
|
249
|
-
sub_hash = {}
|
250
|
-
hash[key] = sub_hash
|
251
|
-
end
|
252
|
-
set_nested_hash_value(sub_hash, sub_key, value, current_depth + 1, delimiter: delimiter, max_depth: max_depth)
|
253
|
-
else
|
254
|
-
hash[key] = value
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
243
|
# Recursively freeze a hash.
|
259
244
|
def deep_freeze_hash(hash)
|
260
245
|
hash.each_value do |value|
|