model-api 0.8.3 → 0.8.4
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/lib/model-api/base_controller.rb +126 -119
- data/lib/model-api/renderer.rb +1 -1
- data/model-api.gemspec +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 14b9b0dc45ee29320fa640f6b209b5481b5f75b7
|
4
|
+
data.tar.gz: 9f693375afd6e922b43eaaad1fc0f02ae6f5908e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f7c8fbbe1fd448f4abfa12b38707b7e15d018a025f2456b7d52cde0ac81212eeff4a3039e9a677386cb3d61795ca89ebdf2c43c80d49f58ab7adfc7e75487c31
|
7
|
+
data.tar.gz: 350cd78aae1b10b904b187b06fbcefb69e1ef57adb306ef8273ea3545fce57704ec0fde053562d4828f6755dd8688e9e08ad585f4d5f8456ca3d539ef96e3e1e
|
@@ -4,45 +4,45 @@ module ModelApi
|
|
4
4
|
def model_class
|
5
5
|
nil
|
6
6
|
end
|
7
|
-
|
7
|
+
|
8
8
|
def base_api_options
|
9
9
|
{}
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
def base_admin_api_options
|
13
13
|
base_api_options.merge(admin_only: true)
|
14
14
|
end
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
def self.included(base)
|
18
18
|
base.extend(ClassMethods)
|
19
|
-
|
19
|
+
|
20
20
|
base.send(:include, InstanceMethods)
|
21
|
-
|
21
|
+
|
22
22
|
base.send(:before_filter, :common_headers)
|
23
|
-
|
23
|
+
|
24
24
|
base.send(:rescue_from, Exception, with: :unhandled_exception)
|
25
25
|
base.send(:respond_to, :json, :xml)
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
module InstanceMethods
|
29
29
|
SIMPLE_ID_REGEX = /\A[0-9]+\Z/
|
30
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
31
|
{12}\Z/x
|
32
32
|
DEFAULT_PAGE_SIZE = 100
|
33
|
-
|
33
|
+
|
34
34
|
protected
|
35
|
-
|
35
|
+
|
36
36
|
def model_class
|
37
37
|
self.class.model_class
|
38
38
|
end
|
39
|
-
|
39
|
+
|
40
40
|
def render_collection(collection, opts = {})
|
41
41
|
return unless ensure_admin_if_admin_only(opts)
|
42
42
|
opts = prepare_options(opts)
|
43
43
|
opts[:operation] ||= :index
|
44
44
|
return unless validate_read_operation(collection, opts[:operation], opts)
|
45
|
-
|
45
|
+
|
46
46
|
coll_route = opts[:collection_route] || self
|
47
47
|
collection_links = { self: coll_route }
|
48
48
|
collection = process_collection_includes(collection, opts)
|
@@ -50,19 +50,19 @@ module ModelApi
|
|
50
50
|
collection, _result_sorts = sort_collection(collection, find_sort_params, opts)
|
51
51
|
collection, collection_links, opts = paginate_collection(collection,
|
52
52
|
collection_links, opts, coll_route)
|
53
|
-
|
53
|
+
|
54
54
|
opts[:collection_links] = collection_links.merge(opts[:collection_links] || {})
|
55
55
|
.reverse_merge(common_response_links(opts))
|
56
56
|
add_collection_object_route(opts)
|
57
57
|
ModelApi::Renderer.render(self, collection, opts)
|
58
58
|
end
|
59
|
-
|
59
|
+
|
60
60
|
def render_object(obj, opts = {})
|
61
61
|
return unless ensure_admin_if_admin_only(opts)
|
62
62
|
opts = prepare_options(opts)
|
63
63
|
klass = Utils.find_class(obj, opts)
|
64
64
|
object_route = opts[:object_route] || self
|
65
|
-
|
65
|
+
|
66
66
|
opts[:object_links] = { self: object_route }
|
67
67
|
if obj.is_a?(ActiveRecord::Base)
|
68
68
|
return unless validate_read_operation(obj, opts[:operation], opts)
|
@@ -75,12 +75,12 @@ module ModelApi
|
|
75
75
|
obj = ModelApi::Utils.ext_value(obj, opts) unless opts[:raw_output]
|
76
76
|
opts[:object_links].merge!(opts[:links] || {})
|
77
77
|
end
|
78
|
-
|
78
|
+
|
79
79
|
opts[:operation] ||= :show
|
80
80
|
opts[:object_links].reverse_merge!(common_response_links(opts))
|
81
81
|
ModelApi::Renderer.render(self, obj, opts)
|
82
82
|
end
|
83
|
-
|
83
|
+
|
84
84
|
def do_create(opts = {})
|
85
85
|
klass = opts[:model_class] || model_class
|
86
86
|
return unless ensure_admin_if_admin_only(opts)
|
@@ -91,19 +91,19 @@ module ModelApi
|
|
91
91
|
return bad_payload(class: klass) if opts[:bad_payload]
|
92
92
|
create_and_render_object(obj, opts)
|
93
93
|
end
|
94
|
-
|
94
|
+
|
95
95
|
def prepare_object_for_create(klass, opts = {})
|
96
96
|
opts = prepare_options(opts)
|
97
97
|
get_updated_object(klass, get_operation(:create, opts), opts)
|
98
98
|
end
|
99
|
-
|
99
|
+
|
100
100
|
def create_and_render_object(obj, opts = {})
|
101
101
|
opts = prepare_options(opts)
|
102
102
|
object_link_options = opts[:object_link_options]
|
103
103
|
object_link_options[:action] = :show
|
104
104
|
save_and_render_object(obj, get_operation(:create, opts), opts.merge(location_header: true))
|
105
105
|
end
|
106
|
-
|
106
|
+
|
107
107
|
def do_update(obj, opts = {})
|
108
108
|
return unless ensure_admin_if_admin_only(opts)
|
109
109
|
obj, opts = prepare_object_for_update(obj, opts)
|
@@ -113,17 +113,17 @@ module ModelApi
|
|
113
113
|
end
|
114
114
|
update_and_render_object(obj, opts)
|
115
115
|
end
|
116
|
-
|
116
|
+
|
117
117
|
def prepare_object_for_update(obj, opts = {})
|
118
118
|
opts = prepare_options(opts)
|
119
119
|
get_updated_object(obj, get_operation(:update, opts), opts)
|
120
120
|
end
|
121
|
-
|
121
|
+
|
122
122
|
def update_and_render_object(obj, opts = {})
|
123
123
|
opts = prepare_options(opts)
|
124
124
|
save_and_render_object(obj, get_operation(:update, opts), opts)
|
125
125
|
end
|
126
|
-
|
126
|
+
|
127
127
|
def save_and_render_object(obj, operation, opts = {})
|
128
128
|
status, msgs = Utils.process_updated_model_save(obj, operation, opts)
|
129
129
|
add_hateoas_links_for_updated_object(operation, opts)
|
@@ -131,31 +131,31 @@ module ModelApi
|
|
131
131
|
ModelApi::Renderer.render(self, successful ? obj : opts[:request_obj],
|
132
132
|
opts.merge(status: status, operation: :show, messages: msgs))
|
133
133
|
end
|
134
|
-
|
134
|
+
|
135
135
|
def do_destroy(obj, opts = {})
|
136
136
|
return unless ensure_admin_if_admin_only(opts)
|
137
137
|
opts = prepare_options(opts)
|
138
138
|
obj = obj.first if obj.is_a?(ActiveRecord::Relation)
|
139
|
-
|
139
|
+
|
140
140
|
add_hateoas_links_for_update(opts)
|
141
141
|
unless obj.present?
|
142
142
|
return not_found(opts.merge(class: klass, field: :id))
|
143
143
|
end
|
144
|
-
|
144
|
+
|
145
145
|
operation = opts[:operation] = get_operation(:destroy, opts)
|
146
146
|
Utils.validate_operation(obj, operation, opts)
|
147
147
|
response_status, errs_or_msgs = Utils.process_object_destroy(obj, operation, opts)
|
148
|
-
|
148
|
+
|
149
149
|
add_hateoas_links_for_updated_object(operation, opts)
|
150
150
|
klass = Utils.find_class(obj, opts)
|
151
151
|
ModelApi::Renderer.render(self, obj, opts.merge(status: response_status,
|
152
152
|
root: ModelApi::Utils.model_name(klass).singular, messages: errs_or_msgs))
|
153
153
|
end
|
154
|
-
|
154
|
+
|
155
155
|
def common_response_links(_opts = {})
|
156
156
|
{}
|
157
157
|
end
|
158
|
-
|
158
|
+
|
159
159
|
def prepare_options(opts)
|
160
160
|
opts = opts.symbolize_keys
|
161
161
|
opts[:user] = user = filter_by_user
|
@@ -166,7 +166,7 @@ module ModelApi
|
|
166
166
|
request.query_parameters.to_h.symbolize_keys
|
167
167
|
opts
|
168
168
|
end
|
169
|
-
|
169
|
+
|
170
170
|
def id_info(opts = {})
|
171
171
|
id_info = {}
|
172
172
|
id_info[:id_attribute] = (opts[:id_attribute] || :id).to_sym
|
@@ -174,7 +174,7 @@ module ModelApi
|
|
174
174
|
id_info[:id_value] = (opts[:id_value] || params[id_info[:id_param]]).to_s
|
175
175
|
id_info
|
176
176
|
end
|
177
|
-
|
177
|
+
|
178
178
|
def api_query(opts = {})
|
179
179
|
klass = opts[:model_class] || model_class
|
180
180
|
unless klass < ActiveRecord::Base
|
@@ -191,7 +191,7 @@ module ModelApi
|
|
191
191
|
end
|
192
192
|
Utils.apply_context(query, opts)
|
193
193
|
end
|
194
|
-
|
194
|
+
|
195
195
|
def common_object_query(opts = {})
|
196
196
|
klass = opts[:model_class] || model_class
|
197
197
|
coll_query = Utils.apply_context(api_query(opts), opts)
|
@@ -215,7 +215,7 @@ module ModelApi
|
|
215
215
|
end
|
216
216
|
query
|
217
217
|
end
|
218
|
-
|
218
|
+
|
219
219
|
def collection_query(opts = {})
|
220
220
|
opts = base_api_options.merge(opts)
|
221
221
|
klass = opts[:model_class] || model_class
|
@@ -226,11 +226,11 @@ module ModelApi
|
|
226
226
|
end
|
227
227
|
query
|
228
228
|
end
|
229
|
-
|
229
|
+
|
230
230
|
def object_query(opts = {})
|
231
231
|
common_object_query(base_api_options.merge(opts))
|
232
232
|
end
|
233
|
-
|
233
|
+
|
234
234
|
def user_query(query, opts = {})
|
235
235
|
user = opts[:user] || filter_by_user
|
236
236
|
klass = opts[:model_class] || model_class
|
@@ -249,23 +249,23 @@ module ModelApi
|
|
249
249
|
end
|
250
250
|
query
|
251
251
|
end
|
252
|
-
|
252
|
+
|
253
253
|
def base_api_options
|
254
254
|
self.class.base_api_options
|
255
255
|
end
|
256
|
-
|
256
|
+
|
257
257
|
def base_admin_api_options
|
258
258
|
base_api_options.merge(admin_only: true)
|
259
259
|
end
|
260
|
-
|
260
|
+
|
261
261
|
def ensure_admin
|
262
262
|
return true if current_user.try(:admin_api_user?)
|
263
|
-
|
263
|
+
|
264
264
|
# Mask presence of endpoint if user is not authorized to access it
|
265
265
|
not_found
|
266
266
|
false
|
267
267
|
end
|
268
|
-
|
268
|
+
|
269
269
|
def unhandled_exception(err)
|
270
270
|
return if handle_api_exceptions(err)
|
271
271
|
error_id = LogUtils.log_and_notify(err)
|
@@ -285,7 +285,7 @@ module ModelApi
|
|
285
285
|
ModelApi::Renderer.render(self, error_details, root: :error_details,
|
286
286
|
status: :internal_server_error)
|
287
287
|
end
|
288
|
-
|
288
|
+
|
289
289
|
def handle_api_exceptions(err)
|
290
290
|
if err.is_a?(ModelApi::NotFoundException)
|
291
291
|
not_found(field: err.field, message: err.message)
|
@@ -296,23 +296,23 @@ module ModelApi
|
|
296
296
|
end
|
297
297
|
true
|
298
298
|
end
|
299
|
-
|
299
|
+
|
300
300
|
def doorkeeper_unauthorized_render_options(error: nil)
|
301
301
|
{ json: unauthorized(error: 'Not authorized to access resource', message: error.description,
|
302
302
|
format: :json, generate_body_only: true) }
|
303
303
|
end
|
304
|
-
|
304
|
+
|
305
305
|
# Indicates whether user has access to data they do not own.
|
306
306
|
def admin_access?
|
307
307
|
false
|
308
308
|
end
|
309
|
-
|
309
|
+
|
310
310
|
# Indicates whether API should render administrator-only content in API responses
|
311
311
|
def admin_content?
|
312
312
|
param = request.query_parameters[:admin]
|
313
313
|
param.present? && param.to_i != 0 && admin_access?
|
314
314
|
end
|
315
|
-
|
315
|
+
|
316
316
|
def resource_parent_id(parent_model_class, opts = {})
|
317
317
|
id_info = id_info(opts.reverse_merge(id_param: "#{parent_model_class.name.underscore}_id"))
|
318
318
|
model_name = parent_model_class.model_name.human
|
@@ -333,7 +333,7 @@ module ModelApi
|
|
333
333
|
end
|
334
334
|
parent_id
|
335
335
|
end
|
336
|
-
|
336
|
+
|
337
337
|
def simple_error(status, error, opts = {})
|
338
338
|
opts = opts.dup
|
339
339
|
klass = opts[:class]
|
@@ -357,14 +357,14 @@ module ModelApi
|
|
357
357
|
ModelApi::Renderer.render(self, opts[:request_obj], opts.merge(status: status,
|
358
358
|
messages: errs_or_msgs))
|
359
359
|
end
|
360
|
-
|
360
|
+
|
361
361
|
def not_found(opts = {})
|
362
362
|
opts = opts.dup
|
363
363
|
opts[:message] ||= 'No resource found at the path provided or matching the criteria ' \
|
364
364
|
'specified'
|
365
365
|
simple_error(:not_found, opts.delete(:error) || 'No resource found', opts)
|
366
366
|
end
|
367
|
-
|
367
|
+
|
368
368
|
def bad_payload(opts = {})
|
369
369
|
opts = opts.dup
|
370
370
|
format = opts[:format] || identify_format
|
@@ -373,24 +373,24 @@ module ModelApi
|
|
373
373
|
simple_error(:bad_request, opts.delete(:error) || 'Missing/invalid request body (payload)',
|
374
374
|
opts)
|
375
375
|
end
|
376
|
-
|
376
|
+
|
377
377
|
def bad_request(error, message, opts = {})
|
378
378
|
opts[:message] = message || 'This request is invalid for the resource in its present state'
|
379
379
|
simple_error(:bad_request, error || 'Invalid API request', opts)
|
380
380
|
end
|
381
|
-
|
381
|
+
|
382
382
|
def unauthorized(opts = {})
|
383
383
|
opts = opts.dup
|
384
384
|
opts[:message] ||= 'Missing one or more privileges required to complete request'
|
385
385
|
simple_error(:unauthorized, opts.delete(:error) || 'Not authorized', opts)
|
386
386
|
end
|
387
|
-
|
387
|
+
|
388
388
|
def not_implemented(opts = {})
|
389
389
|
opts = opts.dup
|
390
390
|
opts[:message] ||= 'This API feature is presently unavailable'
|
391
391
|
simple_error(:not_implemented, opts.delete(:error) || 'Not implemented', opts)
|
392
392
|
end
|
393
|
-
|
393
|
+
|
394
394
|
def validate_read_operation(obj, operation, opts = {})
|
395
395
|
status, errors = Utils.validate_operation(obj, operation, opts)
|
396
396
|
return true if status.nil? && errors.nil?
|
@@ -403,7 +403,7 @@ module ModelApi
|
|
403
403
|
simple_error(status, errors, opts)
|
404
404
|
false
|
405
405
|
end
|
406
|
-
|
406
|
+
|
407
407
|
def current_user
|
408
408
|
return @devise_user if @devise_user.present?
|
409
409
|
return @current_user if instance_variable_defined?(:@current_user)
|
@@ -413,7 +413,7 @@ module ModelApi
|
|
413
413
|
end
|
414
414
|
@current_user = User.find(doorkeeper_token.resource_owner_id)
|
415
415
|
end
|
416
|
-
|
416
|
+
|
417
417
|
def filter_by_user
|
418
418
|
if admin_access?
|
419
419
|
if (user_id = request.query_parameters[:user_id] ||
|
@@ -427,23 +427,23 @@ module ModelApi
|
|
427
427
|
end
|
428
428
|
current_user
|
429
429
|
end
|
430
|
-
|
430
|
+
|
431
431
|
def common_headers
|
432
432
|
ModelApi::Utils.common_http_headers.each do |k, v|
|
433
433
|
response.headers[k] = v
|
434
434
|
end
|
435
435
|
end
|
436
|
-
|
436
|
+
|
437
437
|
def identify_format
|
438
438
|
format = self.request.format.symbol rescue :json
|
439
439
|
format == :xml ? :xml : :json
|
440
440
|
end
|
441
|
-
|
441
|
+
|
442
442
|
def ensure_admin_if_admin_only(opts = {})
|
443
443
|
return true unless opts[:admin_only]
|
444
444
|
ensure_admin
|
445
445
|
end
|
446
|
-
|
446
|
+
|
447
447
|
def get_operation(default_operation, opts = {})
|
448
448
|
if opts.key?(:operation)
|
449
449
|
return opts[:operation]
|
@@ -459,7 +459,7 @@ module ModelApi
|
|
459
459
|
return default_operation
|
460
460
|
end
|
461
461
|
end
|
462
|
-
|
462
|
+
|
463
463
|
def get_updated_object(obj_or_class, operation, opts = {})
|
464
464
|
opts = opts.symbolize_keys
|
465
465
|
opts[:operation] = operation
|
@@ -488,15 +488,15 @@ module ModelApi
|
|
488
488
|
ModelApi::Utils.invoke_callback(model_metadata[:after_initialize], obj, opts)
|
489
489
|
[obj, opts]
|
490
490
|
end
|
491
|
-
|
491
|
+
|
492
492
|
private
|
493
|
-
|
493
|
+
|
494
494
|
def find_filter_params
|
495
495
|
request.query_parameters.reject do |param, _value|
|
496
496
|
%w(access_token sort_by admin).include?(param)
|
497
497
|
end
|
498
498
|
end
|
499
|
-
|
499
|
+
|
500
500
|
def find_sort_params
|
501
501
|
sort_by = params[:sort_by]
|
502
502
|
return {} if sort_by.blank?
|
@@ -507,7 +507,7 @@ module ModelApi
|
|
507
507
|
process_simple_sort_params(sort_by)
|
508
508
|
end
|
509
509
|
end
|
510
|
-
|
510
|
+
|
511
511
|
def process_json_sort_params(sort_by)
|
512
512
|
sort_params = {}
|
513
513
|
sort_json_obj = (JSON.parse(sort_by) rescue {})
|
@@ -526,7 +526,7 @@ module ModelApi
|
|
526
526
|
end
|
527
527
|
sort_params
|
528
528
|
end
|
529
|
-
|
529
|
+
|
530
530
|
def process_simple_sort_params(sort_by)
|
531
531
|
sort_params = {}
|
532
532
|
sort_by.split(',').each do |key|
|
@@ -558,7 +558,7 @@ module ModelApi
|
|
558
558
|
end
|
559
559
|
sort_params
|
560
560
|
end
|
561
|
-
|
561
|
+
|
562
562
|
def process_collection_includes(collection, opts = {})
|
563
563
|
klass = Utils.find_class(collection, opts)
|
564
564
|
metadata = ModelApi::Utils.filtered_ext_attrs(klass, opts[:operation] || :index, opts)
|
@@ -574,7 +574,7 @@ module ModelApi
|
|
574
574
|
collection = collection.includes(includes) if includes.present?
|
575
575
|
collection
|
576
576
|
end
|
577
|
-
|
577
|
+
|
578
578
|
def filter_collection(collection, filter_params, opts = {})
|
579
579
|
return [collection, {}] if filter_params.blank? # Don't filter if no filter params
|
580
580
|
klass = opts[:class] || Utils.find_class(collection, opts)
|
@@ -594,7 +594,7 @@ module ModelApi
|
|
594
594
|
end
|
595
595
|
[collection, result_filters]
|
596
596
|
end
|
597
|
-
|
597
|
+
|
598
598
|
def process_filter_params(filter_params, klass, opts = {})
|
599
599
|
assoc_values = {}
|
600
600
|
filter_metadata = {}
|
@@ -614,7 +614,7 @@ module ModelApi
|
|
614
614
|
end
|
615
615
|
[assoc_values, filter_metadata, attr_values]
|
616
616
|
end
|
617
|
-
|
617
|
+
|
618
618
|
# rubocop:disable Metrics/ParameterLists
|
619
619
|
def process_filter_assoc_param(attr, metadata, assoc_values, value, opts)
|
620
620
|
attr_elems = attr.split('.')
|
@@ -626,7 +626,7 @@ module ModelApi
|
|
626
626
|
assoc_filter_params = (assoc_values[key] ||= {})
|
627
627
|
assoc_filter_params[attr_elems[1..-1].join('.')] = value
|
628
628
|
end
|
629
|
-
|
629
|
+
|
630
630
|
def process_filter_attr_param(attr, metadata, filter_metadata, attr_values, value, opts)
|
631
631
|
attr = attr.strip.to_sym
|
632
632
|
attr_metadata = metadata[attr] ||
|
@@ -636,8 +636,9 @@ module ModelApi
|
|
636
636
|
filter_metadata[key] = attr_metadata
|
637
637
|
attr_values[key] = value
|
638
638
|
end
|
639
|
+
|
639
640
|
# rubocop:enable Metrics/ParameterLists
|
640
|
-
|
641
|
+
|
641
642
|
def apply_filter_param(attr_metadata, collection, opts = {})
|
642
643
|
raw_value = (opts[:attr_values] || params)[attr_metadata[:key]]
|
643
644
|
filter_table = opts[:filter_table]
|
@@ -670,7 +671,7 @@ module ModelApi
|
|
670
671
|
end
|
671
672
|
collection
|
672
673
|
end
|
673
|
-
|
674
|
+
|
674
675
|
def sort_collection(collection, sort_params, opts = {})
|
675
676
|
return [collection, {}] if sort_params.blank? # Don't filter if no filter params
|
676
677
|
klass = opts[:class] || Utils.find_class(collection, opts)
|
@@ -696,7 +697,7 @@ module ModelApi
|
|
696
697
|
end
|
697
698
|
[collection, result_sorts]
|
698
699
|
end
|
699
|
-
|
700
|
+
|
700
701
|
def process_sort_params(sort_params, klass, opts)
|
701
702
|
metadata = ModelApi::Utils.filtered_ext_attrs(klass, :sort, opts)
|
702
703
|
assoc_sorts = {}
|
@@ -724,7 +725,7 @@ module ModelApi
|
|
724
725
|
end
|
725
726
|
[assoc_sorts, attr_sorts, result_sorts]
|
726
727
|
end
|
727
|
-
|
728
|
+
|
728
729
|
# Intentionally disabling parameter list length check for private / internal method
|
729
730
|
# rubocop:disable Metrics/ParameterLists
|
730
731
|
def process_sort_param_assoc(attr, metadata, sort_order, assoc_sorts, opts)
|
@@ -736,9 +737,9 @@ module ModelApi
|
|
736
737
|
assoc_sort_params = (assoc_sorts[key] ||= {})
|
737
738
|
assoc_sort_params[attr_elems[1..-1].join('.')] = sort_order
|
738
739
|
end
|
739
|
-
|
740
|
+
|
740
741
|
# rubocop:enable Metrics/ParameterLists
|
741
|
-
|
742
|
+
|
742
743
|
def filter_process_param(raw_value, attr_metadata, opts)
|
743
744
|
raw_value = raw_value.to_s.strip
|
744
745
|
array = nil
|
@@ -758,7 +759,7 @@ module ModelApi
|
|
758
759
|
operator, value = parse_filter_operator(raw_value)
|
759
760
|
[[operator, ModelApi::Utils.transform_value(value, attr_metadata[:parse], opts)]]
|
760
761
|
end
|
761
|
-
|
762
|
+
|
762
763
|
def filter_process_param_array(array, attr_metadata, opts)
|
763
764
|
operator_value_pairs = []
|
764
765
|
equals_values = []
|
@@ -774,7 +775,7 @@ module ModelApi
|
|
774
775
|
operator_value_pairs << ['=', equals_values.uniq] if equals_values.present?
|
775
776
|
operator_value_pairs
|
776
777
|
end
|
777
|
-
|
778
|
+
|
778
779
|
def parse_filter_operator(value)
|
779
780
|
value = value.to_s.strip
|
780
781
|
if (operator = value.scan(/\A(>=|<=|!=|<>)[[:space:]]*\w/).flatten.first).present?
|
@@ -784,7 +785,7 @@ module ModelApi
|
|
784
785
|
end
|
785
786
|
['=', value]
|
786
787
|
end
|
787
|
-
|
788
|
+
|
788
789
|
def format_value_for_query(column, value, klass)
|
789
790
|
return value.map { |v| format_value_for_query(column, v, klass) } if value.is_a?(Array)
|
790
791
|
column_metadata = klass.columns_hash[column.to_s]
|
@@ -804,7 +805,7 @@ module ModelApi
|
|
804
805
|
end
|
805
806
|
value.to_s
|
806
807
|
end
|
807
|
-
|
808
|
+
|
808
809
|
def params_array(raw_value)
|
809
810
|
index = 0
|
810
811
|
array = []
|
@@ -814,37 +815,37 @@ module ModelApi
|
|
814
815
|
end
|
815
816
|
array
|
816
817
|
end
|
817
|
-
|
818
|
+
|
818
819
|
def paginate_collection(collection, collection_links, opts, coll_route)
|
819
820
|
collection_size = collection.count
|
820
821
|
page_size = (params[:page_size] || DEFAULT_PAGE_SIZE).to_i
|
821
822
|
page = [params[:page].to_i, 1].max
|
822
|
-
page_count = [(collection_size - 1) / page_size, 1].max
|
823
|
+
page_count = [(collection_size + page_size - 1) / page_size, 1].max
|
823
824
|
page = page_count if page > page_count
|
824
825
|
offset = (page - 1) * page_size
|
825
|
-
|
826
|
+
|
826
827
|
opts = opts.dup
|
827
828
|
opts[:count] ||= collection_size
|
828
829
|
opts[:page] ||= page
|
829
830
|
opts[:page_size] ||= page_size
|
830
831
|
opts[:page_count] ||= page_count
|
831
|
-
|
832
|
+
|
832
833
|
response.headers['X-Total-Count'] = collection_size.to_s
|
833
|
-
|
834
|
+
|
834
835
|
opts[:collection_link_options] = (opts[:collection_link_options] || {})
|
835
836
|
.reject { |k, _v| [:page].include?(k.to_sym) }
|
836
837
|
opts[:object_link_options] = (opts[:object_link_options] || {})
|
837
838
|
.reject { |k, _v| [:page, :page_size].include?(k.to_sym) }
|
838
|
-
|
839
|
+
|
839
840
|
if collection_size > page_size
|
840
841
|
opts[:collection_link_options][:page] = page
|
841
842
|
Utils.add_pagination_links(collection_links, coll_route, page, page_count)
|
842
843
|
collection = collection.limit(page_size).offset(offset)
|
843
844
|
end
|
844
|
-
|
845
|
+
|
845
846
|
[collection, collection_links, opts]
|
846
847
|
end
|
847
|
-
|
848
|
+
|
848
849
|
def resolve_key_to_column(klass, attr_metadata)
|
849
850
|
return nil unless klass.respond_to?(:columns_hash)
|
850
851
|
columns_hash = klass.columns_hash
|
@@ -855,7 +856,7 @@ module ModelApi
|
|
855
856
|
return nil unless render_method.is_a?(String)
|
856
857
|
columns_hash.include?(render_method) ? render_method : nil
|
857
858
|
end
|
858
|
-
|
859
|
+
|
859
860
|
def add_collection_object_route(opts)
|
860
861
|
object_route = opts[:object_route]
|
861
862
|
unless object_route.present?
|
@@ -873,31 +874,31 @@ module ModelApi
|
|
873
874
|
return if object_route.blank?
|
874
875
|
opts[:object_links] = (opts[:object_links] || {}).merge(self: object_route)
|
875
876
|
end
|
876
|
-
|
877
|
+
|
877
878
|
def add_hateoas_links_for_update(opts)
|
878
879
|
object_route = opts[:object_route] || self
|
879
880
|
links = { self: object_route }.reverse_merge(common_response_links(opts))
|
880
881
|
opts[:links] = links.merge(opts[:links] || {})
|
881
882
|
end
|
882
|
-
|
883
|
+
|
883
884
|
def add_hateoas_links_for_updated_object(_operation, opts)
|
884
885
|
object_route = opts[:object_route] || self
|
885
886
|
object_links = { self: object_route }
|
886
887
|
opts[:object_links] = object_links.merge(opts[:object_links] || {})
|
887
888
|
end
|
888
|
-
|
889
|
+
|
889
890
|
def verify_update_request_body(request_body, format, opts = {})
|
890
891
|
if request.format.symbol.nil? && format.present?
|
891
892
|
opts[:format] ||= format
|
892
893
|
end
|
893
|
-
|
894
|
+
|
894
895
|
if request_body.is_a?(Array)
|
895
896
|
fail 'Expected object, but collection provided'
|
896
897
|
elsif !request_body.is_a?(Hash)
|
897
898
|
fail 'Expected object'
|
898
899
|
end
|
899
900
|
end
|
900
|
-
|
901
|
+
|
901
902
|
def filtered_by_foreign_key?(query)
|
902
903
|
fk_cache = self.class.instance_variable_get(:@foreign_key_cache)
|
903
904
|
self.class.instance_variable_set(:@foreign_key_cache, fk_cache = {}) if fk_cache.nil?
|
@@ -914,13 +915,13 @@ module ModelApi
|
|
914
915
|
"#{e.backtrace.join("\n")}"
|
915
916
|
end
|
916
917
|
end
|
917
|
-
|
918
|
+
|
918
919
|
class Utils
|
919
920
|
def self.find_class(obj, opts = {})
|
920
921
|
return nil if obj.nil?
|
921
922
|
opts[:class] || (obj.respond_to?(:klass) ? obj.klass : obj.class)
|
922
923
|
end
|
923
|
-
|
924
|
+
|
924
925
|
def self.add_pagination_links(collection_links, coll_route, page, last_page)
|
925
926
|
if page < last_page
|
926
927
|
collection_links[:next] = [coll_route, { page: (page + 1) }]
|
@@ -929,7 +930,7 @@ module ModelApi
|
|
929
930
|
collection_links[:first] = [coll_route, { page: 1 }]
|
930
931
|
collection_links[:last] = [coll_route, { page: last_page }]
|
931
932
|
end
|
932
|
-
|
933
|
+
|
933
934
|
def self.object_from_req_body(root_elem, req_body, format)
|
934
935
|
if format == :json
|
935
936
|
request_obj = req_body
|
@@ -945,7 +946,7 @@ module ModelApi
|
|
945
946
|
fail 'Invalid request format' unless request_obj.present?
|
946
947
|
request_obj
|
947
948
|
end
|
948
|
-
|
949
|
+
|
949
950
|
def self.apply_updates(obj, req_obj, operation, opts = {})
|
950
951
|
opts = opts.merge(object: opts[:object] || obj)
|
951
952
|
metadata = ModelApi::Utils.filtered_ext_attrs(opts[:api_attr_metadata] ||
|
@@ -961,7 +962,7 @@ module ModelApi
|
|
961
962
|
update_api_attr(obj, attr, value, opts.merge(attr_metadata: attr_metadata))
|
962
963
|
end
|
963
964
|
end
|
964
|
-
|
965
|
+
|
965
966
|
def self.set_context_attrs(obj, opts = {})
|
966
967
|
klass = (obj.class < ActiveRecord::Base ? obj.class : nil)
|
967
968
|
(opts[:context] || {}).each do |key, value|
|
@@ -986,7 +987,7 @@ module ModelApi
|
|
986
987
|
end
|
987
988
|
end
|
988
989
|
end
|
989
|
-
|
990
|
+
|
990
991
|
def self.process_updated_model_save(obj, operation, opts = {})
|
991
992
|
opts = opts.dup
|
992
993
|
opts[:operation] = operation
|
@@ -1015,7 +1016,7 @@ module ModelApi
|
|
1015
1016
|
end
|
1016
1017
|
[suggested_response_status, object_errors]
|
1017
1018
|
end
|
1018
|
-
|
1019
|
+
|
1019
1020
|
def self.extract_msgs_for_error(obj, opts = {})
|
1020
1021
|
object_errors = []
|
1021
1022
|
attr_prefix = opts[:attr_prefix] || ''
|
@@ -1043,7 +1044,7 @@ module ModelApi
|
|
1043
1044
|
end
|
1044
1045
|
object_errors
|
1045
1046
|
end
|
1046
|
-
|
1047
|
+
|
1047
1048
|
# rubocop:disable Metrics/MethodLength
|
1048
1049
|
def self.extract_assoc_error_msgs(obj, attr, opts)
|
1049
1050
|
object_errors = []
|
@@ -1085,12 +1086,11 @@ module ModelApi
|
|
1085
1086
|
end
|
1086
1087
|
object_errors
|
1087
1088
|
end
|
1088
|
-
|
1089
1089
|
# rubocop:enable Metrics/MethodLength
|
1090
|
-
|
1090
|
+
|
1091
1091
|
def self.process_object_destroy(obj, operation, opts)
|
1092
1092
|
soft_delete = obj.errors.present? ? false : object_destroy(obj, opts)
|
1093
|
-
|
1093
|
+
|
1094
1094
|
if obj.errors.blank? && (soft_delete || obj.destroyed?)
|
1095
1095
|
response_status = :ok
|
1096
1096
|
object_errors = []
|
@@ -1107,10 +1107,10 @@ module ModelApi
|
|
1107
1107
|
}
|
1108
1108
|
end
|
1109
1109
|
end
|
1110
|
-
|
1110
|
+
|
1111
1111
|
[response_status, object_errors]
|
1112
1112
|
end
|
1113
|
-
|
1113
|
+
|
1114
1114
|
def self.object_destroy(obj, opts = {})
|
1115
1115
|
klass = find_class(obj)
|
1116
1116
|
object_id = obj.send(opts[:id_attribute] || :id)
|
@@ -1134,7 +1134,7 @@ module ModelApi
|
|
1134
1134
|
Rails.logger.warn "Error destroying #{klass.name} \"#{object_id}\": \"#{e.message}\")."
|
1135
1135
|
false
|
1136
1136
|
end
|
1137
|
-
|
1137
|
+
|
1138
1138
|
def self.set_api_attr(obj, attr, value, opts)
|
1139
1139
|
attr_metadata = opts[:attr_metadata]
|
1140
1140
|
internal_field = attr_metadata[:key] || attr
|
@@ -1147,7 +1147,7 @@ module ModelApi
|
|
1147
1147
|
end
|
1148
1148
|
obj.send(setter, value)
|
1149
1149
|
end
|
1150
|
-
|
1150
|
+
|
1151
1151
|
def self.update_api_attr(obj, attr, value, opts = {})
|
1152
1152
|
attr_metadata = opts[:attr_metadata]
|
1153
1153
|
begin
|
@@ -1174,7 +1174,7 @@ module ModelApi
|
|
1174
1174
|
handle_api_setter_exception(e, obj, attr_metadata, opts)
|
1175
1175
|
end
|
1176
1176
|
end
|
1177
|
-
|
1177
|
+
|
1178
1178
|
def self.update_has_many_assoc(obj, attr, value, opts = {})
|
1179
1179
|
attr_metadata = opts[:attr_metadata]
|
1180
1180
|
assoc = attr_metadata[:association]
|
@@ -1190,7 +1190,7 @@ module ModelApi
|
|
1190
1190
|
assoc_objs = []
|
1191
1191
|
value_array.each_with_index do |assoc_payload, index|
|
1192
1192
|
opts[:ignored_fields].clear if opts.include?(:ignored_fields)
|
1193
|
-
assoc_objs << update_has_many_assoc_obj(assoc_class, assoc_payload,
|
1193
|
+
assoc_objs << update_has_many_assoc_obj(obj, assoc, assoc_class, assoc_payload,
|
1194
1194
|
opts.merge(model_metadata: model_metadata))
|
1195
1195
|
if opts[:ignored_fields].present?
|
1196
1196
|
external_attr = ModelApi::Utils.ext_attr(attr, attr_metadata)
|
@@ -1199,8 +1199,8 @@ module ModelApi
|
|
1199
1199
|
end
|
1200
1200
|
set_api_attr(obj, attr, assoc_objs, opts)
|
1201
1201
|
end
|
1202
|
-
|
1203
|
-
def self.update_has_many_assoc_obj(assoc_class, assoc_payload, opts = {})
|
1202
|
+
|
1203
|
+
def self.update_has_many_assoc_obj(parent_obj, assoc, assoc_class, assoc_payload, opts = {})
|
1204
1204
|
model_metadata = opts[:model_metadata] || ModelApi::Utils.model_metadata(assoc_class)
|
1205
1205
|
assoc_obj = find_by_id_attrs(model_metadata[:id_attributes], assoc_class, assoc_payload)
|
1206
1206
|
assoc_obj = assoc_obj.first unless assoc_obj.nil? || assoc_obj.count != 1
|
@@ -1214,14 +1214,21 @@ module ModelApi
|
|
1214
1214
|
assoc_oper = :update
|
1215
1215
|
opts[:update_opts] ||= opts.merge(api_attr_metadata: ModelApi::Utils.filtered_attrs(
|
1216
1216
|
assoc_class, :update, opts))
|
1217
|
+
|
1217
1218
|
assoc_opts = opts[:update_opts]
|
1218
1219
|
end
|
1220
|
+
if (inverse_assoc = assoc.options[:inverse_of]).present? &&
|
1221
|
+
assoc_obj.respond_to?("#{inverse_assoc}=")
|
1222
|
+
assoc_obj.send("#{inverse_assoc}=", parent_obj)
|
1223
|
+
elsif !parent_obj.new_record? && assoc_obj.respond_to?("#{assoc.foreign_key}=")
|
1224
|
+
assoc_obj.send("#{assoc.foreign_key}=", obj.id)
|
1225
|
+
end
|
1219
1226
|
apply_updates(assoc_obj, assoc_payload, assoc_oper, assoc_opts)
|
1220
1227
|
ModelApi::Utils.invoke_callback(model_metadata[:after_initialize], assoc_obj,
|
1221
1228
|
assoc_opts.merge(operation: assoc_oper).freeze)
|
1222
1229
|
assoc_obj
|
1223
1230
|
end
|
1224
|
-
|
1231
|
+
|
1225
1232
|
def self.update_belongs_to_assoc(obj, attr, value, opts = {})
|
1226
1233
|
attr_metadata = opts[:attr_metadata]
|
1227
1234
|
assoc = attr_metadata[:association]
|
@@ -1247,7 +1254,7 @@ module ModelApi
|
|
1247
1254
|
end
|
1248
1255
|
set_api_attr(obj, attr, assoc_obj, opts)
|
1249
1256
|
end
|
1250
|
-
|
1257
|
+
|
1251
1258
|
def self.find_by_id_attrs(id_attributes, assoc_class, assoc_payload)
|
1252
1259
|
return nil unless id_attributes.present?
|
1253
1260
|
query = nil
|
@@ -1260,7 +1267,7 @@ module ModelApi
|
|
1260
1267
|
end
|
1261
1268
|
query
|
1262
1269
|
end
|
1263
|
-
|
1270
|
+
|
1264
1271
|
def self.apply_context(query, opts = {})
|
1265
1272
|
context = opts[:context]
|
1266
1273
|
return query if context.nil?
|
@@ -1271,7 +1278,7 @@ module ModelApi
|
|
1271
1278
|
end
|
1272
1279
|
query
|
1273
1280
|
end
|
1274
|
-
|
1281
|
+
|
1275
1282
|
def self.handle_api_setter_exception(e, obj, attr_metadata, opts = {})
|
1276
1283
|
return unless attr_metadata.is_a?(Hash)
|
1277
1284
|
on_exception = attr_metadata[:on_exception]
|
@@ -1292,7 +1299,7 @@ module ModelApi
|
|
1292
1299
|
break
|
1293
1300
|
end
|
1294
1301
|
end
|
1295
|
-
|
1302
|
+
|
1296
1303
|
def self.add_ignored_field(ignored_fields, attr, value, attr_metadata)
|
1297
1304
|
return unless ignored_fields.is_a?(Array)
|
1298
1305
|
attr_metadata ||= {}
|
@@ -1300,7 +1307,7 @@ module ModelApi
|
|
1300
1307
|
return unless external_attr.present?
|
1301
1308
|
ignored_fields << { external_attr => value }
|
1302
1309
|
end
|
1303
|
-
|
1310
|
+
|
1304
1311
|
def self.validate_operation(obj, operation, opts = {})
|
1305
1312
|
klass = find_class(obj, opts)
|
1306
1313
|
model_metadata = opts[:api_model_metadata] || ModelApi::Utils.model_metadata(klass)
|
@@ -1312,7 +1319,7 @@ module ModelApi
|
|
1312
1319
|
ModelApi::Utils.invoke_callback(model_metadata[:"validate_#{operation}"], obj, opts)
|
1313
1320
|
end
|
1314
1321
|
end
|
1315
|
-
|
1322
|
+
|
1316
1323
|
def self.validate_preserving_existing_errors(obj)
|
1317
1324
|
if obj.errors.present?
|
1318
1325
|
errors = obj.errors.messages.dup
|
@@ -1324,7 +1331,7 @@ module ModelApi
|
|
1324
1331
|
obj.valid?
|
1325
1332
|
end
|
1326
1333
|
end
|
1327
|
-
|
1334
|
+
|
1328
1335
|
def self.class_or_sti_subclass(klass, req_body, operation, opts = {})
|
1329
1336
|
metadata = ModelApi::Utils.filtered_attrs(klass, :create, opts)
|
1330
1337
|
if operation == :create && (attr_metadata = metadata[:type]).is_a?(Hash) &&
|
data/lib/model-api/renderer.rb
CHANGED
@@ -85,7 +85,7 @@ module ModelApi
|
|
85
85
|
end
|
86
86
|
end
|
87
87
|
opts = ModelApi::Utils.contextual_metadata_opts(attr_metadata, opts)
|
88
|
-
opts[:operation]
|
88
|
+
opts[:operation] ||= :show
|
89
89
|
if value.respond_to?(:map)
|
90
90
|
return value.map do |elem|
|
91
91
|
elem.is_a?(ActiveRecord::Base) ? serializable_object(elem, opts) : elem
|
data/model-api.gemspec
CHANGED
@@ -3,7 +3,7 @@ $:.unshift lib unless $:.include?(lib)
|
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
5
|
s.name = 'model-api'
|
6
|
-
s.version = '0.8.
|
6
|
+
s.version = '0.8.4'
|
7
7
|
s.summary = 'Create easy REST API\'s using metadata inside your ActiveRecord models'
|
8
8
|
s.description = 'Ruby gem allowing Ruby on Rails developers to create REST API’s using ' \
|
9
9
|
'metadata defined inside their ActiveRecord models.'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: model-api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew Mead
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-09-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|