motor-admin 0.1.9
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 +7 -0
- data/LICENSE +20 -0
- data/README.md +32 -0
- data/Rakefile +20 -0
- data/app/controllers/motor/alerts_controller.rb +74 -0
- data/app/controllers/motor/api_base_controller.rb +25 -0
- data/app/controllers/motor/application_controller.rb +6 -0
- data/app/controllers/motor/assets_controller.rb +28 -0
- data/app/controllers/motor/configs_controller.rb +30 -0
- data/app/controllers/motor/dashboards_controller.rb +54 -0
- data/app/controllers/motor/data_controller.rb +102 -0
- data/app/controllers/motor/forms_controller.rb +54 -0
- data/app/controllers/motor/queries_controller.rb +54 -0
- data/app/controllers/motor/resource_methods_controller.rb +21 -0
- data/app/controllers/motor/resources_controller.rb +23 -0
- data/app/controllers/motor/run_queries_controller.rb +45 -0
- data/app/controllers/motor/schemas_controller.rb +11 -0
- data/app/controllers/motor/send_alerts_controller.rb +24 -0
- data/app/controllers/motor/tags_controller.rb +11 -0
- data/app/controllers/motor/ui_controller.rb +19 -0
- data/app/jobs/motor/alert_sending_job.rb +13 -0
- data/app/jobs/motor/application_job.rb +6 -0
- data/app/mailers/motor/alerts_mailer.rb +50 -0
- data/app/mailers/motor/application_mailer.rb +7 -0
- data/app/models/motor/alert.rb +22 -0
- data/app/models/motor/alert_lock.rb +7 -0
- data/app/models/motor/application_record.rb +8 -0
- data/app/models/motor/config.rb +7 -0
- data/app/models/motor/dashboard.rb +18 -0
- data/app/models/motor/form.rb +14 -0
- data/app/models/motor/query.rb +14 -0
- data/app/models/motor/resource.rb +7 -0
- data/app/models/motor/tag.rb +7 -0
- data/app/models/motor/taggable_tag.rb +8 -0
- data/app/views/layouts/motor/application.html.erb +14 -0
- data/app/views/motor/alerts_mailer/alert_email.html.erb +126 -0
- data/app/views/motor/ui/show.html.erb +1 -0
- data/config/routes.rb +60 -0
- data/lib/generators/motor/install_generator.rb +22 -0
- data/lib/generators/motor/migration.rb +17 -0
- data/lib/generators/motor/templates/install.rb +135 -0
- data/lib/motor-admin.rb +3 -0
- data/lib/motor.rb +47 -0
- data/lib/motor/active_record_utils.rb +7 -0
- data/lib/motor/active_record_utils/fetch_methods.rb +24 -0
- data/lib/motor/active_record_utils/types.rb +54 -0
- data/lib/motor/admin.rb +12 -0
- data/lib/motor/alerts.rb +10 -0
- data/lib/motor/alerts/persistance.rb +84 -0
- data/lib/motor/alerts/scheduled_alerts_cache.rb +29 -0
- data/lib/motor/alerts/scheduler.rb +30 -0
- data/lib/motor/api.rb +6 -0
- data/lib/motor/api_query.rb +22 -0
- data/lib/motor/api_query/build_json.rb +109 -0
- data/lib/motor/api_query/build_meta.rb +17 -0
- data/lib/motor/api_query/filter.rb +55 -0
- data/lib/motor/api_query/paginate.rb +18 -0
- data/lib/motor/api_query/search.rb +73 -0
- data/lib/motor/api_query/sort.rb +27 -0
- data/lib/motor/assets.rb +45 -0
- data/lib/motor/build_schema.rb +23 -0
- data/lib/motor/build_schema/find_display_column.rb +60 -0
- data/lib/motor/build_schema/load_from_rails.rb +176 -0
- data/lib/motor/build_schema/merge_schema_configs.rb +77 -0
- data/lib/motor/build_schema/persist_resource_configs.rb +208 -0
- data/lib/motor/build_schema/reorder_schema.rb +52 -0
- data/lib/motor/build_schema/utils.rb +17 -0
- data/lib/motor/dashboards.rb +8 -0
- data/lib/motor/dashboards/persistance.rb +63 -0
- data/lib/motor/forms.rb +8 -0
- data/lib/motor/forms/persistance.rb +63 -0
- data/lib/motor/hash_serializer.rb +21 -0
- data/lib/motor/queries.rb +10 -0
- data/lib/motor/queries/persistance.rb +63 -0
- data/lib/motor/queries/postgresql_exec_query.rb +28 -0
- data/lib/motor/queries/run_query.rb +68 -0
- data/lib/motor/tags.rb +31 -0
- data/lib/motor/ui_configs.rb +62 -0
- data/lib/motor/version.rb +5 -0
- data/ui/dist/fonts/ionicons.woff2 +0 -0
- data/ui/dist/main-46621a8bdbb789e17c3f.css.gz +0 -0
- data/ui/dist/main-46621a8bdbb789e17c3f.js.gz +0 -0
- data/ui/dist/manifest.json +13 -0
- metadata +237 -0
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module BuildSchema
|
5
|
+
module PersistResourceConfigs
|
6
|
+
RESOURCE_ATTRS = %w[display_name visible].freeze
|
7
|
+
COLUMN_ATTRS = %w[name display_name column_type access_type default_value virtual].freeze
|
8
|
+
ASSOCIATION_ATTRS = %w[name display_name visible].freeze
|
9
|
+
ACTION_ATTRS = %w[name display_name action_type preferences visible].freeze
|
10
|
+
TAB_ATTRS = %w[name display_name tab_type preferences visible].freeze
|
11
|
+
|
12
|
+
COLUMN_DEFAULTS = {
|
13
|
+
access_type: 'read_write',
|
14
|
+
default_value: nil,
|
15
|
+
validators: []
|
16
|
+
}.with_indifferent_access
|
17
|
+
|
18
|
+
ACTION_DEFAULTS = {
|
19
|
+
visible: true,
|
20
|
+
preferences: {}
|
21
|
+
}.with_indifferent_access
|
22
|
+
|
23
|
+
TAB_DEFAULTS = {
|
24
|
+
visible: true,
|
25
|
+
preferences: {}
|
26
|
+
}.with_indifferent_access
|
27
|
+
|
28
|
+
module_function
|
29
|
+
|
30
|
+
# @param resource [Motor::Resource]
|
31
|
+
# @return [Motor::Resource]
|
32
|
+
def call(resource)
|
33
|
+
preferences = resource.preferences
|
34
|
+
|
35
|
+
resource = Motor::Resource.find_or_initialize_by(name: resource.name)
|
36
|
+
|
37
|
+
assign_preferences!(resource, preferences)
|
38
|
+
|
39
|
+
resource.save!
|
40
|
+
|
41
|
+
resource
|
42
|
+
rescue ActiveRecord::RecordNotUnique
|
43
|
+
retry
|
44
|
+
end
|
45
|
+
|
46
|
+
# @param resource [Motor::Resource]
|
47
|
+
# @param preferences [HashWithIndifferentAccess]
|
48
|
+
# @return [Motor::Resource]
|
49
|
+
def assign_preferences!(resource, preferences)
|
50
|
+
default_schema = fetch_default_schema(resource.name)
|
51
|
+
|
52
|
+
resource.preferences = normalize_preferences(
|
53
|
+
default_schema,
|
54
|
+
resource.preferences,
|
55
|
+
preferences
|
56
|
+
)
|
57
|
+
|
58
|
+
resource
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param default_prefs [HashWithIndifferentAccess]
|
62
|
+
# @param existing_prefs [HashWithIndifferentAccess]
|
63
|
+
# @param new_prefs [HashWithIndifferentAccess]
|
64
|
+
# @return [HashWithIndifferentAccess]
|
65
|
+
def normalize_preferences(default_prefs, existing_prefs, new_prefs)
|
66
|
+
normalized_preferences = new_prefs.slice(*RESOURCE_ATTRS).with_indifferent_access
|
67
|
+
normalized_preferences = existing_prefs.merge(normalized_preferences)
|
68
|
+
normalized_preferences = reject_default(default_prefs, normalized_preferences)
|
69
|
+
|
70
|
+
if new_prefs[:columns].present?
|
71
|
+
normalized_preferences[:columns] = normalize_columns(
|
72
|
+
default_prefs[:columns],
|
73
|
+
existing_prefs.fetch(:columns, []),
|
74
|
+
new_prefs.fetch(:columns, [])
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
if new_prefs[:associations].present?
|
79
|
+
normalized_preferences[:associations] = normalize_associations(
|
80
|
+
default_prefs[:associations],
|
81
|
+
existing_prefs.fetch(:associations, []),
|
82
|
+
new_prefs.fetch(:associations, [])
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
if new_prefs[:actions].present?
|
87
|
+
normalized_preferences[:actions] = normalize_actions(
|
88
|
+
default_prefs[:actions],
|
89
|
+
existing_prefs.fetch(:actions, []),
|
90
|
+
new_prefs.fetch(:actions, [])
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
if new_prefs[:tabs].present?
|
95
|
+
normalized_preferences[:tabs] = normalize_tabs(
|
96
|
+
default_prefs[:tabs],
|
97
|
+
existing_prefs.fetch(:tabs, []),
|
98
|
+
new_prefs.fetch(:tabs, [])
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
normalized_preferences.compact
|
103
|
+
end
|
104
|
+
|
105
|
+
# @param default_columns [Array<HashWithIndifferentAccess>]
|
106
|
+
# @param existing_columns [Array<HashWithIndifferentAccess>]
|
107
|
+
# @param new_columns [Array<HashWithIndifferentAccess>]
|
108
|
+
# @return [Array<HashWithIndifferentAccess>]
|
109
|
+
def normalize_columns(default_columns, existing_columns, new_columns)
|
110
|
+
(existing_columns.pluck(:name) + new_columns.pluck(:name)).uniq.map do |name|
|
111
|
+
new_column = safe_fetch_by_name(new_columns, name)
|
112
|
+
|
113
|
+
next if new_column[:_remove]
|
114
|
+
|
115
|
+
existing_column = safe_fetch_by_name(existing_columns, name)
|
116
|
+
default_column = safe_fetch_by_name(default_columns, name)
|
117
|
+
column_attrs = new_column.slice(*COLUMN_ATTRS)
|
118
|
+
|
119
|
+
normalized_column = existing_column.merge(column_attrs)
|
120
|
+
normalized_column = reject_default(default_column, normalized_column)
|
121
|
+
|
122
|
+
normalized_column.merge(name: name) if normalized_column.present?
|
123
|
+
end.compact.presence
|
124
|
+
end
|
125
|
+
|
126
|
+
# @param default_actions [Array<HashWithIndifferentAccess>]
|
127
|
+
# @param existing_actions [Array<HashWithIndifferentAccess>]
|
128
|
+
# @param new_actions [Array<HashWithIndifferentAccess>]
|
129
|
+
# @return [Array<HashWithIndifferentAccess>]
|
130
|
+
def normalize_actions(default_actions, existing_actions, new_actions)
|
131
|
+
(existing_actions.pluck(:name) + new_actions.pluck(:name)).uniq.map do |name|
|
132
|
+
new_action = safe_fetch_by_name(new_actions, name)
|
133
|
+
|
134
|
+
next if new_action[:_remove]
|
135
|
+
|
136
|
+
existing_action = safe_fetch_by_name(existing_actions, name)
|
137
|
+
default_action = safe_fetch_by_name(default_actions, name)
|
138
|
+
action_attrs = new_action.slice(*ACTION_ATTRS)
|
139
|
+
|
140
|
+
normalized_action = existing_action.merge(action_attrs)
|
141
|
+
normalized_action = reject_default(default_action.presence || TAB_DEFAULTS, normalized_action)
|
142
|
+
|
143
|
+
normalized_action.merge(name: name) if normalized_action.present?
|
144
|
+
end.compact.presence
|
145
|
+
end
|
146
|
+
|
147
|
+
# @param default_tabs [Array<HashWithIndifferentAccess>]
|
148
|
+
# @param existing_tabs [Array<HashWithIndifferentAccess>]
|
149
|
+
# @param new_tabs [Array<HashWithIndifferentAccess>]
|
150
|
+
# @return [Array<HashWithIndifferentAccess>]
|
151
|
+
def normalize_tabs(default_tabs, existing_tabs, new_tabs)
|
152
|
+
(existing_tabs.pluck(:name) + new_tabs.pluck(:name)).uniq.map do |name|
|
153
|
+
new_tab = safe_fetch_by_name(new_tabs, name)
|
154
|
+
|
155
|
+
next if new_tab[:_remove]
|
156
|
+
|
157
|
+
existing_tab = safe_fetch_by_name(existing_tabs, name)
|
158
|
+
default_tab = safe_fetch_by_name(default_tabs, name)
|
159
|
+
tab_attrs = new_tab.slice(*TAB_ATTRS)
|
160
|
+
|
161
|
+
normalized_tab = existing_tab.merge(tab_attrs)
|
162
|
+
normalized_tab = reject_default(default_tab.presence || TAB_DEFAULTS, normalized_tab)
|
163
|
+
|
164
|
+
normalized_tab.merge(name: name) if normalized_tab.present?
|
165
|
+
end.compact.presence
|
166
|
+
end
|
167
|
+
|
168
|
+
# @param default_assocs [Array<HashWithIndifferentAccess>]
|
169
|
+
# @param existing_assocs [Array<HashWithIndifferentAccess>]
|
170
|
+
# @param new_assocs [Array<HashWithIndifferentAccess>]
|
171
|
+
# @return [Array<HashWithIndifferentAccess>]
|
172
|
+
def normalize_associations(default_assocs, existing_assocs, new_assocs)
|
173
|
+
(existing_assocs.pluck(:name) + new_assocs.pluck(:name)).uniq.map do |name|
|
174
|
+
new_assoc = safe_fetch_by_name(new_assocs, name)
|
175
|
+
existing_assoc = safe_fetch_by_name(existing_assocs, name)
|
176
|
+
default_assoc = safe_fetch_by_name(default_assocs, name)
|
177
|
+
assoc_attrs = new_assoc.slice(*ASSOCIATION_ATTRS)
|
178
|
+
|
179
|
+
normalized_assoc = existing_assoc.merge(assoc_attrs)
|
180
|
+
normalized_assoc = reject_default(default_assoc, normalized_assoc)
|
181
|
+
|
182
|
+
normalized_assoc.merge(name: name) if normalized_assoc.present?
|
183
|
+
end.compact.presence
|
184
|
+
end
|
185
|
+
|
186
|
+
def safe_fetch_by_name(list, name)
|
187
|
+
list.find { |e| e[:name] == name } || {}
|
188
|
+
end
|
189
|
+
|
190
|
+
# @param resource_name [String]
|
191
|
+
# @return [HashWithIndifferentAccess]
|
192
|
+
def fetch_default_schema(resource_name)
|
193
|
+
LoadFromRails.build_model_schema(resource_name.classify.constantize)
|
194
|
+
end
|
195
|
+
|
196
|
+
# @param default [HashWithIndifferentAccess]
|
197
|
+
# @param new [HashWithIndifferentAccess]
|
198
|
+
# @return [HashWithIndifferentAccess]
|
199
|
+
def reject_default(default, new)
|
200
|
+
return new unless default
|
201
|
+
|
202
|
+
new.reject do |key, value|
|
203
|
+
default[key].to_json == value.to_json
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module ReorderSchema
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# @param schema [Array<HashWithIndifferentAccess>]
|
8
|
+
# @return [Array<HashWithIndifferentAccess>]
|
9
|
+
def call(schema)
|
10
|
+
configs = load_configs
|
11
|
+
|
12
|
+
schema = sort_by_name(schema, configs['resources.order'])
|
13
|
+
|
14
|
+
schema.map do |model|
|
15
|
+
columns_order = configs["resources.#{model[:name]}.columns.order"]
|
16
|
+
associations_order = configs["resources.#{model[:name]}.associations.order"]
|
17
|
+
actions_order = configs["resources.#{model[:name]}.actions.order"]
|
18
|
+
tabs_order = configs["resources.#{model[:name]}.tabs.order"]
|
19
|
+
|
20
|
+
model.merge(
|
21
|
+
columns: sort_by_name(model[:columns], columns_order, sort_alphabetically: false),
|
22
|
+
associations: sort_by_name(model[:associations], associations_order),
|
23
|
+
actions: sort_by_name(model[:actions], actions_order, sort_alphabetically: false),
|
24
|
+
tabs: sort_by_name(model[:tabs], tabs_order, sort_alphabetically: false)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param list [Array<HashWithIndifferentAccess>]
|
30
|
+
# @param sort_alphabetically [Boolean]
|
31
|
+
# @param order [Array<String>]
|
32
|
+
# @return [Array<HashWithIndifferentAccess>]
|
33
|
+
def sort_by_name(list, order, sort_alphabetically: true)
|
34
|
+
return list if order.blank? && !sort_alphabetically
|
35
|
+
|
36
|
+
list.sort_by do |item|
|
37
|
+
if order.present?
|
38
|
+
order.index(item[:name]) || Float::MAX
|
39
|
+
else
|
40
|
+
item[:display_name]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [Hash<String, HashWithIndifferentAccess>]
|
46
|
+
def load_configs
|
47
|
+
Motor::Config.all.each_with_object({}) do |config, acc|
|
48
|
+
acc[config.key] = config.value
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module BuildSchema
|
5
|
+
module Utils
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def slugify(model)
|
9
|
+
model.name.underscore.pluralize.gsub('/', '__')
|
10
|
+
end
|
11
|
+
|
12
|
+
def classify_slug(slug)
|
13
|
+
slug.singularize.gsub('__', '/').classify.constantize
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Dashboards
|
5
|
+
module Persistance
|
6
|
+
TitleAlreadyExists = Class.new(StandardError)
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def build_from_params(params, current_user = nil)
|
11
|
+
dashboard = assign_attributes(Dashboard.new, params)
|
12
|
+
|
13
|
+
dashboard.author = current_user
|
14
|
+
|
15
|
+
dashboard
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_from_params!(params, current_user = nil)
|
19
|
+
raise TitleAlreadyExists if Dashboard.exists?(['lower(title) = ?', params[:title].to_s.downcase])
|
20
|
+
|
21
|
+
dashboard = build_from_params(params, current_user)
|
22
|
+
|
23
|
+
ApplicationRecord.transaction do
|
24
|
+
dashboard.save!
|
25
|
+
end
|
26
|
+
|
27
|
+
dashboard
|
28
|
+
rescue ActiveRecord::RecordNotUnique
|
29
|
+
retry
|
30
|
+
end
|
31
|
+
|
32
|
+
def update_from_params!(dashboard, params)
|
33
|
+
raise TitleAlreadyExists if title_already_exists?(dashboard)
|
34
|
+
|
35
|
+
dashboard = assign_attributes(dashboard, params)
|
36
|
+
|
37
|
+
ApplicationRecord.transaction do
|
38
|
+
dashboard.save!
|
39
|
+
end
|
40
|
+
|
41
|
+
dashboard.tags.reload
|
42
|
+
|
43
|
+
dashboard
|
44
|
+
rescue ActiveRecord::RecordNotUnique
|
45
|
+
retry
|
46
|
+
end
|
47
|
+
|
48
|
+
def assign_attributes(dashboard, params)
|
49
|
+
dashboard.assign_attributes(params.slice(:title, :description, :preferences))
|
50
|
+
|
51
|
+
Motor::Tags.assign_tags(dashboard, params[:tags])
|
52
|
+
end
|
53
|
+
|
54
|
+
def title_already_exists?(dashboard)
|
55
|
+
if dashboard.new_record?
|
56
|
+
Dashboard.exists?(['lower(title) = ?', dashboard.title.to_s.downcase])
|
57
|
+
else
|
58
|
+
Dashboard.exists?(['lower(title) = ? AND id != ?', dashboard.title.to_s.downcase, dashboard.id])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/motor/forms.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Forms
|
5
|
+
module Persistance
|
6
|
+
NameAlreadyExists = Class.new(StandardError)
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def build_from_params(params, current_user = nil)
|
11
|
+
form = assign_attributes(Form.new, params)
|
12
|
+
|
13
|
+
form.author = current_user
|
14
|
+
|
15
|
+
form
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_from_params!(params, current_user = nil)
|
19
|
+
raise NameAlreadyExists if Form.exists?(['lower(name) = ?', params[:name].to_s.downcase])
|
20
|
+
|
21
|
+
form = build_from_params(params, current_user)
|
22
|
+
|
23
|
+
ApplicationRecord.transaction do
|
24
|
+
form.save!
|
25
|
+
end
|
26
|
+
|
27
|
+
form
|
28
|
+
rescue ActiveRecord::RecordNotUnique
|
29
|
+
retry
|
30
|
+
end
|
31
|
+
|
32
|
+
def update_from_params!(form, params)
|
33
|
+
raise NameAlreadyExists if name_already_exists?(form)
|
34
|
+
|
35
|
+
form = assign_attributes(form, params)
|
36
|
+
|
37
|
+
ApplicationRecord.transaction do
|
38
|
+
form.save!
|
39
|
+
end
|
40
|
+
|
41
|
+
form.tags.reload
|
42
|
+
|
43
|
+
form
|
44
|
+
rescue ActiveRecord::RecordNotUnique
|
45
|
+
retry
|
46
|
+
end
|
47
|
+
|
48
|
+
def assign_attributes(form, params)
|
49
|
+
form.assign_attributes(params.slice(:name, :description, :api_path, :http_method, :preferences))
|
50
|
+
|
51
|
+
Motor::Tags.assign_tags(form, params[:tags])
|
52
|
+
end
|
53
|
+
|
54
|
+
def name_already_exists?(form)
|
55
|
+
if form.new_record?
|
56
|
+
Form.exists?(['lower(name) = ?', form.name.to_s.downcase])
|
57
|
+
else
|
58
|
+
Form.exists?(['lower(name) = ? AND id != ?', form.name.to_s.downcase, form.id])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
class HashSerializer
|
5
|
+
def self.dump(hash)
|
6
|
+
hash.to_json
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.load(hash)
|
10
|
+
return hash unless hash
|
11
|
+
|
12
|
+
hash = JSON.parse(hash.presence || '{}') if hash.is_a?(String)
|
13
|
+
|
14
|
+
if hash.is_a?(Hash)
|
15
|
+
hash.with_indifferent_access
|
16
|
+
else
|
17
|
+
hash || {}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|