model-api 0.8.5 → 0.8.6

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: 3ad27c0bbc9961da1c6b1aa4715e05f3f6af9297
4
- data.tar.gz: a81ac47433b86382e72f21ae7e12113dabc3fcd5
3
+ metadata.gz: 95ae1633c2baa4a79b179c84be9ddd6d7b88a90b
4
+ data.tar.gz: d0c704c3b3da52e804d882b6f2f058990c52204a
5
5
  SHA512:
6
- metadata.gz: 4b99fcc0cffd7e8d24210afab23dea9b8cec64bef71afea263738ca6e4b4169232b7792fa5f2fd33471d610cc4d406f6499724ba06e88f8aacf337e671b98dee
7
- data.tar.gz: 823d85c47661c643d443f8bef2403ce22ee3f20f2a53ce6fbfe45630271a0005d76a411569573bf10d3b9dcf655d02a45d2a86b02eda11a0406f8a086ed31871
6
+ metadata.gz: a15c1b6b310de1fe521ed6b5ed6de3d1f3ad978b64235ed558169b28ebd206e560f85dbdd4be445936c2d4a2e17820e983deb46d46617124888ab571411745ca
7
+ data.tar.gz: 495bd7405fdcc08ebb437e25a19e086317744eede270a862523374add229269089a620839602bd46eec959141e58c21d17d9ec35ae9573df142f927c31571a5e
data/README.md CHANGED
@@ -17,6 +17,13 @@ Developing REST API's in a conventional manner involves many challenges. Betwee
17
17
  sync with what your API actually supports.
18
18
  * Reduce the lines of code required to implement your Rails-based Rest API dramatically.
19
19
 
20
+ ## Guides
21
+ This README file is intended to provide a brief introduction. For more detailed information,
22
+ the following guides are available:
23
+ * [Guide to `api_metadata` options](doc/api_metadata.md)
24
+ * [Guide to `model_metadata` options](doc/api_metadata.md)
25
+ * [Managing data access in `model-api`](doc/api_metadata.md)
26
+
20
27
  ## Installation
21
28
 
22
29
  Put this in your Gemfile:
@@ -62,18 +69,42 @@ end
62
69
 
63
70
  ## Exposing a Resource
64
71
 
65
- To expose a resource, start by adding the resource routes to your `routes.rb` file:
72
+ To expose a resource, start by defining the underlying model for that resource:
66
73
  ``` ruby
67
- namespace :api do
68
- namespace :v1 do
69
- resource :books, except: [:new, :edit], param: :book_id
70
- end
71
- end
72
- ```
74
+ class Book < ActiveRecord::Base
75
+ include ModelApi::Model
76
+
77
+ # Valication rules are utilized by model-api to validate requests
78
+ validates :name, presence: true, uniqueness: true, length: { maximum: 50 }
79
+ validates :description, length: { maximum: 250 }
80
+ validates :isbn, presence: true, uniqueness: true, length: { maximum: 13 }
81
+
82
+ # Define the attributes exposed via the REST API.
83
+ api_attributes \
84
+ id: { filter: true, sort: true },
85
+ name: { filter: true, sort: true },
86
+ description: {},
87
+ isbn: { filter: true, sort: true,
88
+ parse: ->(v) { v.to_s.gsub(%r{[^\d]+}, '') },
89
+ render: ->(v) { "#{v[0..2]}-#{v[3]}-#{v[4..5]}-#{v[6..11]}-#{v[12..-1]}" }
90
+ },
91
+ created_at: { read_only: true, filter: true },
92
+ updated_at: { read_only: true, filter: true }
73
93
 
94
+ end
95
+ ```
96
+ An explanation of the options used in the `api_attributes` example above:
97
+ * `filter` - Allow filtering by query string parameters (e.g. `?id=123`).
98
+ * `sort` - Allow use of this column in the sort_by parameter.
99
+ * `read_only` - Disallow column updates (via `POST`, `PUT`, or `PATCH`).
100
+ * `parse` - Method name (e.g. `:to_i`) or proc / lambda for pre-processing incoming payload values.
101
+ *(The example lambda removes any non-digit values (such as dashes) from supplied ISBN values.)*
102
+ * `render` - Method name or proc / lambda that formats values returned in response payloads.
103
+ *(The example lambda formats the 13-digit ISBN number using the typical dash convention.)*
104
+
74
105
  Next, define the base controller class that all of your API controllers will extend
75
106
  (for this example, in `app/controllers/api/v1/base_controller.rb`):
76
- ``` ruby
107
+ ```ruby
77
108
  module Api
78
109
  module V1
79
110
  class BaseController < ActionController::Base
@@ -117,6 +148,15 @@ Next, define the base controller class that all of your API controllers will ext
117
148
  end
118
149
  ```
119
150
 
151
+ Add the resource routes to your `routes.rb` file:
152
+ ```ruby
153
+ namespace :api do
154
+ namespace :v1 do
155
+ resource :books, except: [:new, :edit], param: :book_id
156
+ end
157
+ end
158
+ ```
159
+
120
160
  Finally, add a controller for your new resource (for this example, in
121
161
  `app/controllers/api/v1/base_controller.rb`):
