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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3bdc8741b309bfe1b4d504a67b34a7101ffc3486
4
- data.tar.gz: c1519aa053d77afb3d1eff0f1861be9a4fc5a804
3
+ metadata.gz: 14b9b0dc45ee29320fa640f6b209b5481b5f75b7
4
+ data.tar.gz: 9f693375afd6e922b43eaaad1fc0f02ae6f5908e
5
5
  SHA512:
6
- metadata.gz: 708e946d85d6516b2dda287d2c5ad4fffe1b302a597bfbcc5f424308a8c1209b0ec9ba65ab388fa527da1488d2de2512c095172e7ffcfa21e28c90e4398af5c4
7
- data.tar.gz: 08fe91d60003278e4e5ce049570fdca9f00be075779f35e7c57627be9a7ce678fdadd9eef0cabbde8b6d0cf3e5b43b85e37e091d786955d4cf6d1ce82ef2be34
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) &&
@@ -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] = :show
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
@@ -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.3'
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.3
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-06-22 00:00:00.000000000 Z
11
+ date: 2016-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails