model-api 0.8.3
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/README.md +2 -0
- data/lib/model-api.rb +38 -0
- data/lib/model-api/base_controller.rb +1352 -0
- data/lib/model-api/bypass_parse_middleware.rb +32 -0
- data/lib/model-api/hash_metadata.rb +48 -0
- data/lib/model-api/model.rb +49 -0
- data/lib/model-api/not_found_exception.rb +10 -0
- data/lib/model-api/open_api_extensions.rb +287 -0
- data/lib/model-api/renderer.rb +504 -0
- data/lib/model-api/simple_metadata.rb +33 -0
- data/lib/model-api/suppress_login_redirect_middleware.rb +38 -0
- data/lib/model-api/unauthorized_exception.rb +4 -0
- data/lib/model-api/utils.rb +392 -0
- data/model-api.gemspec +24 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3bdc8741b309bfe1b4d504a67b34a7101ffc3486
|
4
|
+
data.tar.gz: c1519aa053d77afb3d1eff0f1861be9a4fc5a804
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 708e946d85d6516b2dda287d2c5ad4fffe1b302a597bfbcc5f424308a8c1209b0ec9ba65ab388fa527da1488d2de2512c095172e7ffcfa21e28c90e4398af5c4
|
7
|
+
data.tar.gz: 08fe91d60003278e4e5ce049570fdca9f00be075779f35e7c57627be9a7ce678fdadd9eef0cabbde8b6d0cf3e5b43b85e37e091d786955d4cf6d1ce82ef2be34
|
data/README.md
ADDED
data/lib/model-api.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'model-api/base_controller.rb'
|
2
|
+
require 'model-api/bypass_parse_middleware.rb'
|
3
|
+
require 'model-api/hash_metadata.rb'
|
4
|
+
require 'model-api/model.rb'
|
5
|
+
require 'model-api/not_found_exception.rb'
|
6
|
+
require 'model-api/open_api_extensions.rb'
|
7
|
+
require 'model-api/renderer.rb'
|
8
|
+
require 'model-api/simple_metadata.rb'
|
9
|
+
require 'model-api/suppress_login_redirect_middleware.rb'
|
10
|
+
require 'model-api/unauthorized_exception.rb'
|
11
|
+
require 'model-api/utils.rb'
|
12
|
+
|
13
|
+
module ModelApi
|
14
|
+
class << self
|
15
|
+
def configure(metadata = nil, &block)
|
16
|
+
return unless metadata.is_a?(Hash) || block_given?
|
17
|
+
global_metadata = @model_api_global_metadata || default_global_metadata
|
18
|
+
if metadata.is_a?(Hash)
|
19
|
+
global_metadata = OpenApi::Utils.merge_hash(global_metadata, metadata)
|
20
|
+
end
|
21
|
+
if block_given?
|
22
|
+
config = OpenStruct.new(global_metadata)
|
23
|
+
block.call(config)
|
24
|
+
global_metadata = OpenApi::Utils.merge_hash(global_metadata, config.to_h.symbolize_keys)
|
25
|
+
end
|
26
|
+
@model_api_global_metadata = global_metadata
|
27
|
+
end
|
28
|
+
|
29
|
+
def global_metadata
|
30
|
+
@model_api_global_metadata || default_global_metadata
|
31
|
+
end
|
32
|
+
|
33
|
+
def default_global_metadata
|
34
|
+
{
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,1352 @@
|
|
1
|
+
module ModelApi
|
2
|
+
module BaseController
|
3
|
+
module ClassMethods
|
4
|
+
def model_class
|
5
|
+
nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def base_api_options
|
9
|
+
{}
|
10
|
+
end
|
11
|
+
|
12
|
+
def base_admin_api_options
|
13
|
+
base_api_options.merge(admin_only: true)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.included(base)
|
18
|
+
base.extend(ClassMethods)
|
19
|
+
|
20
|
+
base.send(:include, InstanceMethods)
|
21
|
+
|
22
|
+
base.send(:before_filter, :common_headers)
|
23
|
+
|
24
|
+
base.send(:rescue_from, Exception, with: :unhandled_exception)
|
25
|
+
base.send(:respond_to, :json, :xml)
|
26
|
+
end
|
27
|
+
|
28
|
+
module InstanceMethods
|
29
|
+
SIMPLE_ID_REGEX = /\A[0-9]+\Z/
|
30
|
+
UUID_REGEX = /\A[0-9A-Za-z]{8}-?[0-9A-Za-z]{4}-?[0-9A-Za-z]{4}-?[0-9A-Za-z]{4}-?[0-9A-Za-z]\
|
31
|
+
{12}\Z/x
|
32
|
+
DEFAULT_PAGE_SIZE = 100
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def model_class
|
37
|
+
self.class.model_class
|
38
|
+
end
|
39
|
+
|
40
|
+
def render_collection(collection, opts = {})
|
41
|
+
return unless ensure_admin_if_admin_only(opts)
|
42
|
+
opts = prepare_options(opts)
|
43
|
+
opts[:operation] ||= :index
|
44
|
+
return unless validate_read_operation(collection, opts[:operation], opts)
|
45
|
+
|
46
|
+
coll_route = opts[:collection_route] || self
|
47
|
+
collection_links = { self: coll_route }
|
48
|
+
collection = process_collection_includes(collection, opts)
|
49
|
+
collection, _result_filters = filter_collection(collection, find_filter_params, opts)
|
50
|
+
collection, _result_sorts = sort_collection(collection, find_sort_params, opts)
|
51
|
+
collection, collection_links, opts = paginate_collection(collection,
|
52
|
+
collection_links, opts, coll_route)
|
53
|
+
|
54
|
+
opts[:collection_links] = collection_links.merge(opts[:collection_links] || {})
|
55
|
+
.reverse_merge(common_response_links(opts))
|
56
|
+
add_collection_object_route(opts)
|
57
|
+
ModelApi::Renderer.render(self, collection, opts)
|
58
|
+
end
|
59
|
+
|
60
|
+
def render_object(obj, opts = {})
|
61
|
+
return unless ensure_admin_if_admin_only(opts)
|
62
|
+
opts = prepare_options(opts)
|
63
|
+
klass = Utils.find_class(obj, opts)
|
64
|
+
object_route = opts[:object_route] || self
|
65
|
+
|
66
|
+
opts[:object_links] = { self: object_route }
|
67
|
+
if obj.is_a?(ActiveRecord::Base)
|
68
|
+
return unless validate_read_operation(obj, opts[:operation], opts)
|
69
|
+
unless obj.present?
|
70
|
+
return not_found(opts.merge(class: klass, field: :id))
|
71
|
+
end
|
72
|
+
opts[:object_links].merge!(opts[:object_links] || {})
|
73
|
+
else
|
74
|
+
return not_found(opts) if obj.nil?
|
75
|
+
obj = ModelApi::Utils.ext_value(obj, opts) unless opts[:raw_output]
|
76
|
+
opts[:object_links].merge!(opts[:links] || {})
|
77
|
+
end
|
78
|
+
|
79
|
+
opts[:operation] ||= :show
|
80
|
+
opts[:object_links].reverse_merge!(common_response_links(opts))
|
81
|
+
ModelApi::Renderer.render(self, obj, opts)
|
82
|
+
end
|
83
|
+
|
84
|
+
def do_create(opts = {})
|
85
|
+
klass = opts[:model_class] || model_class
|
86
|
+
return unless ensure_admin_if_admin_only(opts)
|
87
|
+
unless klass.is_a?(Class) && klass < ActiveRecord::Base
|
88
|
+
fail 'Unable to process object creation; Missing or invalid model class'
|
89
|
+
end
|
90
|
+
obj, opts = prepare_object_for_create(klass, opts)
|
91
|
+
return bad_payload(class: klass) if opts[:bad_payload]
|
92
|
+
create_and_render_object(obj, opts)
|
93
|
+
end
|
94
|
+
|
95
|
+
def prepare_object_for_create(klass, opts = {})
|
96
|
+
opts = prepare_options(opts)
|
97
|
+
get_updated_object(klass, get_operation(:create, opts), opts)
|
98
|
+
end
|
99
|
+
|
100
|
+
def create_and_render_object(obj, opts = {})
|
101
|
+
opts = prepare_options(opts)
|
102
|
+
object_link_options = opts[:object_link_options]
|
103
|
+
object_link_options[:action] = :show
|
104
|
+
save_and_render_object(obj, get_operation(:create, opts), opts.merge(location_header: true))
|
105
|
+
end
|
106
|
+
|
107
|
+
def do_update(obj, opts = {})
|
108
|
+
return unless ensure_admin_if_admin_only(opts)
|
109
|
+
obj, opts = prepare_object_for_update(obj, opts)
|
110
|
+
return bad_payload(class: klass) if opts[:bad_payload]
|
111
|
+
unless obj.present?
|
112
|
+
return not_found(opts.merge(class: Utils.find_class(obj, opts), field: :id))
|
113
|
+
end
|
114
|
+
update_and_render_object(obj, opts)
|
115
|
+
end
|
116
|
+
|
117
|
+
def prepare_object_for_update(obj, opts = {})
|
118
|
+
opts = prepare_options(opts)
|
119
|
+
get_updated_object(obj, get_operation(:update, opts), opts)
|
120
|
+
end
|
121
|
+
|
122
|
+
def update_and_render_object(obj, opts = {})
|
123
|
+
opts = prepare_options(opts)
|
124
|
+
save_and_render_object(obj, get_operation(:update, opts), opts)
|
125
|
+
end
|
126
|
+
|
127
|
+
def save_and_render_object(obj, operation, opts = {})
|
128
|
+
status, msgs = Utils.process_updated_model_save(obj, operation, opts)
|
129
|
+
add_hateoas_links_for_updated_object(operation, opts)
|
130
|
+
successful = ModelApi::Utils.response_successful?(status)
|
131
|
+
ModelApi::Renderer.render(self, successful ? obj : opts[:request_obj],
|
132
|
+
opts.merge(status: status, operation: :show, messages: msgs))
|
133
|
+
end
|
134
|
+
|
135
|
+
def do_destroy(obj, opts = {})
|
136
|
+
return unless ensure_admin_if_admin_only(opts)
|
137
|
+
opts = prepare_options(opts)
|
138
|
+
obj = obj.first if obj.is_a?(ActiveRecord::Relation)
|
139
|
+
|
140
|
+
add_hateoas_links_for_update(opts)
|
141
|
+
unless obj.present?
|
142
|
+
return not_found(opts.merge(class: klass, field: :id))
|
143
|
+
end
|
144
|
+
|
145
|
+
operation = opts[:operation] = get_operation(:destroy, opts)
|
146
|
+
Utils.validate_operation(obj, operation, opts)
|
147
|
+
response_status, errs_or_msgs = Utils.process_object_destroy(obj, operation, opts)
|
148
|
+
|
149
|
+
add_hateoas_links_for_updated_object(operation, opts)
|
150
|
+
klass = Utils.find_class(obj, opts)
|
151
|
+
ModelApi::Renderer.render(self, obj, opts.merge(status: response_status,
|
152
|
+
root: ModelApi::Utils.model_name(klass).singular, messages: errs_or_msgs))
|
153
|
+
end
|
154
|
+
|
155
|
+
def common_response_links(_opts = {})
|
156
|
+
{}
|
157
|
+
end
|
158
|
+
|
159
|
+
def prepare_options(opts)
|
160
|
+
opts = opts.symbolize_keys
|
161
|
+
opts[:user] = user = filter_by_user
|
162
|
+
opts[:user_id] = user.try(:id)
|
163
|
+
opts[:admin] = user.try(:admin_api_user?) ? true : false
|
164
|
+
opts[:admin_content] = admin_content?
|
165
|
+
opts[:collection_link_options] = opts[:object_link_options] =
|
166
|
+
request.query_parameters.to_h.symbolize_keys
|
167
|
+
opts
|
168
|
+
end
|
169
|
+
|
170
|
+
def id_info(opts = {})
|
171
|
+
id_info = {}
|
172
|
+
id_info[:id_attribute] = (opts[:id_attribute] || :id).to_sym
|
173
|
+
id_info[:id_param] = (opts[:id_param] || :id).to_sym
|
174
|
+
id_info[:id_value] = (opts[:id_value] || params[id_info[:id_param]]).to_s
|
175
|
+
id_info
|
176
|
+
end
|
177
|
+
|
178
|
+
def api_query(opts = {})
|
179
|
+
klass = opts[:model_class] || model_class
|
180
|
+
unless klass < ActiveRecord::Base
|
181
|
+
fail 'Expected model class to be an ActiveRecord::Base subclass'
|
182
|
+
end
|
183
|
+
query = klass.all
|
184
|
+
if (deleted_col = klass.columns_hash['deleted']).present?
|
185
|
+
case deleted_col.type
|
186
|
+
when :boolean
|
187
|
+
query = query.where(deleted: false)
|
188
|
+
when :integer, :decimal
|
189
|
+
query = query.where(deleted: 0)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
Utils.apply_context(query, opts)
|
193
|
+
end
|
194
|
+
|
195
|
+
def common_object_query(opts = {})
|
196
|
+
klass = opts[:model_class] || model_class
|
197
|
+
coll_query = Utils.apply_context(api_query(opts), opts)
|
198
|
+
id_info = opts[:id_info] || id_info(opts)
|
199
|
+
query = coll_query.where(id_info[:id_attribute] => id_info[:id_value])
|
200
|
+
if !admin_access?
|
201
|
+
unless opts.include?(:user_filter) && !opts[:user_filter]
|
202
|
+
query = user_query(query, opts.merge(model_class: klass))
|
203
|
+
end
|
204
|
+
elsif id_info[:id_attribute] != :id && !id_info[:id_attribute].to_s.ends_with?('.id') &&
|
205
|
+
klass.column_names.include?('id') && !query.exists?
|
206
|
+
# Admins can optionally use record ID's if the ID field happens to be something else
|
207
|
+
query = coll_query.where(id: id_info[:id_value])
|
208
|
+
end
|
209
|
+
unless (not_found_error = opts[:not_found_error]).blank? || query.exists?
|
210
|
+
not_found_error = not_found_error.call(params[:id]) if not_found_error.respond_to?(:call)
|
211
|
+
if not_found_error == true
|
212
|
+
not_found_error = "#{klass.model_name.human} '#{id_info[:id_value]}' not found."
|
213
|
+
end
|
214
|
+
fail ModelApi::NotFoundException.new(id_info[:id_param], not_found_error.to_s)
|
215
|
+
end
|
216
|
+
query
|
217
|
+
end
|
218
|
+
|
219
|
+
def collection_query(opts = {})
|
220
|
+
opts = base_api_options.merge(opts)
|
221
|
+
klass = opts[:model_class] || model_class
|
222
|
+
query = api_query(opts)
|
223
|
+
unless (opts.include?(:user_filter) && !opts[:user_filter]) ||
|
224
|
+
(admin_access? && (admin_content? || filtered_by_foreign_key?(query)))
|
225
|
+
query = user_query(query, opts.merge(model_class: klass))
|
226
|
+
end
|
227
|
+
query
|
228
|
+
end
|
229
|
+
|
230
|
+
def object_query(opts = {})
|
231
|
+
common_object_query(base_api_options.merge(opts))
|
232
|
+
end
|
233
|
+
|
234
|
+
def user_query(query, opts = {})
|
235
|
+
user = opts[:user] || filter_by_user
|
236
|
+
klass = opts[:model_class] || model_class
|
237
|
+
user_id_col = opts[:user_id_column] || :user_id
|
238
|
+
user_assoc = opts[:user_association] || :user
|
239
|
+
user_id = user.send(opts[:user_id_attribute] || :id)
|
240
|
+
if klass.columns_hash.include?(user_id_col.to_s)
|
241
|
+
query = query.where(user_id_col => user_id)
|
242
|
+
elsif (assoc = klass.reflect_on_association(user_assoc)).present? &&
|
243
|
+
[:belongs_to, :has_one].include?(assoc.macro)
|
244
|
+
query = query.joins(user_assoc).where(
|
245
|
+
"#{assoc.klass.table_name}.#{assoc.klass.primary_key}" => user_id)
|
246
|
+
elsif opts[:user_filter]
|
247
|
+
fail "Unable to filter results by user; no '#{user_id_col}' column or " \
|
248
|
+
"'#{user_assoc}' association found!"
|
249
|
+
end
|
250
|
+
query
|
251
|
+
end
|
252
|
+
|
253
|
+
def base_api_options
|
254
|
+
self.class.base_api_options
|
255
|
+
end
|
256
|
+
|
257
|
+
def base_admin_api_options
|
258
|
+
base_api_options.merge(admin_only: true)
|
259
|
+
end
|
260
|
+
|
261
|
+
def ensure_admin
|
262
|
+
return true if current_user.try(:admin_api_user?)
|
263
|
+
|
264
|
+
# Mask presence of endpoint if user is not authorized to access it
|
265
|
+
not_found
|
266
|
+
false
|
267
|
+
end
|
268
|
+
|
269
|
+
def unhandled_exception(err)
|
270
|
+
return if handle_api_exceptions(err)
|
271
|
+
error_id = LogUtils.log_and_notify(err)
|
272
|
+
return if performed?
|
273
|
+
error_details = {}
|
274
|
+
if Rails.env == 'development'
|
275
|
+
error_details[:message] = "Exception: #{err.message}"
|
276
|
+
error_details[:error_event_id] = error_id
|
277
|
+
error_details[:backtrace] = err.backtrace
|
278
|
+
else
|
279
|
+
error_details[:message] = 'An internal server error has occurred ' \
|
280
|
+
'while processing your request. Please contact customer ' \
|
281
|
+
'support, referencing the following error event id, for ' \
|
282
|
+
"assistance: #{error_id}"
|
283
|
+
error_details[:error_event_id] = error_id
|
284
|
+
end
|
285
|
+
ModelApi::Renderer.render(self, error_details, root: :error_details,
|
286
|
+
status: :internal_server_error)
|
287
|
+
end
|
288
|
+
|
289
|
+
def handle_api_exceptions(err)
|
290
|
+
if err.is_a?(ModelApi::NotFoundException)
|
291
|
+
not_found(field: err.field, message: err.message)
|
292
|
+
elsif err.is_a?(ModelApi::UnauthorizedException)
|
293
|
+
unauthorized
|
294
|
+
else
|
295
|
+
return false
|
296
|
+
end
|
297
|
+
true
|
298
|
+
end
|
299
|
+
|
300
|
+
def doorkeeper_unauthorized_render_options(error: nil)
|
301
|
+
{ json: unauthorized(error: 'Not authorized to access resource', message: error.description,
|
302
|
+
format: :json, generate_body_only: true) }
|
303
|
+
end
|
304
|
+
|
305
|
+
# Indicates whether user has access to data they do not own.
|
306
|
+
def admin_access?
|
307
|
+
false
|
308
|
+
end
|
309
|
+
|
310
|
+
# Indicates whether API should render administrator-only content in API responses
|
311
|
+
def admin_content?
|
312
|
+
param = request.query_parameters[:admin]
|
313
|
+
param.present? && param.to_i != 0 && admin_access?
|
314
|
+
end
|
315
|
+
|
316
|
+
def resource_parent_id(parent_model_class, opts = {})
|
317
|
+
id_info = id_info(opts.reverse_merge(id_param: "#{parent_model_class.name.underscore}_id"))
|
318
|
+
model_name = parent_model_class.model_name.human
|
319
|
+
if id_info[:id_value].blank?
|
320
|
+
unless opts[:optional]
|
321
|
+
fail ModelApi::NotFoundException.new(id_info[:id_param], "#{model_name} not found")
|
322
|
+
end
|
323
|
+
return nil
|
324
|
+
end
|
325
|
+
query = common_object_query(opts.merge(model_class: parent_model_class, id_info: id_info))
|
326
|
+
parent_id = query.pluck(:id).first
|
327
|
+
if parent_id.blank?
|
328
|
+
unless opts[:optional]
|
329
|
+
fail ModelApi::NotFoundException.new(id_info[:id_param],
|
330
|
+
"#{model_name} '#{id_info[:id_value]}' not found")
|
331
|
+
end
|
332
|
+
return nil
|
333
|
+
end
|
334
|
+
parent_id
|
335
|
+
end
|
336
|
+
|
337
|
+
def simple_error(status, error, opts = {})
|
338
|
+
opts = opts.dup
|
339
|
+
klass = opts[:class]
|
340
|
+
opts[:root] = ModelApi::Utils.model_name(klass).singular if klass.present?
|
341
|
+
if error.is_a?(Array)
|
342
|
+
errs_or_msgs = error.map do |e|
|
343
|
+
if e.is_a?(Hash)
|
344
|
+
next e if e.include?(:error) && e.include?(:message)
|
345
|
+
next e.reverse_merge(
|
346
|
+
error: e[:error] || 'Unspecified error',
|
347
|
+
message: e[:message] || e[:error] || 'Unspecified error')
|
348
|
+
end
|
349
|
+
{ error: e.to_s, message: e.to_s }
|
350
|
+
end
|
351
|
+
elsif error.is_a?(Hash)
|
352
|
+
errs_or_msgs = [error]
|
353
|
+
else
|
354
|
+
errs_or_msgs = [{ error: error, message: opts[:message] || error }]
|
355
|
+
end
|
356
|
+
errs_or_msgs[0][:field] = opts[:field] if opts.include?(:field)
|
357
|
+
ModelApi::Renderer.render(self, opts[:request_obj], opts.merge(status: status,
|
358
|
+
messages: errs_or_msgs))
|
359
|
+
end
|
360
|
+
|
361
|
+
def not_found(opts = {})
|
362
|
+
opts = opts.dup
|
363
|
+
opts[:message] ||= 'No resource found at the path provided or matching the criteria ' \
|
364
|
+
'specified'
|
365
|
+
simple_error(:not_found, opts.delete(:error) || 'No resource found', opts)
|
366
|
+
end
|
367
|
+
|
368
|
+
def bad_payload(opts = {})
|
369
|
+
opts = opts.dup
|
370
|
+
format = opts[:format] || identify_format
|
371
|
+
opts[:message] ||= "A properly-formatted #{format.to_s.upcase} " \
|
372
|
+
'payload was expected in the HTTP request body but not found'
|
373
|
+
simple_error(:bad_request, opts.delete(:error) || 'Missing/invalid request body (payload)',
|
374
|
+
opts)
|
375
|
+
end
|
376
|
+
|
377
|
+
def bad_request(error, message, opts = {})
|
378
|
+
opts[:message] = message || 'This request is invalid for the resource in its present state'
|
379
|
+
simple_error(:bad_request, error || 'Invalid API request', opts)
|
380
|
+
end
|
381
|
+
|
382
|
+
def unauthorized(opts = {})
|
383
|
+
opts = opts.dup
|
384
|
+
opts[:message] ||= 'Missing one or more privileges required to complete request'
|
385
|
+
simple_error(:unauthorized, opts.delete(:error) || 'Not authorized', opts)
|
386
|
+
end
|
387
|
+
|
388
|
+
def not_implemented(opts = {})
|
389
|
+
opts = opts.dup
|
390
|
+
opts[:message] ||= 'This API feature is presently unavailable'
|
391
|
+
simple_error(:not_implemented, opts.delete(:error) || 'Not implemented', opts)
|
392
|
+
end
|
393
|
+
|
394
|
+
def validate_read_operation(obj, operation, opts = {})
|
395
|
+
status, errors = Utils.validate_operation(obj, operation, opts)
|
396
|
+
return true if status.nil? && errors.nil?
|
397
|
+
if errors.nil? && (status.is_a?(Array) || status.present?)
|
398
|
+
return true if (errors = status).blank?
|
399
|
+
status = :bad_request
|
400
|
+
end
|
401
|
+
return true unless errors.present?
|
402
|
+
errors = [errors] unless errors.is_a?(Array)
|
403
|
+
simple_error(status, errors, opts)
|
404
|
+
false
|
405
|
+
end
|
406
|
+
|
407
|
+
def current_user
|
408
|
+
return @devise_user if @devise_user.present?
|
409
|
+
return @current_user if instance_variable_defined?(:@current_user)
|
410
|
+
unless doorkeeper_token.present? &&
|
411
|
+
doorkeeper_token.resource_owner_id.present?
|
412
|
+
return (@current_user = nil)
|
413
|
+
end
|
414
|
+
@current_user = User.find(doorkeeper_token.resource_owner_id)
|
415
|
+
end
|
416
|
+
|
417
|
+
def filter_by_user
|
418
|
+
if admin_access?
|
419
|
+
if (user_id = request.query_parameters[:user_id] ||
|
420
|
+
request.query_parameters[:user]).present?
|
421
|
+
return User.where(id: user_id.to_i).first || current_user
|
422
|
+
elsif (username = request.query_parameters[:username]).present?
|
423
|
+
return User.where(username: username.to_s).first || current_user
|
424
|
+
elsif (user_email = request.query_parameters[:user_email]).present?
|
425
|
+
return User.where(email: user_email.to_s).first || current_user
|
426
|
+
end
|
427
|
+
end
|
428
|
+
current_user
|
429
|
+
end
|
430
|
+
|
431
|
+
def common_headers
|
432
|
+
ModelApi::Utils.common_http_headers.each do |k, v|
|
433
|
+
response.headers[k] = v
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
def identify_format
|
438
|
+
format = self.request.format.symbol rescue :json
|
439
|
+
format == :xml ? :xml : :json
|
440
|
+
end
|
441
|
+
|
442
|
+
def ensure_admin_if_admin_only(opts = {})
|
443
|
+
return true unless opts[:admin_only]
|
444
|
+
ensure_admin
|
445
|
+
end
|
446
|
+
|
447
|
+
def get_operation(default_operation, opts = {})
|
448
|
+
if opts.key?(:operation)
|
449
|
+
return opts[:operation]
|
450
|
+
elsif action_name.start_with?('create')
|
451
|
+
return :create
|
452
|
+
elsif action_name.start_with?('update')
|
453
|
+
return :update
|
454
|
+
elsif action_name.start_with?('patch')
|
455
|
+
return :patch
|
456
|
+
elsif action_name.start_with?('destroy')
|
457
|
+
return :destroy
|
458
|
+
else
|
459
|
+
return default_operation
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
def get_updated_object(obj_or_class, operation, opts = {})
|
464
|
+
opts = opts.symbolize_keys
|
465
|
+
opts[:operation] = operation
|
466
|
+
req_body, format = ModelApi::Utils.parse_request_body(request)
|
467
|
+
if obj_or_class.is_a?(Class)
|
468
|
+
klass = Utils.class_or_sti_subclass(obj_or_class, req_body, operation, opts)
|
469
|
+
obj = nil
|
470
|
+
elsif obj_or_class.is_a?(ActiveRecord::Base)
|
471
|
+
obj = obj_or_class
|
472
|
+
klass = obj.class
|
473
|
+
elsif obj_or_class.is_a?(ActiveRecord::Relation)
|
474
|
+
klass = obj_or_class.klass
|
475
|
+
obj = obj_or_class.first
|
476
|
+
end
|
477
|
+
opts[:api_attr_metadata] = ModelApi::Utils.filtered_attrs(klass, operation, opts)
|
478
|
+
opts[:api_model_metadata] = model_metadata = ModelApi::Utils.model_metadata(klass)
|
479
|
+
opts[:ignored_fields] = []
|
480
|
+
return [nil, opts.merge(bad_payload: true)] if req_body.nil?
|
481
|
+
obj = klass.new if obj.nil?
|
482
|
+
add_hateoas_links_for_update(opts)
|
483
|
+
verify_update_request_body(req_body, format, opts)
|
484
|
+
root_elem = opts[:root] = ModelApi::Utils.model_name(klass).singular
|
485
|
+
request_obj = opts[:request_obj] = Utils.object_from_req_body(root_elem, req_body, format)
|
486
|
+
Utils.apply_updates(obj, request_obj, operation, opts)
|
487
|
+
opts.freeze
|
488
|
+
ModelApi::Utils.invoke_callback(model_metadata[:after_initialize], obj, opts)
|
489
|
+
[obj, opts]
|
490
|
+
end
|
491
|
+
|
492
|
+
private
|
493
|
+
|
494
|
+
def find_filter_params
|
495
|
+
request.query_parameters.reject do |param, _value|
|
496
|
+
%w(access_token sort_by admin).include?(param)
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
def find_sort_params
|
501
|
+
sort_by = params[:sort_by]
|
502
|
+
return {} if sort_by.blank?
|
503
|
+
sort_by = sort_by.to_s.strip
|
504
|
+
if sort_by.starts_with?('{') || sort_by.starts_with?('[')
|
505
|
+
process_json_sort_params(sort_by)
|
506
|
+
else
|
507
|
+
process_simple_sort_params(sort_by)
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
def process_json_sort_params(sort_by)
|
512
|
+
sort_params = {}
|
513
|
+
sort_json_obj = (JSON.parse(sort_by) rescue {})
|
514
|
+
sort_json_obj = Hash[sort_json_obj.map { |v| [v, nil] }] if sort_json_obj.is_a?(Array)
|
515
|
+
sort_json_obj.each do |key, value|
|
516
|
+
next if key.blank?
|
517
|
+
value_lc = value.to_s.downcase
|
518
|
+
if %w(a asc ascending).include?(value_lc)
|
519
|
+
order = :asc
|
520
|
+
elsif %w(d desc descending).include?(value_lc)
|
521
|
+
order = :desc
|
522
|
+
else
|
523
|
+
order = :default
|
524
|
+
end
|
525
|
+
sort_params[key] = order
|
526
|
+
end
|
527
|
+
sort_params
|
528
|
+
end
|
529
|
+
|
530
|
+
def process_simple_sort_params(sort_by)
|
531
|
+
sort_params = {}
|
532
|
+
sort_by.split(',').each do |key|
|
533
|
+
key = key.to_s.strip
|
534
|
+
key_lc = key.downcase
|
535
|
+
if key_lc.ends_with?('_a') || key_lc.ends_with?(' a')
|
536
|
+
key = sort_by[key[0..-3]]
|
537
|
+
order = :asc
|
538
|
+
elsif key_lc.ends_with?('_asc') || key_lc.ends_with?(' asc')
|
539
|
+
key = sort_by[key[0..-5]]
|
540
|
+
order = :asc
|
541
|
+
elsif key_lc.ends_with?('_ascending') || key_lc.ends_with?(' ascending')
|
542
|
+
key = sort_by[key[0..-11]]
|
543
|
+
order = :asc
|
544
|
+
elsif key_lc.ends_with?('_d') || key_lc.ends_with?(' d')
|
545
|
+
key = sort_by[key[0..-3]]
|
546
|
+
order = :desc
|
547
|
+
elsif key_lc.ends_with?('_desc') || key_lc.ends_with?(' desc')
|
548
|
+
key = sort_by[key[0..-6]]
|
549
|
+
order = :desc
|
550
|
+
elsif key_lc.ends_with?('_descending') || key_lc.ends_with?(' descending')
|
551
|
+
key = sort_by[key[0..-12]]
|
552
|
+
order = :desc
|
553
|
+
else
|
554
|
+
order = :default
|
555
|
+
end
|
556
|
+
next if key.blank?
|
557
|
+
sort_params[key] = order
|
558
|
+
end
|
559
|
+
sort_params
|
560
|
+
end
|
561
|
+
|
562
|
+
def process_collection_includes(collection, opts = {})
|
563
|
+
klass = Utils.find_class(collection, opts)
|
564
|
+
metadata = ModelApi::Utils.filtered_ext_attrs(klass, opts[:operation] || :index, opts)
|
565
|
+
model_metadata = opts[:api_model_metadata] || ModelApi::Utils.model_metadata(klass)
|
566
|
+
includes = []
|
567
|
+
if (metadata_includes = model_metadata[:collection_includes]).is_a?(Array)
|
568
|
+
includes += metadata_includes.map(&:to_sym)
|
569
|
+
end
|
570
|
+
metadata.each do |_attr, attr_metadata|
|
571
|
+
includes << attr_metadata[:key] if attr_metadata[:type] == :association
|
572
|
+
end
|
573
|
+
includes = includes.compact.uniq
|
574
|
+
collection = collection.includes(includes) if includes.present?
|
575
|
+
collection
|
576
|
+
end
|
577
|
+
|
578
|
+
def filter_collection(collection, filter_params, opts = {})
|
579
|
+
return [collection, {}] if filter_params.blank? # Don't filter if no filter params
|
580
|
+
klass = opts[:class] || Utils.find_class(collection, opts)
|
581
|
+
assoc_values, metadata, attr_values = process_filter_params(filter_params, klass, opts)
|
582
|
+
result_filters = {}
|
583
|
+
metadata.values.each do |attr_metadata|
|
584
|
+
collection = apply_filter_param(attr_metadata, collection,
|
585
|
+
opts.merge(attr_values: attr_values, result_filters: result_filters, class: klass))
|
586
|
+
end
|
587
|
+
assoc_values.each do |assoc, assoc_filter_params|
|
588
|
+
ar_assoc = klass.reflect_on_association(assoc)
|
589
|
+
next unless ar_assoc.present?
|
590
|
+
collection = collection.joins(assoc) unless collection.joins_values.include?(assoc)
|
591
|
+
collection, assoc_result_filters = filter_collection(collection, assoc_filter_params,
|
592
|
+
opts.merge(class: ar_assoc.klass, filter_table: ar_assoc.table_name))
|
593
|
+
result_filters[assoc] = assoc_result_filters if assoc_result_filters.present?
|
594
|
+
end
|
595
|
+
[collection, result_filters]
|
596
|
+
end
|
597
|
+
|
598
|
+
def process_filter_params(filter_params, klass, opts = {})
|
599
|
+
assoc_values = {}
|
600
|
+
filter_metadata = {}
|
601
|
+
attr_values = {}
|
602
|
+
metadata = ModelApi::Utils.filtered_ext_attrs(klass, :filter, opts)
|
603
|
+
filter_params.each do |attr, value|
|
604
|
+
attr = attr.to_s
|
605
|
+
if attr.length > 1 && ['>', '<', '!', '='].include?(attr[-1])
|
606
|
+
value = "#{attr[-1]}=#{value}" # Effectively allows >= / <= / != / == in query string
|
607
|
+
attr = attr[0..-2].strip
|
608
|
+
end
|
609
|
+
if attr.include?('.')
|
610
|
+
process_filter_assoc_param(attr, metadata, assoc_values, value, opts)
|
611
|
+
else
|
612
|
+
process_filter_attr_param(attr, metadata, filter_metadata, attr_values, value, opts)
|
613
|
+
end
|
614
|
+
end
|
615
|
+
[assoc_values, filter_metadata, attr_values]
|
616
|
+
end
|
617
|
+
|
618
|
+
# rubocop:disable Metrics/ParameterLists
|
619
|
+
def process_filter_assoc_param(attr, metadata, assoc_values, value, opts)
|
620
|
+
attr_elems = attr.split('.')
|
621
|
+
assoc_name = attr_elems[0].strip.to_sym
|
622
|
+
assoc_metadata = metadata[assoc_name] ||
|
623
|
+
metadata[ModelApi::Utils.ext_query_attr(assoc_name, opts)] || {}
|
624
|
+
key = assoc_metadata[:key]
|
625
|
+
return unless key.present? && ModelApi::Utils.eval_bool(assoc_metadata[:filter], opts)
|
626
|
+
assoc_filter_params = (assoc_values[key] ||= {})
|
627
|
+
assoc_filter_params[attr_elems[1..-1].join('.')] = value
|
628
|
+
end
|
629
|
+
|
630
|
+
def process_filter_attr_param(attr, metadata, filter_metadata, attr_values, value, opts)
|
631
|
+
attr = attr.strip.to_sym
|
632
|
+
attr_metadata = metadata[attr] ||
|
633
|
+
metadata[ModelApi::Utils.ext_query_attr(attr, opts)] || {}
|
634
|
+
key = attr_metadata[:key]
|
635
|
+
return unless key.present? && ModelApi::Utils.eval_bool(attr_metadata[:filter], opts)
|
636
|
+
filter_metadata[key] = attr_metadata
|
637
|
+
attr_values[key] = value
|
638
|
+
end
|
639
|
+
# rubocop:enable Metrics/ParameterLists
|
640
|
+
|
641
|
+
def apply_filter_param(attr_metadata, collection, opts = {})
|
642
|
+
raw_value = (opts[:attr_values] || params)[attr_metadata[:key]]
|
643
|
+
filter_table = opts[:filter_table]
|
644
|
+
klass = opts[:class] || Utils.find_class(collection, opts)
|
645
|
+
if raw_value.is_a?(Hash) && raw_value.include?('0')
|
646
|
+
operator_value_pairs = filter_process_param_array(params_array(raw_value), attr_metadata,
|
647
|
+
opts)
|
648
|
+
else
|
649
|
+
operator_value_pairs = filter_process_param(raw_value, attr_metadata, opts)
|
650
|
+
end
|
651
|
+
if (column = resolve_key_to_column(klass, attr_metadata)).present?
|
652
|
+
operator_value_pairs.each do |operator, value|
|
653
|
+
if operator == '=' && filter_table.blank?
|
654
|
+
collection = collection.where(column => value)
|
655
|
+
else
|
656
|
+
table_name = (filter_table || klass.table_name).to_s.delete('`')
|
657
|
+
column = column.to_s.delete('`')
|
658
|
+
if value.is_a?(Array)
|
659
|
+
operator = 'IN'
|
660
|
+
value = value.map { |_v| format_value_for_query(column, value, klass) }
|
661
|
+
value = "(#{value.map { |v| "'#{v.to_s.gsub("'", "''")}'" }.join(',')})"
|
662
|
+
else
|
663
|
+
value = "'#{value.gsub("'", "''")}'"
|
664
|
+
end
|
665
|
+
collection = collection.where("`#{table_name}`.`#{column}` #{operator} #{value}")
|
666
|
+
end
|
667
|
+
end
|
668
|
+
elsif (key = attr_metadata[:key]).present?
|
669
|
+
opts[:result_filters][key] = operator_value_pairs if opts.include?(:result_filters)
|
670
|
+
end
|
671
|
+
collection
|
672
|
+
end
|
673
|
+
|
674
|
+
def sort_collection(collection, sort_params, opts = {})
|
675
|
+
return [collection, {}] if sort_params.blank? # Don't filter if no filter params
|
676
|
+
klass = opts[:class] || Utils.find_class(collection, opts)
|
677
|
+
assoc_sorts, attr_sorts, result_sorts = process_sort_params(sort_params, klass,
|
678
|
+
opts.merge(result_sorts: result_sorts))
|
679
|
+
sort_table = opts[:sort_table]
|
680
|
+
sort_table = sort_table.to_s.delete('`') if sort_table.present?
|
681
|
+
attr_sorts.each do |key, sort_order|
|
682
|
+
if sort_table.present?
|
683
|
+
collection = collection.order("`#{sort_table}`.`#{key.to_s.delete('`')}` " \
|
684
|
+
"#{sort_order.to_s.upcase}")
|
685
|
+
else
|
686
|
+
collection = collection.order(key => sort_order)
|
687
|
+
end
|
688
|
+
end
|
689
|
+
assoc_sorts.each do |assoc, assoc_sort_params|
|
690
|
+
ar_assoc = klass.reflect_on_association(assoc)
|
691
|
+
next unless ar_assoc.present?
|
692
|
+
collection = collection.joins(assoc) unless collection.joins_values.include?(assoc)
|
693
|
+
collection, assoc_result_sorts = sort_collection(collection, assoc_sort_params,
|
694
|
+
opts.merge(class: ar_assoc.klass, sort_table: ar_assoc.table_name))
|
695
|
+
result_sorts[assoc] = assoc_result_sorts if assoc_result_sorts.present?
|
696
|
+
end
|
697
|
+
[collection, result_sorts]
|
698
|
+
end
|
699
|
+
|
700
|
+
def process_sort_params(sort_params, klass, opts)
|
701
|
+
metadata = ModelApi::Utils.filtered_ext_attrs(klass, :sort, opts)
|
702
|
+
assoc_sorts = {}
|
703
|
+
attr_sorts = {}
|
704
|
+
result_sorts = {}
|
705
|
+
sort_params.each do |attr, sort_order|
|
706
|
+
if attr.include?('.')
|
707
|
+
process_sort_param_assoc(attr, metadata, sort_order, assoc_sorts, opts)
|
708
|
+
else
|
709
|
+
attr = attr.strip.to_sym
|
710
|
+
attr_metadata = metadata[attr] || {}
|
711
|
+
next unless ModelApi::Utils.eval_bool(attr_metadata[:sort], opts)
|
712
|
+
sort_order = sort_order.to_sym
|
713
|
+
sort_order = :default unless [:asc, :desc].include?(sort_order)
|
714
|
+
if sort_order == :default
|
715
|
+
sort_order = (attr_metadata[:default_sort_order] || :asc).to_sym
|
716
|
+
sort_order = :asc unless [:asc, :desc].include?(sort_order)
|
717
|
+
end
|
718
|
+
if (column = resolve_key_to_column(klass, attr_metadata)).present?
|
719
|
+
attr_sorts[column] = sort_order
|
720
|
+
elsif (key = attr_metadata[:key]).present?
|
721
|
+
result_sorts[key] = sort_order
|
722
|
+
end
|
723
|
+
end
|
724
|
+
end
|
725
|
+
[assoc_sorts, attr_sorts, result_sorts]
|
726
|
+
end
|
727
|
+
|
728
|
+
# Intentionally disabling parameter list length check for private / internal method
|
729
|
+
# rubocop:disable Metrics/ParameterLists
|
730
|
+
def process_sort_param_assoc(attr, metadata, sort_order, assoc_sorts, opts)
|
731
|
+
attr_elems = attr.split('.')
|
732
|
+
assoc_name = attr_elems[0].strip.to_sym
|
733
|
+
assoc_metadata = metadata[assoc_name] || {}
|
734
|
+
key = assoc_metadata[:key]
|
735
|
+
return unless key.present? && ModelApi::Utils.eval_bool(assoc_metadata[:sort], opts)
|
736
|
+
assoc_sort_params = (assoc_sorts[key] ||= {})
|
737
|
+
assoc_sort_params[attr_elems[1..-1].join('.')] = sort_order
|
738
|
+
end
|
739
|
+
|
740
|
+
# rubocop:enable Metrics/ParameterLists
|
741
|
+
|
742
|
+
def filter_process_param(raw_value, attr_metadata, opts)
|
743
|
+
raw_value = raw_value.to_s.strip
|
744
|
+
array = nil
|
745
|
+
if raw_value.starts_with?('[') && raw_value.ends_with?(']')
|
746
|
+
array = JSON.parse(raw_value) rescue nil
|
747
|
+
array = array.is_a?(Array) ? array.map(&:to_s) : nil
|
748
|
+
end
|
749
|
+
if array.nil?
|
750
|
+
if attr_metadata.include?(:filter_delimiter)
|
751
|
+
delimiter = attr_metadata[:filter_delimiter]
|
752
|
+
else
|
753
|
+
delimiter = ','
|
754
|
+
end
|
755
|
+
array = raw_value.split(delimiter) if raw_value.include?(delimiter)
|
756
|
+
end
|
757
|
+
return filter_process_param_array(array, attr_metadata, opts) unless array.nil?
|
758
|
+
operator, value = parse_filter_operator(raw_value)
|
759
|
+
[[operator, ModelApi::Utils.transform_value(value, attr_metadata[:parse], opts)]]
|
760
|
+
end
|
761
|
+
|
762
|
+
def filter_process_param_array(array, attr_metadata, opts)
|
763
|
+
operator_value_pairs = []
|
764
|
+
equals_values = []
|
765
|
+
array.map(&:strip).reject(&:blank?).each do |value|
|
766
|
+
operator, value = parse_filter_operator(value)
|
767
|
+
value = ModelApi::Utils.transform_value(value.to_s, attr_metadata[:parse], opts)
|
768
|
+
if operator == '='
|
769
|
+
equals_values << value
|
770
|
+
else
|
771
|
+
operator_value_pairs << [operator, value]
|
772
|
+
end
|
773
|
+
end
|
774
|
+
operator_value_pairs << ['=', equals_values.uniq] if equals_values.present?
|
775
|
+
operator_value_pairs
|
776
|
+
end
|
777
|
+
|
778
|
+
def parse_filter_operator(value)
|
779
|
+
value = value.to_s.strip
|
780
|
+
if (operator = value.scan(/\A(>=|<=|!=|<>)[[:space:]]*\w/).flatten.first).present?
|
781
|
+
return (operator == '<>' ? '!=' : operator), value[2..-1].strip
|
782
|
+
elsif (operator = value.scan(/\A(>|<|=)[[:space:]]*\w/).flatten.first).present?
|
783
|
+
return operator, value[1..-1].strip
|
784
|
+
end
|
785
|
+
['=', value]
|
786
|
+
end
|
787
|
+
|
788
|
+
def format_value_for_query(column, value, klass)
|
789
|
+
return value.map { |v| format_value_for_query(column, v, klass) } if value.is_a?(Array)
|
790
|
+
column_metadata = klass.columns_hash[column.to_s]
|
791
|
+
case column_metadata.try(:type)
|
792
|
+
when :date, :datetime, :time, :timestamp
|
793
|
+
if (user_tz = current_user.try(:preference).try(:time_zone)).present?
|
794
|
+
time_zone = ActiveSupport::TimeZone.new(user_tz)
|
795
|
+
end
|
796
|
+
time_zone ||= ActiveSupport::TimeZone.new('Eastern Time (US & Canada)')
|
797
|
+
return time_zone.parse(value.to_s).try(:to_s, :db)
|
798
|
+
when :float, :decimal
|
799
|
+
return value.to_d.to_s
|
800
|
+
when :integer, :primary_key
|
801
|
+
return value.to_d.to_s.sub(/\.0\Z/, '')
|
802
|
+
when :boolean
|
803
|
+
return value ? 'true' : 'false'
|
804
|
+
end
|
805
|
+
value.to_s
|
806
|
+
end
|
807
|
+
|
808
|
+
def params_array(raw_value)
|
809
|
+
index = 0
|
810
|
+
array = []
|
811
|
+
while raw_value.include?(index.to_s)
|
812
|
+
array << raw_value[index.to_s]
|
813
|
+
index += 1
|
814
|
+
end
|
815
|
+
array
|
816
|
+
end
|
817
|
+
|
818
|
+
def paginate_collection(collection, collection_links, opts, coll_route)
|
819
|
+
collection_size = collection.count
|
820
|
+
page_size = (params[:page_size] || DEFAULT_PAGE_SIZE).to_i
|
821
|
+
page = [params[:page].to_i, 1].max
|
822
|
+
page_count = [(collection_size - 1) / page_size, 1].max
|
823
|
+
page = page_count if page > page_count
|
824
|
+
offset = (page - 1) * page_size
|
825
|
+
|
826
|
+
opts = opts.dup
|
827
|
+
opts[:count] ||= collection_size
|
828
|
+
opts[:page] ||= page
|
829
|
+
opts[:page_size] ||= page_size
|
830
|
+
opts[:page_count] ||= page_count
|
831
|
+
|
832
|
+
response.headers['X-Total-Count'] = collection_size.to_s
|
833
|
+
|
834
|
+
opts[:collection_link_options] = (opts[:collection_link_options] || {})
|
835
|
+
.reject { |k, _v| [:page].include?(k.to_sym) }
|
836
|
+
opts[:object_link_options] = (opts[:object_link_options] || {})
|
837
|
+
.reject { |k, _v| [:page, :page_size].include?(k.to_sym) }
|
838
|
+
|
839
|
+
if collection_size > page_size
|
840
|
+
opts[:collection_link_options][:page] = page
|
841
|
+
Utils.add_pagination_links(collection_links, coll_route, page, page_count)
|
842
|
+
collection = collection.limit(page_size).offset(offset)
|
843
|
+
end
|
844
|
+
|
845
|
+
[collection, collection_links, opts]
|
846
|
+
end
|
847
|
+
|
848
|
+
def resolve_key_to_column(klass, attr_metadata)
|
849
|
+
return nil unless klass.respond_to?(:columns_hash)
|
850
|
+
columns_hash = klass.columns_hash
|
851
|
+
key = attr_metadata[:key]
|
852
|
+
return key if columns_hash.include?(key.to_s)
|
853
|
+
render_method = attr_metadata[:render_method]
|
854
|
+
render_method = render_method.to_s if render_method.is_a?(Symbol)
|
855
|
+
return nil unless render_method.is_a?(String)
|
856
|
+
columns_hash.include?(render_method) ? render_method : nil
|
857
|
+
end
|
858
|
+
|
859
|
+
def add_collection_object_route(opts)
|
860
|
+
object_route = opts[:object_route]
|
861
|
+
unless object_route.present?
|
862
|
+
route_name = ModelApi::Utils.route_name(request)
|
863
|
+
if route_name.present?
|
864
|
+
if (singular_route_name = route_name.singularize) != route_name
|
865
|
+
object_route = singular_route_name
|
866
|
+
end
|
867
|
+
end
|
868
|
+
end
|
869
|
+
if object_route.present? && (object_route.is_a?(String) || object_route.is_a?(Symbol))
|
870
|
+
object_route = nil unless self.respond_to?("#{object_route}_url")
|
871
|
+
end
|
872
|
+
object_route = opts[:default_object_route] if object_route.blank?
|
873
|
+
return if object_route.blank?
|
874
|
+
opts[:object_links] = (opts[:object_links] || {}).merge(self: object_route)
|
875
|
+
end
|
876
|
+
|
877
|
+
def add_hateoas_links_for_update(opts)
|
878
|
+
object_route = opts[:object_route] || self
|
879
|
+
links = { self: object_route }.reverse_merge(common_response_links(opts))
|
880
|
+
opts[:links] = links.merge(opts[:links] || {})
|
881
|
+
end
|
882
|
+
|
883
|
+
def add_hateoas_links_for_updated_object(_operation, opts)
|
884
|
+
object_route = opts[:object_route] || self
|
885
|
+
object_links = { self: object_route }
|
886
|
+
opts[:object_links] = object_links.merge(opts[:object_links] || {})
|
887
|
+
end
|
888
|
+
|
889
|
+
def verify_update_request_body(request_body, format, opts = {})
|
890
|
+
if request.format.symbol.nil? && format.present?
|
891
|
+
opts[:format] ||= format
|
892
|
+
end
|
893
|
+
|
894
|
+
if request_body.is_a?(Array)
|
895
|
+
fail 'Expected object, but collection provided'
|
896
|
+
elsif !request_body.is_a?(Hash)
|
897
|
+
fail 'Expected object'
|
898
|
+
end
|
899
|
+
end
|
900
|
+
|
901
|
+
def filtered_by_foreign_key?(query)
|
902
|
+
fk_cache = self.class.instance_variable_get(:@foreign_key_cache)
|
903
|
+
self.class.instance_variable_set(:@foreign_key_cache, fk_cache = {}) if fk_cache.nil?
|
904
|
+
klass = query.klass
|
905
|
+
foreign_keys = (fk_cache[klass] ||= query.klass.reflections.values
|
906
|
+
.select { |a| a.macro == :belongs_to }.map { |a| a.foreign_key.to_s })
|
907
|
+
(query.values[:where] || []).select { |v| v.is_a?(Arel::Nodes::Equality) }
|
908
|
+
.map { |v| v.left.name }.each do |key|
|
909
|
+
return true if foreign_keys.include?(key)
|
910
|
+
end
|
911
|
+
false
|
912
|
+
rescue Exception => e
|
913
|
+
Rails.logger.warn "Exception encounterd determining if query is filtered: #{e.message}\n" \
|
914
|
+
"#{e.backtrace.join("\n")}"
|
915
|
+
end
|
916
|
+
end
|
917
|
+
|
918
|
+
class Utils
|
919
|
+
def self.find_class(obj, opts = {})
|
920
|
+
return nil if obj.nil?
|
921
|
+
opts[:class] || (obj.respond_to?(:klass) ? obj.klass : obj.class)
|
922
|
+
end
|
923
|
+
|
924
|
+
def self.add_pagination_links(collection_links, coll_route, page, last_page)
|
925
|
+
if page < last_page
|
926
|
+
collection_links[:next] = [coll_route, { page: (page + 1) }]
|
927
|
+
end
|
928
|
+
collection_links[:prev] = [coll_route, { page: (page - 1) }] if page > 1
|
929
|
+
collection_links[:first] = [coll_route, { page: 1 }]
|
930
|
+
collection_links[:last] = [coll_route, { page: last_page }]
|
931
|
+
end
|
932
|
+
|
933
|
+
def self.object_from_req_body(root_elem, req_body, format)
|
934
|
+
if format == :json
|
935
|
+
request_obj = req_body
|
936
|
+
else
|
937
|
+
request_obj = req_body[root_elem]
|
938
|
+
if request_obj.blank?
|
939
|
+
request_obj = req_body['obj']
|
940
|
+
if request_obj.blank? && req_body.size == 1
|
941
|
+
request_obj = req_body.values.first
|
942
|
+
end
|
943
|
+
end
|
944
|
+
end
|
945
|
+
fail 'Invalid request format' unless request_obj.present?
|
946
|
+
request_obj
|
947
|
+
end
|
948
|
+
|
949
|
+
def self.apply_updates(obj, req_obj, operation, opts = {})
|
950
|
+
opts = opts.merge(object: opts[:object] || obj)
|
951
|
+
metadata = ModelApi::Utils.filtered_ext_attrs(opts[:api_attr_metadata] ||
|
952
|
+
ModelApi::Utils.filtered_attrs(obj, operation, opts), operation, opts)
|
953
|
+
set_context_attrs(obj, opts)
|
954
|
+
req_obj.each do |attr, value|
|
955
|
+
attr = attr.to_sym
|
956
|
+
attr_metadata = metadata[attr]
|
957
|
+
unless attr_metadata.present?
|
958
|
+
add_ignored_field(opts[:ignored_fields], attr, value, attr_metadata)
|
959
|
+
next
|
960
|
+
end
|
961
|
+
update_api_attr(obj, attr, value, opts.merge(attr_metadata: attr_metadata))
|
962
|
+
end
|
963
|
+
end
|
964
|
+
|
965
|
+
def self.set_context_attrs(obj, opts = {})
|
966
|
+
klass = (obj.class < ActiveRecord::Base ? obj.class : nil)
|
967
|
+
(opts[:context] || {}).each do |key, value|
|
968
|
+
begin
|
969
|
+
setter = "#{key}="
|
970
|
+
next unless obj.respond_to?(setter)
|
971
|
+
if (column = klass.try(:columns_hash).try(:[], key.to_s)).present?
|
972
|
+
case column.type
|
973
|
+
when :integer, :primary_key then
|
974
|
+
obj.send("#{key}=", value.to_i)
|
975
|
+
when :decimal, :float then
|
976
|
+
obj.send("#{key}=", value.to_f)
|
977
|
+
else
|
978
|
+
obj.send(setter, value.to_s)
|
979
|
+
end
|
980
|
+
else
|
981
|
+
obj.send(setter, value.to_s)
|
982
|
+
end
|
983
|
+
rescue Exception => e
|
984
|
+
Rails.logger.warn "Error encountered assigning context parameter #{key} to " \
|
985
|
+
"'#{value}' (skipping): \"#{e.message}\")."
|
986
|
+
end
|
987
|
+
end
|
988
|
+
end
|
989
|
+
|
990
|
+
def self.process_updated_model_save(obj, operation, opts = {})
|
991
|
+
opts = opts.dup
|
992
|
+
opts[:operation] = operation
|
993
|
+
metadata = opts.delete(:api_attr_metadata) ||
|
994
|
+
ModelApi::Utils.filtered_attrs(obj, operation, opts)
|
995
|
+
model_metadata = opts.delete(:api_model_metadata) || ModelApi::Utils.model_metadata(obj.class)
|
996
|
+
ModelApi::Utils.invoke_callback(model_metadata[:before_validate], obj, opts.dup)
|
997
|
+
validate_operation(obj, operation, opts)
|
998
|
+
validate_preserving_existing_errors(obj)
|
999
|
+
ModelApi::Utils.invoke_callback(model_metadata[:before_save], obj, opts.dup)
|
1000
|
+
obj.instance_variable_set(:@readonly, false) if obj.instance_variable_get(:@readonly)
|
1001
|
+
successful = obj.save unless obj.errors.present?
|
1002
|
+
if successful
|
1003
|
+
suggested_response_status = :ok
|
1004
|
+
object_errors = []
|
1005
|
+
else
|
1006
|
+
suggested_response_status = :bad_request
|
1007
|
+
object_errors = extract_msgs_for_error(obj, opts.merge(api_attr_metadata: metadata))
|
1008
|
+
unless object_errors.present?
|
1009
|
+
object_errors << {
|
1010
|
+
error: 'Unspecified error',
|
1011
|
+
message: "Unspecified error processing #{operation}: " \
|
1012
|
+
'Please contact customer service for further assistance.'
|
1013
|
+
}
|
1014
|
+
end
|
1015
|
+
end
|
1016
|
+
[suggested_response_status, object_errors]
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
def self.extract_msgs_for_error(obj, opts = {})
|
1020
|
+
object_errors = []
|
1021
|
+
attr_prefix = opts[:attr_prefix] || ''
|
1022
|
+
api_metadata = opts[:api_attr_metadata] || ModelApi::Utils.api_attrs(obj.class)
|
1023
|
+
obj.errors.each do |attr, attr_errors|
|
1024
|
+
attr_errors = [attr_errors] unless attr_errors.is_a?(Array)
|
1025
|
+
attr_errors.each do |error|
|
1026
|
+
attr_metadata = api_metadata[attr] || {}
|
1027
|
+
qualified_attr = "#{attr_prefix}#{ModelApi::Utils.ext_attr(attr, attr_metadata)}"
|
1028
|
+
assoc_errors = nil
|
1029
|
+
if attr_metadata[:type] == :association
|
1030
|
+
assoc_errors = extract_assoc_error_msgs(obj, attr, opts.merge(
|
1031
|
+
attr_metadata: attr_metadata))
|
1032
|
+
end
|
1033
|
+
if assoc_errors.present?
|
1034
|
+
object_errors += assoc_errors
|
1035
|
+
else
|
1036
|
+
error_hash = {}
|
1037
|
+
error_hash[:object] = attr_prefix if attr_prefix.present?
|
1038
|
+
error_hash[:attribute] = qualified_attr unless attr == :base
|
1039
|
+
object_errors << error_hash.merge(error: error,
|
1040
|
+
message: (attr == :base ? error : "#{qualified_attr} #{error}"))
|
1041
|
+
end
|
1042
|
+
end
|
1043
|
+
end
|
1044
|
+
object_errors
|
1045
|
+
end
|
1046
|
+
|
1047
|
+
# rubocop:disable Metrics/MethodLength
|
1048
|
+
def self.extract_assoc_error_msgs(obj, attr, opts)
|
1049
|
+
object_errors = []
|
1050
|
+
attr_metadata = opts[:attr_metadata] || {}
|
1051
|
+
processed_assoc_objects = {}
|
1052
|
+
assoc = attr_metadata[:association]
|
1053
|
+
assoc_class = assoc.class_name.constantize
|
1054
|
+
external_attr = ModelApi::Utils.ext_attr(attr, attr_metadata)
|
1055
|
+
attr_metadata_create = attr_metadata_update = nil
|
1056
|
+
if assoc.macro == :has_many
|
1057
|
+
obj.send(attr).each_with_index do |assoc_obj, index|
|
1058
|
+
next if processed_assoc_objects[assoc_obj]
|
1059
|
+
processed_assoc_objects[assoc_obj] = true
|
1060
|
+
attr_prefix = "#{external_attr}[#{index}]."
|
1061
|
+
if assoc_obj.new_record?
|
1062
|
+
attr_metadata_create ||= ModelApi::Utils.filtered_attrs(assoc_class, :create, opts)
|
1063
|
+
object_errors += extract_msgs_for_error(assoc_obj, opts.merge(
|
1064
|
+
attr_prefix: attr_prefix, api_attr_metadata: attr_metadata_create))
|
1065
|
+
else
|
1066
|
+
attr_metadata_update ||= ModelApi::Utils.filtered_attrs(assoc_class, :update, opts)
|
1067
|
+
object_errors += extract_msgs_for_error(assoc_obj, opts.merge(
|
1068
|
+
attr_prefix: attr_prefix, api_attr_metadata: attr_metadata_update))
|
1069
|
+
end
|
1070
|
+
end
|
1071
|
+
else
|
1072
|
+
assoc_obj = obj.send(attr)
|
1073
|
+
return object_errors unless assoc_obj.present? && !processed_assoc_objects[assoc_obj]
|
1074
|
+
processed_assoc_objects[assoc_obj] = true
|
1075
|
+
attr_prefix = "#{external_attr}->"
|
1076
|
+
if assoc_obj.new_record?
|
1077
|
+
attr_metadata_create ||= ModelApi::Utils.filtered_attrs(assoc_class, :create, opts)
|
1078
|
+
object_errors += extract_msgs_for_error(assoc_obj, opts.merge(
|
1079
|
+
attr_prefix: attr_prefix, api_attr_metadata: attr_metadata_create))
|
1080
|
+
else
|
1081
|
+
attr_metadata_update ||= ModelApi::Utils.filtered_attrs(assoc_class, :update, opts)
|
1082
|
+
object_errors += extract_msgs_for_error(assoc_obj, opts.merge(
|
1083
|
+
attr_prefix: attr_prefix, api_attr_metadata: attr_metadata_update))
|
1084
|
+
end
|
1085
|
+
end
|
1086
|
+
object_errors
|
1087
|
+
end
|
1088
|
+
|
1089
|
+
# rubocop:enable Metrics/MethodLength
|
1090
|
+
|
1091
|
+
def self.process_object_destroy(obj, operation, opts)
|
1092
|
+
soft_delete = obj.errors.present? ? false : object_destroy(obj, opts)
|
1093
|
+
|
1094
|
+
if obj.errors.blank? && (soft_delete || obj.destroyed?)
|
1095
|
+
response_status = :ok
|
1096
|
+
object_errors = []
|
1097
|
+
else
|
1098
|
+
object_errors = extract_msgs_for_error(obj, opts)
|
1099
|
+
if object_errors.present?
|
1100
|
+
response_status = :bad_request
|
1101
|
+
else
|
1102
|
+
response_status = :internal_server_error
|
1103
|
+
object_errors << {
|
1104
|
+
error: 'Unspecified error',
|
1105
|
+
message: "Unspecified error processing #{operation}: " \
|
1106
|
+
'Please contact customer service for further assistance.'
|
1107
|
+
}
|
1108
|
+
end
|
1109
|
+
end
|
1110
|
+
|
1111
|
+
[response_status, object_errors]
|
1112
|
+
end
|
1113
|
+
|
1114
|
+
def self.object_destroy(obj, opts = {})
|
1115
|
+
klass = find_class(obj)
|
1116
|
+
object_id = obj.send(opts[:id_attribute] || :id)
|
1117
|
+
obj.instance_variable_set(:@readonly, false) if obj.instance_variable_get(:@readonly)
|
1118
|
+
if (deleted_col = klass.columns_hash['deleted']).present?
|
1119
|
+
case deleted_col.type
|
1120
|
+
when :boolean
|
1121
|
+
obj.update_attribute(:deleted, true)
|
1122
|
+
return true
|
1123
|
+
when :integer, :decimal
|
1124
|
+
obj.update_attribute(:deleted, 1)
|
1125
|
+
return true
|
1126
|
+
else
|
1127
|
+
obj.destroy
|
1128
|
+
end
|
1129
|
+
else
|
1130
|
+
obj.destroy
|
1131
|
+
end
|
1132
|
+
false
|
1133
|
+
rescue Exception => e
|
1134
|
+
Rails.logger.warn "Error destroying #{klass.name} \"#{object_id}\": \"#{e.message}\")."
|
1135
|
+
false
|
1136
|
+
end
|
1137
|
+
|
1138
|
+
def self.set_api_attr(obj, attr, value, opts)
|
1139
|
+
attr_metadata = opts[:attr_metadata]
|
1140
|
+
internal_field = attr_metadata[:key] || attr
|
1141
|
+
setter = attr_metadata[:setter] || "#{(internal_field)}="
|
1142
|
+
unless obj.respond_to?(setter)
|
1143
|
+
Rails.logger.warn "Error encountered assigning API input for attribute \"#{attr}\" " \
|
1144
|
+
'(setter not found): skipping.'
|
1145
|
+
add_ignored_field(opts[:ignored_fields], attr, value, attr_metadata)
|
1146
|
+
return
|
1147
|
+
end
|
1148
|
+
obj.send(setter, value)
|
1149
|
+
end
|
1150
|
+
|
1151
|
+
def self.update_api_attr(obj, attr, value, opts = {})
|
1152
|
+
attr_metadata = opts[:attr_metadata]
|
1153
|
+
begin
|
1154
|
+
value = ModelApi::Utils.transform_value(value, attr_metadata[:parse], opts)
|
1155
|
+
rescue Exception => e
|
1156
|
+
Rails.logger.warn "Error encountered parsing API input for attribute \"#{attr}\" " \
|
1157
|
+
"(\"#{e.message}\"): \"#{value.to_s.first(1000)}\" ... using raw value instead."
|
1158
|
+
end
|
1159
|
+
begin
|
1160
|
+
if attr_metadata[:type] == :association && attr_metadata[:parse].blank?
|
1161
|
+
attr_metadata = opts[:attr_metadata]
|
1162
|
+
assoc = attr_metadata[:association]
|
1163
|
+
if assoc.macro == :has_many
|
1164
|
+
update_has_many_assoc(obj, attr, value, opts)
|
1165
|
+
elsif assoc.macro == :belongs_to
|
1166
|
+
update_belongs_to_assoc(obj, attr, value, opts)
|
1167
|
+
else
|
1168
|
+
add_ignored_field(opts[:ignored_fields], attr, value, attr_metadata)
|
1169
|
+
end
|
1170
|
+
else
|
1171
|
+
set_api_attr(obj, attr, value, opts)
|
1172
|
+
end
|
1173
|
+
rescue Exception => e
|
1174
|
+
handle_api_setter_exception(e, obj, attr_metadata, opts)
|
1175
|
+
end
|
1176
|
+
end
|
1177
|
+
|
1178
|
+
def self.update_has_many_assoc(obj, attr, value, opts = {})
|
1179
|
+
attr_metadata = opts[:attr_metadata]
|
1180
|
+
assoc = attr_metadata[:association]
|
1181
|
+
assoc_class = assoc.class_name.constantize
|
1182
|
+
model_metadata = ModelApi::Utils.model_metadata(assoc_class)
|
1183
|
+
value_array = value.to_a rescue nil
|
1184
|
+
unless value_array.is_a?(Array)
|
1185
|
+
obj.errors.add(attr, 'must be supplied as an array of objects')
|
1186
|
+
return
|
1187
|
+
end
|
1188
|
+
opts = opts.merge(model_metadata: model_metadata)
|
1189
|
+
opts[:ignored_fields] = [] if opts.include?(:ignored_fields)
|
1190
|
+
assoc_objs = []
|
1191
|
+
value_array.each_with_index do |assoc_payload, index|
|
1192
|
+
opts[:ignored_fields].clear if opts.include?(:ignored_fields)
|
1193
|
+
assoc_objs << update_has_many_assoc_obj(assoc_class, assoc_payload,
|
1194
|
+
opts.merge(model_metadata: model_metadata))
|
1195
|
+
if opts[:ignored_fields].present?
|
1196
|
+
external_attr = ModelApi::Utils.ext_attr(attr, attr_metadata)
|
1197
|
+
opts[:ignored_fields] << { "#{external_attr}[#{index}]" => opts[:ignored_fields] }
|
1198
|
+
end
|
1199
|
+
end
|
1200
|
+
set_api_attr(obj, attr, assoc_objs, opts)
|
1201
|
+
end
|
1202
|
+
|
1203
|
+
def self.update_has_many_assoc_obj(assoc_class, assoc_payload, opts = {})
|
1204
|
+
model_metadata = opts[:model_metadata] || ModelApi::Utils.model_metadata(assoc_class)
|
1205
|
+
assoc_obj = find_by_id_attrs(model_metadata[:id_attributes], assoc_class, assoc_payload)
|
1206
|
+
assoc_obj = assoc_obj.first unless assoc_obj.nil? || assoc_obj.count != 1
|
1207
|
+
assoc_obj ||= assoc_class.new
|
1208
|
+
if assoc_obj.new_record?
|
1209
|
+
assoc_oper = :create
|
1210
|
+
opts[:create_opts] ||= opts.merge(api_attr_metadata: ModelApi::Utils.filtered_attrs(
|
1211
|
+
assoc_class, :create, opts))
|
1212
|
+
assoc_opts = opts[:create_opts]
|
1213
|
+
else
|
1214
|
+
assoc_oper = :update
|
1215
|
+
opts[:update_opts] ||= opts.merge(api_attr_metadata: ModelApi::Utils.filtered_attrs(
|
1216
|
+
assoc_class, :update, opts))
|
1217
|
+
assoc_opts = opts[:update_opts]
|
1218
|
+
end
|
1219
|
+
apply_updates(assoc_obj, assoc_payload, assoc_oper, assoc_opts)
|
1220
|
+
ModelApi::Utils.invoke_callback(model_metadata[:after_initialize], assoc_obj,
|
1221
|
+
assoc_opts.merge(operation: assoc_oper).freeze)
|
1222
|
+
assoc_obj
|
1223
|
+
end
|
1224
|
+
|
1225
|
+
def self.update_belongs_to_assoc(obj, attr, value, opts = {})
|
1226
|
+
attr_metadata = opts[:attr_metadata]
|
1227
|
+
assoc = attr_metadata[:association]
|
1228
|
+
assoc_class = assoc.class_name.constantize
|
1229
|
+
assoc_opts = opts[:ignored_fields].is_a?(Array) ? opts.merge(ignored_fields: []) : opts
|
1230
|
+
model_metadata = ModelApi::Utils.model_metadata(assoc_class)
|
1231
|
+
assoc_obj = find_by_id_attrs(model_metadata[:id_attributes], assoc_class, value)
|
1232
|
+
assoc_obj = assoc_obj.first unless assoc_obj.nil? || assoc_obj.count != 1
|
1233
|
+
assoc_obj ||= assoc_class.new
|
1234
|
+
obj_oper = assoc_obj.new_record? ? :create : :update
|
1235
|
+
assoc_opts = assoc_opts.merge(
|
1236
|
+
api_attr_metadata: ModelApi::Utils.filtered_attrs(assoc_class, obj_oper, opts))
|
1237
|
+
unless value.is_a?(Hash)
|
1238
|
+
obj.errors.add(attr, 'must be supplied as an object')
|
1239
|
+
return
|
1240
|
+
end
|
1241
|
+
apply_updates(assoc_obj, value, obj_oper, assoc_opts)
|
1242
|
+
ModelApi::Utils.invoke_callback(model_metadata[:after_initialize], assoc_obj,
|
1243
|
+
opts.merge(operation: obj_oper).freeze)
|
1244
|
+
if assoc_opts[:ignored_fields].present?
|
1245
|
+
external_attr = ModelApi::Utils.ext_attr(attr, attr_metadata)
|
1246
|
+
opts[:ignored_fields] << { external_attr.to_s => assoc_opts[:ignored_fields] }
|
1247
|
+
end
|
1248
|
+
set_api_attr(obj, attr, assoc_obj, opts)
|
1249
|
+
end
|
1250
|
+
|
1251
|
+
def self.find_by_id_attrs(id_attributes, assoc_class, assoc_payload)
|
1252
|
+
return nil unless id_attributes.present?
|
1253
|
+
query = nil
|
1254
|
+
id_attributes.each do |id_attr|
|
1255
|
+
if assoc_payload.include?(id_attr.to_s)
|
1256
|
+
query = (query || assoc_class).where(id_attr => assoc_payload[id_attr.to_s])
|
1257
|
+
else
|
1258
|
+
return nil
|
1259
|
+
end
|
1260
|
+
end
|
1261
|
+
query
|
1262
|
+
end
|
1263
|
+
|
1264
|
+
def self.apply_context(query, opts = {})
|
1265
|
+
context = opts[:context]
|
1266
|
+
return query if context.nil?
|
1267
|
+
if context.respond_to?(:call)
|
1268
|
+
query = context.send(*([:call, query, opts][0..context.parameters.size]))
|
1269
|
+
elsif context.is_a?(Hash)
|
1270
|
+
context.each { |attr, value| query = query.where(attr => value) }
|
1271
|
+
end
|
1272
|
+
query
|
1273
|
+
end
|
1274
|
+
|
1275
|
+
def self.handle_api_setter_exception(e, obj, attr_metadata, opts = {})
|
1276
|
+
return unless attr_metadata.is_a?(Hash)
|
1277
|
+
on_exception = attr_metadata[:on_exception]
|
1278
|
+
fail e unless on_exception.present?
|
1279
|
+
on_exception = { Exception => on_exception } unless on_exception.is_a?(Hash)
|
1280
|
+
opts = opts.frozen? ? opts : opts.dup.freeze
|
1281
|
+
on_exception.each do |klass, handler|
|
1282
|
+
klass = klass.to_s.constantize rescue nil unless klass.is_a?(Class)
|
1283
|
+
next unless klass.is_a?(Class) && e.is_a?(klass)
|
1284
|
+
if handler.respond_to?(:call)
|
1285
|
+
ModelApi::Utils.invoke_callback(handler, obj, e, opts)
|
1286
|
+
elsif handler.present?
|
1287
|
+
# Presume handler is an error message in this case
|
1288
|
+
obj.errors.add(attr_metadata[:key], handler.to_s)
|
1289
|
+
else
|
1290
|
+
add_ignored_field(opts[:ignored_fields], nil, opts[:value], attr_metadata)
|
1291
|
+
end
|
1292
|
+
break
|
1293
|
+
end
|
1294
|
+
end
|
1295
|
+
|
1296
|
+
def self.add_ignored_field(ignored_fields, attr, value, attr_metadata)
|
1297
|
+
return unless ignored_fields.is_a?(Array)
|
1298
|
+
attr_metadata ||= {}
|
1299
|
+
external_attr = ModelApi::Utils.ext_attr(attr, attr_metadata)
|
1300
|
+
return unless external_attr.present?
|
1301
|
+
ignored_fields << { external_attr => value }
|
1302
|
+
end
|
1303
|
+
|
1304
|
+
def self.validate_operation(obj, operation, opts = {})
|
1305
|
+
klass = find_class(obj, opts)
|
1306
|
+
model_metadata = opts[:api_model_metadata] || ModelApi::Utils.model_metadata(klass)
|
1307
|
+
return nil unless operation.present?
|
1308
|
+
opts = opts.frozen? ? opts : opts.dup.freeze
|
1309
|
+
if obj.nil?
|
1310
|
+
ModelApi::Utils.invoke_callback(model_metadata[:"validate_#{operation}"], opts)
|
1311
|
+
else
|
1312
|
+
ModelApi::Utils.invoke_callback(model_metadata[:"validate_#{operation}"], obj, opts)
|
1313
|
+
end
|
1314
|
+
end
|
1315
|
+
|
1316
|
+
def self.validate_preserving_existing_errors(obj)
|
1317
|
+
if obj.errors.present?
|
1318
|
+
errors = obj.errors.messages.dup
|
1319
|
+
obj.valid?
|
1320
|
+
errors = obj.errors.messages.merge(errors)
|
1321
|
+
obj.errors.clear
|
1322
|
+
errors.each { |field, error| obj.errors.add(field, error) }
|
1323
|
+
else
|
1324
|
+
obj.valid?
|
1325
|
+
end
|
1326
|
+
end
|
1327
|
+
|
1328
|
+
def self.class_or_sti_subclass(klass, req_body, operation, opts = {})
|
1329
|
+
metadata = ModelApi::Utils.filtered_attrs(klass, :create, opts)
|
1330
|
+
if operation == :create && (attr_metadata = metadata[:type]).is_a?(Hash) &&
|
1331
|
+
req_body.is_a?(Hash)
|
1332
|
+
external_attr = ModelApi::Utils.ext_attr(:type, attr_metadata)
|
1333
|
+
type = req_body[external_attr.to_s]
|
1334
|
+
begin
|
1335
|
+
type = ModelApi::Utils.transform_value(type, attr_metadata[:parse], opts.dup)
|
1336
|
+
rescue Exception => e
|
1337
|
+
Rails.logger.warn 'Error encountered parsing API input for attribute ' \
|
1338
|
+
"\"#{external_attr}\" (\"#{e.message}\"): \"#{type.to_s.first(1000)}\" ... " \
|
1339
|
+
'using raw value instead.'
|
1340
|
+
end
|
1341
|
+
if type.present? && (type = type.camelize) != klass.name
|
1342
|
+
Rails.application.eager_load!
|
1343
|
+
klass.subclasses.each do |subclass|
|
1344
|
+
return subclass if subclass.name == type
|
1345
|
+
end
|
1346
|
+
end
|
1347
|
+
end
|
1348
|
+
klass
|
1349
|
+
end
|
1350
|
+
end
|
1351
|
+
end
|
1352
|
+
end
|