122
162
  ```ruby
data/lib/model-api.rb CHANGED
@@ -25,11 +25,11 @@ module ModelApi
25
25
  end
26
26
  @model_api_global_metadata = global_metadata
27
27
  end
28
-
28
+
29
29
  def global_metadata
30
30
  @model_api_global_metadata || default_global_metadata
31
31
  end
32
-
32
+
33
33
  def default_global_metadata
34
34
  {
35
35
  }
@@ -4,11 +4,11 @@ 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
@@ -17,54 +17,55 @@ module ModelApi
17
17
  class << self
18
18
  def included(base)
19
19
  base.extend(ClassMethods)
20
-
20
+
21
21
  base.send(:include, InstanceMethods)
22
-
22
+
23
23
  base.send(:before_filter, :common_headers)
24
-
24
+
25
25
  base.send(:rescue_from, Exception, with: :unhandled_exception)
26
26
  base.send(:respond_to, :json, :xml)
27
27
  end
28
28
  end
29
-
29
+
30
30
  module InstanceMethods
31
31
  SIMPLE_ID_REGEX = /\A[0-9]+\Z/
32
32
  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]\
33
33
  {12}\Z/x
34
34
  DEFAULT_PAGE_SIZE = 100
35
-
35
+
36
36
  protected
37
-
37
+
38
38
  def model_class
39
39
  self.class.model_class
40
40
  end
41
-
41
+
42
42
  def render_collection(collection, opts = {})
43
43
  return unless ensure_admin_if_admin_only(opts)
44
44
  opts = prepare_options(opts)
45
45
  opts[:operation] ||= :index
46
46
  return unless validate_read_operation(collection, opts[:operation], opts)
47
-
47
+
48
48
  coll_route = opts[:collection_route] || self
49
49
  collection_links = { self: coll_route }
50
- collection = process_collection_includes(collection, opts)
50
+ collection = ModelApi::Utils.process_collection_includes(collection,
51
+ opts.merge(model_metadata: opts[:api_model_metadata] || opts[:model_metadata]))
51
52
  collection, _result_filters = filter_collection(collection, find_filter_params, opts)
52
53
  collection, _result_sorts = sort_collection(collection, find_sort_params, opts)
53
54
  collection, collection_links, opts = paginate_collection(collection,
54
55
  collection_links, opts, coll_route)
55
-
56
+
56
57
  opts[:collection_links] = collection_links.merge(opts[:collection_links] || {})
57
58
  .reverse_merge(common_response_links(opts))
58
59
  add_collection_object_route(opts)
59
60
  ModelApi::Renderer.render(self, collection, opts)
60
61
  end
61
-
62
+
62
63
  def render_object(obj, opts = {})
63
64
  return unless ensure_admin_if_admin_only(opts)
64
65
  opts = prepare_options(opts)
65
- klass = Utils.find_class(obj, opts)
66
+ klass = ModelApi::Utils.find_class(obj, opts)
66
67
  object_route = opts[:object_route] || self
67
-
68
+
68
69
  opts[:object_links] = { self: object_route }
69
70
  if obj.is_a?(ActiveRecord::Base)
70
71
  return unless validate_read_operation(obj, opts[:operation], opts)
@@ -77,12 +78,12 @@ module ModelApi
77
78
  obj = ModelApi::Utils.ext_value(obj, opts) unless opts[:raw_output]
78
79
  opts[:object_links].merge!(opts[:links] || {})
79
80
  end
80
-
81
+
81
82
  opts[:operation] ||= :show
82
83
  opts[:object_links].reverse_merge!(common_response_links(opts))
83
84
  ModelApi::Renderer.render(self, obj, opts)
84
85
  end
85
-
86
+
86
87
  def do_create(opts = {})
87
88
  klass = opts[:model_class] || model_class
88
89
  return unless ensure_admin_if_admin_only(opts)
@@ -93,82 +94,97 @@ module ModelApi
93
94
  return bad_payload(class: klass) if opts[:bad_payload]
94
95
  create_and_render_object(obj, opts)
95
96
  end
96
-
97
+
97
98
  def prepare_object_for_create(klass, opts = {})
98
99
  opts = prepare_options(opts)
99
100
  get_updated_object(klass, get_operation(:create, opts), opts)
100
101
  end
101
-
102
+
102
103
  def create_and_render_object(obj, opts = {})
103
104
  opts = prepare_options(opts)
104
105
  object_link_options = opts[:object_link_options]
105
106
  object_link_options[:action] = :show
106
107
  save_and_render_object(obj, get_operation(:create, opts), opts.merge(location_header: true))
107
108
  end
108
-
109
+
109
110
  def do_update(obj, opts = {})
110
111
  return unless ensure_admin_if_admin_only(opts)
111
112
  obj, opts = prepare_object_for_update(obj, opts)
112
113
  return bad_payload(class: klass) if opts[:bad_payload]
113
114
  unless obj.present?
114
- return not_found(opts.merge(class: Utils.find_class(obj, opts), field: :id))
115
+ return not_found(opts.merge(class: ModelApi::Utils.find_class(obj, opts), field: :id))
115
116
  end
116
117
  update_and_render_object(obj, opts)
117
118
  end
118
-
119
+
119
120
  def prepare_object_for_update(obj, opts = {})
120
121
  opts = prepare_options(opts)
121
122
  get_updated_object(obj, get_operation(:update, opts), opts)
122
123
  end
123
-
124
+
124
125
  def update_and_render_object(obj, opts = {})
125
126
  opts = prepare_options(opts)
126
127
  save_and_render_object(obj, get_operation(:update, opts), opts)
127
128
  end
128
-
129
+
129
130
  def save_and_render_object(obj, operation, opts = {})
131
+ opts = prepare_options(opts)
130
132
  status, msgs = Utils.process_updated_model_save(obj, operation, opts)
131
133
  add_hateoas_links_for_updated_object(operation, opts)
132
134
  successful = ModelApi::Utils.response_successful?(status)
133
135
  ModelApi::Renderer.render(self, successful ? obj : opts[:request_obj],
134
136
  opts.merge(status: status, operation: :show, messages: msgs))
135
137
  end
136
-
138
+
137
139
  def do_destroy(obj, opts = {})
138
140
  return unless ensure_admin_if_admin_only(opts)
139
141
  opts = prepare_options(opts)
140
142
  obj = obj.first if obj.is_a?(ActiveRecord::Relation)
141
-
143
+
142
144
  add_hateoas_links_for_update(opts)
143
145
  unless obj.present?
144
146
  return not_found(opts.merge(class: klass, field: :id))
145
147
  end
146
-
148
+
147
149
  operation = opts[:operation] = get_operation(:destroy, opts)
148
- Utils.validate_operation(obj, operation, opts)
150
+ ModelApi::Utils.validate_operation(obj, operation,
151
+ opts.merge(model_metadata: opts[:api_model_metadata] || opts[:model_metadata]))
149
152
  response_status, errs_or_msgs = Utils.process_object_destroy(obj, operation, opts)
150
-
153
+
151
154
  add_hateoas_links_for_updated_object(operation, opts)
152
- klass = Utils.find_class(obj, opts)
155
+ klass = ModelApi::Utils.find_class(obj, opts)
153
156
  ModelApi::Renderer.render(self, obj, opts.merge(status: response_status,
154
157
  root: ModelApi::Utils.model_name(klass).singular, messages: errs_or_msgs))
155
158
  end
156
-
159
+
157
160
  def common_response_links(_opts = {})
158
161
  {}
159
162
  end
160
-
161
- def prepare_options(opts)
163
+
164
+ def initialize_options(opts)
165
+ return opts if opts[:options_initialized]
162
166
  opts = opts.symbolize_keys
163
- opts[:user] = user = filter_by_user
164
- opts[:user_id] = user.try(:id)
165
- opts[:admin] = user.try(:admin_api_user?) ? true : false
166
- opts[:admin_content] = admin_content?
167
- opts[:collection_link_options] = opts[:object_link_options] =
168
- request.query_parameters.to_h.symbolize_keys
167
+ opts[:model_class] ||= model_class
168
+ opts[:user] ||= filter_by_user
169
+ opts[:user_id] ||= opts[:user].try(:id)
170
+ opts[:admin_user] ||= admin_user?(opts)
171
+ opts[:admin] ||= admin?(opts)
172
+ unless opts.include?(:collection_link_options) && opts.include?(:object_link_options)
173
+ default_link_options = request.params.to_h.symbolize_keys
174
+ opts[:collection_link_options] ||= default_link_options
175
+ opts[:object_link_options] ||= default_link_options
176
+ end
177
+ opts[:options_initialized] ||= true
169
178
  opts
170
179
  end
171
-
180
+
181
+ # Default implementation, can be hidden by API controller classes to include any
182
+ # application-specific options
183
+ def prepare_options(opts)
184
+ return opts if opts[:options_initialized]
185
+ initialize_options(opts)
186
+ end
187
+
172
188
  def id_info(opts = {})
173
189
  id_info = {}
174
190
  id_info[:id_attribute] = (opts[:id_attribute] || :id).to_sym
@@ -176,13 +192,15 @@ module ModelApi
176
192
  id_info[:id_value] = (opts[:id_value] || params[id_info[:id_param]]).to_s
177
193
  id_info
178
194
  end
179
-
195
+
180
196
  def api_query(opts = {})
197
+ opts = prepare_options(opts)
181
198
  klass = opts[:model_class] || model_class
199
+ model_metadata = opts[:model_metadata] || ModelApi::Utils.model_metadata(klass)
182
200
  unless klass < ActiveRecord::Base
183
201
  fail 'Expected model class to be an ActiveRecord::Base subclass'
184
202
  end
185
- query = klass.all
203
+ query = ModelApi::Utils.invoke_callback(model_metadata[:base_query], opts) || klass.all
186
204
  if (deleted_col = klass.columns_hash['deleted']).present?
187
205
  case deleted_col.type
188
206
  when :boolean
@@ -193,13 +211,13 @@ module ModelApi
193
211
  end
194
212
  Utils.apply_context(query, opts)
195
213
  end
196
-
214
+
197
215
  def common_object_query(opts = {})
198
216
  klass = opts[:model_class] || model_class
199
217
  coll_query = Utils.apply_context(api_query(opts), opts)
200
218
  id_info = opts[:id_info] || id_info(opts)
201
219
  query = coll_query.where(id_info[:id_attribute] => id_info[:id_value])
202
- if !admin_access?
220
+ if !admin_user?
203
221
  unless opts.include?(:user_filter) && !opts[:user_filter]
204
222
  query = user_query(query, opts.merge(model_class: klass))
205
223
  end
@@ -217,25 +235,25 @@ module ModelApi
217
235
  end
218
236
  query
219
237
  end
220
-
238
+
221
239
  def collection_query(opts = {})
222
240
  opts = base_api_options.merge(opts)
223
241
  klass = opts[:model_class] || model_class
224
242
  query = api_query(opts)
225
243
  unless (opts.include?(:user_filter) && !opts[:user_filter]) ||
226
- (admin_access? && (admin_content? || filtered_by_foreign_key?(query)))
244
+ (admin? || filtered_by_foreign_key?(query))
227
245
  query = user_query(query, opts.merge(model_class: klass))
228
246
  end
229
247
  query
230
248
  end
231
-
249
+
232
250
  def object_query(opts = {})
233
251
  common_object_query(base_api_options.merge(opts))
234
252
  end
235
-
253
+
236
254
  def user_query(query, opts = {})
237
255
  user = opts[:user] || filter_by_user
238
- klass = opts[:model_class] || model_class
256
+ klass = opts[:model_class] || query.klass
239
257
  user_id_col = opts[:user_id_column] || :user_id
240
258
  user_assoc = opts[:user_association] || :user
241
259
  user_id = user.try(opts[:user_id_attribute] || :id)
@@ -251,24 +269,23 @@ module ModelApi
251
269
  end
252
270
  query
253
271
  end
254
-
272
+
255
273
  def base_api_options
256
274
  self.class.base_api_options
257
275
  end
258
-
276
+
259
277
  def base_admin_api_options
260
- base_api_options.merge(admin_only: true)
278
+ base_api_options.merge(admin: true, admin_only: true)
261
279
  end
262
-
280
+
263
281
  def ensure_admin
264
- user = current_user
265
- return true if user.respond_to?(:admin_api_user?) && user.admin_api_user?
282
+ return true if admin_user?
266
283
 
267
284
  # Mask presence of endpoint if user is not authorized to access it
268
285
  not_found
269
286
  false
270
287
  end
271
-
288
+
272
289
  def unhandled_exception(err)
273
290
  return if handle_api_exceptions(err)
274
291
  return if performed?
@@ -283,7 +300,7 @@ module ModelApi
283
300
  ModelApi::Renderer.render(self, error_details, root: :error_details,
284
301
  status: :internal_server_error)
285
302
  end
286
-
303
+
287
304
  def handle_api_exceptions(err)
288
305
  if err.is_a?(ModelApi::NotFoundException)
289
306
  not_found(field: err.field, message: err.message)
@@ -294,23 +311,38 @@ module ModelApi
294
311
  end
295
312
  true
296
313
  end
297
-
314
+
298
315
  def doorkeeper_unauthorized_render_options(error: nil)
299
316
  { json: unauthorized(error: 'Not authorized to access resource', message: error.description,
300
317
  format: :json, generate_body_only: true) }
301
318
  end
302
-
319
+
303
320
  # Indicates whether user has access to data they do not own.
304
- def admin_access?
305
- false
321
+ def admin_user?(opts = {})
322
+ return opts[:admin_user] if opts.include?(:admin_user)
323
+ user = current_user
324
+ return nil if user.nil?
325
+ [:admin_api_user?, :admin_user?, :admin?].each do |method|
326
+ next unless user.respond_to?(method)
327
+ opts[:admin_user] = user.send(method) rescue next
328
+ break
329
+ end
330
+ opts[:admin_user] ||= false
306
331
  end
307
-
332
+
308
333
  # Indicates whether API should render administrator-only content in API responses
309
- def admin_content?
310
- param = request.query_parameters[:admin]
311
- param.present? && param.to_i != 0 && admin_access?
334
+ def admin?(opts = {})
335
+ return opts[:admin] if opts.include?(:admin)
336
+ param = request.params[:admin]
337
+ param.present? && admin_user?(opts) &&
338
+ (param.to_i != 0 && params.to_s.strip.downcase != 'false')
339
+ end
340
+
341
+ # Deprecated
342
+ def admin_content?(opts = {})
343
+ admin?(opts)
312
344
  end
313
-
345
+
314
346
  def resource_parent_id(parent_model_class, opts = {})
315
347
  id_info = id_info(opts.reverse_merge(id_param: "#{parent_model_class.name.underscore}_id"))
316
348
  model_name = parent_model_class.model_name.human
@@ -331,7 +363,7 @@ module ModelApi
331
363
  end
332
364
  parent_id
333
365
  end
334
-
366
+
335
367
  def simple_error(status, error, opts = {})
336
368
  opts = opts.dup
337
369
  klass = opts[:class]
@@ -355,14 +387,14 @@ module ModelApi
355
387
  ModelApi::Renderer.render(self, opts[:request_obj], opts.merge(status: status,
356
388
  messages: errs_or_msgs))
357
389
  end
358
-
390
+
359
391
  def not_found(opts = {})
360
392
  opts = opts.dup
361
393
  opts[:message] ||= 'No resource found at the path provided or matching the criteria ' \
362
394
  'specified'
363
395
  simple_error(:not_found, opts.delete(:error) || 'No resource found', opts)
364
396
  end
365
-
397
+
366
398
  def bad_payload(opts = {})
367
399
  opts = opts.dup
368
400
  format = opts[:format] || identify_format
@@ -371,26 +403,28 @@ module ModelApi
371
403
  simple_error(:bad_request, opts.delete(:error) || 'Missing/invalid request body (payload)',
372
404
  opts)
373
405
  end
374
-
406
+
375
407
  def bad_request(error, message, opts = {})
376
408
  opts[:message] = message || 'This request is invalid for the resource in its present state'
377
409
  simple_error(:bad_request, error || 'Invalid API request', opts)
378
410
  end
379
-
411
+
380
412
  def unauthorized(opts = {})
381
413
  opts = opts.dup
382
414
  opts[:message] ||= 'Missing one or more privileges required to complete request'
383
415
  simple_error(:unauthorized, opts.delete(:error) || 'Not authorized', opts)
384
416
  end
385
-
417
+
386
418
  def not_implemented(opts = {})
387
419
  opts = opts.dup
388
420
  opts[:message] ||= 'This API feature is presently unavailable'
389
421
  simple_error(:not_implemented, opts.delete(:error) || 'Not implemented', opts)
390
422
  end
391
-
423
+
392
424
  def validate_read_operation(obj, operation, opts = {})
393
- status, errors = Utils.validate_operation(obj, operation, opts)
425
+ opts = prepare_options(opts)
426
+ status, errors = ModelApi::Utils.validate_operation(obj, operation,
427
+ opts.merge(model_metadata: opts[:api_model_metadata] || opts[:model_metadata]))
394
428
  return true if status.nil? && errors.nil?
395
429
  if errors.nil? && (status.is_a?(Array) || status.present?)
396
430
  return true if (errors = status).blank?
@@ -405,27 +439,27 @@ module ModelApi
405
439
  def filter_by_user
406
440
  current_user
407
441
  end
408
-
442
+
409
443
  def current_user
410
444
  nil
411
445
  end
412
-
446
+
413
447
  def common_headers
414
448
  ModelApi::Utils.common_http_headers.each do |k, v|
415
449
  response.headers[k] = v
416
450
  end
417
451
  end
418
-
452
+
419
453
  def identify_format
420
454
  format = self.request.format.symbol rescue :json
421
455
  format == :xml ? :xml : :json
422
456
  end
423
-
457
+
424
458
  def ensure_admin_if_admin_only(opts = {})
425
459
  return true unless opts[:admin_only]
426
460
  ensure_admin
427
461
  end
428
-
462
+
429
463
  def get_operation(default_operation, opts = {})
430
464
  if opts.key?(:operation)
431
465
  return opts[:operation]
@@ -441,9 +475,9 @@ module ModelApi
441
475
  return default_operation
442
476
  end
443
477
  end
444
-
478
+
445
479
  def get_updated_object(obj_or_class, operation, opts = {})
446
- opts = opts.symbolize_keys
480
+ opts = prepare_options(opts.symbolize_keys)
447
481
  opts[:operation] = operation
448
482
  req_body, format = ModelApi::Utils.parse_request_body(request)
449
483
  if obj_or_class.is_a?(Class)
@@ -465,20 +499,17 @@ module ModelApi
465
499
  verify_update_request_body(req_body, format, opts)
466
500
  root_elem = opts[:root] = ModelApi::Utils.model_name(klass).singular
467
501
  request_obj = opts[:request_obj] = Utils.object_from_req_body(root_elem, req_body, format)
468
- Utils.apply_updates(obj, request_obj, operation, opts)
469
- opts.freeze
502
+ ModelApi::Utils.apply_updates(obj, request_obj, operation, opts)
470
503
  ModelApi::Utils.invoke_callback(model_metadata[:after_initialize], obj, opts)
471
504
  [obj, opts]
472
505
  end
473
-
506
+
474
507
  private
475
-
508
+
476
509
  def find_filter_params
477
- request.query_parameters.reject do |param, _value|
478
- %w(access_token sort_by admin).include?(param)
479
- end
510
+ request.params.reject { |p, _v| %w(access_token sort_by admin).include?(p) }
480
511
  end
481
-
512
+
482
513
  def find_sort_params
483
514
  sort_by = params[:sort_by]
484
515
  return {} if sort_by.blank?
@@ -489,7 +520,7 @@ module ModelApi
489
520
  process_simple_sort_params(sort_by)
490
521
  end
491
522
  end
492
-
523
+
493
524
  def process_json_sort_params(sort_by)
494
525
  sort_params = {}
495
526
  sort_json_obj = (JSON.parse(sort_by) rescue {})
@@ -508,7 +539,7 @@ module ModelApi
508
539
  end
509
540
  sort_params
510
541
  end
511
-
542
+
512
543
  def process_simple_sort_params(sort_by)
513
544
  sort_params = {}
514
545
  sort_by.split(',').each do |key|
@@ -540,26 +571,10 @@ module ModelApi
540
571
  end
541
572
  sort_params
542
573
  end
543
-
544
- def process_collection_includes(collection, opts = {})
545
- klass = Utils.find_class(collection, opts)
546
- metadata = ModelApi::Utils.filtered_ext_attrs(klass, opts[:operation] || :index, opts)
547
- model_metadata = opts[:api_model_metadata] || ModelApi::Utils.model_metadata(klass)
548
- includes = []
549
- if (metadata_includes = model_metadata[:collection_includes]).is_a?(Array)
550
- includes += metadata_includes.map(&:to_sym)
551
- end
552
- metadata.each do |_attr, attr_metadata|
553
- includes << attr_metadata[:key] if attr_metadata[:type] == :association
554
- end
555
- includes = includes.compact.uniq
556
- collection = collection.includes(includes) if includes.present?
557
- collection
558
- end
559
-
574
+
560
575
  def filter_collection(collection, filter_params, opts = {})
561
576
  return [collection, {}] if filter_params.blank? # Don't filter if no filter params
562
- klass = opts[:class] || Utils.find_class(collection, opts)
577
+ klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
563
578
  assoc_values, metadata, attr_values = process_filter_params(filter_params, klass, opts)
564
579
  result_filters = {}
565
580
  metadata.values.each do |attr_metadata|
@@ -576,7 +591,7 @@ module ModelApi
576
591
  end
577
592
  [collection, result_filters]
578
593
  end
579
-
594
+
580
595
  def process_filter_params(filter_params, klass, opts = {})
581
596
  assoc_values = {}
582
597
  filter_metadata = {}
@@ -596,7 +611,7 @@ module ModelApi
596
611
  end
597
612
  [assoc_values, filter_metadata, attr_values]
598
613
  end
599
-
614
+
600
615
  # rubocop:disable Metrics/ParameterLists
601
616
  def process_filter_assoc_param(attr, metadata, assoc_values, value, opts)
602
617
  attr_elems = attr.split('.')
@@ -608,7 +623,7 @@ module ModelApi
608
623
  assoc_filter_params = (assoc_values[key] ||= {})
609
624
  assoc_filter_params[attr_elems[1..-1].join('.')] = value
610
625
  end
611
-
626
+
612
627
  def process_filter_attr_param(attr, metadata, filter_metadata, attr_values, value, opts)
613
628
  attr = attr.strip.to_sym
614
629
  attr_metadata = metadata[attr] ||
@@ -618,13 +633,13 @@ module ModelApi
618
633
  filter_metadata[key] = attr_metadata
619
634
  attr_values[key] = value
620
635
  end
621
-
636
+
622
637
  # rubocop:enable Metrics/ParameterLists
623
-
638
+
624
639
  def apply_filter_param(attr_metadata, collection, opts = {})
625
640
  raw_value = (opts[:attr_values] || params)[attr_metadata[:key]]
626
641
  filter_table = opts[:filter_table]
627
- klass = opts[:class] || Utils.find_class(collection, opts)
642
+ klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
628
643
  if raw_value.is_a?(Hash) && raw_value.include?('0')
629
644
  operator_value_pairs = filter_process_param_array(params_array(raw_value), attr_metadata,
630
645
  opts)
@@ -653,10 +668,10 @@ module ModelApi
653
668
  end
654
669
  collection
655
670
  end
656
-
671
+
657
672
  def sort_collection(collection, sort_params, opts = {})
658
673
  return [collection, {}] if sort_params.blank? # Don't filter if no filter params
659
- klass = opts[:class] || Utils.find_class(collection, opts)
674
+ klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
660
675
  assoc_sorts, attr_sorts, result_sorts = process_sort_params(sort_params, klass,
661
676
  opts.merge(result_sorts: result_sorts))
662
677
  sort_table = opts[:sort_table]
@@ -679,7 +694,7 @@ module ModelApi
679
694
  end
680
695
  [collection, result_sorts]
681
696
  end
682
-
697
+
683
698
  def process_sort_params(sort_params, klass, opts)
684
699
  metadata = ModelApi::Utils.filtered_ext_attrs(klass, :sort, opts)
685
700
  assoc_sorts = {}
@@ -707,7 +722,7 @@ module ModelApi
707
722
  end
708
723
  [assoc_sorts, attr_sorts, result_sorts]
709
724
  end
710
-
725
+
711
726
  # Intentionally disabling parameter list length check for private / internal method
712
727
  # rubocop:disable Metrics/ParameterLists
713
728
  def process_sort_param_assoc(attr, metadata, sort_order, assoc_sorts, opts)
@@ -719,9 +734,9 @@ module ModelApi
719
734
  assoc_sort_params = (assoc_sorts[key] ||= {})
720
735
  assoc_sort_params[attr_elems[1..-1].join('.')] = sort_order
721
736
  end
722
-
737
+
723
738
  # rubocop:enable Metrics/ParameterLists
724
-
739
+
725
740
  def filter_process_param(raw_value, attr_metadata, opts)
726
741
  raw_value = raw_value.to_s.strip
727
742
  array = nil
@@ -741,7 +756,7 @@ module ModelApi
741
756
  operator, value = parse_filter_operator(raw_value)
742
757
  [[operator, ModelApi::Utils.transform_value(value, attr_metadata[:parse], opts)]]
743
758
  end
744
-
759
+
745
760
  def filter_process_param_array(array, attr_metadata, opts)
746
761
  operator_value_pairs = []
747
762
  equals_values = []
@@ -757,7 +772,7 @@ module ModelApi
757
772
  operator_value_pairs << ['=', equals_values.uniq] if equals_values.present?
758
773
  operator_value_pairs
759
774
  end
760
-
775
+
761
776
  def parse_filter_operator(value)
762
777
  value = value.to_s.strip
763
778
  if (operator = value.scan(/\A(>=|<=|!=|<>)[[:space:]]*\w/).flatten.first).present?
@@ -767,7 +782,7 @@ module ModelApi
767
782
  end
768
783
  ['=', value]
769
784
  end
770
-
785
+
771
786
  def format_value_for_query(column, value, klass)
772
787
  return value.map { |v| format_value_for_query(column, v, klass) } if value.is_a?(Array)
773
788
  column_metadata = klass.columns_hash[column.to_s]
@@ -788,7 +803,7 @@ module ModelApi
788
803
  end
789
804
  value.to_s
790
805
  end
791
-
806
+
792
807
  def params_array(raw_value)
793
808
  index = 0
794
809
  array = []
@@ -798,7 +813,7 @@ module ModelApi
798
813
  end
799
814
  array
800
815
  end
801
-
816
+
802
817
  def paginate_collection(collection, collection_links, opts, coll_route)
803
818
  collection_size = collection.count
804
819
  page_size = (params[:page_size] || DEFAULT_PAGE_SIZE).to_i
@@ -806,29 +821,29 @@ module ModelApi
806
821
  page_count = [(collection_size + page_size - 1) / page_size, 1].max
807
822
  page = page_count if page > page_count
808
823
  offset = (page - 1) * page_size
809
-
824
+
810
825
  opts = opts.dup
811
826
  opts[:count] ||= collection_size
812
827
  opts[:page] ||= page
813
828
  opts[:page_size] ||= page_size
814
829
  opts[:page_count] ||= page_count
815
-
830
+
816
831
  response.headers['X-Total-Count'] = collection_size.to_s
817
-
832
+
818
833
  opts[:collection_link_options] = (opts[:collection_link_options] || {})
819
834
  .reject { |k, _v| [:page].include?(k.to_sym) }
820
835
  opts[:object_link_options] = (opts[:object_link_options] || {})
821
836
  .reject { |k, _v| [:page, :page_size].include?(k.to_sym) }
822
-
837
+
823
838
  if collection_size > page_size
824
839
  opts[:collection_link_options][:page] = page
825
840
  Utils.add_pagination_links(collection_links, coll_route, page, page_count)
826
841
  collection = collection.limit(page_size).offset(offset)
827
842
  end
828
-
843
+
829
844
  [collection, collection_links, opts]
830
845
  end
831
-
846
+
832
847
  def resolve_key_to_column(klass, attr_metadata)
833
848
  return nil unless klass.respond_to?(:columns_hash)
834
849
  columns_hash = klass.columns_hash
@@ -839,7 +854,7 @@ module ModelApi
839
854
  return nil unless render_method.is_a?(String)
840
855
  columns_hash.include?(render_method) ? render_method : nil
841
856
  end
842
-
857
+
843
858
  def add_collection_object_route(opts)
844
859
  object_route = opts[:object_route]
845
860
  unless object_route.present?
@@ -857,31 +872,31 @@ module ModelApi
857
872
  return if object_route.blank?
858
873
  opts[:object_links] = (opts[:object_links] || {}).merge(self: object_route)
859
874
  end
860
-
875
+
861
876
  def add_hateoas_links_for_update(opts)
862
877
  object_route = opts[:object_route] || self
863
878
  links = { self: object_route }.reverse_merge(common_response_links(opts))
864
879
  opts[:links] = links.merge(opts[:links] || {})
865
880
  end
866
-
881
+
867
882
  def add_hateoas_links_for_updated_object(_operation, opts)
868
883
  object_route = opts[:object_route] || self
869
884
  object_links = { self: object_route }
870
885
  opts[:object_links] = object_links.merge(opts[:object_links] || {})
871
886
  end
872
-
887
+
873
888
  def verify_update_request_body(request_body, format, opts = {})
874
889
  if request.format.symbol.nil? && format.present?
875
890
  opts[:format] ||= format
876
891
  end
877
-
892
+
878
893
  if request_body.is_a?(Array)
879
894
  fail 'Expected object, but collection provided'
880
895
  elsif !request_body.is_a?(Hash)
881
896
  fail 'Expected object'
882
897
  end
883
898
  end
884
-
899
+
885
900
  def filtered_by_foreign_key?(query)
886
901
  fk_cache = self.class.instance_variable_get(:@foreign_key_cache)
887
902
  self.class.instance_variable_set(:@foreign_key_cache, fk_cache = {}) if fk_cache.nil?
@@ -898,14 +913,9 @@ module ModelApi
898
913
  "#{e.backtrace.join("\n")}"
899
914
  end
900
915
  end
901
-
916
+
902
917
  class Utils
903
918
  class << self
904
- def find_class(obj, opts = {})
905
- return nil if obj.nil?
906
- opts[:class] || (obj.respond_to?(:klass) ? obj.klass : obj.class)
907
- end
908
-
909
919
  def add_pagination_links(collection_links, coll_route, page, last_page)
910
920
  if page < last_page
911
921
  collection_links[:next] = [coll_route, { page: (page + 1) }]
@@ -914,7 +924,7 @@ module ModelApi
914
924
  collection_links[:first] = [coll_route, { page: 1 }]
915
925
  collection_links[:last] = [coll_route, { page: last_page }]
916
926
  end
917
-
927
+
918
928
  def object_from_req_body(root_elem, req_body, format)
919
929
  if format == :json
920
930
  request_obj = req_body
@@ -930,67 +940,21 @@ module ModelApi
930
940
  fail 'Invalid request format' unless request_obj.present?
931
941
  request_obj
932
942
  end
933
-
934
- def apply_updates(obj, req_obj, operation, opts = {})
935
- opts = opts.merge(object: opts[:object] || obj)
936
- metadata = ModelApi::Utils.filtered_ext_attrs(opts[:api_attr_metadata] ||
937
- ModelApi::Utils.filtered_attrs(obj, operation, opts), operation, opts)
938
- set_context_attrs(obj, opts)
939
- req_obj.each do |attr, value|
940
- attr = attr.to_sym
941
- attr_metadata = metadata[attr]
942
- unless attr_metadata.present?
943
- add_ignored_field(opts[:ignored_fields], attr, value, attr_metadata)
944
- next
945
- end
946
- update_api_attr(obj, attr, value, opts.merge(attr_metadata: attr_metadata))
947
- end
948
- end
949
-
950
- def set_context_attrs(obj, opts = {})
951
- klass = (obj.class < ActiveRecord::Base ? obj.class : nil)
952
- (opts[:context] || {}).each do |key, value|
953
- begin
954
- setter = "#{key}="
955
- next unless obj.respond_to?(setter)
956
- if (column = klass.try(:columns_hash).try(:[], key.to_s)).present?
957
- case column.type
958
- when :integer, :primary_key then
959
- obj.send("#{key}=", value.to_i)
960
- when :decimal, :float then
961
- obj.send("#{key}=", value.to_f)
962
- else
963
- obj.send(setter, value.to_s)
964
- end
965
- else
966
- obj.send(setter, value.to_s)
967
- end
968
- rescue Exception => e
969
- Rails.logger.warn "Error encountered assigning context parameter #{key} to " \
970
- "'#{value}' (skipping): \"#{e.message}\")."
971
- end
972
- end
973
- end
974
-
943
+
975
944
  def process_updated_model_save(obj, operation, opts = {})
976
945
  opts = opts.dup
977
946
  opts[:operation] = operation
978
- metadata = opts.delete(:api_attr_metadata) ||
979
- ModelApi::Utils.filtered_attrs(obj, operation, opts)
980
- model_metadata = opts.delete(:api_model_metadata) ||
981
- ModelApi::Utils.model_metadata(obj.class)
982
- ModelApi::Utils.invoke_callback(model_metadata[:before_validate], obj, opts.dup)
983
- validate_operation(obj, operation, opts)
984
- validate_preserving_existing_errors(obj)
985
- ModelApi::Utils.invoke_callback(model_metadata[:before_save], obj, opts.dup)
986
- obj.instance_variable_set(:@readonly, false) if obj.instance_variable_get(:@readonly)
987
- successful = obj.save unless obj.errors.present?
947
+ successful = ModelApi::Utils.save_obj(obj,
948
+ opts.merge(model_metadata: opts[:api_model_metadata]))
988
949
  if successful
989
950
  suggested_response_status = :ok
990
951
  object_errors = []
991
952
  else
992
953
  suggested_response_status = :bad_request
993
- object_errors = extract_msgs_for_error(obj, opts.merge(api_attr_metadata: metadata))
954
+ attr_metadata = opts.delete(:api_attr_metadata) ||
955
+ ModelApi::Utils.filtered_attrs(obj, operation, opts)
956
+ object_errors = ModelApi::Utils.extract_error_msgs(obj,
957
+ opts.merge(api_attr_metadata: attr_metadata))
994
958
  unless object_errors.present?
995
959
  object_errors << {
996
960
  error: 'Unspecified error',
@@ -1001,87 +965,15 @@ module ModelApi
1001
965
  end
1002
966
  [suggested_response_status, object_errors]
1003
967
  end
1004
-
1005
- def extract_msgs_for_error(obj, opts = {})
1006
- object_errors = []
1007
- attr_prefix = opts[:attr_prefix] || ''
1008
- api_metadata = opts[:api_attr_metadata] || ModelApi::Utils.api_attrs(obj.class)
1009
- obj.errors.each do |attr, attr_errors|
1010
- attr_errors = [attr_errors] unless attr_errors.is_a?(Array)
1011
- attr_errors.each do |error|
1012
- attr_metadata = api_metadata[attr] || {}
1013
- qualified_attr = "#{attr_prefix}#{ModelApi::Utils.ext_attr(attr, attr_metadata)}"
1014
- assoc_errors = nil
1015
- if attr_metadata[:type] == :association
1016
- assoc_errors = extract_assoc_error_msgs(obj, attr, opts.merge(
1017
- attr_metadata: attr_metadata))
1018
- end
1019
- if assoc_errors.present?
1020
- object_errors += assoc_errors
1021
- else
1022
- error_hash = {}
1023
- error_hash[:object] = attr_prefix if attr_prefix.present?
1024
- error_hash[:attribute] = qualified_attr unless attr == :base
1025
- object_errors << error_hash.merge(error: error,
1026
- message: (attr == :base ? error : "#{qualified_attr} #{error}"))
1027
- end
1028
- end
1029
- end
1030
- object_errors
1031
- end
1032
-
1033
- # rubocop:disable Metrics/MethodLength
1034
- def extract_assoc_error_msgs(obj, attr, opts)
1035
- object_errors = []
1036
- attr_metadata = opts[:attr_metadata] || {}
1037
- processed_assoc_objects = {}
1038
- assoc = attr_metadata[:association]
1039
- assoc_class = assoc.class_name.constantize
1040
- external_attr = ModelApi::Utils.ext_attr(attr, attr_metadata)
1041
- attr_metadata_create = attr_metadata_update = nil
1042
- if assoc.macro == :has_many
1043
- obj.send(attr).each_with_index do |assoc_obj, index|
1044
- next if processed_assoc_objects[assoc_obj]
1045
- processed_assoc_objects[assoc_obj] = true
1046
- attr_prefix = "#{external_attr}[#{index}]."
1047
- if assoc_obj.new_record?
1048
- attr_metadata_create ||= ModelApi::Utils.filtered_attrs(assoc_class, :create, opts)
1049
- object_errors += extract_msgs_for_error(assoc_obj, opts.merge(
1050
- attr_prefix: attr_prefix, api_attr_metadata: attr_metadata_create))
1051
- else
1052
- attr_metadata_update ||= ModelApi::Utils.filtered_attrs(assoc_class, :update, opts)
1053
- object_errors += extract_msgs_for_error(assoc_obj, opts.merge(
1054
- attr_prefix: attr_prefix, api_attr_metadata: attr_metadata_update))
1055
- end
1056
- end
1057
- else
1058
- assoc_obj = obj.send(attr)
1059
- return object_errors unless assoc_obj.present? && !processed_assoc_objects[assoc_obj]
1060
- processed_assoc_objects[assoc_obj] = true
1061
- attr_prefix = "#{external_attr}->"
1062
- if assoc_obj.new_record?
1063
- attr_metadata_create ||= ModelApi::Utils.filtered_attrs(assoc_class, :create, opts)
1064
- object_errors += extract_msgs_for_error(assoc_obj, opts.merge(
1065
- attr_prefix: attr_prefix, api_attr_metadata: attr_metadata_create))
1066
- else
1067
- attr_metadata_update ||= ModelApi::Utils.filtered_attrs(assoc_class, :update, opts)
1068
- object_errors += extract_msgs_for_error(assoc_obj, opts.merge(
1069
- attr_prefix: attr_prefix, api_attr_metadata: attr_metadata_update))
1070
- end
1071
- end
1072
- object_errors
1073
- end
1074
-
1075
- # rubocop:enable Metrics/MethodLength
1076
-
968
+
1077
969
  def process_object_destroy(obj, operation, opts)
1078
970
  soft_delete = obj.errors.present? ? false : object_destroy(obj, opts)
1079
-
971
+
1080
972
  if obj.errors.blank? && (soft_delete || obj.destroyed?)
1081
973
  response_status = :ok
1082
974
  object_errors = []
1083
975
  else
1084
- object_errors = extract_msgs_for_error(obj, opts)
976
+ object_errors = ModelApi::Utils.extract_error_msgs(obj, opts)
1085
977
  if object_errors.present?
1086
978
  response_status = :bad_request
1087
979
  else
@@ -1093,12 +985,12 @@ module ModelApi
1093
985
  }
1094
986
  end
1095
987
  end
1096
-
988
+
1097
989
  [response_status, object_errors]
1098
990
  end
1099
-
991
+
1100
992
  def object_destroy(obj, opts = {})
1101
- klass = find_class(obj)
993
+ klass = ModelApi::Utils.find_class(obj)
1102
994
  object_id = obj.send(opts[:id_attribute] || :id)
1103
995
  obj.instance_variable_set(:@readonly, false) if obj.instance_variable_get(:@readonly)
1104
996
  if (deleted_col = klass.columns_hash['deleted']).present?
@@ -1120,168 +1012,7 @@ module ModelApi
1120
1012
  Rails.logger.warn "Error destroying #{klass.name} \"#{object_id}\": \"#{e.message}\")."
1121
1013
  false
1122
1014
  end
1123
-
1124
- def set_api_attr(obj, attr, value, opts)
1125
- attr_metadata = opts[:attr_metadata]
1126
- internal_field = attr_metadata[:key] || attr
1127
- setter = attr_metadata[:setter] || "#{(internal_field)}="
1128
- unless obj.respond_to?(setter)
1129
- Rails.logger.warn "Error encountered assigning API input for attribute \"#{attr}\" " \
1130
- '(setter not found): skipping.'
1131
- add_ignored_field(opts[:ignored_fields], attr, value, attr_metadata)
1132
- return
1133
- end
1134
- obj.send(setter, value)
1135
- end
1136
-
1137
- def update_api_attr(obj, attr, value, opts = {})
1138
- attr_metadata = opts[:attr_metadata]
1139
- begin
1140
- value = ModelApi::Utils.transform_value(value, attr_metadata[:parse], opts)
1141
- rescue Exception => e
1142
- Rails.logger.warn "Error encountered parsing API input for attribute \"#{attr}\" " \
1143
- "(\"#{e.message}\"): \"#{value.to_s.first(1000)}\" ... using raw value instead."
1144
- end
1145
- begin
1146
- if attr_metadata[:type] == :association && attr_metadata[:parse].blank?
1147
- attr_metadata = opts[:attr_metadata]
1148
- assoc = attr_metadata[:association]
1149
- if assoc.macro == :has_many
1150
- update_has_many_assoc(obj, attr, value, opts)
1151
- elsif assoc.macro == :belongs_to
1152
- update_belongs_to_assoc(obj, attr, value, opts)
1153
- else
1154
- add_ignored_field(opts[:ignored_fields], attr, value, attr_metadata)
1155
- end
1156
- else
1157
- set_api_attr(obj, attr, value, opts)
1158
- end
1159
- rescue Exception => e
1160
- handle_api_setter_exception(e, obj, attr_metadata, opts)
1161
- end
1162
- end
1163
-
1164
- def update_has_many_assoc(obj, attr, value, opts = {})
1165
- attr_metadata = opts[:attr_metadata]
1166
- assoc = attr_metadata[:association]
1167
- assoc_class = assoc.class_name.constantize
1168
- model_metadata = ModelApi::Utils.model_metadata(assoc_class)
1169
- value_array = value.to_a rescue nil
1170
- unless value_array.is_a?(Array)
1171
- obj.errors.add(attr, 'must be supplied as an array of objects')
1172
- return
1173
- end
1174
- opts = opts.merge(model_metadata: model_metadata)
1175
- opts[:ignored_fields] = [] if opts.include?(:ignored_fields)
1176
- assoc_objs = []
1177
- value_array.each_with_index do |assoc_payload, index|
1178
- opts[:ignored_fields].clear if opts.include?(:ignored_fields)
1179
- assoc_objs << update_has_many_assoc_obj(obj, assoc, assoc_class, assoc_payload,
1180
- opts.merge(model_metadata: model_metadata))
1181
- if opts[:ignored_fields].present?
1182
- external_attr = ModelApi::Utils.ext_attr(attr, attr_metadata)
1183
- opts[:ignored_fields] << { "#{external_attr}[#{index}]" => opts[:ignored_fields] }
1184
- end
1185
- end
1186
- set_api_attr(obj, attr, assoc_objs, opts)
1187
- end
1188
-
1189
- def update_has_many_assoc_obj(parent_obj, assoc, assoc_class, assoc_payload, opts = {})
1190
- model_metadata = opts[:model_metadata] || ModelApi::Utils.model_metadata(assoc_class)
1191
- assoc_obj, assoc_oper, assoc_opts = resolve_has_many_assoc_obj(model_metadata, assoc,
1192
- assoc_class, assoc_payload, parent_obj, opts)
1193
- if (inverse_assoc = assoc.options[:inverse_of]).present? &&
1194
- assoc_obj.respond_to?("#{inverse_assoc}=")
1195
- assoc_obj.send("#{inverse_assoc}=", parent_obj)
1196
- elsif !parent_obj.new_record? && assoc_obj.respond_to?("#{assoc.foreign_key}=")
1197
- assoc_obj.send("#{assoc.foreign_key}=", obj.id)
1198
- end
1199
- apply_updates(assoc_obj, assoc_payload, assoc_oper, assoc_opts)
1200
- ModelApi::Utils.invoke_callback(model_metadata[:after_initialize], assoc_obj,
1201
- assoc_opts.merge(operation: assoc_oper).freeze)
1202
- assoc_obj
1203
- end
1204
-
1205
- def resolve_has_many_assoc_obj(model_metadata, assoc, assoc_class, assoc_payload,
1206
- parent_obj, opts = {})
1207
- assoc_obj = resolve_assoc_obj(model_metadata, assoc, assoc_class, assoc_payload,
1208
- parent_obj, opts)
1209
- if assoc_obj.new_record?
1210
- assoc_oper = :create
1211
- opts[:create_opts] ||= opts.merge(api_attr_metadata: ModelApi::Utils.filtered_attrs(
1212
- assoc_class, :create, opts))
1213
- assoc_opts = opts[:create_opts]
1214
- else
1215
- assoc_oper = :update
1216
- opts[:update_opts] ||= opts.merge(api_attr_metadata: ModelApi::Utils.filtered_attrs(
1217
- assoc_class, :update, opts))
1218
-
1219
- assoc_opts = opts[:update_opts]
1220
- end
1221
- [assoc_obj, assoc_oper, assoc_opts]
1222
- end
1223
-
1224
- def update_belongs_to_assoc(parent_obj, attr, assoc_payload, opts = {})
1225
- unless assoc_payload.is_a?(Hash)
1226
- parent_obj.errors.add(attr, 'must be supplied as an object')
1227
- return
1228
- end
1229
- attr_metadata = opts[:attr_metadata]
1230
- assoc = attr_metadata[:association]
1231
- assoc_class = assoc.class_name.constantize
1232
- model_metadata = ModelApi::Utils.model_metadata(assoc_class)
1233
- assoc_obj, assoc_oper, assoc_opts = resolve_belongs_to_assoc_obj(model_metadata, assoc,
1234
- assoc_class, assoc_payload, parent_obj, opts)
1235
- apply_updates(assoc_obj, assoc_payload, assoc_oper, assoc_opts)
1236
- ModelApi::Utils.invoke_callback(model_metadata[:after_initialize], assoc_obj,
1237
- opts.merge(operation: assoc_oper).freeze)
1238
- if assoc_opts[:ignored_fields].present?
1239
- external_attr = ModelApi::Utils.ext_attr(attr, attr_metadata)
1240
- opts[:ignored_fields] << { external_attr.to_s => assoc_opts[:ignored_fields] }
1241
- end
1242
- set_api_attr(parent_obj, attr, assoc_obj, opts)
1243
- end
1244
-
1245
- def resolve_belongs_to_assoc_obj(model_metadata, assoc, assoc_class, assoc_payload,
1246
- parent_obj, opts = {})
1247
- assoc_opts = opts[:ignored_fields].is_a?(Array) ? opts.merge(ignored_fields: []) : opts
1248
- assoc_obj = resolve_assoc_obj(model_metadata, assoc, assoc_class, assoc_payload,
1249
- parent_obj, opts)
1250
- assoc_oper = assoc_obj.new_record? ? :create : :update
1251
- assoc_opts = assoc_opts.merge(
1252
- api_attr_metadata: ModelApi::Utils.filtered_attrs(assoc_class, assoc_oper, opts))
1253
- return [assoc_obj, assoc_oper, assoc_opts]
1254
- end
1255
-
1256
- def resolve_assoc_obj(model_metadata, assoc, assoc_class, assoc_payload, parent_obj,
1257
- opts = {})
1258
- if opts[:resolve].try(:respond_to?, :call)
1259
- assoc_obj = ModelApi::Utils.invoke_callback(opts[:resolve], assoc_payload, opts.merge(
1260
- parent: parent_obj, association: assoc, association_metadata: model_metadata))
1261
- else
1262
- assoc_obj = find_by_id_attrs(model_metadata[:id_attributes], assoc_class, assoc_payload)
1263
- assoc_obj = assoc_obj.first unless assoc_obj.nil? || assoc_obj.count != 1
1264
- assoc_obj ||= assoc_class.new
1265
- end
1266
- assoc_obj
1267
- end
1268
-
1269
- def find_by_id_attrs(id_attributes, assoc_class, assoc_payload)
1270
- return nil unless id_attributes.present?
1271
- id_attributes.each do |id_attr_set|
1272
- query = nil
1273
- id_attr_set.each do |id_attr|
1274
- unless assoc_payload.include?(id_attr.to_s)
1275
- query = nil
1276
- break
1277
- end
1278
- query = (query || assoc_class).where(id_attr => assoc_payload[id_attr.to_s])
1279
- end
1280
- return query unless query.nil?
1281
- end
1282
- nil
1283
- end
1284
-
1015
+
1285
1016
  def apply_context(query, opts = {})
1286
1017
  context = opts[:context]
1287
1018
  return query if context.nil?
@@ -1292,60 +1023,7 @@ module ModelApi
1292
1023
  end
1293
1024
  query
1294
1025
  end
1295
-
1296
- def handle_api_setter_exception(e, obj, attr_metadata, opts = {})
1297
- return unless attr_metadata.is_a?(Hash)
1298
- on_exception = attr_metadata[:on_exception]
1299
- fail e unless on_exception.present?
1300
- on_exception = { Exception => on_exception } unless on_exception.is_a?(Hash)
1301
- opts = opts.frozen? ? opts : opts.dup.freeze
1302
- on_exception.each do |klass, handler|
1303
- klass = klass.to_s.constantize rescue nil unless klass.is_a?(Class)
1304
- next unless klass.is_a?(Class) && e.is_a?(klass)
1305
- if handler.respond_to?(:call)
1306
- ModelApi::Utils.invoke_callback(handler, obj, e, opts)
1307
- elsif handler.present?
1308
- # Presume handler is an error message in this case
1309
- obj.errors.add(attr_metadata[:key], handler.to_s)
1310
- else
1311
- add_ignored_field(opts[:ignored_fields], nil, opts[:value], attr_metadata)
1312
- end
1313
- break
1314
- end
1315
- end
1316
-
1317
- def add_ignored_field(ignored_fields, attr, value, attr_metadata)
1318
- return unless ignored_fields.is_a?(Array)
1319
- attr_metadata ||= {}
1320
- external_attr = ModelApi::Utils.ext_attr(attr, attr_metadata)
1321
- return unless external_attr.present?
1322
- ignored_fields << { external_attr => value }
1323
- end
1324
-
1325
- def validate_operation(obj, operation, opts = {})
1326
- klass = find_class(obj, opts)
1327
- model_metadata = opts[:api_model_metadata] || ModelApi::Utils.model_metadata(klass)
1328
- return nil unless operation.present?
1329
- opts = opts.frozen? ? opts : opts.dup.freeze
1330
- if obj.nil?
1331
- ModelApi::Utils.invoke_callback(model_metadata[:"validate_#{operation}"], opts)
1332
- else
1333
- ModelApi::Utils.invoke_callback(model_metadata[:"validate_#{operation}"], obj, opts)
1334
- end
1335
- end
1336
-
1337
- def validate_preserving_existing_errors(obj)
1338
- if obj.errors.present?
1339
- errors = obj.errors.messages.dup
1340
- obj.valid?
1341
- errors = obj.errors.messages.merge(errors)
1342
- obj.errors.clear
1343
- errors.each { |field, error| obj.errors.add(field, error) }
1344
- else
1345
- obj.valid?
1346
- end
1347
- end
1348
-
1026
+
1349
1027
  def class_or_sti_subclass(klass, req_body, operation, opts = {})
1350
1028
  metadata = ModelApi::Utils.filtered_attrs(klass, :create, opts)
1351
1029
  if operation == :create && (attr_metadata = metadata[:type]).is_a?(Hash) &&